Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JSON errors parsing enhancement #62

Merged
merged 8 commits into from
Sep 19, 2016
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
229 changes: 33 additions & 196 deletions lib/cargo.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ if (atom.config.get('build-cargo.cargoClippy')) {
atom.config.unset('build-cargo.showBacktrace');
atom.config.unset('build-cargo.cargoCheck');
atom.config.unset('build-cargo.cargoClippy');
atom.config.unset('build-cargo.jsonErrors');

export const config = {
cargoPath: {
Expand Down Expand Up @@ -46,11 +47,11 @@ export const config = {
enum: [ 'Off', 'Compact', 'Full' ],
order: 4
},
jsonErrors: {
title: 'Use json errors format',
description: 'Instead of using regex to parse the human readable output (requires rustc version 1.7)\nNote: this is an unstable feature of the Rust compiler and prone to change and break frequently.',
jsonErrorFormat: {
title: 'Use JSON error format',
description: 'Use JSON error format instead of human readable output. When switched off, Linter is not used to display compiler messages.',
type: 'boolean',
default: false,
default: true,
order: 5
},
openDocs: {
Expand Down Expand Up @@ -98,183 +99,15 @@ export function provideBuilder() {

settings() {
const path = require('path');

// Constants to detect links to Rust's source code and make them followable
const unixRustSrcPrefix = '../src/';
const windowsRustSrcPrefix = '..\\src\\';
const err = require('./errors');
const jsonParser = require('./json-parser');
const panicParser = require('./panic-parser');

let buildWorkDir; // The last build workding directory (might differ from the project root for multi-crate projects)
let panicsCounter = 0; // Counts all panics
const panicsLimit = 10; // Max number of panics to show at once

function level2severity(level) {
switch (level) {
case 'warning': return 'warning';
case 'error': return 'error';
case 'note': return 'info';
case 'help': return 'info';
default: return 'error';
}
}

function level2type(level) {
return level.charAt(0).toUpperCase() + level.slice(1);
}

// Checks if the given file path returned by rustc or cargo points to the Rust source code
function isRustSourceLink(filePath) {
return filePath.startsWith(unixRustSrcPrefix) || filePath.startsWith(windowsRustSrcPrefix);
}

function parseJsonSpan(span, msg) {
if (span && span.file_name && !span.file_name.startsWith('<')) {
msg.file = span.file_name;
msg.line = span.line_start;
msg.line_end = span.line_end;
msg.col = span.column_start;
msg.col_end = span.column_end;
return true;
} else if (span.expansion) {
return parseJsonSpan(span.expansion.span, msg);
}
return false;
}

function parseJsonSpans(jsonObj, msg) {
if (jsonObj.spans) {
jsonObj.spans.forEach(span => {
if (parseJsonSpan(span, msg)) {
return;
}
});
}
}

// Parses a compile message in json format
function parseJsonMessage(line, messages) {
const json = JSON.parse(line);
const msg = {
message: json.message,
type: level2type(json.level),
severity: level2severity(json.level),
trace: []
};
parseJsonSpans(json, msg);
json.children.forEach(child => {
const tr = {
message: child.message,
type: level2type(child.level),
severity: level2severity(child.level)
};
parseJsonSpans(child, tr);
msg.trace.push(tr);
});
if (json.code && json.code.explanation) {
msg.trace.push({
message: json.code.explanation,
type: 'Explanation',
severity: 'info'
});
}
if (msg.file) { // Root message without `file` is the summary, skip it
messages.push(msg);
}
}

// Shows panic info
function showPanic(panic) {
// Only add link if we have panic.filePath, otherwise it's an external link
atom.notifications.addError(
'A thread panicked at '
+ (panic.filePath ? '<a id="' + panic.id + '" href="#">' : '')
+ 'line ' + panic.line + ' in ' + panic.file
+ (panic.filePath ? '</a>' : ''), {
detail: panic.message,
stack: panic.stack,
dismissable: true
});
if (panic.filePath) {
const link = document.getElementById(panic.id);
if (link) {
link.panic = panic;
link.addEventListener('click', function (e) {
atom.workspace.open(e.target.panic.filePath, {
searchAllPanes: true,
initialLine: e.target.panic.line - 1
});
});
}
}
}

// Tries to parse a stack trace. Returns the quantity of actually parsed lines.
function tryParseStackTrace(lines, i, panic) {
let parsedQty = 0;
let line = lines[i];
if (line.substring(0, 16) === 'stack backtrace:') {
parsedQty += 1;
const panicLines = [];
for (let j = i + 1; j < lines.length; j++) {
line = lines[j];
const matchFunc = /^(\s+\d+):\s+0x[a-f0-9]+ - (?:(.+)::h[0-9a-f]+|(.+))$/g.exec(line);
if (matchFunc) {
// A line with a function call
if (atom.config.get('build-cargo.backtraceType') === 'Compact') {
line = matchFunc[1] + ': ' + (matchFunc[2] || matchFunc[3]);
}
panicLines.push(line);
} else {
const matchLink = /(at (.+):(\d+))$/g.exec(line);
if (matchLink) {
// A line with a file link
if (!panic.file && !isRustSourceLink(matchLink[2])) {
panic.file = matchLink[2]; // Found a link to our source code
panic.line = matchLink[3];
}
panicLines.push(' ' + matchLink[1]); // less leading spaces
} else {
// Stack trace has ended
break;
}
}
parsedQty += 1;
}
panic.stack = panicLines.join('\n');
}
return parsedQty;
}

// Tries to parse a panic and its stack trace. Returns the quantity of actually
// parsed lines.
function tryParsePanic(lines, i, show) {
const line = lines[i];
const match = /(thread '.+' panicked at '.+'), ([^\/][^\:]+):(\d+)/g.exec(line);
let parsedQty = 0;
if (match) {
parsedQty = 1;
const panic = {
id: 'build-cargo-panic-' + (++panicsCounter), // Unique panic ID
message: match[1],
file: isRustSourceLink(match[2]) ? undefined : match[2],
filePath: undefined,
line: parseInt(match[3], 10),
stack: undefined
};
parsedQty = 1 + tryParseStackTrace(lines, i + 1, panic);
if (panic.file) {
panic.filePath = path.isAbsolute(panic.file) ? panic.file : path.join(buildWorkDir, panic.file);
} else {
panic.file = match[2]; // We failed to find a link to our source code, use Rust's
}
if (show) {
showPanic(panic);
}
}
return parsedQty;
}

function matchFunction(output) {
const useJson = atom.config.get('build-cargo.jsonErrors');
const useJson = atom.config.get('build-cargo.jsonErrorFormat');
const messages = []; // resulting collection of high-level messages
let msg = null; // current high-level message (error, warning or panic)
let sub = null; // current submessage (note or help)
Expand All @@ -287,12 +120,12 @@ export function provideBuilder() {
line = line.replace(/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, '');
}
// Cargo final error messages start with 'error:', skip them
if (line === null || line === '' || line.substring(0, 6) === 'error:') {
if (line === null || line === '' || line.startsWith('error:')) {
msg = null;
sub = null;
} else if (useJson && line[0] === '{') {
// Parse a JSON block
parseJsonMessage(line, messages);
jsonParser.parseMessage(line, messages);
} else {
// Check for compilation messages
const match = /^(.+):(\d+):(\d+):(?: (\d+):(\d+))? (error|warning|help|note): (.*)/g.exec(line);
Expand All @@ -304,16 +137,16 @@ export function provideBuilder() {
let endCol = match[5];
const level = match[6];
const message = match[7];
if (level === 'error' || level === 'warning' || msg === null) {
if (level === 'error' || level === 'warning') {
msg = {
message: message,
file: filePath,
line: startLine,
line_end: endLine,
col: startCol,
col_end: endCol,
type: level2type(level),
severity: level2severity(level),
type: err.level2type(level),
severity: err.level2severity(level),
trace: []
};
messages.push(msg);
Expand All @@ -326,29 +159,31 @@ export function provideBuilder() {
startCol = undefined;
endLine = undefined;
endCol = undefined;
} else if (msg.file.startsWith('<')) {
} else if (msg && msg.file.startsWith('<')) {
// The root message has incorrect file link, use the one from the extended messsage
msg.file = filePath;
msg.line = startLine;
msg.line_end = endLine;
msg.col = startCol;
msg.col_end = endCol;
}
sub = {
message: message,
file: filePath,
line: startLine,
line_end: endLine,
col: startCol,
col_end: endCol,
type: level2type(level),
severity: level2severity(level)
};
msg.trace.push(sub);
if (msg) {
sub = {
message: message,
file: filePath,
line: startLine,
line_end: endLine,
col: startCol,
col_end: endCol,
type: err.level2type(level),
severity: err.level2severity(level)
};
msg.trace.push(sub);
}
}
} else {
// Check for panic
const parsedQty = tryParsePanic(lines, i, panicsN < panicsLimit);
const parsedQty = panicParser.tryParsePanic(lines, i, panicsN < panicsLimit, buildWorkDir);
if (parsedQty > 0) {
msg = null;
sub = null;
Expand All @@ -368,7 +203,9 @@ export function provideBuilder() {
} else if (hiddenPanicsN > 1) {
atom.notifications.addError(hiddenPanicsN + ' more panics are hidden', { dismissable: true });
}
return messages;
return messages.filter(function (m) {
return err.preprocessMessage(m, buildWorkDir);
});
}

// Checks if the given object represents the root of the project or file system
Expand Down Expand Up @@ -414,7 +251,7 @@ export function provideBuilder() {
// Common build command parameters
buildCfg.exec = atom.config.get('build-cargo.cargoPath');
buildCfg.env = {};
if (atom.config.get('build-cargo.jsonErrors')) {
if (atom.config.get('build-cargo.jsonErrorFormat')) {
buildCfg.env.RUSTFLAGS = '-Z unstable-options --error-format=json';
} else if (process.platform !== 'win32') {
buildCfg.env.TERM = 'xterm';
Expand Down
47 changes: 47 additions & 0 deletions lib/errors.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
'use babel';

//
// Utility functions for parsing errors
//

const notificationCfg = { dismissable: true };
const abortRegex = /aborting due to (\d+ )?previous error[s]?/;

const level2severity = (level) => {
switch (level) {
case 'warning': return 'warning';
case 'error': return 'error';
case 'note': return 'info';
case 'help': return 'info';
default: return 'error';
}
};

const level2type = (level) => {
return level.charAt(0).toUpperCase() + level.slice(1);
};

// Set location for special cases when the compiler doesn't provide it
function preprocessMessage(msg, buildWorkDir) {
if (msg.file) {
return true;
}
if (!abortRegex.test(msg.message)) { // This meta error is ignored
// Location is not provided for the message, so it cannot be added to Linter.
// Display it as a notification.
switch (msg.level) {
case 'info':
case 'note':
atom.notifications.addInfo(msg.message, notificationCfg);
break;
case 'warning':
atom.notifications.addWarning(msg.message, notificationCfg);
break;
default:
atom.notifications.addError(msg.message, notificationCfg);
}
}
return false;
}

export { level2severity, level2type, preprocessMessage };
Loading