11fs = require ' fs'
22path = require ' path'
3- spawn = require (' child_process' )
4- semver = require ' semver'
5- {BufferedProcess , CompositeDisposable } = require ' atom'
63XRegExp = require ' xregexp'
7-
8-
9- pattern = XRegExp (
10- ' (?<file>[^\n\r ]+):(?<from_line>\\ d+):(?<from_col>\\ d+):\\ s*\
11- (?<to_line>\\ d+):(?<to_col>\\ d+)\\ s+\
12- ((?<error>error|fatal error)|(?<warning>warning)|(?<info>note|help)):\\ s+\
13- (?<message>.+?)[\n\r ]+($|(?=[^\n\r ]+:\\ d+))' , ' s'
14- )
15- patternRustcVersion = XRegExp (
16- ' rustc (?<version>1.\\ d+.\\ d+)(?:(?:-(?<nightly>nightly)|(?:[^\\ s]+))? \
17- \\ ((?:[^\\ s]+) (?<date>\\ d{4}-\\ d{2}-\\ d{2})\\ ))?'
18- )
4+ semver = require ' semver'
5+ sb_exec = require ' sb-exec'
6+ {CompositeDisposable } = require ' atom'
197
208
219class LinterRust
10+ pattern : XRegExp (' (?<file>[^\n\r ]+):(?<from_line>\\ d+):(?<from_col>\\ d+):\\ s*\
11+ (?<to_line>\\ d+):(?<to_col>\\ d+)\\ s+\
12+ ((?<error>error|fatal error)|(?<warning>warning)|(?<info>note|help)):\\ s+\
13+ (?<message>.+?)[\n\r ]+($|(?=[^\n\r ]+:\\ d+))' , ' s' )
14+ patternRustcVersion : XRegExp (' rustc (?<version>1.\\ d+.\\ d+)(?:(?:-(?<nightly>nightly)|(?:[^\\ s]+))? \
15+ \\ ((?:[^\\ s]+) (?<date>\\ d{4}-\\ d{2}-\\ d{2})\\ ))?' )
2216 cargoDependencyDir : " target/debug/deps"
23- lintProcess : null
24- cachedAbleToJsonErrors : null
25-
2617
2718 constructor : ->
2819 @subscriptions = new CompositeDisposable
@@ -64,102 +55,106 @@ class LinterRust
6455 (specifiedFeatures ) =>
6556 @specifiedFeatures = specifiedFeatures
6657
67-
6858 destroy : ->
6959 do @subscriptions .dispose
7060
71-
7261 lint : (textEditor ) =>
73- return new Promise (resolve , reject ) =>
74- results = []
75- file = @ initCmd do textEditor .getPath
76- curDir = path .dirname file
77- PATH = path .dirname @cmd [0 ]
78- options =
79- env : JSON .parse JSON .stringify process .env
80- options .env .PATH = PATH + path .delimiter + options .env .PATH
81- options .cwd = curDir
82- command = @cmd [0 ]
83- args = @cmd .slice 1
84- @cachedAbleToJsonErrors = null
85- @cachedAbleToJsonErrors = do @ableToJSONErrors
86-
87- stdout = (data ) ->
88- console .log data if do atom .inDevMode
89- stderr = (err ) ->
90- if err .indexOf (' does not have these features' ) >= 0
91- atom .notifications .addError " Invalid specified features" ,
92- detail : " #{ err} "
93- dismissable : true
94- else
95- if do atom .inDevMode
96- atom .notifications .addWarning " Output from stderr while linting" ,
97- detail : " #{ err} "
98- description : " This is shown because Atom is running in dev-mode and probably not an actual error"
62+ curDir = path .dirname textEditor .getPath ()
63+ @ ableToJSONErrors (curDir).then (ableToJSONErrors) =>
64+ @ initCmd (textEditor .getPath (), ableToJSONErrors).then (result) =>
65+ [file , cmd ] = result
66+ env = JSON .parse JSON .stringify process .env
67+ cwd = curDir
68+ command = cmd[0 ]
69+ args = cmd .slice 1
70+
71+ if ableToJSONErrors
72+ additional = if env .RUSTFLAGS ? then ' ' + env .RUSTFLAGS else ' '
73+ env .RUSTFLAGS = ' --error-format=json' + additional
74+
75+ sb_exec .exec (command, args, {env : env, cwd : cwd, stream : ' both' })
76+ .then (result) =>
77+ {stdout , stderr , exitCode } = result
78+ # first, check if an output says specified features are invalid
79+ if stderr .indexOf (' does not have these features' ) >= 0
80+ atom .notifications .addError " Invalid specified features" ,
81+ detail : " #{ stderr} "
82+ dismissable : true
83+ []
84+ # then, if exit code looks okay, process an output
85+ else if exitCode is 101 or exitCode is 0
86+ # in dev mode show message boxes with output
87+ showDevModeWarning = (stream , message ) ->
88+ atom .notifications .addWarning " Output from #{ stream} while linting" ,
89+ detail : " #{ message} "
90+ description : " This is shown because Atom is running in dev-mode and probably not an actual error"
91+ dismissable : true
92+ if do atom .inDevMode
93+ showDevModeWarning (' stderr' , stderr) if stderr
94+ showDevModeWarning (' stdout' , stdout) if stdout
95+
96+ # call a needed parser
97+ messages = unless ableToJSONErrors
98+ @ parse stderr
99+ else
100+ @ parseJSON stderr
101+
102+ # correct file paths
103+ messages .forEach (message) ->
104+ if ! (path .isAbsolute message .filePath )
105+ message .filePath = path .join curDir, message .filePath
106+ messages
107+ else
108+ # whoops, we're in trouble -- let's output as much as we can
109+ atom .notifications .addError " Failed to run #{ command} with exit code #{ exitCode} " ,
110+ detail : " with args:\n #{ args .join (' ' )} \n See console for more information"
111+ dismissable : true
112+ console .log " stdout:"
113+ console .log stdout
114+ console .log " stderr:"
115+ console .log stderr
116+ []
117+ .catch (error) ->
118+ console .log error
119+ atom .notifications .addError " Failed to run #{ command} " ,
120+ detail : " #{ error .message } "
99121 dismissable : true
100- results .push err
101-
102- exit = (code ) =>
103- if code is 101 or code is 0
104- unless do @ableToJSONErrors
105- messages = @ parse results .join (' ' )
106- else
107- messages = @ parseJSON results
108- messages .forEach (message) ->
109- if ! (path .isAbsolute message .filePath )
110- message .filePath = path .join curDir, message .filePath
111- resolve messages
112- else
113- resolve []
114-
115- if do @ableToJSONErrors
116- additional = if options .env .RUSTFLAGS ? then ' ' + options .env .RUSTFLAGS else ' '
117- options .env .RUSTFLAGS = ' --error-format=json' + additional
118- @lintProcess = new BufferedProcess ({command, args, options, stdout, stderr, exit})
119- @lintProcess .onWillThrowError ({error, handle}) ->
120- atom .notifications .addError " Failed to run #{ command} " ,
121- detail : " #{ error .message } "
122- dismissable : true
123- handle ()
124- resolve []
125-
122+ []
126123
127- parseJSON : (results ) =>
124+ parseJSON : (output ) =>
128125 elements = []
126+ results = output .split ' \n '
129127 for result in results
130- subresults = result .split ' \n '
131- for result in subresults
132- if result .startsWith ' {'
133- input = JSON .parse result
134- continue unless input .spans
135- primary_span = input .spans .find (span) -> span .is_primary
136- continue unless primary_span
137- range = [
138- [primary_span .line_start - 1 , primary_span .column_start - 1 ],
139- [primary_span .line_end - 1 , primary_span .column_end - 1 ]
140- ]
141- input .level = ' error' if input == ' fatal error'
142- element =
143- type : input .level
144- message : input .message
145- file : primary_span .file_name
146- range : range
147- children : input .children
148- for span in input .spans
149- unless span .is_primary
150- element .children .push
151- message : span .label
152- range : [
153- [span .line_start - 1 , span .column_start - 1 ],
154- [span .line_end - 1 , span .column_end - 1 ]
155- ]
156- elements .push element
128+ if result .startsWith ' {'
129+ input = JSON .parse result .trim ()
130+ continue unless input .spans
131+ primary_span = input .spans .find (span) -> span .is_primary
132+ continue unless primary_span
133+ range = [
134+ [primary_span .line_start - 1 , primary_span .column_start - 1 ],
135+ [primary_span .line_end - 1 , primary_span .column_end - 1 ]
136+ ]
137+ input .level = ' error' if input == ' fatal error'
138+ element =
139+ type : input .level
140+ message : input .message
141+ file : primary_span .file_name
142+ range : range
143+ children : input .children
144+ for span in input .spans
145+ unless span .is_primary
146+ element .children .push
147+ message : span .label
148+ range : [
149+ [span .line_start - 1 , span .column_start - 1 ],
150+ [span .line_end - 1 , span .column_end - 1 ]
151+ ]
152+ elements .push element
157153 @ buildMessages (elements)
158154
159-
160155 parse : (output ) =>
161156 elements = []
162- XRegExp .forEach output, pattern, (match ) ->
157+ XRegExp .forEach output, @ pattern , (match ) ->
163158 if match .from_col == match .to_col
164159 match .to_col = parseInt (match .to_col ) + 1
165160 range = [
@@ -179,7 +174,6 @@ class LinterRust
179174 elements .push element
180175 @ buildMessages elements
181176
182-
183177 buildMessages : (elements ) =>
184178 messages = []
185179 lastMessage = null
@@ -216,7 +210,6 @@ class LinterRust
216210 messages .push lastMessage
217211 return messages
218212
219-
220213 constructMessage : (type , element ) ->
221214 message =
222215 type : type
@@ -234,8 +227,7 @@ class LinterRust
234227 range : children .range or element .range
235228 message
236229
237-
238- initCmd : (editingFile ) =>
230+ initCmd : (editingFile , ableToJSONErrors ) =>
239231 rustcArgs = switch @rustcBuildTest
240232 when true then [' --cfg' , ' test' , ' -Z' , ' no-trans' , ' --color' , ' never' ]
241233 else [' -Z' , ' no-trans' , ' --color' , ' never' ]
@@ -246,28 +238,30 @@ class LinterRust
246238 when ' clippy' then [' clippy' ]
247239 else [' build' ]
248240
249- cargoManifestPath = @ locateCargo path .dirname editingFile
250- if not @useCargo or not cargoManifestPath
251- @cmd = [@rustcPath ]
252- .concat rustcArgs
253- if cargoManifestPath
254- @cmd .push ' -L'
255- @cmd .push path .join path .dirname (cargoManifestPath), @cargoDependencyDir
256- @cmd = @cmd .concat @ compilationFeatures (false )
257- @cmd = @cmd .concat [editingFile]
258- @cmd = @cmd .concat [' --error-format=json' ] if do @ableToJSONErrors
259- return editingFile
241+ if not @useCargo or not @cargoManifestFilename
242+ Promise .resolve ().then () =>
243+ cmd = [@rustcPath ]
244+ .concat rustcArgs
245+ if @cargoManifestFilename
246+ cmd .push ' -L'
247+ cmd .push path .join path .dirname (@cargoManifestFilename ), @cargoDependencyDir
248+ compilationFeatures = @ compilationFeatures (false )
249+ cmd = cmd .concat compilationFeatures if compilationFeatures
250+ cmd = cmd .concat [editingFile]
251+ cmd = cmd .concat [' --error-format=json' ] if ableToJSONErrors
252+ [editingFile, cmd]
260253 else
261- @cmd = @ buildCargoPath @cargoPath
262- .concat cargoArgs
263- .concat [' -j' , @jobsNumber ]
264- @cmd = @cmd .concat @ compilationFeatures (true )
265- @cmd = @cmd .concat [' --manifest-path' , cargoManifestPath]
266- return cargoManifestPath
267-
254+ @ buildCargoPath (@cargoPath ).then (cmd) =>
255+ compilationFeatures = @ compilationFeatures (true )
256+ cmd = cmd
257+ .concat cargoArgs
258+ .concat [' -j' , @jobsNumber ]
259+ cmd = cmd .concat compilationFeatures if compilationFeatures
260+ cmd = cmd .concat [' --manifest-path' , @cargoManifestFilename ]
261+ [@cargoManifestFilename , cmd]
268262
269263 compilationFeatures : (cargo ) =>
270- if @specifiedFeatures
264+ if @specifiedFeatures . length > 0
271265 if cargo
272266 [' --features' , @specifiedFeatures .join (' ' )]
273267 else
@@ -276,18 +270,19 @@ class LinterRust
276270 result .push [' --cfg' , " feature=\" #{ f} \" " ]
277271 result
278272
279-
280- ableToJSONErrors : () =>
281- return @cachedAbleToJsonErrors if @cachedAbleToJsonErrors ?
282- result = spawn .execSync @rustcPath + ' --version' , {stdio : ' pipe' }
283- match = XRegExp .exec result, patternRustcVersion
284- if match and match .nightly and match .date > ' 2016-08-08'
285- true
286- else if match and not match .nightly and semver .gte (match .version , ' 1.12.0' )
287- true
288- else
289- false
290-
273+ ableToJSONErrors : (curDir ) =>
274+ # current dir is set to handle overrides
275+
276+ sb_exec .exec (@rustcPath , [' --version' ], {stream : ' stdout' , cwd : curDir, stdio : ' pipe' }).then (stdout) =>
277+ console .log stdout
278+ try
279+ match = XRegExp .exec (stdout, @patternRustcVersion )
280+ if match and match .nightly and match .date > ' 2016-08-08'
281+ true
282+ else if match and not match .nightly and semver .gte (match .version , ' 1.12.0' )
283+ true
284+ else
285+ false
291286
292287 locateCargo : (curDir ) =>
293288 root_dir = if / ^ win/ .test process .platform then / ^ . :\\ $ / else / ^ \/ $ /
@@ -298,19 +293,24 @@ class LinterRust
298293 directory = path .resolve path .join (directory, ' ..' )
299294 return false
300295
301-
302- buildCargoPath : (cargoPath ) =>
303- if @cargoCommand == ' clippy' and @ usingMultirustForClippy ()
304- return [' multirust' ,' run' , ' nightly' , ' cargo' ]
305- else
306- return [cargoPath]
307-
308-
309- usingMultirustForClippy : () =>
310- try
311- result = spawn .execSync ' multirust --version'
312- true
313- catch
314- false
296+ buildCargoPath : (cargoPath ) =>
297+ @ usingMultitoolForClippy ().then (canUseMultirust) =>
298+ if @cargoCommand == ' clippy' and canUseMultirust .result
299+ [canUseMultirust .tool , ' run' , ' nightly' , ' cargo' ]
300+ else
301+ [cargoPath]
302+
303+ usingMultitoolForClippy : () =>
304+ # Try to use rustup
305+ sb_exec .exec ' rustup' , [' --version' ], {ignoreExitCode : true }
306+ .then ->
307+ result : true , tool : ' rustup'
308+ .catch ->
309+ # Try to use odler multirust at least
310+ sb_exec .exec ' multirust' , [' --version' ], {ignoreExitCode : true }
311+ .then ->
312+ result : true , tool : ' multirust'
313+ .catch ->
314+ result : false
315315
316316module .exports = LinterRust
0 commit comments