Skip to content
This repository was archived by the owner on Aug 7, 2023. It is now read-only.

Commit 486c3ee

Browse files
committed
Added a proper support for cargo --message-format json
Massively refactored code, so the error mode behaviour is less dependent on conditions and more polymorphic.
1 parent 2b04862 commit 486c3ee

File tree

2 files changed

+324
-230
lines changed

2 files changed

+324
-230
lines changed

lib/linter-rust.coffee

Lines changed: 90 additions & 230 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,9 @@ XRegExp = require 'xregexp'
44
semver = require 'semver'
55
sb_exec = require 'sb-exec'
66
{CompositeDisposable} = require 'atom'
7-
7+
errorModes = require './mode'
88

99
class 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')
1410
patternRustcVersion: XRegExp('rustc (?<version>1.\\d+.\\d+)(?:(?:-(?<nightly>nightly)|(?:[^\\s]+))? \
1511
\\((?:[^\\s]+) (?<date>\\d{4}-\\d{2}-\\d{2})\\))?')
1612
cargoDependencyDir: "target/debug/deps"
@@ -59,209 +55,80 @@ class LinterRust
5955
do @subscriptions.dispose
6056

6157
lint: (textEditor) =>
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-
curDir = path.dirname file
68-
cwd = curDir
69-
command = cmd[0]
70-
args = cmd.slice 1
71-
env.PATH = path.dirname(cmd[0]) + path.delimiter + env.PATH
72-
73-
if ableToJSONErrors
74-
if !env.RUSTFLAGS? or !(env.RUSTFLAGS.indexOf('--error-format=json') >= 0)
75-
additional = if env.RUSTFLAGS? then ' ' + env.RUSTFLAGS else ''
76-
env.RUSTFLAGS = '--error-format=json' + additional
77-
sb_exec.exec(command, args, {env: env, cwd: cwd, stream: 'both'})
78-
.then (result) =>
79-
{stdout, stderr, exitCode} = result
80-
# first, check if an output says specified features are invalid
81-
if stderr.indexOf('does not have these features') >= 0
82-
atom.notifications.addError "Invalid specified features",
83-
detail: "#{stderr}"
84-
dismissable: true
85-
[]
86-
# then, if exit code looks okay, process an output
87-
else if exitCode is 101 or exitCode is 0
88-
# in dev mode show message boxes with output
89-
showDevModeWarning = (stream, message) ->
90-
atom.notifications.addWarning "Output from #{stream} while linting",
91-
detail: "#{message}"
92-
description: "This is shown because Atom is running in dev-mode and probably not an actual error"
93-
dismissable: true
94-
if do atom.inDevMode
95-
showDevModeWarning('stderr', stderr) if stderr
96-
showDevModeWarning('stdout', stdout) if stdout
97-
98-
# call a needed parser
99-
messages = unless ableToJSONErrors
100-
@parse stderr
101-
else
102-
@parseJSON stderr
103-
104-
# correct file paths
105-
messages.forEach (message) ->
106-
if !(path.isAbsolute message.filePath)
107-
message.filePath = path.join curDir, message.filePath
108-
messages
109-
else
110-
# whoops, we're in trouble -- let's output as much as we can
111-
atom.notifications.addError "Failed to run #{command} with exit code #{exitCode}",
112-
detail: "with args:\n #{args.join(' ')}\nSee console for more information"
113-
dismissable: true
114-
console.log "stdout:"
115-
console.log stdout
116-
console.log "stderr:"
117-
console.log stderr
118-
[]
119-
.catch (error) ->
120-
console.log error
121-
atom.notifications.addError "Failed to run #{command}",
122-
detail: "#{error.message}"
58+
@initCmd(textEditor.getPath()).then (result) =>
59+
[cmd_res, errorMode] = result
60+
[file, cmd] = cmd_res
61+
env = JSON.parse JSON.stringify process.env
62+
curDir = path.dirname file
63+
cwd = curDir
64+
command = cmd[0]
65+
args = cmd.slice 1
66+
env.PATH = path.dirname(cmd[0]) + path.delimiter + env.PATH
67+
68+
# we set flags only for intermediate json support
69+
if errorMode == errorModes.FLAGS_JSON_CARGO
70+
if !env.RUSTFLAGS? or !(env.RUSTFLAGS.indexOf('--error-format=json') >= 0)
71+
additional = if env.RUSTFLAGS? then ' ' + env.RUSTFLAGS else ''
72+
env.RUSTFLAGS = '--error-format=json' + additional
73+
74+
sb_exec.exec(command, args, {env: env, cwd: cwd, stream: 'both'})
75+
.then (result) =>
76+
{stdout, stderr, exitCode} = result
77+
# first, check if an output says specified features are invalid
78+
if stderr.indexOf('does not have these features') >= 0
79+
atom.notifications.addError "Invalid specified features",
80+
detail: "#{stderr}"
12381
dismissable: true
12482
[]
125-
126-
parseJSON: (output) =>
127-
elements = []
128-
results = output.split '\n'
129-
for result in results
130-
if result.startsWith '{'
131-
input = JSON.parse result.trim()
132-
continue unless input.spans
133-
primary_span = input.spans.find (span) -> span.is_primary
134-
continue unless primary_span
135-
range = [
136-
[primary_span.line_start - 1, primary_span.column_start - 1],
137-
[primary_span.line_end - 1, primary_span.column_end - 1]
138-
]
139-
input.level = 'error' if input == 'fatal error'
140-
element =
141-
type: input.level
142-
message: input.message
143-
file: primary_span.file_name
144-
range: range
145-
children: input.children
146-
for span in input.spans
147-
unless span.is_primary
148-
element.children.push
149-
message: span.label
150-
range: [
151-
[span.line_start - 1, span.column_start - 1],
152-
[span.line_end - 1, span.column_end - 1]
153-
]
154-
elements.push element
155-
@buildMessages(elements)
156-
157-
parse: (output) =>
158-
elements = []
159-
XRegExp.forEach output, @pattern, (match) ->
160-
if match.from_col == match.to_col
161-
match.to_col = parseInt(match.to_col) + 1
162-
range = [
163-
[match.from_line - 1, match.from_col - 1],
164-
[match.to_line - 1, match.to_col - 1]
165-
]
166-
level = if match.error then 'error'
167-
else if match.warning then 'warning'
168-
else if match.info then 'info'
169-
else if match.trace then 'trace'
170-
else if match.note then 'note'
171-
element =
172-
type: level
173-
message: match.message
174-
file: match.file
175-
range: range
176-
elements.push element
177-
@buildMessages elements
178-
179-
buildMessages: (elements) =>
180-
messages = []
181-
lastMessage = null
182-
for element in elements
183-
switch element.type
184-
when 'info', 'trace', 'note'
185-
# Add only if there is a last message
186-
if lastMessage
187-
lastMessage.trace or= []
188-
lastMessage.trace.push
189-
type: "Trace"
190-
text: element.message
191-
filePath: element.file
192-
range: element.range
193-
when 'warning'
194-
# If the message is warning and user enabled disabling warnings
195-
# Check if this warning is disabled
196-
if @disabledWarnings and @disabledWarnings.length > 0
197-
messageIsDisabledLint = false
198-
for disabledWarning in @disabledWarnings
199-
# Find a disabled lint in warning message
200-
if element.message.indexOf(disabledWarning) >= 0
201-
messageIsDisabledLint = true
202-
lastMessage = null
203-
break
204-
if not messageIsDisabledLint
205-
lastMessage = @constructMessage "Warning", element
206-
messages.push lastMessage
83+
# then, if exit code looks okay, process an output
84+
else if exitCode is 101 or exitCode is 0
85+
# in dev mode show message boxes with output
86+
showDevModeWarning = (stream, message) ->
87+
atom.notifications.addWarning "Output from #{stream} while linting",
88+
detail: "#{message}"
89+
description: "This is shown because Atom is running in dev-mode and probably not an actual error"
90+
dismissable: true
91+
if do atom.inDevMode
92+
showDevModeWarning('stderr', stderr) if stderr
93+
showDevModeWarning('stdout', stdout) if stdout
94+
95+
# call a needed parser
96+
output = errorMode.neededOutput(stdout, stderr)
97+
messages = errorMode.parse output, @disabledWarnings
98+
99+
# correct file paths
100+
messages.forEach (message) ->
101+
if !(path.isAbsolute message.filePath)
102+
message.filePath = path.join curDir, message.filePath
103+
messages
207104
else
208-
lastMessage = @constructMessage "Warning" , element
209-
messages.push lastMessage
210-
when 'error', 'fatal error'
211-
lastMessage = @constructMessage "Error", element
212-
messages.push lastMessage
213-
return messages
214-
215-
constructMessage: (type, element) ->
216-
message =
217-
type: type
218-
text: element.message
219-
filePath: element.file
220-
range: element.range
221-
# children exists only in JSON messages
222-
if element.children
223-
message.trace = []
224-
for children in element.children
225-
message.trace.push
226-
type: "Trace"
227-
text: children.message
228-
filePath: element.file
229-
range: children.range or element.range
230-
message
231-
232-
initCmd: (editingFile, ableToJSONErrors) =>
233-
rustcArgs = switch @rustcBuildTest
234-
when true then ['--cfg', 'test', '-Z', 'no-trans', '--color', 'never']
235-
else ['-Z', 'no-trans', '--color', 'never']
236-
cargoArgs = switch @cargoCommand
237-
when 'check' then ['check']
238-
when 'test' then ['test', '--no-run']
239-
when 'rustc' then ['rustc', '-Zno-trans', '--color', 'never']
240-
when 'clippy' then ['clippy']
241-
else ['build']
242-
105+
# whoops, we're in trouble -- let's output as much as we can
106+
atom.notifications.addError "Failed to run #{command} with exit code #{exitCode}",
107+
detail: "with args:\n #{args.join(' ')}\nSee console for more information"
108+
dismissable: true
109+
console.log "stdout:"
110+
console.log stdout
111+
console.log "stderr:"
112+
console.log stderr
113+
[]
114+
.catch (error) ->
115+
console.log error
116+
atom.notifications.addError "Failed to run #{command}",
117+
detail: "#{error.message}"
118+
dismissable: true
119+
[]
120+
121+
initCmd: (editingFile) =>
122+
curDir = path.dirname editingFile
243123
cargoManifestPath = @locateCargo path.dirname editingFile
244124
if not @useCargo or not cargoManifestPath
245-
Promise.resolve().then () =>
246-
cmd = [@rustcPath]
247-
.concat rustcArgs
248-
if cargoManifestPath
249-
cmd.push '-L'
250-
cmd.push path.join path.dirname(cargoManifestPath), @cargoDependencyDir
251-
compilationFeatures = @compilationFeatures(false)
252-
cmd = cmd.concat compilationFeatures if compilationFeatures
253-
cmd = cmd.concat [editingFile]
254-
cmd = cmd.concat ['--error-format=json'] if ableToJSONErrors
255-
[editingFile, cmd]
125+
@decideErrorMode(curDir, 'rustc').then (mode) =>
126+
mode.buildArguments(this, [editingFile, cargoManifestPath]).then (cmd) =>
127+
[cmd, mode]
256128
else
257-
@buildCargoPath(@cargoPath).then (cmd) =>
258-
compilationFeatures = @compilationFeatures(true)
259-
cmd = cmd
260-
.concat cargoArgs
261-
.concat ['-j', @jobsNumber]
262-
cmd = cmd.concat compilationFeatures if compilationFeatures
263-
cmd = cmd.concat ['--manifest-path', cargoManifestPath]
264-
[cargoManifestPath, cmd]
129+
@decideErrorMode(curDir, 'cargo').then (mode) =>
130+
mode.buildArguments(this, cargoManifestPath).then (cmd) =>
131+
[cmd, mode]
265132

