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

Commit 7da1096

Browse files
authored
Added a proper support for cargo --message-format json (#84)
* 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. * Fixed tests * Fixed code and tests * Fixed json messages being filtered out * Removed cahing heuristic and added an option to disallow caching
1 parent c0bc0ae commit 7da1096

File tree

5 files changed

+377
-253
lines changed

5 files changed

+377
-253
lines changed

lib/init.coffee

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,10 @@ module.exports =
4949
type: 'boolean'
5050
default: false
5151
description: "Lint test code, when using `rustc`"
52+
allowedToCacheVersions:
53+
type: 'boolean'
54+
default: true
55+
description: "Uncheck this if you need to change toolchains during one Atom session. Otherwise toolchains' versions are saved for an entire Atom session to increase performance."
5256

5357

5458
activate: ->

lib/linter-rust.coffee

Lines changed: 109 additions & 236 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,11 @@ fs = require 'fs'
22
path = require 'path'
33
XRegExp = require 'xregexp'
44
semver = require 'semver'
5-
sb_exec = require 'sb-exec'
65
{CompositeDisposable} = require 'atom'
7-
6+
atom_linter = require 'atom-linter'
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"
@@ -55,213 +51,88 @@ class LinterRust
5551
(specifiedFeatures) =>
5652
@specifiedFeatures = specifiedFeatures
5753

54+
@subscriptions.add atom.config.observe 'linter-rust.allowedToCacheVersions',
55+
(allowedToCacheVersions) =>
56+
@allowedToCacheVersions = allowedToCacheVersions
57+
5858
destroy: ->
5959
do @subscriptions.dispose
6060

6161
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}"
62+
@initCmd(textEditor.getPath()).then (result) =>
63+
[cmd_res, errorMode] = result
64+
[file, cmd] = cmd_res
65+
env = JSON.parse JSON.stringify process.env
66+
curDir = path.dirname file
67+
cwd = curDir
68+
command = cmd[0]
69+
args = cmd.slice 1
70+
env.PATH = path.dirname(cmd[0]) + path.delimiter + env.PATH
71+
72+
# we set flags only for intermediate json support
73+
if errorMode == errorModes.FLAGS_JSON_CARGO
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+
78+
atom_linter.exec(command, args, {env: env, cwd: cwd, stream: 'both'})
79+
.then (result) =>
80+
{stdout, stderr, exitCode} = result
81+
# first, check if an output says specified features are invalid
82+
if stderr.indexOf('does not have these features') >= 0
83+
atom.notifications.addError "Invalid specified features",
84+
detail: "#{stderr}"
12385
dismissable: true
12486
[]
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
87+
# then, if exit code looks okay, process an output
88+
else if exitCode is 101 or exitCode is 0
89+
# in dev mode show message boxes with output
90+
showDevModeWarning = (stream, message) ->
91+
atom.notifications.addWarning "Output from #{stream} while linting",
92+
detail: "#{message}"
93+
description: "This is shown because Atom is running in dev-mode and probably not an actual error"
94+
dismissable: true
95+
if do atom.inDevMode
96+
showDevModeWarning('stderr', stderr) if stderr
97+
showDevModeWarning('stdout', stdout) if stdout
98+
99+
# call a needed parser
100+
output = errorMode.neededOutput(stdout, stderr)
101+
messages = errorMode.parse output, {@disabledWarnings, textEditor}
102+
103+
# correct file paths
104+
messages.forEach (message) ->
105+
if !(path.isAbsolute message.filePath)
106+
message.filePath = path.join curDir, message.filePath
107+
messages
207108
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-
243-
cargoManifestPath = @locateCargo path.dirname editingFile
109+
# whoops, we're in trouble -- let's output as much as we can
110+
atom.notifications.addError "Failed to run #{command} with exit code #{exitCode}",
111+
detail: "with args:\n #{args.join(' ')}\nSee console for more information"
112+
dismissable: true
113+
console.log "stdout:"
114+
console.log stdout
115+
console.log "stderr:"
116+
console.log stderr
117+
[]
118+
.catch (error) ->
119+
console.log error
120+
atom.notifications.addError "Failed to run #{command}",
121+
detail: "#{error.message}"
122+
dismissable: true
123+
[]
124+
125+
initCmd: (editingFile) =>
126+
curDir = path.dirname editingFile
127+
cargoManifestPath = @locateCargo curDir
244128
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]
129+
@decideErrorMode(curDir, 'rustc').then (mode) =>
130+
mode.buildArguments(this, [editingFile, cargoManifestPath]).then (cmd) =>
131+
[cmd, mode]
256132
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]
133+
@decideErrorMode(curDir, 'cargo').then (mode) =>
134+
mode.buildArguments(this, cargoManifestPath).then (cmd) =>
135+
[cmd, mode]
265136

266137
compilationFeatures: (cargo) =>
267138
if @specifiedFeatures.length > 0
@@ -273,19 +144,41 @@ class LinterRust
273144
result.push ['--cfg', "feature=\"#{f}\""]
274145
result
275146

276-
ableToJSONErrors: (curDir) =>
277-
# current dir is set to handle overrides
147+
decideErrorMode: (curDir, commandMode) =>
148+
# error mode is cached to avoid delays
149+
if @cachedErrorMode? and @allowedToCacheVersions
150+
Promise.resolve().then () =>
151+
@cachedErrorMode
152+
else
153+
# current dir is set to handle overrides
154+
atom_linter.exec(@rustcPath, ['--version'], {cwd: curDir}).then (stdout) =>
155+
try
156+
match = XRegExp.exec(stdout, @patternRustcVersion)
157+
if match
158+
nightlyWithJSON = match.nightly and match.date > '2016-08-08'
159+
stableWithJSON = not match.nightly and semver.gte(match.version, '1.12.0')
160+
canUseIntermediateJSON = nightlyWithJSON or stableWithJSON
161+
switch commandMode
162+
when 'cargo'
163+
canUseProperCargoJSON = match.nightly and match.date >= '2016-10-10'
164+
if canUseProperCargoJSON
165+
errorModes.JSON_CARGO
166+
# this mode is used only through August till October, 2016
167+
else if canUseIntermediateJSON
168+
errorModes.FLAGS_JSON_CARGO
169+
else
170+
errorModes.OLD_CARGO
171+
when 'rustc'
172+
if canUseIntermediateJSON
173+
errorModes.JSON_RUSTC
174+
else
175+
errorModes.OLD_RUSTC
176+
else
177+
throw 'rustc returned unexpected result: ' + stdout
178+
.then (result) =>
179+
@cachedErrorMode = result
180+
result
278181

279-
sb_exec.exec(@rustcPath, ['--version'], {stream: 'stdout', cwd: curDir, stdio: 'pipe'}).then (stdout) =>
280-
console.log stdout
281-
try
282-
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
287-
else
288-
false
289182

290183
locateCargo: (curDir) =>
291184
root_dir = if /^win/.test process.platform then /^.:\\$/ else /^\/$/
@@ -296,24 +189,4 @@ class LinterRust
296189
directory = path.resolve path.join(directory, '..')
297190
return false
298191

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-
319192
module.exports = LinterRust

0 commit comments

Comments
 (0)