ucn 5.4.2 → 5.5.1

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.
Files changed (85) hide show
  1. package/.claude/skills/ucn/SKILL.md +58 -236
  2. package/.claude/skills/ucn/references/commands.md +144 -68
  3. package/.claude/skills/ucn/references/resolution-rules.md +171 -0
  4. package/.claude/skills/ucn/references/trust-contract.md +27 -33
  5. package/README.md +27 -16
  6. package/cli/index.js +45 -14
  7. package/core/accessors.js +118 -3
  8. package/core/account.js +62 -11
  9. package/core/analysis.js +517 -266
  10. package/core/ast-analysis.js +76 -117
  11. package/core/async-producers.js +1729 -0
  12. package/core/bridge.js +1877 -268
  13. package/core/brief.js +6 -0
  14. package/core/build-worker.js +78 -14
  15. package/core/cache.js +529 -31
  16. package/core/callers.js +12228 -921
  17. package/core/check.js +30 -38
  18. package/core/class-identity.js +1028 -0
  19. package/core/command-contracts.js +6 -6
  20. package/core/configuration-items.js +86 -0
  21. package/core/contract-membership.js +2262 -0
  22. package/core/cpp-scope.js +532 -0
  23. package/core/deadcode.js +171 -49
  24. package/core/decorator-binding.js +260 -0
  25. package/core/discovery.js +281 -164
  26. package/core/errors.js +52 -0
  27. package/core/execute.js +21 -9
  28. package/core/external-macros.js +326 -0
  29. package/core/graph-build.js +277 -8
  30. package/core/graph.js +35 -2
  31. package/core/ignore-rules.js +472 -0
  32. package/core/imports.js +504 -42
  33. package/core/index-ir.js +94 -11
  34. package/core/inprocess-clients.js +636 -0
  35. package/core/ir.js +87 -8
  36. package/core/macro-expansion.js +1416 -0
  37. package/core/output/analysis.js +18 -2
  38. package/core/output/brief.js +3 -0
  39. package/core/output/doctor.js +4 -0
  40. package/core/output/endpoints.js +15 -6
  41. package/core/output/find.js +7 -2
  42. package/core/output/refactoring.js +121 -14
  43. package/core/output/reporting.js +31 -3
  44. package/core/output/shared.js +14 -0
  45. package/core/output-budget.js +305 -122
  46. package/core/parallel-build.js +124 -47
  47. package/core/parser.js +3 -2
  48. package/core/project.js +472 -66
  49. package/core/provenance-facts.js +156 -12
  50. package/core/provenance-overload.js +30 -3
  51. package/core/provenance.js +104 -30
  52. package/core/pytest-fixtures.js +434 -0
  53. package/core/python-fixture-flow.js +17 -3
  54. package/core/python-modules.js +173 -0
  55. package/core/reflection.js +877 -0
  56. package/core/registry.js +1 -1
  57. package/core/reporting.js +234 -52
  58. package/core/request-helpers.js +561 -0
  59. package/core/route-graph.js +1922 -0
  60. package/core/route-lists.js +2414 -0
  61. package/core/rust-macro-expansion.js +3026 -0
  62. package/core/rust-modules.js +203 -0
  63. package/core/search.js +121 -29
  64. package/core/shared.js +19 -1
  65. package/core/test-name-bindings.js +256 -0
  66. package/core/type-denotation.js +1185 -0
  67. package/core/type-references.js +3229 -0
  68. package/core/verify.js +3360 -672
  69. package/languages/adapter.js +31 -5
  70. package/languages/c-family.js +4061 -212
  71. package/languages/c-macro-invocations.js +380 -0
  72. package/languages/c-preprocessor.js +254 -0
  73. package/languages/csharp.js +1406 -49
  74. package/languages/go.js +1243 -70
  75. package/languages/index.js +728 -6
  76. package/languages/java.js +473 -61
  77. package/languages/javascript.js +1270 -197
  78. package/languages/lexical-scope.js +1280 -0
  79. package/languages/python.js +1557 -72
  80. package/languages/rust-macro-rules.js +1020 -0
  81. package/languages/rust-value-flow.js +416 -0
  82. package/languages/rust.js +2265 -112
  83. package/languages/utils.js +298 -4
  84. package/mcp/server.js +22 -8
  85. package/package.json +1 -1
@@ -5,6 +5,7 @@
5
5
  */
6
6
 
7
7
  const path = require('path');
8
+ const { UcnError } = require('../core/errors');
8
9
  const { createLanguageAdapter } = require('./adapter');
9
10
 
10
11
  // Lazy-loaded tree-sitter
@@ -15,8 +16,106 @@ const parsers = {};
15
16
  const adapters = {};
16
17
 
17
18
  // Shared trait presets for languages with the same type-system characteristics
