diff --git a/lib/init.coffee b/lib/init.coffee index b680b1c..822fcd3 100644 --- a/lib/init.coffee +++ b/lib/init.coffee @@ -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: -> diff --git a/lib/linter-rust.coffee b/lib/linter-rust.coffee index 06575fd..9a7ffcf 100644 --- a/lib/linter-rust.coffee +++ b/lib/linter-rust.coffee @@ -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('(?[^\n\r]+):(?\\d+):(?\\d+):\\s*\ - (?\\d+):(?\\d+)\\s+\ - ((?error|fatal error)|(?warning)|(?note|help)):\\s+\ - (?.+?)[\n\r]+($|(?=[^\n\r]+:\\d+))', 's') patternRustcVersion: XRegExp('rustc (?1.\\d+.\\d+)(?:(?:-(?nightly)|(?:[^\\s]+))? \ \\((?:[^\\s]+) (?\\d{4}-\\d{2}-\\d{2})\\))?') cargoDependencyDir: "target/debug/deps" @@ -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 @@ -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 /^\/$/ @@ -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 diff --git a/lib/mode.coffee b/lib/mode.coffee new file mode 100644 index 0000000..eb5a3da --- /dev/null +++ b/lib/mode.coffee @@ -0,0 +1,243 @@ +XRegExp = require 'xregexp' +path = require 'path' +atom_linter = require 'atom-linter' + +pattern = XRegExp('(?[^\n\r]+):(?\\d+):(?\\d+):\\s*\ + (?\\d+):(?\\d+)\\s+\ + ((?error|fatal error)|(?warning)|(?note|help)):\\s+\ + (?.+?)[\n\r]+($|(?=[^\n\r]+:\\d+))', 's') + +parseOldMessages = (output, {disabledWarnings, textEditor}) -> + elements = [] + XRegExp.forEach output, pattern, (match) -> + range = if match.from_col == match.to_col and match.from_line == match.to_line + atom_linter.rangeFromLineNumber(textEditor, Number.parseInt(match.from_line, 10) - 1, Number.parseInt(match.from_col, 10) - 1) + else + [ + [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, disabledWarnings + +parseJsonMessages = (messages, {disabledWarnings}) -> + elements = [] + for input in messages + continue unless input and 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.level == '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, disabledWarnings + +parseJsonOutput = (output, {disabledWarnings, additionalFilter} ) -> + results = output.split('\n').map (message) -> + message = message.trim() + if message.startsWith '{' + json = JSON.parse message + if additionalFilter? + additionalFilter(json) + else + json + .filter (m) -> m? + parseJsonMessages results, {disabledWarnings} + +buildMessages = (elements, disabledWarnings) -> + 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 + 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 + +buildRustcArguments = (linter, paths) -> + [editingFile, cargoManifestPath] = paths + Promise.resolve().then () => + rustcArgs = switch linter.rustcBuildTest + when true then ['--cfg', 'test', '-Z', 'no-trans', '--color', 'never'] + else ['-Z', 'no-trans', '--color', 'never'] + cmd = [linter.rustcPath] + .concat rustcArgs + if cargoManifestPath + cmd.push '-L' + cmd.push path.join path.dirname(cargoManifestPath), linter.cargoDependencyDir + compilationFeatures = linter.compilationFeatures(false) + cmd = cmd.concat compilationFeatures if compilationFeatures + cmd = cmd.concat [editingFile] + [editingFile, cmd] + +cachedUsingMultitoolForClippy = null + +buildCargoArguments = (linter, cargoManifestPath) -> + buildCargoPath = (cargoPath, cargoCommand) -> + # the result is cached to avoid delays + if cachedUsingMultitoolForClippy? and linter.allowedToCacheVersions + Promise.resolve().then () => + cachedUsingMultitoolForClippy + else + # Decide if should use older multirust or newer rustup + usingMultitoolForClippy = + atom_linter.exec 'rustup', ['--version'], {ignoreExitCode: true} + .then -> + result: true, tool: 'rustup' + .catch -> + # Try to use older multirust at least + atom_linter.exec 'multirust', ['--version'], {ignoreExitCode: true} + .then -> + result: true, tool: 'multirust' + .catch -> + result: false + usingMultitoolForClippy.then (canUseMultirust) -> + if cargoCommand == 'clippy' and canUseMultirust.result + [canUseMultirust.tool, 'run', 'nightly', 'cargo'] + else + [cargoPath] + .then (cached) => + cachedUsingMultitoolForClippy = cached + cached + + cargoArgs = switch linter.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'] + + compilationFeatures = linter.compilationFeatures(true) + buildCargoPath(linter.cargoPath, linter.cargoCommand).then (cmd) -> + cmd = cmd + .concat cargoArgs + .concat ['-j', linter.jobsNumber] + cmd = cmd.concat compilationFeatures if compilationFeatures + cmd = cmd.concat ['--manifest-path', cargoManifestPath] + [cargoManifestPath, cmd] + +# These define the behabiour of each error mode linter-rust has +errorModes = + JSON_RUSTC: + neededOutput: (stdout, stderr) -> + stderr + + parse: (output, options) => + parseJsonOutput output, options + + buildArguments: (linter, file) -> + buildRustcArguments(linter, file).then (cmd_res) -> + [file, cmd] = cmd_res + cmd = cmd.concat ['--error-format=json'] + [file, cmd] + + JSON_CARGO: + neededOutput: (stdout, stderr) -> + stdout + + parse: (output, options) -> + options.additionalFilter = (json) -> + if json? and json.reason == "compiler-message" + json.message + parseJsonOutput output, options + + buildArguments: (linter, file) -> + buildCargoArguments(linter, file).then (cmd_res) -> + [file, cmd] = cmd_res + cmd = cmd.concat ['--message-format', 'json'] + [file, cmd] + + FLAGS_JSON_CARGO: + neededOutput: (stdout, stderr) -> + stderr + + parse: parseJsonOutput + + buildArguments: buildCargoArguments + + OLD_RUSTC: + neededOutput: (stdout, stderr) -> + stderr + + parse: parseOldMessages + + buildArguments: buildRustcArguments + + OLD_CARGO: + neededOutput: (stdout, stderr) -> + stderr + + parse: parseOldMessages + + buildArguments: buildCargoArguments + +module.exports = errorModes diff --git a/package.json b/package.json index d41f168..b209a19 100644 --- a/package.json +++ b/package.json @@ -16,9 +16,9 @@ } }, "dependencies": { + "atom-linter": "^8.0.0", "atom-package-deps": "^4.3.0", "semver": "^5.3.0", - "sb-exec": "^3.1.0", "xregexp": "~3.1.0" }, "package-deps": [ diff --git a/spec/parse-spec.coffee b/spec/parse-spec.coffee index 180a807..c3be7cc 100644 --- a/spec/parse-spec.coffee +++ b/spec/parse-spec.coffee @@ -1,13 +1,14 @@ +errorModes = require '../lib/mode' LinterRust = require '../lib/linter-rust' linter = new LinterRust() -describe "LinterRust::parse", -> +describe "errorModes::OLD_RUSTC::parse", -> it "should return 0 messages for an empty string", -> - expect(linter.parse('')).toEqual([]) + expect(errorModes.OLD_RUSTC.parse('', {})).toEqual([]) it "should properly parse one line error message", -> - expect(linter.parse('my/awesome file.rs:1:2: 3:4 error: my awesome text\n')) + expect(errorModes.OLD_RUSTC.parse('my/awesome file.rs:1:2: 3:4 error: my awesome text\n', {})) .toEqual([{ type: 'Error' text: 'my awesome text' @@ -16,7 +17,7 @@ describe "LinterRust::parse", -> }]) it "should properly parse one line warning message", -> - expect(linter.parse('foo:33:44: 22:33 warning: äüö<>\n')) + expect(errorModes.OLD_RUSTC.parse('foo:33:44: 22:33 warning: äüö<>\n', {})) .toEqual([{ type: 'Warning', text: 'äüö<>' @@ -25,47 +26,50 @@ describe "LinterRust::parse", -> }]) it "should return messages with a range of at least one character", -> - expect(linter.parse('foo:1:1: 1:1 error: text\n')) + editor = atom.workspace.buildTextEditor() + editor.setText 'fn main() {\nprintln!("Hi test");}\n' + # expect(editor.getPath()).toContain 'c.coffee' + expect(errorModes.OLD_RUSTC.parse('foo:1:1: 1:1 error: text\n', {textEditor: editor})) .toEqual([{ type: 'Error' text: 'text' filePath: 'foo' - range: [[0, 0], [0, 1]] + range: [[0, 0], [0, 2]] }]) - expect(linter.parse('foo:1:1: 2:1 error: text\n')) + expect(errorModes.OLD_RUSTC.parse('foo:2:1: 2:1 error: text\n', {textEditor: editor})) .toEqual([{ type: 'Error' text: 'text' filePath: 'foo' - range: [[0, 0], [1, 1]] + range: [[1, 0], [1, 7]] }]) it "should properly parse multiline messages", -> - expect(linter.parse('bar:1:2: 3:4 error: line one\n\ - two\n')) + expect(errorModes.OLD_RUSTC.parse('bar:1:2: 3:4 error: line one\n\ + two\n', {})) .toEqual([ { type: 'Error', text: 'line one\ntwo', filePath: 'bar', range: [[0, 1], [2, 3]] } ]) - expect(linter.parse('bar:1:2: 3:4 error: line one\n\ + expect(errorModes.OLD_RUSTC.parse('bar:1:2: 3:4 error: line one\n\ two\n\ - foo:1:1: 1:2 warning: simple line\n')) + foo:1:1: 1:2 warning: simple line\n', {})) .toEqual([ { type: 'Error', text: 'line one\ntwo', filePath: 'bar', range: [[0, 1], [2, 3]] }, { type: 'Warning', text: 'simple line', filePath: 'foo', range: [[0, 0], [0, 1]] } ]) - expect(linter.parse('bar:1:2: 3:4 error: line one\n\ + expect(errorModes.OLD_RUSTC.parse('bar:1:2: 3:4 error: line one\n\ two\n\ three\n\ - foo:1 shouldnt match')) + foo:1 shouldnt match', {})) .toEqual([ { type: 'Error', text: 'line one\ntwo\nthree', filePath: 'bar', range: [[0, 1], [2, 3]] } ]) it "should also cope with windows line breaks", -> - expect(linter.parse('a:1:2: 3:4 error: a\r\nb\n')[0].text) + expect(errorModes.OLD_RUSTC.parse('a:1:2: 3:4 error: a\r\nb\n', {})[0].text) .toEqual('a\r\nb') - multi = linter.parse('a:1:2: 3:4 error: a\n\rb\n\rx:1:2: 3:4 error: asd\r\n') + multi = errorModes.OLD_RUSTC.parse('a:1:2: 3:4 error: a\n\rb\n\rx:1:2: 3:4 error: asd\r\n', {}) expect(multi[0].text).toEqual('a\n\rb') expect(multi[1].text).toEqual('asd')