yuku-analyzer 0.5.48 → 0.6.2

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 CHANGED
@@ -37,7 +37,7 @@ console.log(def.symbol.has(SymbolFlags.Const)); // true
37
37
 
38
38
  ## How it works
39
39
 
40
- Nothing semantic is reimplemented in JavaScript. The binder, scope tree, reference resolution, and module records are computed by the same well-tested native analyzer that powers the rest of Yuku, then shipped to JavaScript as one compact buffer that the JS side only decodes, through lazy zero-copy views. That handoff is usually where native tooling stalls: either every query pays an FFI round trip, or the semantics get a hand-written JS twin that slowly drifts. Here there is one implementation and one crossing, so it cannot drift, and the corner cases arrive already correct: catch-clause scope sharing, named function expression scopes, `var` hoist targets, TS declaration merging, value space versus type space, and write detection through destructuring patterns.
40
+ Nothing semantic is reimplemented in JavaScript. The binder, scope tree, reference resolution, and module records are computed by the same well-tested native analyzer that powers the rest of Yuku, then shipped to JavaScript as one compact buffer that the JS side only decodes, through lazy zero-copy views. That handoff is usually where native tooling stalls: either every query pays an FFI round trip, or the semantics get a hand-written JS twin that slowly drifts. Here there is one implementation and one crossing, so it cannot drift, and the corner cases arrive already correct: catch-clause scope sharing, named function expression scopes, `var` hoist targets, TS declaration merging, space-aware resolution (a `const T` never captures a type-position `T` away from an outer `type T`), and write detection through destructuring patterns.
41
41
 
42
42
  ## What one `addFile` gives you
43
43
 
@@ -49,8 +49,9 @@ module.scopes // every lexical scope, as a tree
49
49
  module.symbols // every declared binding
50
50
  module.references // every identifier use, resolved to its symbol
51
51
  module.unresolvedReferences // free names and globals
52
- module.imports // spec-true import records
52
+ module.imports // import records, dynamic import() and require() included
53
53
  module.exports // spec-true export records
54
+ module.moduleFlags // CommonJS classification signals
54
55
  module.diagnostics // syntax + semantic errors
55
56
  ```
56
57
 
@@ -61,9 +62,12 @@ module.symbolOf(node) // the symbol a node declares or references
61
62
  module.referenceOf(node) // the reference recorded for an identifier
62
63
  module.scopeOf(node) // the innermost scope containing the node
63
64
  module.parentOf(node) // the node that structurally contains it, or null
64
- module.resolve("name") // scope-chain lookup, like the engine does at runtime
65
+ module.resolve("name") // scope-chain lookup, like the engine at runtime
66
+ module.resolve("T", scope, "type") // or in another declaration space
65
67
  ```
66
68
 
69
+ Every reference carries the declaration space its position resolves in (`ref.space`: `"value"`, `"type"`, `"namespace"`, `"typeof"`, or `"any"`), and resolution is space-aware the way TypeScript's is: an inner `const T` does not capture a type annotation's `T` away from an outer `type T`.
70
+
67
71
  Node identity is exact: the node you reach by walking `module.ast` and the node a semantic query returns are the same JavaScript object, so `===` always works.
68
72
 
69
73
  ## Editing the AST
