|
3 | 3 | * Functions for converting between `LineColumn` and offsets, and splitting source text into lines. |
4 | 4 | */ |
5 | 5 |
|
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"; |
7 | 8 | import { debugAssertIsNonNull } from "../utils/asserts.js"; |
8 | 9 |
|
9 | 10 | import type { Node } from "./types.ts"; |
| 11 | +import type { Node as ESTreeNode } from "../generated/types.d.ts"; |
10 | 12 |
|
11 | | -const { defineProperty } = Object; |
| 13 | +const { defineProperty } = Object, |
| 14 | + { isArray } = Array; |
12 | 15 |
|
13 | 16 | /** |
14 | 17 | * Range of source offsets. |
@@ -237,3 +240,72 @@ export function getNodeLoc(node: Node): Location { |
237 | 240 |
|
238 | 241 | return loc; |
239 | 242 | } |
| 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 | +} |
0 commit comments