Skip to content

Commit 3b6e594

Browse files
committed
feat(linter/plugins): comment-related APIs
1 parent 273f0fe commit 3b6e594

File tree

9 files changed

+202
-79
lines changed

9 files changed

+202
-79
lines changed

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

Lines changed: 52 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -169,29 +169,66 @@ export const SOURCE_CODE = Object.freeze({
169169
* @param nodeOrToken - The AST node or token to check for adjacent comment tokens.
170170
* @returns Array of `Comment`s in occurrence order.
171171
*/
172-
// oxlint-disable-next-line no-unused-vars
173172
getCommentsBefore(nodeOrToken: NodeOrToken): Comment[] {
174-
throw new Error('`sourceCode.getCommentsBefore` not implemented yet'); // TODO
173+
if (ast === null) initAst();
174+
const { comments } = ast;
175+
if (comments.length === 0) return [];
176+
const targetStart = nodeOrToken.range[0];
177+
let i = comments.length - 1;
178+
// linear backwards search for the index of the last comment before the provided node or token, TODO(perf): binary
179+
while (i >= 0 && comments[i].range[1] > targetStart) {
180+
i--;
181+
}
182+
return comments.slice(0, i + 1);
175183
},
176184

177185
/**
178186
* Get all comment tokens directly after the given node or token.
179187
* @param nodeOrToken - The AST node or token to check for adjacent comment tokens.
180188
* @returns Array of `Comment`s in occurrence order.
181189
*/
182-
// oxlint-disable-next-line no-unused-vars
183190
getCommentsAfter(nodeOrToken: NodeOrToken): Comment[] {
184-
throw new Error('`sourceCode.getCommentsAfter` not implemented yet'); // TODO
191+
if (ast === null) initAst();
192+
const { comments } = ast;
193+
if (comments.length === 0) return [];
194+
const targetEnd = nodeOrToken.range[1];
195+
let i = 0;
196+
// linear forwards search for the index of the first comment after the provided node or token, TODO(perf): binary
197+
while (i < comments.length && comments[i].range[0] < targetEnd) {
198+
i++;
199+
}
200+
return comments.slice(i);
185201
},
186202

187203
/**
188204
* Get all comment tokens inside the given node.
189205
* @param node - The AST node to get the comments for.
190206
* @returns Array of `Comment`s in occurrence order.
191207
*/
192-
// oxlint-disable-next-line no-unused-vars
193208
getCommentsInside(node: Node): Comment[] {
194-
throw new Error('`sourceCode.getCommentsInside` not implemented yet'); // TODO
209+
if (ast === null) initAst();
210+
const { comments } = ast;
211+
if (comments.length === 0) return [];
212+
const [rangeStart, rangeEnd] = node.range;
213+
let indexStart = null, indexEnd = null;
214+
// linear search for the first comment inside the node's range, TODO(perf): binary
215+
for (let i = 0; i < comments.length; i++) {
216+
const c = comments[i];
217+
if (c.range[0] >= rangeStart && c.range[1] <= rangeEnd) {
218+
indexStart = i;
219+
break;
220+
}
221+
}
222+
if (indexStart === null) return [];
223+
// linear search for the last comment inside the node's range, TODO(perf): binary
224+
for (let i = indexStart; i < comments.length; i++) {
225+
const c = comments[i];
226+
if (c.range[1] > rangeEnd) {
227+
indexEnd = i;
228+
break;
229+
}
230+
}
231+
return comments.slice(indexStart, indexEnd ?? comments.length);
195232
},
196233

197234
/**
@@ -468,9 +505,16 @@ export const SOURCE_CODE = Object.freeze({
468505
* @param nodeOrToken2 - The node to check.
469506
* @returns `true` if one or more comments exist.
470507
*/
471-
// oxlint-disable-next-line no-unused-vars
472508
commentsExistBetween(nodeOrToken1: NodeOrToken, nodeOrToken2: NodeOrToken): boolean {
473-
throw new Error('`sourceCode.commentsExistBetween` not implemented yet'); // TODO
509+
// find the first comment after nodeOrToken1 end, check if it ends before nodeOrToken2 starts
510+
const nodeOrToken1End = nodeOrToken1.range[1];
511+
for (let i = 0; i < ast.comments.length; i++) {
512+
const c = ast.comments[i];
513+
if (c.range[0] >= nodeOrToken1End) {
514+
return c.range[1] <= nodeOrToken2.range[0];
515+
}
516+
}
517+
return false;
474518
},
475519

476520
getAncestors,

apps/oxlint/test/e2e.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,7 @@ describe('oxlint CLI', () => {
217217
}
218218
});
219219

220-
it('SourceCode.getAllComments() should return all comments', async () => {
221-
await testFixture('getAllComments');
220+
it('should support comments-related APIs in `context.sourceCode`', async () => {
221+
await testFixture('comments');
222222
});
223223
});
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"jsPlugins": ["./plugin.ts"],
3+
"rules": {
4+
"test-comments/test-comments": "error"
5+
}
6+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
# Exit code
2+
1
3+
4+
# stdout
5+
```
6+
x test-comments(test-comments): getCommentsAfter(x) returned 5 comments:
7+
| [0] Block: " Block comment 1 " at [31, 52]
8+
| [1] Block: "*\n * JSDoc comment\n " at [54, 78]
9+
| [2] Line: " Line comment 2" at [105, 122]
10+
| [3] Line: " Line comment 3" at [135, 152]
11+
| [4] Block: " Block comment 2 " at [156, 177]
12+
,-[files/test.js:2:1]
13+
1 | // Line comment 1
14+
2 | const x = 1; /* Block comment 1 */
15+
: ^^^^^^^^^^^^
16+
3 |
17+
`----
18+
19+
x test-comments(test-comments): getCommentsBefore(x) returned 1 comments:
20+
| [0] Line: " Line comment 1" at [0, 17]
21+
,-[files/test.js:2:1]
22+
1 | // Line comment 1
23+
2 | const x = 1; /* Block comment 1 */
24+
: ^^^^^^^^^^^^
25+
3 |
26+
`----
27+
28+
x test-comments(test-comments): commentsExistBetween(x, foo): true
29+
,-[files/test.js:2:1]
30+
1 | // Line comment 1
31+
2 | const x = 1; /* Block comment 1 */
32+
: ^^^^^^^^^^^^
33+
3 |
34+
`----
35+
36+
x test-comments(test-comments): getAllComments() returned 6 comments:
37+
| [0] Line: " Line comment 1" at [0, 17]
38+
| [1] Block: " Block comment 1 " at [31, 52]
39+
| [2] Block: "*\n * JSDoc comment\n " at [54, 78]
40+
| [3] Line: " Line comment 2" at [105, 122]
41+
| [4] Line: " Line comment 3" at [135, 152]
42+
| [5] Block: " Block comment 2 " at [156, 177]
43+
,-[files/test.js:2:1]
44+
1 | // Line comment 1
45+
2 | ,-> const x = 1; /* Block comment 1 */
46+
3 | |
47+
4 | | /**
48+
5 | | * JSDoc comment
49+
6 | | */
50+
7 | | export function foo() {
51+
8 | | // Line comment 2
52+
9 | | return x; // Line comment 3
53+
10 | | }
54+
11 | |
55+
12 | `-> /* Block comment 2 */
56+
`----
57+
58+
x test-comments(test-comments): getCommentsInside(foo) returned 2 comments:
59+
| [0] Line: " Line comment 2" at [105, 122]
60+
| [1] Line: " Line comment 3" at [135, 152]
61+
,-[files/test.js:7:1]
62+
6 | */
63+
7 | ,-> export function foo() {
64+
8 | | // Line comment 2
65+
9 | | return x; // Line comment 3
66+
10 | `-> }
67+
11 |
68+
`----
69+
70+
Found 0 warnings and 5 errors.
71+
Finished in Xms on 1 file using X threads.
72+
```
73+
74+
# stderr
75+
```
76+
WARNING: JS plugins are experimental and not subject to semver.
77+
Breaking changes are possible while JS plugins support is under development.
78+
```
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { definePlugin } from '../../../dist/index.js';
2+
import type { Rule } from '../../../dist/index.js';
3+
4+
const testCommentsRule: Rule = {
5+
create(context) {
6+
const { sourceCode } = context;
7+
const { ast } = sourceCode;
8+
const { body } = ast;
9+
10+
const comments = sourceCode.getAllComments();
11+
context.report({
12+
message: `getAllComments() returned ${comments.length} comments:\n` +
13+
comments.map((c, i) => ` [${i}] ${c.type}: ${JSON.stringify(c.value)} at [${c.range[0]}, ${c.range[1]}]`).join(
14+
'\n',
15+
),
16+
node: context.sourceCode.ast,
17+
});
18+
19+
const constX = body.find((n) => n.type === 'VariableDeclaration');
20+
const before = sourceCode.getCommentsBefore(constX);
21+
const after = sourceCode.getCommentsAfter(constX);
22+
context.report({
23+
message: `getCommentsBefore(x) returned ${before.length} comments:\n` +
24+
before.map((c, i) => ` [${i}] ${c.type}: ${JSON.stringify(c.value)} at [${c.range[0]}, ${c.range[1]}]`).join(
25+
'\n',
26+
),
27+
node: constX,
28+
});
29+
context.report({
30+
message: `getCommentsAfter(x) returned ${after.length} comments:\n` +
31+
after.map((c, i) => ` [${i}] ${c.type}: ${JSON.stringify(c.value)} at [${c.range[0]}, ${c.range[1]}]`).join(
32+
'\n',
33+
),
34+
node: constX,
35+
});
36+
37+
const functionFoo = body.find((n) => n.type === 'ExportNamedDeclaration');
38+
const insideFn = sourceCode.getCommentsInside(functionFoo);
39+
context.report({
40+
message: `getCommentsInside(foo) returned ${insideFn.length} comments:\n` +
41+
insideFn.map((c, i) => ` [${i}] ${c.type}: ${JSON.stringify(c.value)} at [${c.range[0]}, ${c.range[1]}]`).join(
42+
'\n',
43+
),
44+
node: functionFoo,
45+
});
46+
47+
const commentsBetween = sourceCode.commentsExistBetween(constX, functionFoo);
48+
context.report({
49+
message: `commentsExistBetween(x, foo): ${commentsBetween}`,
50+
node: constX,
51+
});
52+
53+
return {};
54+
},
55+
};
56+
57+
export default definePlugin({
58+
meta: {
59+
name: 'test-comments',
60+
},
61+
rules: {
62+
'test-comments': testCommentsRule,
63+
},
64+
});

apps/oxlint/test/fixtures/getAllComments/.oxlintrc.json

Lines changed: 0 additions & 6 deletions
This file was deleted.

apps/oxlint/test/fixtures/getAllComments/output.snap.md

Lines changed: 0 additions & 36 deletions
This file was deleted.

apps/oxlint/test/fixtures/getAllComments/plugin.ts

Lines changed: 0 additions & 27 deletions
This file was deleted.

0 commit comments

Comments
 (0)