Skip to content

Commit 7150209

Browse files
committed
feat(linter/plugins): implement SourceCode#getNodeByRangeIndex (#16256)
Implement `SourceCode#getNodeByRangeIndex` method. Behaves the same as ESLint's version, but this implementation should be more performant as it's much simpler, avoiding ESLint's `Traverser` abstraction, and exiting loops earlier.
1 parent 0df1901 commit 7150209

File tree

6 files changed

+518
-12
lines changed

6 files changed

+518
-12
lines changed

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

Lines changed: 74 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,15 @@
33
* Functions for converting between `LineColumn` and offsets, and splitting source text into lines.
44
*/
55

6-
import { initSourceText, sourceText } from "./source_code.js";
6+
import { ast, initAst, initSourceText, sourceText } from "./source_code.js";
7+
import visitorKeys from "../generated/keys.js";
78
import { debugAssertIsNonNull } from "../utils/asserts.js";
89

910
import type { Node } from "./types.ts";
11+
import type { Node as ESTreeNode } from "../generated/types.d.ts";
1012

11-
const { defineProperty } = Object;
13+
const { defineProperty } = Object,
14+
{ isArray } = Array;
1215

1316
/**
1417
* Range of source offsets.
@@ -237,3 +240,72 @@ export function getNodeLoc(node: Node): Location {
237240

238241
return loc;
239242
}
243+
244+
/**
245+
* Get the deepest node containing a range index.
246+
* @param offset - Range index of the desired node
247+
* @returns The node if found, or `null` if not found
248+
*/
249+
export function getNodeByRangeIndex(offset: number): ESTreeNode | null {
250+
if (ast === null) initAst();
251+
debugAssertIsNonNull(ast);
252+
253+
// If index is outside of `Program`, return `null`
254+
// TODO: Once `Program`'s span covers the entire file (as per ESLint v10), `index < ast.start` check can be removed
255+
// (or changed to `index < 0` if we want to check for negative indices)
256+
if (offset < ast.start || offset >= ast.end) return null;
257+
258+
// Search for the node containing the index
259+
index = offset;
260+
return traverse(ast);
261+
}
262+
263+
let index: number = 0;
264+
265+
/**
266+
* Find deepest node containing `index`.
267+
* `node` must contain `index` itself. This function finds a deeper node if one exists.
268+
*
269+
* @param node - Node to start traversal from
270+
* @returns Deepest node containing `index`
271+
*/
272+
function traverse(node: ESTreeNode): ESTreeNode {
273+
// TODO: Handle decorators on exports e.g. `@dec export class C {}`.
274+
// Decorators in that position have spans outside of the `export` node's span.
275+
// ESLint doesn't handle this case correctly, so not a big deal that we don't at present either.
276+
277+
const keys = (visitorKeys as Record<string, string[]>)[node.type];
278+
279+
// All nodes' properties are in source order, so we could use binary search here.
280+
// But the max number of visitable properties is 5, so linear search is fine. Possibly linear is faster anyway.
281+
for (let keyIndex = 0, keysLen = keys.length; keyIndex < keysLen; keyIndex++) {
282+
const child = (node as unknown as Record<string, ESTreeNode | ESTreeNode[]>)[keys[keyIndex]];
283+
284+
if (isArray(child)) {
285+
// TODO: Binary search would be faster, especially for arrays of statements, which can be large
286+
for (let arrIndex = 0, arrLen = child.length; arrIndex < arrLen; arrIndex++) {
287+
const entry = child[arrIndex];
288+
if (entry !== null) {
289+
// Array entries are in source order, so if this node is after the index,
290+
// all remaining nodes in the array are after the index too. So we can skip checking the rest of them.
291+
// We cannot skip all the rest of the outer loop, because in `TemplateLiteral`,
292+
// the 2 arrays `quasis` and `expressions` are interleaved. Ditto `TSTemplateLiteralType`.
293+
if (entry.start > index) break;
294+
// This node starts on or before the index. If it ends after the index, index is within this node.
295+
// Traverse into this node to find a deeper node if there is one.
296+
if (entry.end > index) return traverse(entry);
297+
}
298+
}
299+
} else if (child !== null) {
300+
// Node properties are in source order, so if this node is after the index,
301+
// all other properties are too. So we can skip checking the rest of them.
302+
if (child.start > index) break;
303+
// This node starts on or before the index. If it ends after the index, index is within this node.
304+
// Traverse into this node to find a deeper node if there is one.
305+
if (child.end > index) return traverse(child);
306+
}
307+
}
308+
309+
// Index is not within any child node, so this is the deepest node containing the index
310+
return node;
311+
}

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

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import visitorKeys from "../generated/keys.js";
99
import * as commentMethods from "./comments.js";
1010
import {
1111
getLineColumnFromOffset,
12+
getNodeByRangeIndex,
1213
getNodeLoc,
1314
getOffsetFromLineColumn,
1415
initLines,
@@ -78,7 +79,10 @@ export function initSourceText(): void {
7879
*/
7980
export function initAst(): void {
8081
if (sourceText === null) initSourceText();
82+
debugAssertIsNonNull(sourceText);
83+
8184
ast = deserializeProgramOnly(buffer, sourceText, sourceByteLen, getNodeLoc);
85+
debugAssertIsNonNull(ast);
8286
}
8387

8488
/**
@@ -196,17 +200,8 @@ export const SOURCE_CODE = Object.freeze({
196200
return ancestors.reverse();
197201
},
198202

199-
/**
200-
* Get the deepest node containing a range index.
201-
* @param index Range index of the desired node.
202-
* @returns The node if found, or `null` if not found.
203-
*/
204-
// oxlint-disable-next-line no-unused-vars
205-
getNodeByRangeIndex(index: number): Node | null {
206-
throw new Error("`sourceCode.getNodeByRangeIndex` not implemented yet"); // TODO
207-
},
208-
209203
// Location methods
204+
getNodeByRangeIndex,
210205
getLocFromIndex: getLineColumnFromOffset,
211206
getIndexFromLoc: getOffsetFromLineColumn,
212207

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"jsPlugins": ["./plugin.ts"],
3+
"categories": {
4+
"correctness": "off"
5+
},
6+
"rules": {
7+
"getNode-plugin/getNode": "error"
8+
}
9+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
// Comment
2+
3+
let foo = 1 + 2;
4+
5+
`___${123}___`;
6+
7+
type T = `___${123}___`;
8+
9+
// Comment

0 commit comments

Comments
 (0)