Skip to content

Commit e570e1e

Browse files
committed
feat(linter/plugins): accept diagnostics with loc
1 parent 69b7c32 commit e570e1e

File tree

8 files changed

+136
-9
lines changed

8 files changed

+136
-9
lines changed

apps/oxlint/src-js/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { Context } from './plugins/context.ts';
22
import type { CreateOnceRule, Plugin, Rule } from './plugins/load.ts';
33
import type { BeforeHook, Visitor, VisitorWithHooks } from './plugins/types.ts';
44

5-
export type { Context, Diagnostic } from './plugins/context.ts';
5+
export type { Context, Diagnostic, DiagnosticBase, DiagnosticWithLoc, DiagnosticWithNode } from './plugins/context.ts';
66
export type { Fix, Fixer, FixFn, Range } from './plugins/fix.ts';
77
export type { CreateOnceRule, CreateRule, Plugin, Rule } from './plugins/load.ts';
88
export type {

apps/oxlint/src-js/plugins/context.ts

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,28 @@
11
import { getFixes } from './fix.js';
2-
import { SOURCE_CODE } from './source_code.js';
2+
import { getIndexFromLoc, SOURCE_CODE } from './source_code.js';
33

44
import type { Fix, FixFn } from './fix.ts';
55
import type { SourceCode } from './source_code.ts';
6-
import type { Node } from './types.ts';
6+
import type { Location, Node } from './types.ts';
7+
8+
const { hasOwn } = Object;
79

810
// Diagnostic in form passed by user to `Context#report()`
9-
export interface Diagnostic {
11+
export type Diagnostic = DiagnosticWithNode | DiagnosticWithLoc;
12+
13+
export interface DiagnosticBase {
1014
message: string;
11-
node: Node;
1215
fix?: FixFn;
1316
}
1417

18+
export interface DiagnosticWithNode extends DiagnosticBase {
19+
node: Node;
20+
}
21+
22+
export interface DiagnosticWithLoc extends DiagnosticBase {
23+
loc: Location;
24+
}
25+
1526
// Diagnostic in form sent to Rust
1627
interface DiagnosticReport {
1728
message: string;
@@ -127,16 +138,38 @@ export class Context {
127138
/**
128139
* Report error.
129140
* @param diagnostic - Diagnostic object
141+
* @throws {TypeError} If `diagnostic` is invalid
130142
*/
131143
report(diagnostic: Diagnostic): void {
132144
const internal = getInternal(this, 'report errors');
133145

134146
// TODO: Validate `diagnostic`
135-
const { node } = diagnostic;
147+
let start: number, end: number, loc: Location;
148+
if (hasOwn(diagnostic, 'loc') && (loc = (diagnostic as DiagnosticWithLoc).loc) != null) {
149+
// `loc`
150+
if (typeof loc !== 'object') throw new TypeError('`loc` must be an object');
151+
start = getIndexFromLoc(loc.start);
152+
end = getIndexFromLoc(loc.end);
153+
} else {
154+
// `node`
155+
const { node } = diagnostic as DiagnosticWithNode;
156+
if (node == null) throw new TypeError('Either `node` or `loc` is required');
157+
if (typeof node !== 'object') throw new TypeError('`node` must be an object');
158+
({ start, end } = node);
159+
// Do type validation checks here, to ensure no error in serialization / deserialization.
160+
// Range validation happens on Rust side.
161+
if (
162+
typeof start !== 'number' || typeof end !== 'number' ||
163+
start < 0 || end < 0 || (start | 0) !== start || (end | 0) !== end
164+
) {
165+
throw new TypeError('`node.start` and `node.end` must be non-negative integers');
166+
}
167+
}
168+
136169
diagnostics.push({
137170
message: diagnostic.message,
138-
start: node.start,
139-
end: node.end,
171+
start,
172+
end,
140173
ruleIndex: internal.ruleIndex,
141174
fixes: getFixes(diagnostic, internal),
142175
});

apps/oxlint/src-js/plugins/source_code.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -608,7 +608,7 @@ function getLocFromIndex(offset: number): LineColumn {
608608
* @throws {TypeError|RangeError} If `loc` is not an object with a numeric `line` and `column`,
609609
* or if the `line` is less than or equal to zero, or the line or column is out of the expected range.
610610
*/
611-
function getIndexFromLoc(loc: LineColumn): number {
611+
export function getIndexFromLoc(loc: LineColumn): number {
612612
if (loc !== null && typeof loc === 'object') {
613613
const { line, column } = loc;
614614
if (typeof line === 'number' && typeof column === 'number' && (line | 0) === line && (column | 0) === column) {

apps/oxlint/test/e2e.test.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,10 @@ describe('oxlint CLI', () => {
115115
await testFixture('basic_custom_plugin_multiple_rules');
116116
});
117117

118+
it('should support reporting diagnostic with `loc`', async () => {
119+
await testFixture('diagnostic_loc');
120+
});
121+
118122
it('should receive ESTree-compatible AST', async () => {
119123
await testFixture('estree');
120124
});
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"jsPlugins": ["./plugin.ts"],
3+
"categories": { "correctness": "off" },
4+
"rules": {
5+
"loc-plugin/no-bugger": "error"
6+
}
7+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
debugger;
2+
debugger;
3+
debugger;
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# Exit code
2+
1
3+
4+
# stdout
5+
```
6+
x loc-plugin(no-bugger): Bugger!
7+
,-[files/index.js:1:3]
8+
1 | debugger;
9+
: ^^^^^^
10+
2 | debugger;
11+
`----
12+
13+
x loc-plugin(no-bugger): Bugger debugger debug!
14+
,-[files/index.js:1:3]
15+
1 | ,-> debugger;
16+
2 | | debugger;
17+
3 | `-> debugger;
18+
`----
19+
20+
x loc-plugin(no-bugger): Bugger!
21+
,-[files/index.js:2:3]
22+
1 | debugger;
23+
2 | debugger;
24+
: ^^^^^^
25+
3 | debugger;
26+
`----
27+
28+
x loc-plugin(no-bugger): Bugger!
29+
,-[files/index.js:3:3]
30+
2 | debugger;
31+
3 | debugger;
32+
: ^^^^^^
33+
`----
34+
35+
Found 0 warnings and 4 errors.
36+
Finished in Xms on 1 file using X threads.
37+
```
38+
39+
# stderr
40+
```
41+
WARNING: JS plugins are experimental and not subject to semver.
42+
Breaking changes are possible while JS plugins support is under development.
43+
```
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import type { Plugin } from '../../../dist/index.js';
2+
3+
const plugin: Plugin = {
4+
meta: {
5+
name: 'loc-plugin',
6+
},
7+
rules: {
8+
'no-bugger': {
9+
create(context) {
10+
let debuggerCount = 0;
11+
return {
12+
Program(_node) {
13+
context.report({
14+
message: 'Bugger debugger debug!',
15+
loc: {
16+
start: { line: 1, column: 2 },
17+
end: { line: 3, column: 5 },
18+
},
19+
});
20+
},
21+
DebuggerStatement(_node) {
22+
debuggerCount++;
23+
context.report({
24+
message: 'Bugger!',
25+
loc: {
26+
start: { line: debuggerCount, column: 2 },
27+
end: { line: debuggerCount, column: 8 },
28+
},
29+
});
30+
},
31+
};
32+
},
33+
},
34+
},
35+
};
36+
37+
export default plugin;

0 commit comments

Comments
 (0)