yuku-analyzer 0.5.32

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/module.js ADDED
@@ -0,0 +1,611 @@
1
+ // One analyzed file, the AST plus the scope/symbol/reference/import/
2
+ // export object graph, built lazily over the decoded analyzer buffer.
3
+ // All wire reads go through the generated `semantic` accessors from
4
+ // decode.js. Nothing in this file knows the buffer layout.
5
+
6
+ import binding from "./binding.js";
7
+ import { decode, SymbolFlags } from "./decode.js";
8
+ import { walkModule } from "./walk.js";
9
+
10
+ export function langFromPath(path) {
11
+ if (path.endsWith(".d.ts") || path.endsWith(".d.mts") || path.endsWith(".d.cts")) return "dts";
12
+ if (path.endsWith(".tsx")) return "tsx";
13
+ if (path.endsWith(".ts") || path.endsWith(".mts") || path.endsWith(".cts")) return "ts";
14
+ if (path.endsWith(".jsx")) return "jsx";
15
+ return "js";
16
+ }
17
+
18
+ export function sourceTypeFromPath(path) {
19
+ return path.endsWith(".cjs") || path.endsWith(".cts") ? "script" : "module";
20
+ }
21
+
22
+ class Scope {
23
+ #sem;
24
+ constructor(module, sem, id) {
25
+ this.module = module;
26
+ this.id = id;
27
+ this.#sem = sem;
28
+ }
29
+ get kind() {
30
+ return this.#sem.scope.kind(this.id);
31
+ }
32
+ get strict() {
33
+ return this.#sem.scope.strict(this.id);
34
+ }
35
+ get node() {
36
+ return this.#sem.scope.node(this.id);
37
+ }
38
+ get parent() {
39
+ const p = this.#sem.scope.parentId(this.id);
40
+ return p === null ? null : this.module.scopes[p];
41
+ }
42
+ get hoistTarget() {
43
+ return this.module.scopes[this.#sem.scope.hoistTargetId(this.id)];
44
+ }
45
+ get bindings() {
46
+ return this.module._scopeBindings(this.id);
47
+ }
48
+ find(name) {
49
+ for (const symbol of this.bindings) if (symbol.name === name) return symbol;
50
+ return null;
51
+ }
52
+ contains(other) {
53
+ for (let s = other; s; s = s.parent) if (s === this) return true;
54
+ return false;
55
+ }
56
+ *ancestors() {
57
+ for (let s = this; s; s = s.parent) yield s;
58
+ }
59
+ }
60
+
61
+ class Symbol {
62
+ #sem;
63
+ constructor(module, sem, id) {
64
+ this.module = module;
65
+ this.id = id;
66
+ this.#sem = sem;
67
+ }
68
+ get name() {
69
+ return this.#sem.symbol.name(this.id);
70
+ }
71
+ get flags() {
72
+ return this.#sem.symbol.flags(this.id);
73
+ }
74
+ get scope() {
75
+ return this.module.scopes[this.#sem.symbol.scopeId(this.id)];
76
+ }
77
+ get declarations() {
78
+ const { symbol } = this.#sem;
79
+ const out = Array.from({ length: symbol.declCount(this.id) });
80
+ for (let i = 0; i < out.length; i++) out[i] = symbol.declNode(this.id, i);
81
+ return out;
82
+ }
83
+ get references() {
84
+ return this.module._referencesOfSymbol(this.id);
85
+ }
86
+ has(mask) {
87
+ return (this.flags & mask) !== 0;
88
+ }
89
+ hasAll(mask) {
90
+ return (this.flags & mask) === mask;
91
+ }
92
+ definition() {
93
+ return this.module.analyzer.definitionOf(this);
94
+ }
95
+ }
96
+
97
+ class Reference {
98
+ #sem;
99
+ constructor(module, sem, id) {
100
+ this.module = module;
101
+ this.id = id;
102
+ this.#sem = sem;
103
+ }
104
+ get name() {
105
+ return this.#sem.reference.name(this.id);
106
+ }
107
+ get scope() {
108
+ return this.module.scopes[this.#sem.reference.scopeId(this.id)];
109
+ }
110
+ get node() {
111
+ return this.#sem.reference.node(this.id);
112
+ }
113
+ get kind() {
114
+ return this.#sem.reference.kind(this.id);
115
+ }
116
+ get isWrite() {
117
+ return this.#sem.reference.isWrite(this.id);
118
+ }
119
+ get symbol() {
120
+ const s = this.#sem.reference.symbolId(this.id);
121
+ return s === null ? null : this.module.symbols[s];
122
+ }
123
+ }
124
+
125
+ class Import {
126
+ #sem;
127
+ constructor(module, sem, id) {
128
+ this.module = module;
129
+ this.id = id;
130
+ this.#sem = sem;
131
+ this._resolved = null;
132
+ }
133
+ get #nameKind() {
134
+ return this.#sem.import.nameKind(this.id);
135
+ }
136
+ get local() {
137
+ const s = this.#sem.import.symbolId(this.id);
138
+ return s === null ? null : this.module.symbols[s];
139
+ }
140
+ get name() {
141
+ return this.#nameKind === "named" ? this.#sem.import.name(this.id) : null;
142
+ }
143
+ get isNamespace() {
144
+ return this.#nameKind === "star" && this.local !== null;
145
+ }
146
+ get isSideEffect() {
147
+ return this.#nameKind === "none";
148
+ }
149
+ get typeOnly() {
150
+ return this.#sem.import.typeOnly(this.id);
151
+ }
152
+ get phase() {
153
+ return this.#sem.import.phase(this.id);
154
+ }
155
+ get specifier() {
156
+ return this.#sem.import.specifier(this.id);
157
+ }
158
+ get node() {
159
+ return this.#sem.import.node(this.id);
160
+ }
161
+ get resolvedModule() {
162
+ this.module.analyzer._ensureLinked();
163
+ return this._resolved;
164
+ }
165
+ }
166
+
167
+ class Export {
168
+ #sem;
169
+ constructor(module, sem, id) {
170
+ this.module = module;
171
+ this.id = id;
172
+ this.#sem = sem;
173
+ this._resolved = null;
174
+ }
175
+ get #nameKind() {
176
+ return this.#sem.export.nameKind(this.id);
177
+ }
178
+ get #fromKind() {
179
+ return this.#sem.export.fromKind(this.id);
180
+ }
181
+ get name() {
182
+ return this.#nameKind === "named" ? this.#sem.export.name(this.id) : null;
183
+ }
184
+ get isStar() {
185
+ return this.#nameKind === "star";
186
+ }
187
+ get isExportEquals() {
188
+ return this.#nameKind === "equals";
189
+ }
190
+ get globalName() {
191
+ return this.#nameKind === "global" ? this.#sem.export.name(this.id) : null;
192
+ }
193
+ get typeOnly() {
194
+ return this.#sem.export.typeOnly(this.id);
195
+ }
196
+ get local() {
197
+ const s = this.#sem.export.symbolId(this.id);
198
+ return s === null ? null : this.module.symbols[s];
199
+ }
200
+ get specifier() {
201
+ return this.#fromKind === "none" ? null : this.#sem.export.specifier(this.id);
202
+ }
203
+ get fromName() {
204
+ return this.#fromKind === "named" ? this.#sem.export.fromName(this.id) : null;
205
+ }
206
+ get isNamespaceReexport() {
207
+ return this.#fromKind === "star";
208
+ }
209
+ get node() {
210
+ return this.#sem.export.node(this.id);
211
+ }
212
+ get resolvedModule() {
213
+ this.module.analyzer._ensureLinked();
214
+ return this._resolved;
215
+ }
216
+ }
217
+
218
+ // The semantic scan cursor, the generated buffer cursor plus symbol and
219
+ // reference lookups, which are index-keyed and so need no AST nodes.
220
+ class ScanCursor {
221
+ _raw = null;
222
+ #module;
223
+ constructor(module) {
224
+ this.#module = module;
225
+ }
226
+ get module() {
227
+ return this.#module;
228
+ }
229
+ get index() {
230
+ return this._raw.index;
231
+ }
232
+ get type() {
233
+ return this._raw.type;
234
+ }
235
+ get start() {
236
+ return this._raw.start;
237
+ }
238
+ get end() {
239
+ return this._raw.end;
240
+ }
241
+ get symbol() {
242
+ return this.#module._symbolByIndex(this._raw.index);
243
+ }
244
+ get reference() {
245
+ return this.#module._referenceByIndex(this._raw.index);
246
+ }
247
+ node() {
248
+ return this._raw.node();
249
+ }
250
+ skip() {
251
+ this._raw.skip();
252
+ }
253
+ stop() {
254
+ this._raw.stop();
255
+ }
256
+ }
257
+
258
+ export class Module {
259
+ #r;
260
+ #sem;
261
+ #scopes = null;
262
+ #symbols = null;
263
+ #references = null;
264
+ #unresolved = null;
265
+ #imports = null;
266
+ #exports = null;
267
+ #scopeBindings = null;
268
+ #symbolReferences = null;
269
+ #declToSymbol = null;
270
+ #nodeToReference = null;
271
+ #functionScopes = null;
272
+ #scopeMap = null;
273
+ #exportMap = null;
274
+ #starExports = null;
275
+ #importBySymbol = null;
276
+ _deps = [];
277
+ _dependents = [];
278
+
279
+ constructor(analyzer, path, source, options = {}) {
280
+ this.analyzer = analyzer;
281
+ this.path = path;
282
+ this.source = source;
283
+ this.#r = decode(
284
+ binding.analyze(source, {
285
+ lang: options.lang ?? langFromPath(path),
286
+ sourceType: options.sourceType ?? sourceTypeFromPath(path),
287
+ preserveParens: options.preserveParens,
288
+ allowReturnOutsideFunction: options.allowReturnOutsideFunction,
289
+ attachComments: options.attachComments,
290
+ }),
291
+ source,
292
+ );
293
+ this.#sem = this.#r.semantic;
294
+ }
295
+
296
+ get ast() {
297
+ return this.#r.program;
298
+ }
299
+ get diagnostics() {
300
+ return this.#r.diagnostics;
301
+ }
302
+ get comments() {
303
+ return this.#r.comments;
304
+ }
305
+ get lineStarts() {
306
+ return this.#r.lineStarts;
307
+ }
308
+ locOf(offset) {
309
+ return this.#r.locOf(offset);
310
+ }
311
+
312
+ get scopes() {
313
+ return (this.#scopes ??= this.#rows(Scope, this.#sem.scope.count));
314
+ }
315
+ get symbols() {
316
+ return (this.#symbols ??= this.#rows(Symbol, this.#sem.symbol.count));
317
+ }
318
+ get references() {
319
+ return (this.#references ??= this.#rows(Reference, this.#sem.reference.count));
320
+ }
321
+ get imports() {
322
+ return (this.#imports ??= this.#rows(Import, this.#sem.import.count));
323
+ }
324
+ get exports() {
325
+ return (this.#exports ??= this.#rows(Export, this.#sem.export.count));
326
+ }
327
+ get unresolvedReferences() {
328
+ return (this.#unresolved ??= this.references.filter((r) => r.symbol === null));
329
+ }
330
+
331
+ get rootScope() {
332
+ const scopes = this.scopes;
333
+ if (scopes.length > 1 && scopes[1].kind === "module") return scopes[1];
334
+ return scopes[0];
335
+ }
336
+
337
+ get dependencies() {
338
+ this.analyzer._ensureLinked();
339
+ return this._deps;
340
+ }
341
+ get dependents() {
342
+ this.analyzer._ensureLinked();
343
+ return this._dependents;
344
+ }
345
+
346
+ symbolOf(node) {
347
+ const index = this.#r.indexOf(node);
348
+ return index === undefined ? null : this._symbolByIndex(index);
349
+ }
350
+
351
+ referenceOf(node) {
352
+ const index = this.#r.indexOf(node);
353
+ return index === undefined ? null : this._referenceByIndex(index);
354
+ }
355
+
356
+ scopeOf(node) {
357
+ const index = this.#r.indexOf(node);
358
+ if (index === undefined) return this.rootScope;
359
+ const start = this.#r.startOf(index);
360
+ const end = this.#r.endOf(index);
361
+ const { scope } = this.#sem;
362
+ let best = this.scopes[0];
363
+ let bestDepth = 0;
364
+ for (let i = 1; i < scope.count; i++) {
365
+ if (scope.start(i) > start || scope.end(i) < end) continue;
366
+ let depth = 0;
367
+ for (let p = i; p !== null; p = scope.parentId(p)) depth++;
368
+ if (depth > bestDepth) {
369
+ best = this.scopes[i];
370
+ bestDepth = depth;
371
+ }
372
+ }
373
+ return best;
374
+ }
375
+
376
+ resolve(name, from = this.rootScope) {
377
+ for (let s = from; s; s = s.parent) {
378
+ const found = s.find(name);
379
+ if (found) return found;
380
+ }
381
+ return null;
382
+ }
383
+
384
+ // GetExportedNames(exportStarSet), 16.2.1.7.2.1: every name exported
385
+ // directly or through `export *` chains. ambiguous star names are not
386
+ // filtered (spec note), "default" never crosses a star boundary, and
387
+ // circular `export *` terminates via exportStarSet. TS `export =` /
388
+ // `export as namespace` are not ESM export names and do not appear.
389
+ exportedNames(exportStarSet = new Set()) {
390
+ if (exportStarSet.has(this)) return [];
391
+ exportStarSet.add(this);
392
+ this.analyzer._ensureLinked();
393
+ const names = new Set(this._exportMap().keys());
394
+ for (const star of this._starExports()) {
395
+ if (star._resolved === null) continue;
396
+ for (const name of star._resolved.exportedNames(exportStarSet)) {
397
+ if (name !== "default") names.add(name);
398
+ }
399
+ }
400
+ return [...names];
401
+ }
402
+
403
+ // the free variables of a function: every binding referenced inside
404
+ // `fn` (nested functions included, value positions only) that is
405
+ // declared outside its scope subtree, deduplicated by symbol with
406
+ // `isWritten` OR-ed across the references. only bindings count:
407
+ // `this`, `arguments`, and unresolved/global names carry no symbol
408
+ // and never appear; module-scope and import bindings count like any
409
+ // other outer binding. computed method keys evaluate in the enclosing
410
+ // scope, outside the function node's span, so they are not captures.
411
+ capturesOf(fn) {
412
+ const index = this.#r.indexOf(fn);
413
+ if (index === undefined) {
414
+ throw new TypeError("capturesOf: node does not belong to this module's AST");
415
+ }
416
+ const fnScopeId = this.#functionScopeOf(index);
417
+ if (fnScopeId === undefined) {
418
+ throw new TypeError("capturesOf: node does not create a function scope");
419
+ }
420
+ const start = this.#r.startOf(index);
421
+ const end = this.#r.endOf(index);
422
+ const { scope, symbol, reference } = this.#sem;
423
+ const captures = new Map();
424
+ for (let i = 0; i < reference.count; i++) {
425
+ const symbolId = reference.symbolId(i);
426
+ if (symbolId === null || reference.kind(i) === "type") continue;
427
+ if (reference.start(i) < start || reference.end(i) > end) continue;
428
+ let inside = false;
429
+ for (let s = symbol.scopeId(symbolId); s !== null; s = scope.parentId(s)) {
430
+ if (s === fnScopeId) {
431
+ inside = true;
432
+ break;
433
+ }
434
+ }
435
+ if (inside) continue;
436
+ let capture = captures.get(symbolId);
437
+ if (capture === undefined) {
438
+ capture = { symbol: this.symbols[symbolId], references: [], isWritten: false };
439
+ captures.set(symbolId, capture);
440
+ }
441
+ capture.references.push(this.references[i]);
442
+ if (reference.isWrite(i)) capture.isWritten = true;
443
+ }
444
+ return [...captures.values()];
445
+ }
446
+
447
+ walk(visitor, root) {
448
+ walkModule(this, visitor, root);
449
+ }
450
+
451
+ // readonly buffer scan with a semantic cursor, resolving symbols and
452
+ // references in index space, materializing no AST nodes.
453
+ scan(visitors) {
454
+ const cursor = new ScanCursor(this);
455
+ const wrapped = {};
456
+ for (const key of Object.keys(visitors)) {
457
+ const fn = visitors[key];
458
+ if (typeof fn !== "function") continue;
459
+ wrapped[key] = (raw) => {
460
+ cursor._raw = raw;
461
+ fn(cursor);
462
+ };
463
+ }
464
+ this.#r.scan(wrapped);
465
+ }
466
+
467
+ findAll(types) {
468
+ const single = typeof types === "string" ? types : null;
469
+ const set = single === null ? new Set(types) : null;
470
+ const out = [];
471
+ this.walk({
472
+ enter(node) {
473
+ if (single === null ? set.has(node.type) : node.type === single) out.push(node);
474
+ },
475
+ });
476
+ return out;
477
+ }
478
+
479
+ _symbolByIndex(index) {
480
+ const declared = this.#declMap().get(index);
481
+ if (declared !== undefined) return this.symbols[declared];
482
+ const ref = this.#refMap().get(index);
483
+ return ref !== undefined ? this.references[ref].symbol : null;
484
+ }
485
+
486
+ _referenceByIndex(index) {
487
+ const ref = this.#refMap().get(index);
488
+ return ref !== undefined ? this.references[ref] : null;
489
+ }
490
+
491
+ _scopeBindings(scopeId) {
492
+ if (this.#scopeBindings === null) {
493
+ const lists = Array.from({ length: this.#sem.scope.count }, () => []);
494
+ for (const symbol of this.symbols) {
495
+ lists[this.#sem.symbol.scopeId(symbol.id)].push(symbol);
496
+ }
497
+ this.#scopeBindings = lists;
498
+ }
499
+ return this.#scopeBindings[scopeId];
500
+ }
501
+
502
+ _referencesOfSymbol(symbolId) {
503
+ if (this.#symbolReferences === null) {
504
+ const lists = Array.from({ length: this.#sem.symbol.count }, () => []);
505
+ for (const ref of this.references) {
506
+ const s = this.#sem.reference.symbolId(ref.id);
507
+ if (s !== null) lists[s].push(ref);
508
+ }
509
+ this.#symbolReferences = lists;
510
+ }
511
+ return this.#symbolReferences[symbolId];
512
+ }
513
+
514
+ // scope-creating node object -> innermost Scope (keep-last covers
515
+ // shared nodes, global/module on program, expression_name/function).
516
+ _scopeMap() {
517
+ if (this.#scopeMap === null) {
518
+ const byNode = new WeakMap();
519
+ const types = new Set();
520
+ for (const scope of this.scopes) {
521
+ const node = this.#sem.scope.node(scope.id);
522
+ byNode.set(node, scope);
523
+ types.add(node.type);
524
+ }
525
+ this.#scopeMap = { byNode, types };
526
+ }
527
+ return this.#scopeMap;
528
+ }
529
+
530
+ // the spec's export-entry partition (ParseModule, 16.2.1.7.1): a
531
+ // name-keyed map of the local + indirect entries and the star-entry
532
+ // list (`export *` only). name-less records (`export *`, TS
533
+ // `export =` / `export as namespace`) stay out of the map. duplicate
534
+ // names are an early error; in recovery trees the first record wins.
535
+ _exportMap() {
536
+ if (this.#exportMap === null) {
537
+ const map = new Map();
538
+ const stars = [];
539
+ for (const record of this.exports) {
540
+ if (record.isStar) stars.push(record);
541
+ else if (record.name !== null && !map.has(record.name)) map.set(record.name, record);
542
+ }
543
+ this.#exportMap = map;
544
+ this.#starExports = stars;
545
+ }
546
+ return this.#exportMap;
547
+ }
548
+
549
+ _starExports() {
550
+ this._exportMap();
551
+ return this.#starExports;
552
+ }
553
+
554
+ _importOfSymbol(symbolId) {
555
+ if (this.#importBySymbol === null) {
556
+ const map = new Map();
557
+ for (const record of this.imports) {
558
+ const local = record.local;
559
+ if (local !== null) map.set(local.id, record);
560
+ }
561
+ this.#importBySymbol = map;
562
+ }
563
+ return this.#importBySymbol.get(symbolId);
564
+ }
565
+
566
+ #rows(Row, count) {
567
+ const out = Array.from({ length: count });
568
+ for (let i = 0; i < count; i++) out[i] = new Row(this, this.#sem, i);
569
+ return out;
570
+ }
571
+
572
+ #declMap() {
573
+ if (this.#declToSymbol === null) {
574
+ const map = new Map();
575
+ const { symbol } = this.#sem;
576
+ for (let s = 0; s < symbol.count; s++) {
577
+ const len = symbol.declCount(s);
578
+ for (let i = 0; i < len; i++) map.set(symbol.declNodeIndex(s, i), s);
579
+ }
580
+ this.#declToSymbol = map;
581
+ }
582
+ return this.#declToSymbol;
583
+ }
584
+
585
+ #refMap() {
586
+ if (this.#nodeToReference === null) {
587
+ const map = new Map();
588
+ const { reference } = this.#sem;
589
+ for (let i = 0; i < reference.count; i++) map.set(reference.nodeIndex(i), i);
590
+ this.#nodeToReference = map;
591
+ }
592
+ return this.#nodeToReference;
593
+ }
594
+
595
+ // node index -> the function scope it creates. named function
596
+ // expressions share the node with their expression_name scope. the
597
+ // function scope has the higher id, so keep-last wins.
598
+ #functionScopeOf(nodeIndex) {
599
+ if (this.#functionScopes === null) {
600
+ const map = new Map();
601
+ const { scope } = this.#sem;
602
+ for (let i = 0; i < scope.count; i++) {
603
+ if (scope.kind(i) === "function") map.set(scope.nodeIndex(i), i);
604
+ }
605
+ this.#functionScopes = map;
606
+ }
607
+ return this.#functionScopes.get(nodeIndex);
608
+ }
609
+ }
610
+
611
+ export { SymbolFlags };
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "yuku-analyzer",
3
+ "version": "0.5.32",
4
+ "description": "High-performance JavaScript/TypeScript semantic analyzer: per-file scopes, symbols, references, and cross-file module linking",
5
+ "keywords": [
6
+ "analyzer",
7
+ "ast",
8
+ "bindings",
9
+ "compiler",
10
+ "cross-file",
11
+ "javascript",
12
+ "linker",
13
+ "module-graph",
14
+ "references",
15
+ "scope",
16
+ "semantic",
17
+ "symbols",
18
+ "typescript"
19
+ ],
20
+ "license": "MIT",
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "https://github.com/yuku-toolchain/yuku"
24
+ },
25
+ "files": [
26
+ "index.js",
27
+ "index.d.ts",
28
+ "analyzer.js",
29
+ "module.js",
30
+ "walk.js",
31
+ "engine.js",
32
+ "binding.js",
33
+ "decode.js"
34
+ ],
35
+ "type": "module",
36
+ "main": "index.js",
37
+ "types": "index.d.ts",
38
+ "dependencies": {
39
+ "@yuku/types": "workspace:*"
40
+ },
41
+ "optionalDependencies": {
42
+ "@yuku-analyzer/binding-darwin-arm64": "0.5.32",
43
+ "@yuku-analyzer/binding-darwin-x64": "0.5.32",
44
+ "@yuku-analyzer/binding-freebsd-x64": "0.5.32",
45
+ "@yuku-analyzer/binding-linux-arm-gnu": "0.5.32",
46
+ "@yuku-analyzer/binding-linux-arm-musl": "0.5.32",
47
+ "@yuku-analyzer/binding-linux-arm64-gnu": "0.5.32",
48
+ "@yuku-analyzer/binding-linux-arm64-musl": "0.5.32",
49
+ "@yuku-analyzer/binding-linux-x64-gnu": "0.5.32",
50
+ "@yuku-analyzer/binding-linux-x64-musl": "0.5.32",
51
+ "@yuku-analyzer/binding-win32-arm64": "0.5.32",
52
+ "@yuku-analyzer/binding-win32-x64": "0.5.32"
53
+ }
54
+ }
package/walk.js ADDED
@@ -0,0 +1,43 @@
1
+ import { WalkContext, _walk } from "./engine.js";
2
+
3
+ class SemanticWalkContext extends WalkContext {
4
+ #module;
5
+ #scopes;
6
+ constructor(module, scopes) {
7
+ super();
8
+ this.#module = module;
9
+ this.#scopes = scopes;
10
+ }
11
+ get module() {
12
+ return this.#module;
13
+ }
14
+ get scope() {
15
+ return this.#scopes[this.#scopes.length - 1];
16
+ }
17
+ get symbol() {
18
+ return this.#module.symbolOf(this._node);
19
+ }
20
+ get reference() {
21
+ return this.#module.referenceOf(this._node);
22
+ }
23
+ }
24
+
25
+ export function walkModule(module, visitors, root) {
26
+ const { byNode, types } = module._scopeMap();
27
+ const start = root ?? module.ast;
28
+ const scopes = [module.scopeOf(start)];
29
+ const hooks = {
30
+ // the scope was created for the original node. a replacement keeps
31
+ // the same lexical position, so push/pop stays balanced on it
32
+ enter(node) {
33
+ const scope = types.has(node.type) ? byNode.get(node) : undefined;
34
+ if (scope === undefined) return false;
35
+ scopes.push(scope);
36
+ return true;
37
+ },
38
+ exit(pushed) {
39
+ if (pushed) scopes.pop();
40
+ },
41
+ };
42
+ _walk(start, visitors, undefined, hooks, new SemanticWalkContext(module, scopes));
43
+ }