266133
compilationFeatures: (cargo) =>
267134
if @specifiedFeatures.length > 0
@@ -273,19 +140,32 @@ class LinterRust
273140
result.push ['--cfg', "feature=\"#{f}\""]
274141
result
275142

276-
ableToJSONErrors: (curDir) =>
143+
decideErrorMode: (curDir, commandMode) =>
277144
# current dir is set to handle overrides
278-
279145
sb_exec.exec(@rustcPath, ['--version'], {stream: 'stdout', cwd: curDir, stdio: 'pipe'}).then (stdout) =>
280-
console.log stdout
281146
try
282147
match = XRegExp.exec(stdout, @patternRustcVersion)
283-
if match and match.nightly and match.date > '2016-08-08'
284-
true
285-
else if match and not match.nightly and semver.gte(match.version, '1.12.0')
286-
true
148+
if match
149+
nightlyWithJSON = match.nightly and match.date > '2016-08-08'
150+
stableWithJSON = not match.nightly and semver.gte(match.version, '1.12.0')
151+
canUseIntermediateJSON = nightlyWithJSON or stableWithJSON
152+
switch commandMode
153+
when 'cargo'
154+
canUseProperCargoJSON = match.nightly and match.date > '2016-10-10'
155+
if canUseProperCargoJSON
156+
errorModes.JSON_CARGO
157+
# this mode is used only through August till October, 2016
158+
else if canUseIntermediateJSON
159+
errorModes.FLAGS_JSON_CARGO
160+
else
161+
errorModes.OLD_CARGO
162+
when 'rustc'
163+
if canUseIntermediateJSON
164+
errorModes.JSON_RUSTC
165+
else
166+
errorModes.OLD_RUSTC
287167
else
288-
false
168+
throw 'rustc returned inapplicable result: ' + stdout
289169

290170
locateCargo: (curDir) =>
291171
root_dir = if /^win/.test process.platform then /^.:\\$/ else /^\/$/
@@ -296,24 +176,4 @@ class LinterRust
296176
directory = path.resolve path.join(directory, '..')
297177
return false
298178

299-
buildCargoPath: (cargoPath) =>
300-
@usingMultitoolForClippy().then (canUseMultirust) =>
301-
if @cargoCommand == 'clippy' and canUseMultirust.result
302-
[canUseMultirust.tool, 'run', 'nightly', 'cargo']
303-
else
304-
[cargoPath]
305-
306-
usingMultitoolForClippy: () =>
307-
# Try to use rustup
308-
sb_exec.exec 'rustup', ['--version'], {ignoreExitCode: true}
309-
.then ->
310-
result: true, tool: 'rustup'
311-
.catch ->
312-
# Try to use odler multirust at least
313-
sb_exec.exec 'multirust', ['--version'], {ignoreExitCode: true}
314-
.then ->
315-
result: true, tool: 'multirust'
316-
.catch ->
317-
result: false
318-
319179
module.exports = LinterRust

0 commit comments

Comments
 (0)