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

Commit 7656da0

Browse files
authored
Merge pull request #197 from AtomLinter/arcanemagus/rewrite-parsing
Re-write the parsing engine
2 parents 518e85f + 44746b5 commit 7656da0

File tree

7 files changed

+187
-20
lines changed

7 files changed

+187
-20
lines changed

lib/main.js

Lines changed: 118 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,77 @@ import { dirname, extname } from 'path';
66

77
let helpers = null;
88
let clangFlags = null;
9-
const regex = /(.+):(\d+):(\d+):(?:{(\d+):(\d+)-(\d+):(\d+)}.*:)? ([\w \\-]+): (.*)/g;
9+
10+
const regex = new RegExp([
11+
'^(<stdin>|.+):', // Path, usually <stdin>
12+
'(\\d+):(\\d+):', // Base line and column
13+
'(?:({.+}):)?', // Range position(s), if present
14+
' ([\\w \\\\-]+):', // Message type
15+
' ([^[\\n\\r]+)', // The message
16+
'(?: \\[(.+)\\])?\\r?$', // -W flag, if any
17+
'(?:\\r?\\n^ .+$)+', // The visual caret diagnostics, necessary to include in output for fix-its
18+
'(?:\\r?\\n^fix-it:".+":', // Start of fix-it block
19+
'{(\\d+):(\\d+)-(\\d+):(\\d+)}:', // fix-it range
20+
'"(.+)"', // fix-it replacement text
21+
'$)?', // End of fix-it block
22+
].join(''), 'gm');
23+
24+
/**
25+
* Given a set of ranges in clangs format, determine the range encompasing all points
26+
* @param {String} ranges The raw range string to parse
27+
* @return {Range} An Atom Range object encompasing all given ranges
28+
*/
29+
const parseClangRanges = (ranges) => {
30+
const rangeRE = /{(\d+):(\d+)-(\d+):(\d+)}/g;
31+
let lineStart;
32+
let colStart;
33+
let lineEnd;
34+
let colEnd;
35+
36+
let match = rangeRE.exec(ranges);
37+
while (match !== null) {
38+
const rangeLineStart = Number.parseInt(match[1], 10) - 1;
39+
const rangeColStart = Number.parseInt(match[2], 10) - 1;
40+
const rangeLineEnd = Number.parseInt(match[3], 10) - 1;
41+
const rangeColEnd = Number.parseInt(match[4], 10) - 1;
42+
if (lineStart === undefined) {
43+
// First match
44+
lineStart = rangeLineStart;
45+
colStart = rangeColStart;
46+
lineEnd = rangeLineEnd;
47+
colEnd = rangeColEnd;
48+
} else {
49+
if (rangeLineStart > lineEnd) {
50+
// Higher starting line
51+
lineEnd = rangeLineStart;
52+
colEnd = rangeColStart;
53+
}
54+
if (rangeLineEnd > lineEnd) {
55+
// Higher ending line
56+
lineEnd = rangeLineEnd;
57+
colEnd = rangeColEnd;
58+
}
59+
if (rangeColEnd > colEnd) {
60+
// Higher ending column
61+
colEnd = rangeColEnd;
62+
}
63+
}
64+
match = rangeRE.exec(ranges);
65+
}
66+
return [[lineStart, colStart], [lineEnd, colEnd]];
67+
};
68+
69+
/**
70+
* Determine if a given path is open in an existing TextEditor
71+
* @param {String} filePath The file path to search for an editor of
72+
* @return {TextEditor | false} The TextEditor or false if none found
73+
*/
74+
const findTextEditor = (filePath) => {
75+
const allEditors = atom.workspace.getTextEditors();
76+
const matchingEditor = allEditors.find(
77+
textEditor => textEditor.getPath() === filePath);
78+
return matchingEditor || false;
79+
};
1080

