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

Commit

Permalink
Added a proper support for cargo --message-format json (#84)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
White-Oak authored Oct 24, 2016
1 parent c0bc0ae commit 7da1096
Show file tree
Hide file tree
Showing 5 changed files with 377 additions and 253 deletions.
4 changes: 4 additions & 0 deletions lib/init.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ module.exports =
type: 'boolean'
default: false
description: "Lint test code, when using `rustc`"
allowedToCacheVersions:
type: 'boolean'
default: true
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."


activate: ->
Expand Down
345 changes: 109 additions & 236 deletions lib/linter-rust.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,11 @@ fs = require 'fs'
path = require 'path'
XRegExp = require 'xregexp'
semver = require 'semver'
sb_exec = require 'sb-exec'
{CompositeDisposable} = require 'atom'

atom_linter = require 'atom-linter'
errorModes = require './mode'

class LinterRust
pattern: XRegExp('(?<file>[^\n\r]+):(?<from_line>\\d+):(?<from_col>\\d+):\\s*\
(?<to_line>\\d+):(?<to_col>\\d+)\\s+\
((?<error>error|fatal error)|(?<warning>warning)|(?<info>note|help)):\\s+\
(?<message>.+?)[\n\r]+($|(?=[^\n\r]+:\\d+))', 's')
patternRustcVersion: XRegExp('rustc (?<version>1.\\d+.\\d+)(?:(?:-(?<nightly>nightly)|(?:[^\\s]+))? \
\\((?:[^\\s]+) (?<date>\\d{4}-\\d{2}-\\d{2})\\))?')
cargoDependencyDir: "target/debug/deps"
Expand Down Expand Up @@ -55,213 +51,88 @@ class LinterRust
(specifiedFeatures) =>
@specifiedFeatures = specifiedFeatures

@subscriptions.add atom.config.observe 'linter-rust.allowedToCacheVersions',
(allowedToCacheVersions) =>
@allowedToCacheVersions = allowedToCacheVersions

destroy: ->
do @subscriptions.dispose

lint: (textEditor) =>
curDir = path.dirname textEditor.getPath()
@ableToJSONErrors(curDir).then (ableToJSONErrors) =>
@initCmd(textEditor.getPath(), ableToJSONErrors).then (result) =>
[file, cmd] = result
env = JSON.parse JSON.stringify process.env
curDir = path.dirname file
cwd = curDir
command = cmd[0]
args = cmd.slice 1
env.PATH = path.dirname(cmd[0]) + path.delimiter + env.PATH

if ableToJSONErrors
if !env.RUSTFLAGS? or !(env.RUSTFLAGS.indexOf('--error-format=json') >= 0)
additional = if env.RUSTFLAGS? then ' ' + env.RUSTFLAGS else ''
env.RUSTFLAGS = '--error-format=json' + additional
sb_exec.exec(command, args, {env: env, cwd: cwd, stream: 'both'})
.then (result) =>
{stdout, stderr, exitCode} = result
# first, check if an output says specified features are invalid
if stderr.indexOf('does not have these features') >= 0
atom.notifications.addError "Invalid specified features",
detail: "#{stderr}"
dismissable: true
[]
# then, if exit code looks okay, process an output
else if exitCode is 101 or exitCode is 0
# in dev mode show message boxes with output
showDevModeWarning = (stream, message) ->
atom.notifications.addWarning "Output from #{stream} while linting",
detail: "#{message}"
description: "This is shown because Atom is running in dev-mode and probably not an actual error"
dismissable: true
if do atom.inDevMode
showDevModeWarning('stderr', stderr) if stderr
showDevModeWarning('stdout', stdout) if stdout

# call a needed parser
messages = unless ableToJSONErrors
@parse stderr
else
@parseJSON stderr

# correct file paths
messages.forEach (message) ->
if !(path.isAbsolute message.filePath)
message.filePath = path.join curDir, message.filePath
messages
else
# whoops, we're in trouble -- let's output as much as we can
atom.notifications.addError "Failed to run #{command} with exit code #{exitCode}",
detail: "with args:\n #{args.join(' ')}\nSee console for more information"
dismissable: true
console.log "stdout:"
console.log stdout
console.log "stderr:"
console.log stderr
[]
.catch (error) ->
console.log error
atom.notifications.addError "Failed to run #{command}",
detail: "#{error.message}"
@initCmd(textEditor.getPath()).then (result) =>
[cmd_res, errorMode] = result
[file, cmd] = cmd_res
env = JSON.parse JSON.stringify process.env
curDir = path.dirname file
cwd = curDir
command = cmd[0]
args = cmd.slice 1
env.PATH = path.dirname(cmd[0]) + path.delimiter + env.PATH

# we set flags only for intermediate json support
if errorMode == errorModes.FLAGS_JSON_CARGO
if !env.RUSTFLAGS? or !(env.RUSTFLAGS.indexOf('--error-format=json') >= 0)
additional = if env.RUSTFLAGS? then ' ' + env.RUSTFLAGS else ''
env.RUSTFLAGS = '--error-format=json' + additional

atom_linter.exec(command, args, {env: env, cwd: cwd, stream: 'both'})
.then (result) =>
{stdout, stderr, exitCode} = result
# first, check if an output says specified features are invalid
if stderr.indexOf('does not have these features') >= 0
atom.notifications.addError "Invalid specified features",
detail: "#{stderr}"
dismissable: true
[]

parseJSON: (output) =>
elements = []
results = output.split '\n'
for result in results
if result.startsWith '{'
input = JSON.parse result.trim()
continue unless input.spans
primary_span = input.spans.find (span) -> span.is_primary
continue unless primary_span
range = [
[primary_span.line_start - 1, primary_span.column_start - 1],
[primary_span.line_end - 1, primary_span.column_end - 1]
]
input.level = 'error' if input == 'fatal error'
element =
type: input.level
message: input.message
file: primary_span.file_name
range: range
children: input.children
for span in input.spans
unless span.is_primary
element.children.push
message: span.label
range: [
[span.line_start - 1, span.column_start - 1],
[span.line_end - 1, span.column_end - 1]
]
elements.push element
@buildMessages(elements)

parse: (output) =>
elements = []
XRegExp.forEach output, @pattern, (match) ->
if match.from_col == match.to_col
match.to_col = parseInt(match.to_col) + 1
range = [
[match.from_line - 1, match.from_col - 1],
[match.to_line - 1, match.to_col - 1]
]
level = if match.error then 'error'
else if match.warning then 'warning'
else if match.info then 'info'
else if match.trace then 'trace'
else if match.note then 'note'
element =
type: level
message: match.message
file: match.file
range: range
elements.push element
@buildMessages elements

buildMessages: (elements) =>
messages = []
lastMessage = null
for element in elements
switch element.type
when 'info', 'trace', 'note'
# Add only if there is a last message
if lastMessage
lastMessage.trace or= []
lastMessage.trace.push
type: "Trace"
text: element.message
filePath: element.file
range: element.range
when 'warning'
# If the message is warning and user enabled disabling warnings
# Check if this warning is disabled
if @disabledWarnings and @disabledWarnings.length > 0
messageIsDisabledLint = false
for disabledWarning in @disabledWarnings
# Find a disabled lint in warning message
if element.message.indexOf(disabledWarning) >= 0
messageIsDisabledLint = true
lastMessage = null
break
if not messageIsDisabledLint
lastMessage = @constructMessage "Warning", element
messages.push lastMessage
# then, if exit code looks okay, process an output
else if exitCode is 101 or exitCode is 0
# in dev mode show message boxes with output
showDevModeWarning = (stream, message) ->
atom.notifications.addWarning "Output from #{stream} while linting",
detail: "#{message}"
description: "This is shown because Atom is running in dev-mode and probably not an actual error"
dismissable: true
if do atom.inDevMode
showDevModeWarning('stderr', stderr) if stderr
showDevModeWarning('stdout', stdout) if stdout

# call a needed parser
output = errorMode.neededOutput(stdout, stderr)
messages = errorMode.parse output, {@disabledWarnings, textEditor}

# correct file paths
messages.forEach (message) ->
if !(path.isAbsolute message.filePath)
message.filePath = path.join curDir, message.filePath
messages
else
lastMessage = @constructMessage "Warning" , element
messages.push lastMessage
when 'error', 'fatal error'
lastMessage = @constructMessage "Error", element
messages.push lastMessage
return messages

constructMessage: (type, element) ->
message =
type: type
text: element.message
filePath: element.file
range: element.range
# children exists only in JSON messages
if element.children
message.trace = []
for children in element.children
message.trace.push
type: "Trace"
text: children.message
filePath: element.file
range: children.range or element.range
message

initCmd: (editingFile, ableToJSONErrors) =>
rustcArgs = switch @rustcBuildTest
when true then ['--cfg', 'test', '-Z', 'no-trans', '--color', 'never']
else ['-Z', 'no-trans', '--color', 'never']
cargoArgs = switch @cargoCommand
when 'check' then ['check']
when 'test' then ['test', '--no-run']
when 'rustc' then ['rustc', '-Zno-trans', '--color', 'never']
when 'clippy' then ['clippy']
else ['build']

cargoManifestPath = @locateCargo path.dirname editingFile
# whoops, we're in trouble -- let's output as much as we can
atom.notifications.addError "Failed to run #{command} with exit code #{exitCode}",
detail: "with args:\n #{args.join(' ')}\nSee console for more information"
dismissable: true
console.log "stdout:"
console.log stdout
console.log "stderr:"
console.log stderr
[]
.catch (error) ->
console.log error
atom.notifications.addError "Failed to run #{command}",
detail: "#{error.message}"
dismissable: true
[]

initCmd: (editingFile) =>
curDir = path.dirname editingFile
cargoManifestPath = @locateCargo curDir
if not @useCargo or not cargoManifestPath
Promise.resolve().then () =>
cmd = [@rustcPath]
.concat rustcArgs
if cargoManifestPath
cmd.push '-L'
cmd.push path.join path.dirname(cargoManifestPath), @cargoDependencyDir
compilationFeatures = @compilationFeatures(false)
cmd = cmd.concat compilationFeatures if compilationFeatures
cmd = cmd.concat [editingFile]
cmd = cmd.concat ['--error-format=json'] if ableToJSONErrors
[editingFile, cmd]
@decideErrorMode(curDir, 'rustc').then (mode) =>
mode.buildArguments(this, [editingFile, cargoManifestPath]).then (cmd) =>
[cmd, mode]
else
@buildCargoPath(@cargoPath).then (cmd) =>
compilationFeatures = @compilationFeatures(true)
cmd = cmd
.concat cargoArgs
.concat ['-j', @jobsNumber]
cmd = cmd.concat compilationFeatures if compilationFeatures
cmd = cmd.concat ['--manifest-path', cargoManifestPath]
[cargoManifestPath, cmd]
@decideErrorMode(curDir, 'cargo').then (mode) =>
mode.buildArguments(this, cargoManifestPath).then (cmd) =>
[cmd, mode]

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

ableToJSONErrors: (curDir) =>
# current dir is set to handle overrides
decideErrorMode: (curDir, commandMode) =>
# error mode is cached to avoid delays
if @cachedErrorMode? and @allowedToCacheVersions
Promise.resolve().then () =>
@cachedErrorMode
else
# current dir is set to handle overrides
atom_linter.exec(@rustcPath, ['--version'], {cwd: curDir}).then (stdout) =>
try
match = XRegExp.exec(stdout, @patternRustcVersion)
if match
nightlyWithJSON = match.nightly and match.date > '2016-08-08'
stableWithJSON = not match.nightly and semver.gte(match.version, '1.12.0')
canUseIntermediateJSON = nightlyWithJSON or stableWithJSON
switch commandMode
when 'cargo'
canUseProperCargoJSON = match.nightly and match.date >= '2016-10-10'
if canUseProperCargoJSON
errorModes.JSON_CARGO
# this mode is used only through August till October, 2016
else if canUseIntermediateJSON
errorModes.FLAGS_JSON_CARGO
else
errorModes.OLD_CARGO
when 'rustc'
if canUseIntermediateJSON
errorModes.JSON_RUSTC
else
errorModes.OLD_RUSTC
else
throw 'rustc returned unexpected result: ' + stdout
.then (result) =>
@cachedErrorMode = result
result

sb_exec.exec(@rustcPath, ['--version'], {stream: 'stdout', cwd: curDir, stdio: 'pipe'}).then (stdout) =>
console.log stdout
try
match = XRegExp.exec(stdout, @patternRustcVersion)
if match and match.nightly and match.date > '2016-08-08'
true
else if match and not match.nightly and semver.gte(match.version, '1.12.0')
true
else
false

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

buildCargoPath: (cargoPath) =>
@usingMultitoolForClippy().then (canUseMultirust) =>
if @cargoCommand == 'clippy' and canUseMultirust.result
[canUseMultirust.tool, 'run', 'nightly', 'cargo']
else
[cargoPath]

usingMultitoolForClippy: () =>
# Try to use rustup
sb_exec.exec 'rustup', ['--version'], {ignoreExitCode: true}
.then ->
result: true, tool: 'rustup'
.catch ->
# Try to use odler multirust at least
sb_exec.exec 'multirust', ['--version'], {ignoreExitCode: true}
.then ->
result: true, tool: 'multirust'
.catch ->
result: false

module.exports = LinterRust
Loading

0 comments on commit 7da1096

Please sign in to comment.