@@ -151,7 +155,7 @@ const analyzer = new Analyzer({
151
155
 
152
156
  ## Performance
153
157
 
154
- Analysis runs in the native parser pass, so full semantics cost roughly half of parsing time on top of the parse itself. Validated against 55,000+ real-world files.
158
+ Analysis runs in the native parser pass, so full semantics cost roughly half of parsing time on top of the parse itself. Validated against every file in the [parser test corpus](https://yuku.fyi/testing/).
155
159
 
156
160
  Concretely, on an Apple M-series machine: parsing plus complete semantic analysis of a typical source file lands well under a millisecond, walking sustains tens of millions of nodes per second, and linking a 2,000-module graph takes about a millisecond.
157
161
 
package/decode.js CHANGED
@@ -204,8 +204,11 @@ _ck("JSXText", []);
204
204
  _ck("JSXSpreadChild", ["expression"]);
205
205
  _ck("Hashbang", []);
206
206
  const SCOPE_KINDS = ["global", "module", "function", "block", "class", "staticBlock", "expressionName", "tsModule"];
207
- const NAME_KINDS = ["named", "star", "none", "equals", "global"];
208
207
  const IMPORT_PHASES = ["source", "defer"];
208
+ const IMPORT_KINDS = ["named", "namespace", "sideEffect", "importEquals", "dynamic", "require"];
209
+ const EXPORT_KINDS = ["named", "reExport", "namespace", "star", "equals", "global"];
210
+ const REFERENCE_SPACES = ["value", "type", "namespace", "typeof", "any"];
211
+ const REFERENCE_TYPE_POSITION = [false, true, true, true, false];
209
212
  const SymbolFlags = Object.freeze({
210
213
  FunctionScopedVariable: 1 << 0,
211
214
  BlockScopedVariable: 1 << 1,
@@ -230,6 +233,7 @@ const SymbolFlags = Object.freeze({
230
233
  Import: 6144,
231
234
  ValueSpace: 127,
232
235
  TypeSpace: 952,
236
+ NamespaceSpace: 1136,
233
237
  });
234
238
  const CHILD_SLOTS = [
235
239
  [2, 2],
@@ -1265,7 +1269,7 @@ function decode(buffer, source) {
1265
1269
  const scopeCount = _u32[o], symbolCount = _u32[o + 1],
1266
1270
  referenceCount = _u32[o + 2], declNodeCount = _u32[o + 3],
1267
1271
  importCount = _u32[o + 4], exportCount = _u32[o + 5],
1268
- nodeScopeCount = _u32[o + 6];
1272
+ nodeScopeCount = _u32[o + 6], moduleFlags = _u32[o + 7];
1269
1273
  o += 8;
1270
1274
  const scopes = _u32.subarray(o, o + scopeCount * 4);
1271
1275
  o += scopeCount * 4;
@@ -1311,16 +1315,17 @@ function decode(buffer, source) {
1311
1315
  scopeId: (i) => references[i * 6 + 2],
1312
1316
  node: (i) => node(references[i * 6 + 3]),
1313
1317
  nodeIndex: (i) => references[i * 6 + 3],
1314
- kind: (i) => IMPORT_EXPORT_KINDS[(references[i * 6 + 4] >> 0) & 1],
1315
- isWrite: (i) => ((references[i * 6 + 4] >> 1) & 1) !== 0,
1318
+ space: (i) => REFERENCE_SPACES[(references[i * 6 + 4] >> 1) & 7],
1319
+ inTypePosition: (i) => REFERENCE_TYPE_POSITION[(references[i * 6 + 4] >> 1) & 7],
1320
+ isWrite: (i) => ((references[i * 6 + 4] >> 0) & 1) !== 0,
1316
1321
  symbolId: (i) => _id(references[i * 6 + 5]),
1317
1322
  start: (i) => startOf(references[i * 6 + 3]),
1318
1323
  end: (i) => endOf(references[i * 6 + 3]),
1319
1324
  },
1320
1325
  import: {
1321
1326
  count: importCount,
1327
+ kind: (i) => IMPORT_KINDS[imports[i * 8 + 1] & 7],
1322
1328
  symbolId: (i) => _id(imports[i * 8 + 0]),
1323
- nameKind: (i) => NAME_KINDS[imports[i * 8 + 1] & 7],
1324
1329
  name: (i) => str(imports[i * 8 + 2], imports[i * 8 + 3]),
1325
1330
  specifier: (i) => str(imports[i * 8 + 4], imports[i * 8 + 5]),
1326
1331
  typeOnly: (i) => ((imports[i * 8 + 1] >> 3) & 1) !== 0,
@@ -1332,15 +1337,20 @@ function decode(buffer, source) {
1332
1337
  },
1333
1338
  export: {
1334
1339
  count: exportCount,
1335
- nameKind: (i) => NAME_KINDS[exports[i * 10 + 0] & 7],
1336
- fromKind: (i) => NAME_KINDS[(exports[i * 10 + 0] >> 3) & 7],
1337
- typeOnly: (i) => ((exports[i * 10 + 0] >> 6) & 1) !== 0,
1340
+ kind: (i) => EXPORT_KINDS[exports[i * 10 + 0] & 7],
1341
+ typeOnly: (i) => ((exports[i * 10 + 0] >> 3) & 1) !== 0,
1338
1342
  name: (i) => str(exports[i * 10 + 1], exports[i * 10 + 2]),
1339
1343
  fromName: (i) => str(exports[i * 10 + 4], exports[i * 10 + 5]),
1340
1344
  specifier: (i) => str(exports[i * 10 + 6], exports[i * 10 + 7]),
1341
1345
  symbolId: (i) => _id(exports[i * 10 + 3]),
1342
1346
  node: (i) => node(exports[i * 10 + 8]),
1343
1347
  },
1348
+ moduleFlags: {
1349
+ usesRequire: (moduleFlags & 1) !== 0,
1350
+ usesModule: (moduleFlags & 2) !== 0,
1351
+ usesExports: (moduleFlags & 4) !== 0,
1352
+ usesImportMeta: (moduleFlags & 8) !== 0,
1353
+ },
1344
1354
  nodeScope: (i) => nodeScopes[i],
1345
1355
  });
1346
1356
  }
package/index.d.ts CHANGED
@@ -123,8 +123,25 @@ declare const SymbolFlags: {
123
123
  readonly ValueSpace: number;
124
124
  /** Composite: referencable from a type position (class, enum, interface, alias, type param). */
125
125
  readonly TypeSpace: number;
126
+ /** Composite: what a dotted type name starts from (namespace, enum). */
127
+ readonly NamespaceSpace: number;
126
128
  };
127
129
 
130
+ /**
131
+ * The declaration space a reference position resolves in, matching
132
+ * TypeScript name resolution. A binding outside a reference's space
133
+ * does not shadow: an inner `const T` never captures a type-position
134
+ * `T` away from an outer `type T`, and vice versa.
135
+ *
136
+ * - `"value"`: runtime uses
137
+ * - `"type"`: annotations, heritage clauses, type arguments
138
+ * - `"namespace"`: the qualifier of a dotted type name (`ns.T`, `E.A`)
139
+ * - `"typeof"`: value uses inside a type (`typeof x`, `x is T` params)
140
+ * - `"any"`: alias positions accepting every space (`export { x }`,
141
+ * `export default x`, `export = x`, `import a = x`)
142
+ */
143
+ type Space = "value" | "type" | "namespace" | "typeof" | "any";
144
+
128
145
  /** What kind of construct created a {@link Scope}. */
129
146
  type ScopeKind =
130
147
  | "global"
@@ -191,6 +208,12 @@ interface Symbol {
191
208
  has(mask: number): boolean;
192
209
  /** True when every flag in `mask` is set. */
193
210
  hasAll(mask: number): boolean;
211
+ /**
212
+ * True when a reference resolving in `space` may bind to this
213
+ * symbol, the acceptance rule of name resolution. Import bindings
214
+ * alias symbols of unknowable space and are visible in every space.
215
+ */
216
+ visibleIn(space: Space): boolean;
194
217
  /**
195
218
  * The defining site of this symbol, following import/re-export chains
196
219
  * across modules. Shorthand for {@link Analyzer.definitionOf}.
@@ -209,8 +232,14 @@ interface Reference {
209
232
  readonly scope: Scope;
210
233
  /** The identifier node, the same object as in the walked AST. */
211
234
  readonly node: Identifier | JSXIdentifier;
212
- /** `"value"` for runtime uses, `"type"` for TS type-position uses. */
213
- readonly kind: "value" | "type";
235
+ /** The declaration {@link Space} this position resolves in. */
236
+ readonly space: Space;
237
+ /**
238
+ * True when the position sits inside a type-only subtree (`"type"`,
239
+ * `"namespace"`, `"typeof"` spaces): erased at compile time, so a
240
+ * rename tool can change a value without touching a same-named type.
241
+ */
242
+ readonly inTypePosition: boolean;
214
243
  /**
215
244
  * True when this reference (re)assigns its binding: assignment
216
245
  * targets, `++`/`--` operands, for-in/of iteration variables, and
@@ -218,7 +247,10 @@ interface Reference {
218
247
  * and write.
219
248
  */
220
249
  readonly isWrite: boolean;
221
- /** The symbol this resolves to, or null for free/global names. */
250
+ /**
251
+ * The symbol this resolves to, or null when no binding is visible
252
+ * in this reference's space (globals, undeclared names).
253
+ */
222
254
  readonly symbol: Symbol | null;
223
255
  }
224
256
 
@@ -396,9 +428,12 @@ interface Module {
396
428
  parentOf(node: Node): Node | null;
397
429
  /**
398
430
  * Walks the scope chain from `from` (default: the root scope) to
399
- * find the nearest binding of `name`.
431
+ * find the nearest binding of `name` visible in `space` (default:
432
+ * `"value"`, resolving like runtime code). A binding outside the
433
+ * space does not shadow, the walk keeps going. `"any"` matches by
434
+ * name alone.
400
435
  */
401
- resolve(name: string, from?: Scope): Symbol | null;
436
+ resolve(name: string, from?: Scope, space?: Space): Symbol | null;
402
437
  /**
403
438
  * The free variables of a function or arrow: every binding referenced
404
439
  * inside it (nested closures included, value positions only) that is
@@ -542,6 +577,7 @@ export {
542
577
  type Reference,
543
578
  type Scope,
544
579
  type ScopeKind,
580
+ type Space,
545
581
  type Symbol,
546
582
  type Visitors,
547
583
  type WalkContext,
package/module.js CHANGED
@@ -85,6 +85,24 @@ class Symbol {
85
85
  hasAll(mask) {
86
86
  return (this.flags & mask) === mask;
87
87
  }
88
+ // the acceptance rule of name resolution. import bindings alias
89
+ // symbols of unknowable space and are visible in every space
90
+ visibleIn(space) {
91
+ const flags = this.flags;
92
+ if ((flags & SymbolFlags.Import) !== 0) return true;
93
+ switch (space) {
94
+ case "value":
95
+ case "typeof":
96
+ return (flags & SymbolFlags.ValueSpace) !== 0;
97
+ case "type":
98
+ return (flags & SymbolFlags.TypeSpace) !== 0;
99
+ case "namespace":
100
+ return (flags & SymbolFlags.NamespaceSpace) !== 0;
101
+ case "any":
102
+ return true;
103
+ }
104
+ throw new TypeError(`visibleIn: unknown space "${space}"`);
105
+ }
88
106
  definition() {
89
107
  return this.module.analyzer.definitionOf(this);
90
108
  }
@@ -106,8 +124,11 @@ class Reference {
106
124
  get node() {
107
125
  return this.#sem.reference.node(this.id);
108
126
  }
109
- get kind() {
110
- return this.#sem.reference.kind(this.id);
127
+ get space() {
128
+ return this.#sem.reference.space(this.id);
129
+ }
130
+ get inTypePosition() {
131
+ return this.#sem.reference.inTypePosition(this.id);
111
132
  }
112
133
  get isWrite() {
113
134
  return this.#sem.reference.isWrite(this.id);
@@ -126,21 +147,28 @@ class Import {
126
147
  this.#sem = sem;
127
148
  this._resolved = null;
128
149
  }
129
- get #nameKind() {
130
- return this.#sem.import.nameKind(this.id);
150
+ get kind() {
151
+ return this.#sem.import.kind(this.id);
131
152
  }
132
153
  get local() {
133
154
  const s = this.#sem.import.symbolId(this.id);
134
155
  return s === null ? null : this.module.symbols[s];
135
156
  }
136
157
  get name() {
137
- return this.#nameKind === "named" ? this.#sem.import.name(this.id) : null;
158
+ return this.kind === "named" ? this.#sem.import.name(this.id) : null;
138
159
  }
139
160
  get isNamespace() {
140
- return this.#nameKind === "star" && this.local !== null;
161
+ const kind = this.kind;
162
+ return kind === "namespace" || kind === "importEquals";
141
163
  }
142
164
  get isSideEffect() {
143
- return this.#nameKind === "none";
165
+ return this.kind === "sideEffect";
166
+ }
167
+ get isDynamic() {
168
+ return this.kind === "dynamic";
169
+ }
170
+ get isRequire() {
171
+ return this.kind === "require";
144
172
  }
145
173
  get typeOnly() {
146
174
  return this.#sem.import.typeOnly(this.id);
@@ -168,23 +196,23 @@ class Export {
168
196
  this.#sem = sem;
169
197
  this._resolved = null;
170
198
  }
171
- get #nameKind() {
172
- return this.#sem.export.nameKind(this.id);
173
- }
174
- get #fromKind() {
175
- return this.#sem.export.fromKind(this.id);
199
+ get kind() {
200
+ return this.#sem.export.kind(this.id);
176
201
  }
177
202
  get name() {
178
- return this.#nameKind === "named" ? this.#sem.export.name(this.id) : null;
203
+ const kind = this.kind;
204
+ return kind === "named" || kind === "reExport" || kind === "namespace"
205
+ ? this.#sem.export.name(this.id)
206
+ : null;
179
207
  }
180
208
  get isStar() {
181
- return this.#nameKind === "star";
209
+ return this.kind === "star";
182
210
  }
183
211
  get isExportEquals() {
184
- return this.#nameKind === "equals";
212
+ return this.kind === "equals";
185
213
  }
186
214
  get globalName() {
187
- return this.#nameKind === "global" ? this.#sem.export.name(this.id) : null;
215
+ return this.kind === "global" ? this.#sem.export.name(this.id) : null;
188
216
  }
189
217
  get typeOnly() {
190
218
  return this.#sem.export.typeOnly(this.id);
@@ -194,13 +222,16 @@ class Export {
194
222
  return s === null ? null : this.module.symbols[s];
195
223
  }
196
224
  get specifier() {
197
- return this.#fromKind === "none" ? null : this.#sem.export.specifier(this.id);
225
+ const kind = this.kind;
226
+ return kind === "reExport" || kind === "namespace" || kind === "star"
227
+ ? this.#sem.export.specifier(this.id)
228
+ : null;
198
229
  }
199
230
  get fromName() {
200
- return this.#fromKind === "named" ? this.#sem.export.fromName(this.id) : null;
231
+ return this.kind === "reExport" ? this.#sem.export.fromName(this.id) : null;
201
232
  }
202
233
  get isNamespaceReexport() {
203
- return this.#fromKind === "star";
234
+ return this.kind === "namespace";
204
235
  }
205
236
  get node() {
206
237
  return this.#sem.export.node(this.id);
@@ -272,6 +303,9 @@ export class Module {
272
303
  get exports() {
273
304
  return (this.#exports ??= this.#rows(Export, this.#sem.export.count));
274
305
  }
306
+ get moduleFlags() {
307
+ return this.#sem.moduleFlags;
308
+ }
275
309
  get unresolvedReferences() {
276
310
  return (this.#unresolved ??= this.references.filter((r) => r.symbol === null));
277
311
  }
@@ -316,10 +350,13 @@ export class Module {
316
350
  return parent < 0 ? null : this.#r.nodeOf(parent);
317
351
  }
318
352
 
319
- resolve(name, from = this.rootScope) {
353
+ // the space filters what the name can see, exactly like reference
354
+ // resolution: a binding outside the space does not shadow, the walk
355
+ // keeps going. "any" matches by name alone
356
+ resolve(name, from = this.rootScope, space = "value") {
320
357
  for (let s = from; s; s = s.parent) {
321
358
  const found = s.find(name);
322
- if (found) return found;
359
+ if (found && found.visibleIn(space)) return found;
323
360
  }
324
361
  return null;
325
362
  }
@@ -355,7 +392,7 @@ export class Module {
355
392
  const captures = new Map();
356
393
  for (let i = 0; i < reference.count; i++) {
357
394
  const symbolId = reference.symbolId(i);
358
- if (symbolId === null || reference.kind(i) === "type") continue;
395
+ if (symbolId === null || reference.inTypePosition(i)) continue;
359
396
  if (reference.start(i) < start || reference.end(i) > end) continue;
360
397
  let inside = false;
361
398
  for (let s = symbol.scopeId(symbolId); s !== null; s = scope.parentId(s)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yuku-analyzer",
3
- "version": "0.5.48",
3
+ "version": "0.6.2",
4
4
  "description": "Full JavaScript and TypeScript semantic analysis: scopes, symbols, resolved references, closures, and cross-file module linking, computed natively in Zig and queried as plain JavaScript objects",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -21,17 +21,17 @@
21
21
  "decode.js"
22
22
  ],
23
23
  "optionalDependencies": {
24
- "@yuku-analyzer/binding-linux-x64-gnu": "0.5.48",
25
- "@yuku-analyzer/binding-linux-arm64-gnu": "0.5.48",
26
- "@yuku-analyzer/binding-linux-arm-gnu": "0.5.48",
27
- "@yuku-analyzer/binding-linux-x64-musl": "0.5.48",
28
- "@yuku-analyzer/binding-linux-arm64-musl": "0.5.48",
29
- "@yuku-analyzer/binding-linux-arm-musl": "0.5.48",
30
- "@yuku-analyzer/binding-darwin-x64": "0.5.48",
31
- "@yuku-analyzer/binding-darwin-arm64": "0.5.48",
32
- "@yuku-analyzer/binding-win32-x64": "0.5.48",
33
- "@yuku-analyzer/binding-win32-arm64": "0.5.48",
34
- "@yuku-analyzer/binding-freebsd-x64": "0.5.48"
24
+ "@yuku-analyzer/binding-linux-x64-gnu": "0.6.2",
25
+ "@yuku-analyzer/binding-linux-arm64-gnu": "0.6.2",
26
+ "@yuku-analyzer/binding-linux-arm-gnu": "0.6.2",
27
+ "@yuku-analyzer/binding-linux-x64-musl": "0.6.2",
28
+ "@yuku-analyzer/binding-linux-arm64-musl": "0.6.2",
29
+ "@yuku-analyzer/binding-linux-arm-musl": "0.6.2",
30
+ "@yuku-analyzer/binding-darwin-x64": "0.6.2",
31
+ "@yuku-analyzer/binding-darwin-arm64": "0.6.2",
32
+ "@yuku-analyzer/binding-win32-x64": "0.6.2",
33
+ "@yuku-analyzer/binding-win32-arm64": "0.6.2",
34
+ "@yuku-analyzer/binding-freebsd-x64": "0.6.2"
35
35
  },
36
36
  "keywords": [
37
37
  "analyzer",