1181
export default {
1282
activate() {
@@ -84,7 +154,7 @@ export default {
84154
}
85155

86156
const filePath = editor.getPath();
87-
if (filePath === undefined) {
157+
if (typeof filePath === 'undefined') {
88158
// The editor has no path, meaning it hasn't been saved. Although
89159
// clang could give us results for this, Linter needs a path
90160
return [];
@@ -95,8 +165,9 @@ export default {
95165

96166
const args = [
97167
'-fsyntax-only',
98-
'-fno-caret-diagnostics',
99-
'-fno-diagnostics-fixit-info',
168+
'-fno-color-diagnostics',
169+
'-fdiagnostics-absolute-paths',
170+
'-fdiagnostics-parseable-fixits',
100171
'-fdiagnostics-print-source-range-info',
101172
'-fexceptions',
102173
`-ferror-limit=${this.clangErrorLimit}`,
@@ -168,35 +239,62 @@ export default {
168239

169240
if (editor.getText() !== fileText) {
170241
// Editor contents have changed, tell Linter not to update results
171-
// eslint-disable-next-line no-console
172-
console.warn('linter-clang: Editor contents changed, not updating results');
173242
return null;
174243
}
175244

176245
const toReturn = [];
177-
let match = regex.exec(output);
178246

247+
let match = regex.exec(output);
179248
while (match !== null) {
249+
const isCurrentFile = match[1] === '<stdin>';
250+
// If the "file" is stdin, override to the current editor's path
251+
const file = isCurrentFile ? filePath : match[1];
180252
let position;
181-
if (match[4] !== undefined) {
182-
const lineStart = Number.parseInt(match[4], 10) - 1;
183-
const colStart = Number.parseInt(match[5], 10) - 1;
184-
const lineEnd = Number.parseInt(match[6], 10) - 1;
185-
const colEnd = Number.parseInt(match[7], 10) - 1;
186-
position = [[lineStart, colStart], [lineEnd, colEnd]];
253+
if (match[4]) {
254+
// Clang gave us a range, use that
255+
position = parseClangRanges(match[4]);
187256
} else {
257+
// Generate a range based on the single point
188258
const line = Number.parseInt(match[2], 10) - 1;
189259
const col = Number.parseInt(match[3], 10) - 1;
190-
position = helpers.generateRange(editor, line, col);
260+
if (!isCurrentFile) {
261+
const fileEditor = findTextEditor(match[1]);
262+
if (fileEditor !== false) {
263+
// Found an open editor for the file
264+
position = helpers.generateRange(fileEditor, line, col);
265+
} else {
266+
// Generate a one character range in the file
267+
position = [[line, col], [line, col + 1]];
268+
}
269+
} else {
270+
position = helpers.generateRange(editor, line, col);
271+
}
191272
}
192-
const severity = /error/.test(match[8]) ? 'error' : 'warning';
193-
// Handle paths other than the file being linted still
194-
const file = match[1] === '<stdin>' ? filePath : match[1];
195-
toReturn.push({
273+
const severity = /error/.test(match[5]) ? 'error' : 'warning';
274+
let excerpt;
275+
if (match[7]) {
276+
// There is a -Wflag specified, for now just re-insert that into the excerpt
277+
excerpt = `${match[6]} [${match[7]}]`;
278+
} else {
279+
excerpt = match[6];
280+
}
281+
const message = {
196282
severity,
197283
location: { file, position },
198-
excerpt: match[9],
199-
});
284+
excerpt,
285+
};
286+
if (match[8]) {
287+
// We have a suggested fix available
288+
const fixLineStart = Number.parseInt(match[8], 10) - 1;
289+
const fixColStart = Number.parseInt(match[9], 10) - 1;
290+
const fixLineEnd = Number.parseInt(match[10], 10) - 1;
291+
const fixColEnd = Number.parseInt(match[11], 10) - 1;
292+
message.solutions = [{
293+
position: [[fixLineStart, fixColStart], [fixLineEnd, fixColEnd]],
294+
replaceWith: match[12],
295+
}];
296+
}
297+
toReturn.push(message);
200298
match = regex.exec(output);
201299
}
202300

spec/files/fixit.hpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
#ifndef _FIXIT_H
2+
#define _FIXIT_H
3+
4+
#endif FIXIT_H

spec/files/multi-range.cpp

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
int main(int argc, char const *argv[]) {
2+
double foo = 1;
3+
double bar = 2;
4+
int foobar = foo % bar;
5+
return 0;
6+
}

spec/files/otherFile/a.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
#include "a.hpp"

spec/files/otherFile/a.hpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
#pragma once
2+
3+
#include "b.hpp"

spec/files/otherFile/b.hpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
#pragma once
2+
3+
int foo += 1;

spec/linter-clang-spec.js

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,19 @@
22

33
// eslint-disable-next-line import/no-extraneous-dependencies
44
import { beforeEach, it } from 'jasmine-fix';
5+
// Note, when testing if using fit you must import it!
56
import { join } from 'path';
67

78
const lint = require('../lib/main').provideLinter().lint;
89

910
const miPath = join(__dirname, 'files', 'missing_import');
1011
const poPath = join(__dirname, 'files', 'pragma', 'pragma_once');
1112
const validPath = join(__dirname, 'files', 'valid.c');
13+
const multiRangePath = join(__dirname, 'files', 'multi-range.cpp');
14+
const fixitPath = join(__dirname, 'files', 'fixit.hpp');
15+
const otherPath = join(__dirname, 'files', 'otherFile');
16+
const otherAPath = join(otherPath, 'a.cpp');
17+
const otherBPath = join(otherPath, 'b.hpp');
1218
const fileText = `// This is a comment, this will not return any errors.
1319
#include "nothing.h"
1420
@@ -119,4 +125,50 @@ describe('The Clang provider for AtomLinter', () => {
119125
expect(messages[0].location.file).toBe(validPath);
120126
expect(messages[0].location.position).toEqual([[1, 9], [1, 20]]);
121127
});
128+
129+
it('handles multiple ranges', async () => {
130+
const editor = await atom.workspace.open(multiRangePath);
131+
const messages = await lint(editor);
132+
expect(messages.length).toBe(1);
133+
expect(messages[0].severity).toBe('error');
134+
expect(messages[0].excerpt).toBe("invalid operands to binary expression ('double' and 'double')");
135+
expect(messages[0].location.file).toBe(multiRangePath);
136+
expect(messages[0].location.position).toEqual([[3, 15], [3, 24]]);
137+
});
138+
139+
it('handles suggested fixes', async () => {
140+
const editor = await atom.workspace.open(fixitPath);
141+
const messages = await lint(editor);
142+
expect(messages.length).toBe(1);
143+
expect(messages[0].severity).toBe('warning');
144+
expect(messages[0].excerpt).toBe('extra tokens at end of #endif directive [-Wextra-tokens]');
145+
expect(messages[0].location.file).toBe(fixitPath);
146+
expect(messages[0].location.position).toEqual([[3, 7], [3, 14]]);
147+
expect(messages[0].solutions.length).toBe(1);
148+
expect(messages[0].solutions[0].position).toEqual([[3, 7], [3, 7]]);
149+
expect(messages[0].solutions[0].replaceWith).toBe('//');
150+
});
151+
152+
describe('handles messages in other files', () => {
153+
it('without another editor open', async () => {
154+
const editorA = await atom.workspace.open(otherAPath);
155+
const messages = await lint(editorA);
156+
expect(messages.length).toBe(1);
157+
expect(messages[0].severity).toBe('error');
158+
expect(messages[0].excerpt).toBe("invalid '+=' at end of declaration; did you mean '='?");
159+
expect(messages[0].location.file).toBe(otherBPath);
160+
expect(messages[0].location.position).toEqual([[2, 8], [2, 9]]);
161+
});
162+
163+
it('with another editor open', async () => {
164+
const editorA = await atom.workspace.open(otherAPath);
165+
await atom.workspace.open(otherBPath);
166+
const messages = await lint(editorA);
167+
expect(messages.length).toBe(1);
168+
expect(messages[0].severity).toBe('error');
169+
expect(messages[0].excerpt).toBe("invalid '+=' at end of declaration; did you mean '='?");
170+
expect(messages[0].location.file).toBe(otherBPath);
171+
expect(messages[0].location.position).toEqual([[2, 8], [2, 12]]);
172+
});
173+
});
122174
});

0 commit comments

Comments
 (0)