@ontrails/source 1.0.0-beta.41
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +53 -0
- package/README.md +80 -0
- package/package.json +29 -0
- package/src/edits.ts +57 -0
- package/src/index.ts +24 -0
- package/src/literals.ts +226 -0
- package/src/locations.ts +35 -0
- package/src/nodes.ts +650 -0
- package/src/parse.ts +55 -0
- package/src/scopes.ts +517 -0
- package/src/trails.ts +826 -0
- package/src/walk.ts +87 -0
package/src/scopes.ts
ADDED
|
@@ -0,0 +1,517 @@
|
|
|
1
|
+
/** Shared lexical-scope helpers for AST analysis. */
|
|
2
|
+
|
|
3
|
+
import { ScopeTracker } from 'oxc-walker';
|
|
4
|
+
import type { ScopeTrackerNode } from 'oxc-walker';
|
|
5
|
+
|
|
6
|
+
import type { AstNode, AstScopeContext, AstScopeDeclaration } from './nodes.js';
|
|
7
|
+
import { identifierName } from './literals.js';
|
|
8
|
+
import { walkChildren, walkWithOxcFacade } from './walk.js';
|
|
9
|
+
import type { WalkFn } from './walk.js';
|
|
10
|
+
|
|
11
|
+
const toScopeDeclaration = (
|
|
12
|
+
declaration: ScopeTrackerNode | null
|
|
13
|
+
): AstScopeDeclaration | null => {
|
|
14
|
+
if (!declaration) {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return {
|
|
19
|
+
end: declaration.end,
|
|
20
|
+
node: declaration.node as unknown as AstNode,
|
|
21
|
+
start: declaration.start,
|
|
22
|
+
type: declaration.type,
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const walkWithScopeContext = (
|
|
27
|
+
node: unknown,
|
|
28
|
+
visit: (node: AstNode, context: AstScopeContext) => void
|
|
29
|
+
): void => {
|
|
30
|
+
const scopeTracker = new ScopeTracker();
|
|
31
|
+
|
|
32
|
+
walkWithOxcFacade(
|
|
33
|
+
node,
|
|
34
|
+
(candidate, context) => {
|
|
35
|
+
visit(candidate, {
|
|
36
|
+
...context,
|
|
37
|
+
currentScope: scopeTracker.getCurrentScope(),
|
|
38
|
+
getDeclaration: (name) =>
|
|
39
|
+
toScopeDeclaration(scopeTracker.getDeclaration(name)),
|
|
40
|
+
isCurrentScopeUnder: (scope) => scopeTracker.isCurrentScopeUnder(scope),
|
|
41
|
+
isDeclared: (name) => scopeTracker.isDeclared(name),
|
|
42
|
+
});
|
|
43
|
+
},
|
|
44
|
+
scopeTracker
|
|
45
|
+
);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const NESTED_SCOPE_TYPES = new Set([
|
|
49
|
+
'ArrowFunctionExpression',
|
|
50
|
+
'FunctionExpression',
|
|
51
|
+
'FunctionDeclaration',
|
|
52
|
+
]);
|
|
53
|
+
|
|
54
|
+
const walkScopeInner: WalkFn = (node, visit) => {
|
|
55
|
+
if (!node || typeof node !== 'object') {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const n = node as AstNode;
|
|
59
|
+
if (n.type) {
|
|
60
|
+
visit(n);
|
|
61
|
+
if (NESTED_SCOPE_TYPES.has(n.type)) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
walkChildren(n, visit, walkScopeInner);
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Walk an AST node tree without descending into nested function scopes.
|
|
70
|
+
* The root node is always traversed; only inner function boundaries are skipped.
|
|
71
|
+
* Useful for resource-access analysis where inner functions may shadow
|
|
72
|
+
* the trail context parameter name.
|
|
73
|
+
*/
|
|
74
|
+
export const walkScope: WalkFn = (node, visit) => {
|
|
75
|
+
if (!node || typeof node !== 'object') {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
const n = node as AstNode;
|
|
79
|
+
if (n.type) {
|
|
80
|
+
visit(n);
|
|
81
|
+
}
|
|
82
|
+
walkChildren(n, visit, walkScopeInner);
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
type PatternExpander = (node: AstNode) => readonly AstNode[];
|
|
86
|
+
|
|
87
|
+
const expandAssignmentPattern: PatternExpander = (node) => {
|
|
88
|
+
const { left } = node as unknown as { left?: AstNode };
|
|
89
|
+
return left ? [left] : [];
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const expandRestElement: PatternExpander = (node) => {
|
|
93
|
+
const { argument } = node as unknown as { argument?: AstNode };
|
|
94
|
+
return argument ? [argument] : [];
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const expandArrayPattern: PatternExpander = (node) => {
|
|
98
|
+
const elements =
|
|
99
|
+
(node as unknown as { elements?: readonly (AstNode | null)[] }).elements ??
|
|
100
|
+
[];
|
|
101
|
+
return elements.filter((e): e is AstNode => e !== null);
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const expandObjectPatternProperty = (prop: AstNode): AstNode | null => {
|
|
105
|
+
if (prop.type === 'RestElement') {
|
|
106
|
+
return prop;
|
|
107
|
+
}
|
|
108
|
+
const { value } = prop as unknown as { value?: AstNode };
|
|
109
|
+
return value ?? null;
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const expandObjectPattern: PatternExpander = (node) => {
|
|
113
|
+
const properties =
|
|
114
|
+
(node as unknown as { properties?: readonly AstNode[] }).properties ?? [];
|
|
115
|
+
return properties
|
|
116
|
+
.map(expandObjectPatternProperty)
|
|
117
|
+
.filter((n): n is AstNode => n !== null);
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const PATTERN_EXPANDERS: Record<string, PatternExpander> = {
|
|
121
|
+
ArrayPattern: expandArrayPattern,
|
|
122
|
+
AssignmentPattern: expandAssignmentPattern,
|
|
123
|
+
ObjectPattern: expandObjectPattern,
|
|
124
|
+
RestElement: expandRestElement,
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const processPatternNode = (
|
|
128
|
+
node: AstNode,
|
|
129
|
+
into: Set<string>,
|
|
130
|
+
stack: AstNode[]
|
|
131
|
+
): void => {
|
|
132
|
+
if (node.type === 'Identifier') {
|
|
133
|
+
const { name } = node as unknown as { name?: string };
|
|
134
|
+
if (name) {
|
|
135
|
+
into.add(name);
|
|
136
|
+
}
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
const expand = PATTERN_EXPANDERS[node.type];
|
|
140
|
+
if (expand) {
|
|
141
|
+
stack.push(...expand(node));
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const addPatternBindingNames = (
|
|
146
|
+
pattern: AstNode | undefined,
|
|
147
|
+
into: Set<string>
|
|
148
|
+
): void => {
|
|
149
|
+
if (!pattern) {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
const stack: AstNode[] = [pattern];
|
|
153
|
+
while (stack.length > 0) {
|
|
154
|
+
const node = stack.pop();
|
|
155
|
+
if (node) {
|
|
156
|
+
processPatternNode(node, into, stack);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
const addVarDeclarationBindingNames = (
|
|
162
|
+
decl: AstNode,
|
|
163
|
+
into: Set<string>
|
|
164
|
+
): void => {
|
|
165
|
+
const declarations =
|
|
166
|
+
(decl as unknown as { declarations?: readonly AstNode[] }).declarations ??
|
|
167
|
+
[];
|
|
168
|
+
for (const d of declarations) {
|
|
169
|
+
addPatternBindingNames((d as unknown as { id?: AstNode }).id, into);
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const addFunctionOrClassBindingName = (
|
|
174
|
+
node: AstNode,
|
|
175
|
+
into: Set<string>
|
|
176
|
+
): void => {
|
|
177
|
+
const { id } = node as unknown as { id?: AstNode };
|
|
178
|
+
const name = identifierName(id);
|
|
179
|
+
if (name) {
|
|
180
|
+
into.add(name);
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
const addBlockStatementBindings = (stmt: AstNode, into: Set<string>): void => {
|
|
185
|
+
if (stmt.type === 'VariableDeclaration') {
|
|
186
|
+
addVarDeclarationBindingNames(stmt, into);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
if (
|
|
190
|
+
stmt.type === 'FunctionDeclaration' ||
|
|
191
|
+
stmt.type === 'ClassDeclaration' ||
|
|
192
|
+
stmt.type === 'TSEnumDeclaration' ||
|
|
193
|
+
stmt.type === 'TSModuleDeclaration'
|
|
194
|
+
) {
|
|
195
|
+
addFunctionOrClassBindingName(stmt, into);
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
const collectTopLevelStatementBindings = (
|
|
200
|
+
stmt: AstNode,
|
|
201
|
+
into: Set<string>
|
|
202
|
+
): void => {
|
|
203
|
+
if (
|
|
204
|
+
stmt.type === 'ExportNamedDeclaration' ||
|
|
205
|
+
stmt.type === 'ExportDefaultDeclaration'
|
|
206
|
+
) {
|
|
207
|
+
const { declaration } = stmt as unknown as { declaration?: AstNode };
|
|
208
|
+
if (declaration) {
|
|
209
|
+
collectTopLevelStatementBindings(declaration, into);
|
|
210
|
+
}
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
addBlockStatementBindings(stmt, into);
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
const FUNCTION_BOUNDARY_TYPES = new Set([
|
|
217
|
+
'ArrowFunctionExpression',
|
|
218
|
+
'FunctionDeclaration',
|
|
219
|
+
'FunctionExpression',
|
|
220
|
+
'StaticBlock',
|
|
221
|
+
]);
|
|
222
|
+
|
|
223
|
+
export const forEachAstChild = (
|
|
224
|
+
node: AstNode,
|
|
225
|
+
visit: (child: AstNode) => void
|
|
226
|
+
): void => {
|
|
227
|
+
for (const val of Object.values(node)) {
|
|
228
|
+
if (Array.isArray(val)) {
|
|
229
|
+
for (const item of val) {
|
|
230
|
+
if (item && typeof item === 'object' && (item as AstNode).type) {
|
|
231
|
+
visit(item as AstNode);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
} else if (val && typeof val === 'object' && (val as AstNode).type) {
|
|
235
|
+
visit(val as AstNode);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
const recordHoistedBinding = (
|
|
241
|
+
node: AstNode,
|
|
242
|
+
into: Set<string>,
|
|
243
|
+
inNestedBlock: boolean
|
|
244
|
+
): void => {
|
|
245
|
+
if (node.type === 'VariableDeclaration') {
|
|
246
|
+
const { kind } = node as unknown as { kind?: string };
|
|
247
|
+
if (kind === 'var') {
|
|
248
|
+
addVarDeclarationBindingNames(node, into);
|
|
249
|
+
}
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
// In strict/module code, function/class/enum/module declarations inside a
|
|
253
|
+
// nested block (`if { function foo() {} }`, `switch` case, etc.) are
|
|
254
|
+
// block-scoped. Only hoist them to the enclosing function frame when they
|
|
255
|
+
// sit directly in the function body, not inside a further block.
|
|
256
|
+
if (inNestedBlock) {
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
if (
|
|
260
|
+
node.type === 'FunctionDeclaration' ||
|
|
261
|
+
node.type === 'ClassDeclaration' ||
|
|
262
|
+
node.type === 'TSEnumDeclaration' ||
|
|
263
|
+
node.type === 'TSModuleDeclaration'
|
|
264
|
+
) {
|
|
265
|
+
addFunctionOrClassBindingName(node, into);
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
const NESTED_BLOCK_BOUNDARY_TYPES = new Set([
|
|
270
|
+
'BlockStatement',
|
|
271
|
+
'ForStatement',
|
|
272
|
+
'ForInStatement',
|
|
273
|
+
'ForOfStatement',
|
|
274
|
+
'SwitchStatement',
|
|
275
|
+
'CatchClause',
|
|
276
|
+
]);
|
|
277
|
+
|
|
278
|
+
const visitForHoisted = (
|
|
279
|
+
node: AstNode,
|
|
280
|
+
isRoot: boolean,
|
|
281
|
+
into: Set<string>,
|
|
282
|
+
inNestedBlock: boolean
|
|
283
|
+
): void => {
|
|
284
|
+
if (!isRoot && FUNCTION_BOUNDARY_TYPES.has(node.type)) {
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
recordHoistedBinding(node, into, inNestedBlock);
|
|
288
|
+
const childInNestedBlock =
|
|
289
|
+
inNestedBlock || (!isRoot && NESTED_BLOCK_BOUNDARY_TYPES.has(node.type));
|
|
290
|
+
forEachAstChild(node, (child) => {
|
|
291
|
+
visitForHoisted(child, false, into, childInNestedBlock);
|
|
292
|
+
});
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Collect `var` declarations and `function` declarations hoisted to the
|
|
297
|
+
* nearest function scope from anywhere inside `root`, without composing a
|
|
298
|
+
* nested function or static-block boundary.
|
|
299
|
+
*/
|
|
300
|
+
const collectHoistedVarAndFunctionBindings = (
|
|
301
|
+
root: AstNode,
|
|
302
|
+
into: Set<string>
|
|
303
|
+
): void => {
|
|
304
|
+
visitForHoisted(root, true, into, false);
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
type FrameCollector = (node: AstNode, into: Set<string>) => void;
|
|
308
|
+
|
|
309
|
+
const collectProgramFrame: FrameCollector = (node, into) => {
|
|
310
|
+
const body = (node as unknown as { body?: readonly AstNode[] }).body ?? [];
|
|
311
|
+
for (const stmt of body) {
|
|
312
|
+
collectTopLevelStatementBindings(stmt, into);
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
const collectFunctionFrame: FrameCollector = (node, into) => {
|
|
317
|
+
const params =
|
|
318
|
+
(node as unknown as { params?: readonly AstNode[] }).params ?? [];
|
|
319
|
+
for (const param of params) {
|
|
320
|
+
addPatternBindingNames(param, into);
|
|
321
|
+
}
|
|
322
|
+
// Hoisted vars and function declarations inside the body live in the
|
|
323
|
+
// function's var-environment. A `var ns = ...;` inside an `if` still
|
|
324
|
+
// shadows a module-level `ns` for the whole function.
|
|
325
|
+
const { body } = node as unknown as { body?: AstNode };
|
|
326
|
+
if (body) {
|
|
327
|
+
collectHoistedVarAndFunctionBindings(body, into);
|
|
328
|
+
}
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
const collectBlockFrame: FrameCollector = (node, into) => {
|
|
332
|
+
const body = (node as unknown as { body?: readonly AstNode[] }).body ?? [];
|
|
333
|
+
for (const stmt of body) {
|
|
334
|
+
addBlockStatementBindings(stmt, into);
|
|
335
|
+
}
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
const collectForStatementFrame: FrameCollector = (node, into) => {
|
|
339
|
+
const { init } = node as unknown as { init?: AstNode };
|
|
340
|
+
if (init && init.type === 'VariableDeclaration') {
|
|
341
|
+
addVarDeclarationBindingNames(init, into);
|
|
342
|
+
}
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
const collectForInOfFrame: FrameCollector = (node, into) => {
|
|
346
|
+
const { left } = node as unknown as { left?: AstNode };
|
|
347
|
+
if (left && left.type === 'VariableDeclaration') {
|
|
348
|
+
addVarDeclarationBindingNames(left, into);
|
|
349
|
+
}
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
const collectSwitchStatementFrame: FrameCollector = (node, into) => {
|
|
353
|
+
// `switch` shares one scope across every case. A binding in one case
|
|
354
|
+
// shadows the namespace across sibling cases (fall-through or otherwise).
|
|
355
|
+
const cases = (node as unknown as { cases?: readonly AstNode[] }).cases ?? [];
|
|
356
|
+
for (const c of cases) {
|
|
357
|
+
const consequent =
|
|
358
|
+
(c as unknown as { consequent?: readonly AstNode[] }).consequent ?? [];
|
|
359
|
+
for (const stmt of consequent) {
|
|
360
|
+
addBlockStatementBindings(stmt, into);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
const collectCatchClauseFrame: FrameCollector = (node, into) => {
|
|
366
|
+
const { param } = node as unknown as { param?: AstNode };
|
|
367
|
+
addPatternBindingNames(param, into);
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
const collectClassExpressionFrame: FrameCollector = (node, into) => {
|
|
371
|
+
// A named `class expr` (`const C = class foo { ... }`) binds its own name
|
|
372
|
+
// inside its body only. ClassDeclaration names are hoisted into the
|
|
373
|
+
// enclosing block/program frame instead, so only class *expression* names
|
|
374
|
+
// need their own frame here.
|
|
375
|
+
addFunctionOrClassBindingName(node, into);
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
export const SCOPE_FRAME_COLLECTORS: Record<string, FrameCollector> = {
|
|
379
|
+
ArrowFunctionExpression: collectFunctionFrame,
|
|
380
|
+
BlockStatement: collectBlockFrame,
|
|
381
|
+
CatchClause: collectCatchClauseFrame,
|
|
382
|
+
ClassExpression: collectClassExpressionFrame,
|
|
383
|
+
ForInStatement: collectForInOfFrame,
|
|
384
|
+
ForOfStatement: collectForInOfFrame,
|
|
385
|
+
ForStatement: collectForStatementFrame,
|
|
386
|
+
// oxc-parser emits `FunctionBody` for `function` expression bodies; without
|
|
387
|
+
// this entry, a `const ns = ...` at the top of a function-expression body
|
|
388
|
+
// would not push a scope frame, and a module-level namespace import with
|
|
389
|
+
// the same name would be incorrectly recognized inside.
|
|
390
|
+
FunctionBody: collectBlockFrame,
|
|
391
|
+
FunctionDeclaration: collectFunctionFrame,
|
|
392
|
+
FunctionExpression: collectFunctionFrame,
|
|
393
|
+
Program: collectProgramFrame,
|
|
394
|
+
StaticBlock: collectBlockFrame,
|
|
395
|
+
SwitchStatement: collectSwitchStatementFrame,
|
|
396
|
+
};
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Collect the identifier bindings introduced *directly* by a scope frame
|
|
400
|
+
* node. Scope frames correspond to JS lexical scopes (function bodies, blocks,
|
|
401
|
+
* catch clauses, for-statements, switch statements, module/script roots).
|
|
402
|
+
*/
|
|
403
|
+
export const collectScopeFrameBindings = (
|
|
404
|
+
node: AstNode
|
|
405
|
+
): ReadonlySet<string> => {
|
|
406
|
+
const names = new Set<string>();
|
|
407
|
+
const collector = SCOPE_FRAME_COLLECTORS[node.type];
|
|
408
|
+
if (collector) {
|
|
409
|
+
collector(node, names);
|
|
410
|
+
}
|
|
411
|
+
return names;
|
|
412
|
+
};
|
|
413
|
+
|
|
414
|
+
export type ScopeAwareVisitor = (
|
|
415
|
+
node: AstNode,
|
|
416
|
+
scopes: readonly ReadonlySet<string>[]
|
|
417
|
+
) => void;
|
|
418
|
+
|
|
419
|
+
export interface ScopeWalkOptions {
|
|
420
|
+
readonly initialScopes?: readonly ReadonlySet<string>[];
|
|
421
|
+
readonly stopAtNestedFunctions?: boolean;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const asAstNode = (node: unknown): AstNode | null => {
|
|
425
|
+
if (!node || typeof node !== 'object') {
|
|
426
|
+
return null;
|
|
427
|
+
}
|
|
428
|
+
const astNode = node as AstNode;
|
|
429
|
+
return astNode.type ? astNode : null;
|
|
430
|
+
};
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Walk an AST subtree while threading lexical scope bindings through each
|
|
434
|
+
* visit. Callers can seed outer scopes and optionally stop at nested function
|
|
435
|
+
* boundaries when only the current implementation body should be analyzed.
|
|
436
|
+
*/
|
|
437
|
+
export const walkWithScopes = (
|
|
438
|
+
node: unknown,
|
|
439
|
+
visit: ScopeAwareVisitor,
|
|
440
|
+
options: ScopeWalkOptions = {}
|
|
441
|
+
): void => {
|
|
442
|
+
const root = asAstNode(node);
|
|
443
|
+
if (!root) {
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const stack = [...(options.initialScopes ?? [])];
|
|
448
|
+
|
|
449
|
+
const walkNode = (current: AstNode, isRoot: boolean): void => {
|
|
450
|
+
if (
|
|
451
|
+
!isRoot &&
|
|
452
|
+
options.stopAtNestedFunctions &&
|
|
453
|
+
FUNCTION_BOUNDARY_TYPES.has(current.type)
|
|
454
|
+
) {
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
const isScope = current.type in SCOPE_FRAME_COLLECTORS;
|
|
459
|
+
if (isScope) {
|
|
460
|
+
stack.unshift(collectScopeFrameBindings(current));
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
try {
|
|
464
|
+
visit(current, stack);
|
|
465
|
+
forEachAstChild(current, (child) => {
|
|
466
|
+
walkNode(child, false);
|
|
467
|
+
});
|
|
468
|
+
} finally {
|
|
469
|
+
if (isScope) {
|
|
470
|
+
stack.shift();
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
};
|
|
474
|
+
|
|
475
|
+
walkNode(root, true);
|
|
476
|
+
};
|
|
477
|
+
|
|
478
|
+
export const isShadowed = (
|
|
479
|
+
receiverName: string,
|
|
480
|
+
scopeStack: readonly ReadonlySet<string>[]
|
|
481
|
+
): boolean => {
|
|
482
|
+
// The module-level Program frame is the last entry and contains the
|
|
483
|
+
// namespace imports themselves. A "shadow" must come from a frame *inside*
|
|
484
|
+
// that one — i.e. any frame except the outermost.
|
|
485
|
+
for (let i = 0; i < scopeStack.length - 1; i += 1) {
|
|
486
|
+
const frame = scopeStack[i];
|
|
487
|
+
if (frame?.has(receiverName)) {
|
|
488
|
+
return true;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
return false;
|
|
492
|
+
};
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Return `true` when `node` is a non-computed member access (`a.b` /
|
|
496
|
+
* `a?.b`) and `false` for anything else, including computed access
|
|
497
|
+
* (`a[b]`) or non-member nodes. Exported as the canonical predicate so
|
|
498
|
+
* rule modules do not re-implement the check.
|
|
499
|
+
*
|
|
500
|
+
* @remarks
|
|
501
|
+
* Declared near the top of the file so the scope walker can use it
|
|
502
|
+
* without hitting `no-use-before-define`. A few sibling helpers in this
|
|
503
|
+
* module still inline the same shape under different local names for
|
|
504
|
+
* historical reasons; prefer this export for new call sites.
|
|
505
|
+
*/
|
|
506
|
+
export const isMemberAccessNonComputed = (node: AstNode): boolean => {
|
|
507
|
+
if (
|
|
508
|
+
node.type !== 'MemberExpression' &&
|
|
509
|
+
node.type !== 'StaticMemberExpression'
|
|
510
|
+
) {
|
|
511
|
+
return false;
|
|
512
|
+
}
|
|
513
|
+
return (node as unknown as { computed?: boolean }).computed !== true;
|
|
514
|
+
};
|
|
515
|
+
|
|
516
|
+
export const isScopeFrameNode = (node: AstNode): boolean =>
|
|
517
|
+
node.type in SCOPE_FRAME_COLLECTORS;
|