19
+ /**
20
+ * Build a `languageProtocolMember` predicate from rules (fix #360, #363).
21
+ * A rule names members the language runtime or compiler invokes BY NAME:
22
+ * { pattern | names, member?: true, owners?: [bare type names] }
23
+ * `member` restricts it to class members; `owners` restricts it to classes
24
+ * whose heritage reaches one of those out-of-project types (Enum hooks on
25
+ * Enum subclasses only). The predicate takes (name, ctx) with ctx
26
+ * { isMember, derivesFrom(ownerNames) }; without derivesFrom an owner-gated
27
+ * rule never matches.
28
+ */
29
+ function protocolMembers(rules) {
30
+ return (name, ctx = {}) => {
31
+ let possible = false;
32
+ for (const rule of rules) {
33
+ const named = rule.pattern ? rule.pattern.test(name) : rule.names.includes(name);
34
+ if (!named) continue;
35
+ if (rule.member && ctx.isMember === false) continue;
36
+ // Owner kinds the runtime exempts from the protocol (fix #392:
37
+ // Java enum constants serialize by name and ignore the callbacks).
38
+ if (rule.exceptOwnerKinds && typeof ctx.ownerKinds === 'function' &&
39
+ ctx.ownerKinds().some(kind => rule.exceptOwnerKinds.includes(kind))) continue;
40
+ if (!rule.owners) return 'definite';
41
+ if (typeof ctx.derivesFrom === 'function' && ctx.derivesFrom(rule.owners)) return 'definite';
42
+ // An owner whose heritage leaves the project through another
43
+ // out-of-project supertype may still derive from the contract
44
+ // (fix #389: `extends IOException` is Serializable through the
45
+ // JDK): possible, when the member has the protocol's shape.
46
+ const via = rule.possibleViaExternal;
47
+ if (via && typeof ctx.externalSupertypes === 'function' &&
48
+ ctx.externalSupertypes().some(type => !via.except.includes(type)) &&
49
+ (!via.shape || via.shape(name, ctx))) possible = true;
50
+ }
51
+ return possible ? 'possible' : false;
52
+ };
53
+ }
54
+
55
+ /**
56
+ * Build a `languageProtocolType` predicate from rules (fix #380): types the
57
+ * compiler references by their full name when a language feature appears
58
+ * (C# `init` lowers to `System.Runtime.CompilerServices.IsExternalInit`).
59
+ * A project declaration of such a type in that namespace is used exactly
60
+ * when the project uses the feature. `ctx.hasFeature(name)` answers from the
61
+ * parser's per-file `languageFeatures`.
62
+ */
63
+ function protocolTypes(rules) {
64
+ return (name, ctx = {}) => rules.some(rule => rule.names.includes(name) &&
65
+ (ctx.namespace || '') === rule.namespace &&
66
+ typeof ctx.hasFeature === 'function' && rule.features.some(feature => ctx.hasFeature(feature)));
67
+ }
68
+
69
+ /**
70
+ * The shape the Java serialization runtime looks for (fix #389): instance
71
+ * methods `writeObject(ObjectOutputStream)` / `readObject(ObjectInputStream)`,
72
+ * `readResolve()` / `writeReplace()` / `readObjectNoData()` without
73
+ * parameters, static fields `serialVersionUID` and `serialPersistentFields`.
74
+ */
75
+ function javaSerializationShape(name, ctx) {
76
+ const modifiers = ctx.modifiers || [];
77
+ if (name === 'serialVersionUID' || name === 'serialPersistentFields') {
78
+ return ctx.isField === true && modifiers.includes('static');
79
+ }
80
+ if (ctx.isField || modifiers.includes('static')) return false;
81
+ const params = ctx.paramTypes;
82
+ if (!Array.isArray(params)) return true;
83
+ if (name === 'writeObject' || name === 'readObject') {
84
+ const stream = name === 'writeObject' ? 'ObjectOutputStream' : 'ObjectInputStream';
85
+ return params.length === 1 && (!params[0] || params[0] === stream);
86
+ }
87
+ return params.length === 0;
88
+ }
89
+
90
+ // java.lang types that do not implement java.io.Serializable (JDK API):
91
+ // an unqualified supertype spelled like one of these, and not imported
92
+ // from elsewhere, carries no serialization contract.
93
+ const JAVA_LANG_NOT_SERIALIZABLE = ['Object', 'Runnable', 'AutoCloseable', 'Comparable', 'Iterable',
94
+ 'CharSequence', 'Cloneable', 'Appendable', 'Readable', 'Thread', 'ThreadGroup', 'ClassLoader',
95
+ 'ThreadLocal', 'InheritableThreadLocal', 'ClassValue', 'SecurityManager', 'Process', 'Record'];
96
+
18
97
  const STRUCTURAL_TRAITS = {
19
98
  typeSystem: 'structural',
99
+ // A type's identity is its declaring module (a source file or inline
100
+ // module), not the directory (fix #384): two same-name types in two
101
+ // modules of one directory are distinct (JS/TS, Python modules, Rust
102
+ // modules). Go packages, Java packages, C# namespaces and C/C++
103
+ // namespaces keep their own identity rules.
104
+ moduleTypeIdentity: true,
105
+ // Leading path segments that always name the language's own standard
106
+ // library (fix #371): a type written under one of them (`std::fs::File`)
107
+ // denotes an external type, never a same-name project type.
108
+ standardPathRoots: [],
109
+ // How an unqualified type name reaches a file beyond its own
110
+ // declarations and single-name imports (fix #372): 'package-on-demand'
111
+ // (Java: same package, then on-demand imports plus the implicit ones),
112
+ // 'namespace-usings' (C#: enclosing namespaces, then using directives,
113
+ // global usings and project-file <Using> items), or null.
114
+ typeNameScoping: null,
115
+ // Types every file imports on demand without writing it (Java java.lang,
116
+ // JLS 7.3): { module, names }. A project type of the same name in another
117
+ // package is visible only through an import.
118
+ implicitTypeImport: null,
20
119
  methodCallInclusion: 'explicit',
21
120
  packageScope: 'file',
22
121
  hasReceiverPackageCalls: false,
@@ -42,6 +141,14 @@ const STRUCTURAL_TRAITS = {
42
141
  // captured as modifiers) or capitalization rules (Go) set false: there
43
142
  // the member's own marker decides.
44
143
  implicitlyPublicMembers: true,
144
+ // Member names the language itself keeps private to the declaring class
145
+ // (JS/TS `#x`; Python mangles `__x`): such a member never implements or
146
+ // overrides a supertype's member. A leading `_` is a naming convention
147
+ // only: Node stream hooks (`_transform`, `_flush`) and Python
148
+ // `TextWrapper._handle_long_word` are members an outside base calls
149
+ // (fix #397). Languages without the trait keep the conservative
150
+ // underscore rule.
151
+ privateMemberName: name => String(name).startsWith('#'),
45
152
  // A bare (receiver-less) name can never denote a METHOD here — JS/Python
46
153
  // methods are reached through their receiver; only a rebound alias could,
47
154
  // which is separate name-level evidence. Java sets true: `execute()`
@@ -52,9 +159,63 @@ const STRUCTURAL_TRAITS = {
52
159
  // assignment rebinds functions onto objects (obj.print = print), so the
53
160
  // #218b gate routes such calls visible instead of excluding (fix #220).
54
161
  methodCallReachesFunctions: true,
162
+ // Generic-parameter receivers can reach inherited/structural satisfiers
163
+ // (see the nominal preset); only Rust closes dispatch over bound traits.
164
+ genericBoundDispatchClosed: false,
55
165
  // Whether `Type(...)` constructs a class without a `new` token. Python
56
166
  // classes are ordinary callable objects; JS/TS classes require `new`.
57
167
  classesCallableWithoutNew: false,
168
+ // Whether a decorator REPLACES the callable it decorates with its own
169
+ // return value (Python/JS decorators, fix #364). What a decorated async
170
+ // function's call returns is then decided by the decorator, not by the
171
+ // `async` keyword. Attribute/annotation languages (C#, Java, Rust) only
172
+ // attach metadata and set false.
173
+ decoratorsWrapCallables: true,
174
+ callableDecorators: null,
175
+ // Runtime-provided global functions that return an awaitable (JS
176
+ // `fetch`), audited by audit-async as bare calls (fix #364).
177
+ knownAsyncGlobals: null,
178
+ // Whether a plain assignment `x = v` inside a function declares a local
179
+ // (Python, unless `global`/`nonlocal` names it). Elsewhere an undeclared
180
+ // name is an outer variable or an implicit-this field, so an awaitable
181
+ // stored there flows on (audit-async, fix #380).
182
+ assignmentDeclaresLocal: false,
183
+ // Whether a type's generic arity is part of its identity (C#: `Outcome`,
184
+ // `Outcome<T>` and `Outcome<T1, T2>` are distinct types that coexist).
185
+ // Java erases generics and TS/Rust/C++ cannot declare two same-name types
186
+ // differing only in arity, so a written arity decides nothing there.
187
+ // Drives arity-aware constructor, type-qualified and typed-receiver
188
+ // resolution (fix #380).
189
+ genericArityIsIdentity: false,
190
+ // Whether a generic method's type arguments are inferred from its call
191
+ // ARGUMENTS only (C#: a type parameter no parameter type mentions makes
192
+ // the method inapplicable without explicit type arguments). Java infers
193
+ // from the target type too; C++ templates are not overload-applicability
194
+ // evidence here (fix #393).
195
+ typeArgumentsFromArgumentsOnly: false,
196
+ // Test functions the toolchain binds to a declared identifier by NAME
197
+ // (fix #383): Go examples (GO_EXAMPLE_NAMES). Rust doc tests and
198
+ // Java/C#/Python doc references are comment text, listed by plan as
199
+ // text dependencies; no other language checks a test function's name
200
+ // against a declaration.
201
+ nameBoundTestFunctions: null,
202
+ // Whether a top-level type is declared in a source file named after it
203
+ // (fix #386): javac requires `public class Widget` in Widget.java, so a
204
+ // type rename renames that file too.
205
+ typeNamedSourceFile: false,
206
+ // Documentation references the compiler resolves (fix #390): C# XML doc
207
+ // `cref` attributes (CS1574 when documentation is generated). A member
208
+ // rename edits the crefs that name the renamed member.
209
+ docCrefReferences: false,
210
+ // Whether a method's simple name in value position is a method group
211
+ // (fix #395, C#: `new(Build, 4)`, `x += OnChanged`, `.Select(Build)`,
212
+ // `Func<int> f = Build`): plan renames it by the language's simple-name
213
+ // lookup. Java spells method values apart (`Type::m`).
214
+ methodGroupReferences: false,
215
+ // Whether a class may declare a field and a method of one name, so a
216
+ // paren-less member access (`this.size`) names the field and a method
217
+ // value is spelled apart (`obj::m`, `Type::m`) (fix #390: Java, Rust).
218
+ fieldsAndMethodsSeparate: false,
58
219
  // Whether call sites bind arguments by parameter NAME (Python keyword
59
220
  // arguments). Drives verify/check keyword binding validation (fix #281):
60
221
  // keyword names checked against the signature, keyword-only/positional-only
@@ -62,9 +223,49 @@ const STRUCTURAL_TRAITS = {
62
223
  // arguments must stay false — a JS call with fewer args than params is
63
224
  // legal, so required-coverage checks would false-flag.
64
225
  keywordArguments: false,
226
+ // Members whose NAME is fixed by the language itself (fix #360): Python
227
+ // data-model dunders (`__eq__`, `__iter__`) implement a protocol the
228
+ // interpreter dispatches by name. Renaming one never renames a project
229
+ // symbol; it withdraws the protocol. null = no such name class.
230
+ languageProtocolMember: null,
231
+ // Types the compiler itself references when a language feature is used
232
+ // (fix #380; C# polyfills such as IsExternalInit). null = none.
233
+ languageProtocolType: null,
234
+ // Runtime reflection vocabulary (fix #307, #363): calls that select a
235
+ // member by a string name. `functions` match the callee text (name and
236
+ // receiver by argument position), `members` match a method name on a
237
+ // receiver object; `computedMembers` treats `obj[expr]` as member access;
238
+ // `membersOnly` when the API can only reach class members; `formatCalls`
239
+ // are string-formatting calls whose first argument is a format template;
240
+ // `selfTypeCalls`/`typeLiteralNodes` spell "the runtime class of this" and
241
+ // class literals. `hintPattern` locates vocabulary spellings whose
242
+ // enclosing call nodes are inspected (the AST decides); `namespaceLookups` are module-namespace lookups by
243
+ // name (eval, globals()[k]). `family` groups languages sharing one
244
+ // runtime. null = no runtime reflection.
245
+ reflectionApi: null,
246
+ // Members every class inherits from the implicit root supertype, keyed by
247
+ // name with their parameter count (Java Object.toString/equals/...,
248
+ // C# object.ToString/Equals/...). A same-name same-arity member overrides
249
+ // the root contract even without an override marker (fix #360). null =
250
+ // no universal supertype surface.
251
+ universalSupertypeMembers: null,
252
+ // Whether macro definitions hold token-level TEMPLATES whose identifiers
253
+ // bind at each expansion site (Rust macro_rules!). Plan renames model
254
+ // their transcriber tokens explicitly (fix #360).
255
+ macroTemplateBodies: false,
256
+ // Whether a translation unit sees every declaration reachable through its
257
+ // transitive #include closure (C/C++ textual inclusion, fix #361). Free
258
+ // function visibility then follows the include closure, not one import
259
+ // hop, and several external-linkage definitions of one name are resolved
260
+ // by the linker, which source evidence alone cannot choose between.
261
+ textualIncludes: false,
65
262
  };
66
263
  const NOMINAL_TRAITS = {
67
264
  typeSystem: 'nominal',
265
+ moduleTypeIdentity: false,
266
+ standardPathRoots: [],
267
+ typeNameScoping: null,
268
+ implicitTypeImport: null,
68
269
  methodCallInclusion: 'auto',
69
270
  packageScope: 'file',
70
271
  hasReceiverPackageCalls: false,
@@ -76,6 +277,13 @@ const NOMINAL_TRAITS = {
76
277
  // named arguments but its parser doesn't record them yet; the trait stays
77
278
  // false there until that family is measured (fix #281, classified-deferred).
78
279
  keywordArguments: false,
280
+ // See STRUCTURAL_TRAITS for the three contract-membership traits (fix #360).
281
+ languageProtocolMember: null,
282
+ languageProtocolType: null,
283
+ reflectionApi: null,
284
+ universalSupertypeMembers: null,
285
+ macroTemplateBodies: false,
286
+ textualIncludes: false,
79
287
  // Go/Java/Rust have no default parameter values — see STRUCTURAL_TRAITS.
80
288
  hasDefaultParams: false,
81
289
  // Whether ANY instance method call can dynamically dispatch to a subtype
@@ -122,6 +330,13 @@ const NOMINAL_TRAITS = {
122
330
  // parens and Java requires .apply() — a dot-call provably never binds a
123
331
  // free function (fix #220, ripgrep-measured).
124
332
  methodCallReachesFunctions: false,
333
+ // Whether a call on a receiver typed by a GENERIC PARAMETER can reach a
334
+ // concrete method only through the traits in that parameter's bound
335
+ // closure (fix #368). Rust: no implementation inheritance, so
336
+ // `c.split_at()` with `C: Consumer` never reaches a Producer impl member
337
+ // or an inherent method. Java/C#/Go/TS: a subclass or structural
338
+ // satisfier can inherit the target through the bound, so false.
339
+ genericBoundDispatchClosed: false,
125
340
  // Whether a paren-less member access (x.name) can denote a METHOD. Rust
126
341
  // sets true for the inverse — `x.name` is ALWAYS a field there (method
127
342
  // values are path-only: Type::method), so a member-access reference
@@ -131,6 +346,81 @@ const NOMINAL_TRAITS = {
131
346
  // methods — false. Per-language override, not preset-wide.
132
347
  memberAccessNeverMethod: false,
133
348
  classesCallableWithoutNew: false,
349
+ decoratorsWrapCallables: false,
350
+ callableDecorators: null,
351
+ knownAsyncGlobals: null,
352
+ assignmentDeclaresLocal: false,
353
+ genericArityIsIdentity: false,
354
+ typeArgumentsFromArgumentsOnly: false,
355
+ // Test functions the toolchain binds to a declared identifier by NAME
356
+ // (fix #383); see GO_EXAMPLE_NAMES. null: no such convention.
357
+ nameBoundTestFunctions: null,
358
+ // A top-level type lives in a file named after it (fix #386, Java).
359
+ typeNamedSourceFile: false,
360
+ // Compiler-resolved doc references (fix #390, C# crefs).
361
+ docCrefReferences: false,
362
+ // Simple-name method groups (fix #395, C#).
363
+ methodGroupReferences: false,
364
+ // A static import of a type brings only the members the type itself
365
+ // declares, never inherited ones (fix #392, C# `using static`). Java's
366
+ // static imports reach inherited static members.
367
+ staticImportsDeclaredOnly: false,
368
+ // Fields and methods are separate namespaces (fix #390, Java/Rust).
369
+ fieldsAndMethodsSeparate: false,
370
+ };
371
+
372
+ /**
373
+ * Go example functions (fix #383): go vet's `tests` analyzer (run by
374
+ * `go test`) and godoc bind `Example<Ident>[_<Member>][_<suffix>]` in a
375
+ * _test.go file to the identifier it names. The name is split the way vet
376
+ * splits it: at most three parts at '_', and a part starting with a
377
+ * lower-case letter is a suffix, not a member. Renaming the identifier or
378
+ * member without the example fails vet ("refers to unknown field or method").
379
+ */
380
+ const GO_EXAMPLE_NAMES = {
381
+ kind: 'go-example',
382
+ prefix: 'Example',
383
+ testFile: /_test\.go$/,
384
+ parse(fnName) {
385
+ if (typeof fnName !== 'string' || !fnName.startsWith('Example')) return null;
386
+ const exName = fnName.slice('Example'.length);
387
+ const first = exName.indexOf('_');
388
+ const ident = first < 0 ? exName : exName.slice(0, first);
389
+ if (!ident) return null; // Example / Example_suffix: package examples
390
+ let member = null;
391
+ if (first >= 0) {
392
+ const after = exName.slice(first + 1);
393
+ const second = after.indexOf('_');
394
+ const part = second < 0 ? after : after.slice(0, second);
395
+ if (part && !/^\p{Ll}/u.test(part)) member = part;
396
+ }
397
+ return {
398
+ ident,
399
+ member,
400
+ // The function name with its ident and/or member part replaced.
401
+ rename({ ident: newIdent = ident, member: newMember = member } = {}) {
402
+ const tail = exName.slice(ident.length);
403
+ if (!member) return `Example${newIdent}${tail}`;
404
+ return `Example${newIdent}_${newMember}${tail.slice(1 + member.length)}`;
405
+ },
406
+ };
407
+ },
408
+ };
409
+
410
+ // JS/TS runtime protocol members: JSON.stringify calls toJSON, await and
411
+ // promise resolution call then, coercion calls toString/valueOf, and
412
+ // well-known symbols (`[Symbol.iterator]`) are invoked by the runtime.
413
+ const JS_PROTOCOL_MEMBERS = protocolMembers([
414
+ { names: ['toJSON', 'then', 'toString', 'valueOf', 'toLocaleString'], member: true },
415
+ { pattern: /^\[Symbol\.[A-Za-z]+\]$/, member: true },
416
+ ]);
417
+ const JS_REFLECTION_API = {
418
+ hintPattern: /\bReflect\s*\.\s*(?:get|set|has|deleteProperty)\b|\beval\s*\(/,
419
+ functions: [{ callee: /^(?:globalThis\.)?Reflect\.(?:get|set|has|deleteProperty)$/,
420
+ nameArg: 1, receiverArg: 0, qualified: true }],
421
+ computedMembers: true,
422
+ namespaceLookups: { calls: /^eval$/ },
423
+ family: 'javascript',
134
424
  };
135
425
 
136
426
  // Language configurations
@@ -144,7 +434,10 @@ const LANGUAGES = {
144
434
  traits: {
145
435
  ...STRUCTURAL_TRAITS,
146
436
  selfParam: ['this'],
437
+ reflectionApi: JS_REFLECTION_API,
438
+ languageProtocolMember: JS_PROTOCOL_MEMBERS,
147
439
  storedPromises: true,
440
+ knownAsyncGlobals: ['fetch'],
148
441
  testFileCandidates: (base, ext) => [`${base}.test${ext}`, `${base}.spec${ext}`, `${base}.test.ts`, `${base}.test.js`, `${base}.spec.ts`, `${base}.spec.js`],
149
442
  testDirs: ['__tests__'],
150
443
  },
@@ -158,7 +451,10 @@ const LANGUAGES = {
158
451
  traits: {
159
452
  ...STRUCTURAL_TRAITS,
160
453
  selfParam: ['this'],
454
+ reflectionApi: JS_REFLECTION_API,
455
+ languageProtocolMember: JS_PROTOCOL_MEMBERS,
161
456
  storedPromises: true,
457
+ knownAsyncGlobals: ['fetch'],
162
458
  testFileCandidates: (base, ext) => [`${base}.test${ext}`, `${base}.spec${ext}`, `${base}.test.ts`, `${base}.test.js`, `${base}.spec.ts`, `${base}.spec.js`],
163
459
  testDirs: ['__tests__'],
164
460
  },
@@ -172,7 +468,10 @@ const LANGUAGES = {
172
468
  traits: {
173
469
  ...STRUCTURAL_TRAITS,
174
470
  selfParam: ['this'],
471
+ reflectionApi: JS_REFLECTION_API,
472
+ languageProtocolMember: JS_PROTOCOL_MEMBERS,
175
473
  storedPromises: true,
474
+ knownAsyncGlobals: ['fetch'],
176
475
  testFileCandidates: (base, ext) => [`${base}.test${ext}`, `${base}.spec${ext}`, `${base}.test.ts`, `${base}.test.js`, `${base}.spec.ts`, `${base}.spec.js`],
177
476
  testDirs: ['__tests__'],
178
477
  },
@@ -191,10 +490,70 @@ const LANGUAGES = {
191
490
  // and query code treats such receivers as module receivers. JS
192
491
  // from-imports bind values only (`import * as ns` is parser-marked).
193
492
  submoduleImports: true,
493
+ // `__x` (not a dunder) is name-mangled per class (fix #397).
494
+ privateMemberName: name => /^__/.test(String(name)) && !/__$/.test(String(name)),
194
495
  classesCallableWithoutNew: true,
195
496
  // Call sites bind by parameter name (f(x=1)); signatures carry
196
497
  // keyword-only (`*`) and positional-only (`/`) markers (fix #281).
197
498
  keywordArguments: true,
499
+ assignmentDeclaresLocal: true,
500
+ // Calling an async def creates a coroutine object in any scope:
501
+ // audit-async checks such calls in sync functions too, and
502
+ // follows a local assigned from the call to the uses that need
503
+ // its result (fix #398).
504
+ storedCoroutines: true,
505
+ // A name bound in one scope only in exclusive branches (`if
506
+ // sys.platform ...: def f` / `else: def f`, `try: from m import
507
+ // f` / `except ImportError: def f`) is one item with an
508
+ // alternative per runtime configuration (fix #398F).
509
+ conditionalDefinitions: 'branch',
510
+ // A def inside a function body binds a local of that function: a
511
+ // bare name outside it never names it (fix #402), unless the
512
+ // function declares the name `global`.
513
+ nestedItemsBlockScoped: true,
514
+ // Decorators with a known effect on what calling the decorated
515
+ // function returns (fix #364), as resolved qualified names (a
516
+ // trailing `()` marks a decorator factory call). Any other
517
+ // decorator makes an async function's result unknown.
518
+ callableDecorators: {
519
+ builtins: ['staticmethod', 'classmethod', 'property', 'callable', 'isinstance'],
520
+ transparent: [
521
+ 'builtins.staticmethod', 'builtins.classmethod', 'abc.abstractmethod',
522
+ 'typing.override', 'typing_extensions.override', 'typing.final',
523
+ 'typing_extensions.final', 'typing.overload', 'typing_extensions.overload',
524
+ 'functools.wraps()',
525
+ ],
526
+ asyncContextManagers: ['contextlib.asynccontextmanager'],
527
+ // Standard-library decorators that never read the decorated
528
+ // callable's name (fix #392): a rename of the definition and
529
+ // its references is complete under them.
530
+ namePreserving: [
531
+ 'builtins.property', 'abc.abstractproperty', 'abc.abstractclassmethod',
532
+ 'abc.abstractstaticmethod', 'functools.cached_property', 'functools.cache',
533
+ 'functools.lru_cache', 'functools.lru_cache()', 'functools.singledispatch',
534
+ 'functools.singledispatchmethod', 'functools.total_ordering',
535
+ 'contextlib.contextmanager', 'typing.no_type_check',
536
+ 'typing.runtime_checkable', 'typing_extensions.runtime_checkable',
537
+ 'typing_extensions.deprecated()', 'warnings.deprecated()',
538
+ 'dataclasses.dataclass', 'dataclasses.dataclass()', 'enum.unique',
539
+ 'unittest.mock.patch()', 'unittest.mock.patch.object()', 'unittest.mock.patch.dict()',
540
+ 'unittest.mock.patch.multiple()', 'unittest.skip()', 'unittest.skipIf()',
541
+ 'unittest.skipUnless()', 'unittest.expectedFailure',
542
+ ],
543
+ },
544
+ languageProtocolMember: protocolMembers([
545
+ // Data-model dunders (__eq__, __init_subclass__, __set_name__,
546
+ // __reduce__, module __getattr__) are dispatched by name.
547
+ { pattern: /^__[A-Za-z][A-Za-z0-9_]*__$/ },
548
+ // Enum machinery hooks (auto() values, lookup fallback).
549
+ { names: ['_generate_next_value_', '_missing_'], member: true,
550
+ owners: ['Enum', 'IntEnum', 'StrEnum', 'Flag', 'IntFlag', 'ReprEnum'] },
551
+ ]),
552
+ reflectionApi: {
553
+ hintPattern: /\b(?:getattr|setattr|hasattr|delattr|eval)\s*\(|\b(?:globals|vars)\s*\(\s*\)\s*\[/,
554
+ functions: [{ callee: /^(?:getattr|setattr|hasattr|delattr)$/, nameArg: 1, receiverArg: 0 }],
555
+ namespaceLookups: { calls: /^eval$/, subscriptOf: /^(?:globals|vars)\(\)$/ },
556
+ },
198
557
  testFileCandidates: (base, ext) => [`test_${base}.py`, `${base}_test.py`],
199
558
  testDirs: ['tests'],
200
559
  },
@@ -214,7 +573,14 @@ const LANGUAGES = {
214
573
  hasDynamicImports: false,
215
574
  typeQualifiedCallStyle: 'method-expr',
216
575
  methodCallReachesFunctions: true,
576
+ reflectionApi: {
577
+ hintPattern: /\b(?:MethodByName|FieldByName)\b/,
578
+ members: [{ member: /^(?:MethodByName|FieldByName)$/, nameArg: 0 }],
579
+ membersOnly: true,
580
+ formatCalls: { 'fmt.Sprintf': 'percent' },
581
+ },
217
582
  testFileCandidates: (base, ext) => [`${base}_test.go`],
583
+ nameBoundTestFunctions: GO_EXAMPLE_NAMES,
218
584
  },
219
585
  },
220
586
  rust: {
@@ -227,8 +593,39 @@ const LANGUAGES = {
227
593
  ...NOMINAL_TRAITS,
228
594
  selfParam: ['self', '&self', '&mut self', 'mut self'],
229
595
  hasDynamicImports: false,
596
+ moduleTypeIdentity: true,
597
+ // Same-scope definitions of one name are configuration
598
+ // alternatives selected by `#[cfg]` (fix #376).
599
+ conditionalDefinitions: 'attribute',
230
600
  typeQualifiedCallStyle: 'path',
601
+ standardPathRoots: ['std', 'core', 'alloc'],
602
+ // Call records say whether their value is consumed where it is
603
+ // produced (`valueConsumed`, `consumingMethod`; fix #371).
604
+ callValueFacts: true,
231
605
  memberAccessNeverMethod: true,
606
+ fieldsAndMethodsSeparate: true,
607
+ genericBoundDispatchClosed: true,
608
+ macroTemplateBodies: true,
609
+ // Macro invocations are written `name!(...)` / `#[name]`: a
610
+ // separate namespace from fns and values (fix #377).
611
+ macroInvocationSyntax: 'marked',
612
+ // A fn declared inside a fn body is an item of that block: a
613
+ // bare path outside the block never names it (fix #377).
614
+ nestedItemsBlockScoped: true,
615
+ // Names every module sees without a `use` (the std prelude of
616
+ // editions 2015-2024 and the primitive types): a name no `use`,
617
+ // glob or item binds must be one of them, or come from a macro
618
+ // expansion (fix #386).
619
+ implicitPreludeNames: new Set([
620
+ 'Option', 'Some', 'None', 'Result', 'Ok', 'Err', 'Vec', 'String', 'Box',
621
+ 'ToString', 'ToOwned', 'Clone', 'Copy', 'Send', 'Sync', 'Sized', 'Unpin',
622
+ 'Drop', 'Fn', 'FnMut', 'FnOnce', 'AsyncFn', 'AsyncFnMut', 'AsyncFnOnce',
623
+ 'Iterator', 'IntoIterator', 'Extend', 'DoubleEndedIterator', 'ExactSizeIterator',
624
+ 'FromIterator', 'Default', 'Eq', 'PartialEq', 'Ord', 'PartialOrd', 'AsRef',
625
+ 'AsMut', 'Into', 'From', 'TryFrom', 'TryInto', 'Future', 'IntoFuture', 'Debug',
626
+ 'Hash', 'bool', 'char', 'str', 'u8', 'u16', 'u32', 'u64', 'u128', 'usize',
627
+ 'i8', 'i16', 'i32', 'i64', 'i128', 'isize', 'f32', 'f64',
628
+ ]),
232
629
  testFileCandidates: (base, ext) => [`${base}_test.rs`],
233
630
  testDirs: ['tests'],
234
631
  },
@@ -241,10 +638,72 @@ const LANGUAGES = {
241
638
  treeSitterModule: () => require('tree-sitter-java'),
242
639
  traits: {
243
640
  ...NOMINAL_TRAITS,
641
+ fieldsAndMethodsSeparate: true,
642
+ standardPathRoots: ['java', 'javax'],
643
+ typeNameScoping: 'package-on-demand',
644
+ typeNamedSourceFile: true,
645
+ // The public top-level types of java.lang (JDK 8-21, preview
646
+ // included): a spec-level list, not project knowledge.
647
+ implicitTypeImport: { module: 'java.lang', names: new Set([
648
+ 'AbstractMethodError', 'Appendable', 'ArithmeticException',
649
+ 'ArrayIndexOutOfBoundsException', 'ArrayStoreException', 'AssertionError',
650
+ 'AutoCloseable', 'Boolean', 'BootstrapMethodError', 'Byte', 'Character',
651
+ 'CharSequence', 'Class', 'ClassCastException', 'ClassCircularityError',
652
+ 'ClassFormatError', 'ClassLoader', 'ClassNotFoundException', 'ClassValue',
653
+ 'CloneNotSupportedException', 'Cloneable', 'Comparable', 'Compiler', 'Deprecated',
654
+ 'Double', 'Enum', 'EnumConstantNotPresentException', 'Error', 'Exception',
655
+ 'ExceptionInInitializerError', 'Float', 'FunctionalInterface',
656
+ 'IllegalAccessError', 'IllegalAccessException', 'IllegalArgumentException',
657
+ 'IllegalCallerException', 'IllegalMonitorStateException', 'IllegalStateException',
658
+ 'IllegalThreadStateException', 'IncompatibleClassChangeError',
659
+ 'IndexOutOfBoundsException', 'InheritableThreadLocal', 'InstantiationError',
660
+ 'InstantiationException', 'Integer', 'InternalError', 'InterruptedException',
661
+ 'Iterable', 'LayerInstantiationException', 'LinkageError', 'Long',
662
+ 'MatchException', 'Math', 'Module', 'ModuleLayer', 'NegativeArraySizeException',
663
+ 'NoClassDefFoundError', 'NoSuchFieldError', 'NoSuchFieldException',
664
+ 'NoSuchMethodError', 'NoSuchMethodException', 'NullPointerException', 'Number',
665
+ 'NumberFormatException', 'Object', 'OutOfMemoryError', 'Override', 'Package',
666
+ 'Process', 'ProcessBuilder', 'ProcessHandle', 'Readable', 'Record',
667
+ 'ReflectiveOperationException', 'Runnable', 'Runtime', 'RuntimeException',
668
+ 'RuntimePermission', 'SafeVarargs', 'ScopedValue', 'SecurityException',
669
+ 'SecurityManager', 'Short', 'StackOverflowError', 'StackTraceElement',
670
+ 'StackWalker', 'StrictMath', 'String', 'StringBuffer', 'StringBuilder',
671
+ 'StringIndexOutOfBoundsException', 'StringTemplate', 'SuppressWarnings', 'System',
672
+ 'Thread', 'ThreadDeath', 'ThreadGroup', 'ThreadLocal', 'Throwable',
673
+ 'TypeNotPresentException', 'UnknownError', 'UnsatisfiedLinkError',
674
+ 'UnsupportedClassVersionError', 'UnsupportedOperationException', 'VerifyError',
675
+ 'VirtualMachineError', 'Void', 'WrongThreadException',
676
+ ]) },
244
677
  selfParam: ['this'],
245
678
  allMethodsVirtual: true,
246
679
  hasArityOverloads: true,
247
680
  universalSupertype: 'Object',
681
+ universalSupertypeMembers: { toString: 0, equals: 1, hashCode: 0, clone: 0, finalize: 0 },
682
+ // Serialization callbacks and fields the JVM uses reflectively.
683
+ languageProtocolMember: protocolMembers([
684
+ // java.lang roots that implement java.io.Serializable (JDK API):
685
+ // a project subclass inherits the contract (fix #380). Any
686
+ // other out-of-project supertype but Object may implement it
687
+ // too (`extends IOException`, `extends ArrayList`): possible.
688
+ { names: ['readObject', 'writeObject', 'readResolve', 'writeReplace', 'readObjectNoData',
689
+ 'serialVersionUID', 'serialPersistentFields'],
690
+ member: true, owners: ['Serializable', 'Externalizable', 'Number', 'Throwable',
691
+ 'Exception', 'RuntimeException', 'Error'],
692
+ // Enum constants are serialized by name: readObject,
693
+ // writeObject, readResolve, writeReplace, readObjectNoData and
694
+ // serialVersionUID/serialPersistentFields of an enum type are
695
+ // ignored (Java Object Serialization Specification 1.12).
696
+ exceptOwnerKinds: ['enum'],
697
+ possibleViaExternal: { except: JAVA_LANG_NOT_SERIALIZABLE, shape: javaSerializationShape } },
698
+ ]),
699
+ reflectionApi: {
700
+ hintPattern: /\b(?:getMethod|getDeclaredMethod|getField|getDeclaredField)\b/,
701
+ members: [{ member: /^(?:getMethod|getDeclaredMethod|getField|getDeclaredField)$/, nameArg: 0 }],
702
+ membersOnly: true,
703
+ formatCalls: { 'String.format': 'percent' },
704
+ selfTypeCalls: ['getClass'],
705
+ typeLiteralNodes: ['class_literal'],
706
+ },
248
707
  bareCallReachesMethods: true,
249
708
  testFileCandidates: (base, ext) => [`${base}Test.java`, `${base}Tests.java`, `${base}TestCase.java`],
250
709
  },
@@ -261,6 +720,13 @@ const LANGUAGES = {
261
720
  packageScope: 'file',
262
721
  hasDynamicImports: false,
263
722
  exportVisibility: 'linkage',
723
+ // Same-signature definitions in one file are `#if` alternatives.
724
+ conditionalDefinitions: 'preprocessor',
725
+ textualIncludes: true,
726
+ // Function-like macros are invoked with call syntax: a call
727
+ // spelled NAME( where #define NAME( is in effect is the macro
728
+ // (fix #377).
729
+ macroInvocationSyntax: 'call',
264
730
  typeQualifiedCallStyle: 'static',
265
731
  bareCallReachesMethods: false,
266
732
  methodCallReachesFunctions: true,
@@ -281,9 +747,20 @@ const LANGUAGES = {
281
747
  selfParam: ['this'],
282
748
  packageScope: 'file',
283
749
  hasDynamicImports: false,
750
+ conditionalDefinitions: 'preprocessor',
284
751
  exportVisibility: 'linkage',
752
+ textualIncludes: true,
753
+ // Function-like macros are invoked with call syntax: a call
754
+ // spelled NAME( where #define NAME( is in effect is the macro
755
+ // (fix #377).
756
+ macroInvocationSyntax: 'call',
285
757
  typeQualifiedCallStyle: 'path',
758
+ standardPathRoots: ['std'],
286
759
  hasArityOverloads: true,
760
+ // Range-for lowers onto member begin()/end() with no name token.
761
+ languageProtocolMember: protocolMembers([
762
+ { names: ['begin', 'end'], member: true },
763
+ ]),
287
764
  // C++ instance dispatch is virtual only when the declaration
288
765
  // explicitly says virtual/override (unlike Java's implicit rule).
289
766
  explicitVirtualDispatch: true,
@@ -305,6 +782,11 @@ const LANGUAGES = {
305
782
  treeSitterModule: () => require('tree-sitter-c-sharp'),
306
783
  traits: {
307
784
  ...NOMINAL_TRAITS,
785
+ standardPathRoots: ['System'],
786
+ conditionalDefinitions: 'preprocessor',
787
+ typeNameScoping: 'namespace-usings',
788
+ genericArityIsIdentity: true,
789
+ typeArgumentsFromArgumentsOnly: true,
308
790
  selfParam: ['this', 'base'],
309
791
  packageScope: 'namespace',
310
792
  hasDynamicImports: false,
@@ -312,6 +794,42 @@ const LANGUAGES = {
312
794
  bareCallReachesMethods: true,
313
795
  typeQualifiedCallStyle: 'static',
314
796
  universalSupertype: 'Object',
797
+ universalSupertypeMembers: { ToString: 0, Equals: 1, GetHashCode: 0, Finalize: 0 },
798
+ docCrefReferences: true,
799
+ methodGroupReferences: true,
800
+ staticImportsDeclaredOnly: true,
801
+ // Pattern-bound members the compiler lowers syntax onto
802
+ // (foreach, await, deconstruction) - invoked with no name token.
803
+ languageProtocolMember: protocolMembers([
804
+ { names: ['GetEnumerator', 'GetAsyncEnumerator', 'MoveNext', 'MoveNextAsync', 'Current',
805
+ 'GetAwaiter', 'GetResult', 'IsCompleted', 'OnCompleted', 'UnsafeOnCompleted',
806
+ 'Deconstruct'], member: true },
807
+ ]),
808
+ // Compiler-required types a feature's lowering names (C# spec;
809
+ // polyfilled by projects targeting older frameworks).
810
+ languageProtocolType: protocolTypes([
811
+ { names: ['IsExternalInit'], namespace: 'System.Runtime.CompilerServices', features: ['init'] },
812
+ { names: ['RequiredMemberAttribute', 'CompilerFeatureRequiredAttribute'],
813
+ namespace: 'System.Runtime.CompilerServices', features: ['required'] },
814
+ { names: ['ExtensionAttribute'], namespace: 'System.Runtime.CompilerServices',
815
+ features: ['extension-method'] },
816
+ { names: ['NullableAttribute', 'NullableContextAttribute', 'NullablePublicOnlyAttribute'],
817
+ namespace: 'System.Runtime.CompilerServices', features: ['nullable'] },
818
+ { names: ['IsByRefLikeAttribute'], namespace: 'System.Runtime.CompilerServices', features: ['ref-struct'] },
819
+ { names: ['IsReadOnlyAttribute'], namespace: 'System.Runtime.CompilerServices', features: ['readonly'] },
820
+ { names: ['IsUnmanagedAttribute'], namespace: 'System.Runtime.CompilerServices', features: ['unmanaged'] },
821
+ { names: ['TupleElementNamesAttribute'], namespace: 'System.Runtime.CompilerServices',
822
+ features: ['tuple-names'] },
823
+ { names: ['Index', 'Range'], namespace: 'System', features: ['index-range'] },
824
+ ]),
825
+ reflectionApi: {
826
+ hintPattern: /\b(?:GetMethod|GetProperty|GetField|GetMember)\b/,
827
+ members: [{ member: /^(?:GetMethod|GetProperty|GetField|GetMember)$/, nameArg: 0 }],
828
+ membersOnly: true,
829
+ formatCalls: { 'string.Format': 'brace', 'String.Format': 'brace' },
830
+ selfTypeCalls: ['GetType'],
831
+ typeLiteralNodes: ['typeof_expression'],
832
+ },
315
833
  testFileCandidates: (base, ext) => [
316
834
  `${base}Tests${ext}`, `${base}Test${ext}`, `${base}.Tests${ext}`,
317
835
  ],
@@ -328,6 +846,7 @@ const LANGUAGES = {
328
846
  ...STRUCTURAL_TRAITS,
329
847
  selfParam: ['this'],
330
848
  storedPromises: true,
849
+ knownAsyncGlobals: ['fetch'],
331
850
  testFileCandidates: (base, ext) => [`${base}.test${ext}`, `${base}.spec${ext}`],
332
851
  },
333
852
  }
@@ -350,17 +869,215 @@ function loadTreeSitter() {
350
869
  try {
351
870
  TreeSitter = require('tree-sitter');
352
871
  } catch (e) {
353
- throw new Error(
872
+ throw new UcnError(
354
873
  'tree-sitter is required but not installed.\n' +
355
874
  'Install with: npm install',
356
875
  { cause: e }
357
876
  );
358
877
  }
359
878
  _installNodeTypeCache(TreeSitter);
879
+ _installNodeParentCache(TreeSitter);
880
+ _installNodeFieldCaches(TreeSitter);
360
881
  }
361
882
  return TreeSitter;
362
883
  }
363
884
 
885
+ // Immutable facts of a node wrapper, read natively once (fix #385). They
886
+ // live in one record per wrapper, created with every field at once (fix
887
+ // #388): a wrapper gains a single property whichever fact is read first,
888
+ // instead of one per fact in read order.
889
+ const NODE_FACTS = Symbol('ucn.nodeFacts');
890
+ const UNREAD = Symbol('ucn.unread');
891
+
892
+ function NodeFacts() {
893
+ this.parent = UNREAD;
894
+ this.startIndex = -1;
895
+ this.endIndex = -1;
896
+ this.fields = null;
897
+ this.namedChildren = null;
898
+ }
899
+
900
+ // Internal read of a node's named children without a defensive copy: the
901
+ // caller never mutates the array (the flat node list builder in utils.js).
902
+ const NAMED_CHILDREN_LIST = Symbol.for('ucn.namedChildrenList');
903
+ const NO_CHILDREN = Object.freeze([]);
904
+
905
+ function nodeFactsOf(node) {
906
+ let facts = node[NODE_FACTS];
907
+ if (facts === undefined) {
908
+ facts = new NodeFacts();
909
+ node[NODE_FACTS] = facts;
910
+ }
911
+ return facts;
912
+ }
913
+
914
+ /**
915
+ * Same discipline as the parent cache for the other immutable facts hot
916
+ * walks re-read: byte offsets and field children (fix #385).
917
+ */
918
+ function _installNodeFieldCaches(TS) {
919
+ try {
920
+ const base = TS?.SyntaxNode?.prototype;
921
+ if (!base) return;
922
+ const startDesc = Object.getOwnPropertyDescriptor(base, 'startIndex');
923
+ if (startDesc?.get && !startDesc.set) {
924
+ const nativeStart = startDesc.get;
925
+ Object.defineProperty(base, 'startIndex', {
926
+ configurable: true,
927
+ get() {
928
+ const facts = nodeFactsOf(this);
929
+ let value = facts.startIndex;
930
+ if (value < 0) {
931
+ value = nativeStart.call(this);
932
+ facts.startIndex = value;
933
+ }
934
+ return value;
935
+ },
936
+ });
937
+ }
938
+ const endDesc = Object.getOwnPropertyDescriptor(base, 'endIndex');
939
+ if (endDesc?.get && !endDesc.set) {
940
+ const nativeEnd = endDesc.get;
941
+ Object.defineProperty(base, 'endIndex', {
942
+ configurable: true,
943
+ get() {
944
+ const facts = nodeFactsOf(this);
945
+ let value = facts.endIndex;
946
+ if (value < 0) {
947
+ value = nativeEnd.call(this);
948
+ facts.endIndex = value;
949
+ }
950
+ return value;
951
+ },
952
+ });
953
+ }
954
+ // Named children (fix #388): the flat node list reads every named
955
+ // node's children once; later reads of the same node (child loops,
956
+ // counts, walks) are served from that array. Callers always get a
957
+ // fresh array, as from the binding.
958
+ const namedDesc = Object.getOwnPropertyDescriptor(base, 'namedChildren');
959
+ const countDesc = Object.getOwnPropertyDescriptor(base, 'namedChildCount');
960
+ const firstDesc = Object.getOwnPropertyDescriptor(base, 'firstNamedChild');
961
+ const lastDesc = Object.getOwnPropertyDescriptor(base, 'lastNamedChild');
962
+ const nativeNamedChild = base.namedChild;
963
+ if (namedDesc?.get && !namedDesc.set && countDesc?.get && !countDesc.set &&
964
+ firstDesc?.get && !firstDesc.set && lastDesc?.get && !lastDesc.set &&
965
+ typeof nativeNamedChild === 'function') {
966
+ const nativeNamed = namedDesc.get;
967
+ const nativeCount = countDesc.get;
968
+ const nativeFirst = firstDesc.get;
969
+ const nativeLast = lastDesc.get;
970
+ // A leaf's empty list is not stored unless the node already has
971
+ // a facts record: most named nodes are leaves, and a record per
972
+ // leaf would only hold an empty array (build-worker RSS).
973
+ const namedList = node => {
974
+ const existing = node[NODE_FACTS];
975
+ if (existing !== undefined && existing.namedChildren !== null) return existing.namedChildren;
976
+ const list = nativeNamed.call(node);
977
+ if (list.length > 0) nodeFactsOf(node).namedChildren = list;
978
+ else if (existing !== undefined) existing.namedChildren = NO_CHILDREN;
979
+ return list;
980
+ };
981
+ Object.defineProperty(base, NAMED_CHILDREN_LIST, {
982
+ configurable: true,
983
+ writable: true,
984
+ value() { return namedList(this); },
985
+ });
986
+ Object.defineProperty(base, 'namedChildren', {
987
+ configurable: true,
988
+ get() { return namedList(this).slice(); },
989
+ });
990
+ const cachedList = node => {
991
+ const facts = node[NODE_FACTS];
992
+ return facts === undefined ? null : facts.namedChildren;
993
+ };
994
+ Object.defineProperty(base, 'namedChildCount', {
995
+ configurable: true,
996
+ get() {
997
+ const list = cachedList(this);
998
+ return list === null ? nativeCount.call(this) : list.length;
999
+ },
1000
+ });
1001
+ Object.defineProperty(base, 'namedChild', {
1002
+ configurable: true,
1003
+ writable: true,
1004
+ value(index) {
1005
+ const list = cachedList(this);
1006
+ if (list !== null && Number.isInteger(index) && index >= 0 && index < list.length) {
1007
+ return list[index];
1008
+ }
1009
+ return nativeNamedChild.call(this, index);
1010
+ },
1011
+ });
1012
+ Object.defineProperty(base, 'firstNamedChild', {
1013
+ configurable: true,
1014
+ get() {
1015
+ const list = cachedList(this);
1016
+ return list === null ? nativeFirst.call(this) : (list.length > 0 ? list[0] : null);
1017
+ },
1018
+ });
1019
+ Object.defineProperty(base, 'lastNamedChild', {
1020
+ configurable: true,
1021
+ get() {
1022
+ const list = cachedList(this);
1023
+ return list === null ? nativeLast.call(this) : (list.length > 0 ? list[list.length - 1] : null);
1024
+ },
1025
+ });
1026
+ }
1027
+ const nativeField = base.childForFieldName;
1028
+ if (typeof nativeField === 'function') {
1029
+ Object.defineProperty(base, 'childForFieldName', {
1030
+ configurable: true,
1031
+ writable: true,
1032
+ value(fieldName) {
1033
+ const facts = nodeFactsOf(this);
1034
+ let fields = facts.fields;
1035
+ if (fields === null) {
1036
+ fields = new Map();
1037
+ facts.fields = fields;
1038
+ } else if (fields.has(fieldName)) {
1039
+ return fields.get(fieldName);
1040
+ }
1041
+ const child = nativeField.call(this, fieldName);
1042
+ fields.set(fieldName, child);
1043
+ return child;
1044
+ },
1045
+ });
1046
+ }
1047
+ } catch { /* stock accessors keep working */ }
1048
+ }
1049
+
1050
+ /**
1051
+ * Make SyntaxNode#parent a native call only once per node wrapper.
1052
+ *
1053
+ * The binding returns one wrapper object per node of a tree (its node cache),
1054
+ * trees are never edited here, and a node's parent is immutable, so the first
1055
+ * native read is stored on the wrapper and later reads are property hits.
1056
+ * Enclosing-scope helpers climb to the translation unit from every call
1057
+ * site and declaration; on C++ headers those climbs were the single largest
1058
+ * native-bridge cost of a cold build (fix #385).
1059
+ */
1060
+ function _installNodeParentCache(TS) {
1061
+ try {
1062
+ const base = TS?.SyntaxNode?.prototype;
1063
+ const desc = base && Object.getOwnPropertyDescriptor(base, 'parent');
1064
+ if (!desc?.get || desc.set) return; // shape changed upstream: leave it alone
1065
+ const nativeGet = desc.get;
1066
+ Object.defineProperty(base, 'parent', {
1067
+ configurable: true,
1068
+ get() {
1069
+ const facts = nodeFactsOf(this);
1070
+ let parent = facts.parent;
1071
+ if (parent === UNREAD) {
1072
+ parent = nativeGet.call(this);
1073
+ facts.parent = parent;
1074
+ }
1075
+ return parent;
1076
+ },
1077
+ });
1078
+ } catch { /* stock getter keeps working */ }
1079
+ }
1080
+
364
1081
  /**
365
1082
  * Make SyntaxNode#type a cheap data-property read.
366
1083
  *
@@ -412,14 +1129,14 @@ function getParser(language) {
412
1129
  const config = LANGUAGES[language];
413
1130
 
414
1131
  if (!config) {
415
- throw new Error(`Unsupported language: ${language}`);
1132
+ throw new UcnError(`Unsupported language: ${language}`);
416
1133
  }
417
1134
 
418
1135
  try {
419
1136
  const lang = config.treeSitterModule();
420
1137
  parser.setLanguage(lang);
421
1138
  } catch (e) {
422
- throw new Error(
1139
+ throw new UcnError(
423
1140
  `Failed to load tree-sitter grammar for ${language}.\n` +
424
1141
  `Install with: npm install tree-sitter-${language}\n` +
425
1142
  `Original error: ${e.message}`,
@@ -455,7 +1172,7 @@ function getLanguageAdapter(language) {
455
1172
  if (adapters[language]) return adapters[language];
456
1173
  const config = LANGUAGES[language];
457
1174
  if (!config) {
458
- throw new Error(`Unsupported language: ${language}`);
1175
+ throw new UcnError(`Unsupported language: ${language}`);
459
1176
  }
460
1177
  adapters[language] = createLanguageAdapter(config);
461
1178
  return adapters[language];
@@ -490,6 +1207,7 @@ function getSupportedLanguages() {
490
1207
  // Default 1MB handles most files; can be overridden via UCN_BUFFER_SIZE env var
491
1208
  const DEFAULT_BUFFER_SIZE = 1024 * 1024; // 1MB
492
1209
  const MAX_BUFFER_SIZE = 64 * 1024 * 1024; // 64MB cap
1210
+ const SMALL_BUFFER_SIZE = 32 * 1024;
493
1211
 
494
1212
  const PARSE_OPTIONS = {
495
1213
  bufferSize: parseInt(process.env.UCN_BUFFER_SIZE, 10) || DEFAULT_BUFFER_SIZE
@@ -532,9 +1250,13 @@ function safeParse(parser, content, oldTree = undefined, options = {}) {
532
1250
 
533
1251
  const contentLength = content.length;
534
1252
 
535
- // Try with escalating buffer sizes
1253
+ // Try with escalating buffer sizes. The first holds the content with
1254
+ // room to spare but no more: a 1MB buffer for a one-line probe or a
1255
+ // macro body costs 20x the parse itself (fix #396).
536
1256
  const bufferSizes = [
537
- parseInt(process.env.UCN_BUFFER_SIZE, 10) || DEFAULT_BUFFER_SIZE,
1257
+ parseInt(process.env.UCN_BUFFER_SIZE, 10) ||
1258
+ Math.min(DEFAULT_BUFFER_SIZE, Math.max(SMALL_BUFFER_SIZE, contentLength * 2)),
1259
+ DEFAULT_BUFFER_SIZE,
538
1260
  Math.max(DEFAULT_BUFFER_SIZE, contentLength * 2),
539
1261
  Math.max(4 * 1024 * 1024, contentLength * 3),
540
1262
  Math.max(16 * 1024 * 1024, contentLength * 4),