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/README.md +164 -0
- package/analyzer.js +290 -0
- package/binding.js +63 -0
- package/decode.js +1462 -0
- package/engine.js +198 -0
- package/index.d.ts +578 -0
- package/index.js +2 -0
- package/module.js +611 -0
- package/package.json +54 -0
- package/walk.js +43 -0
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
|
+
}
|