ucn 5.4.1 → 5.5.0

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 (87) 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 +18 -12
  6. package/cli/index.js +42 -16
  7. package/core/accessors.js +118 -3
  8. package/core/account.js +30 -0
  9. package/core/analysis.js +508 -264
  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 +522 -31
  16. package/core/callers.js +12053 -1047
  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-ext.js +6 -6
  38. package/core/output/analysis.js +18 -2
  39. package/core/output/brief.js +3 -0
  40. package/core/output/doctor.js +4 -0
  41. package/core/output/endpoints.js +15 -6
  42. package/core/output/find.js +11 -2
  43. package/core/output/lines.js +8 -2
  44. package/core/output/refactoring.js +121 -14
  45. package/core/output/reporting.js +31 -3
  46. package/core/output/shared.js +14 -0
  47. package/core/output-budget.js +316 -122
  48. package/core/parallel-build.js +124 -47
  49. package/core/parser.js +3 -2
  50. package/core/project.js +457 -60
  51. package/core/provenance-facts.js +156 -12
  52. package/core/provenance-overload.js +30 -3
  53. package/core/provenance.js +104 -30
  54. package/core/pytest-fixtures.js +434 -0
  55. package/core/python-fixture-flow.js +17 -3
  56. package/core/python-modules.js +173 -0
  57. package/core/reflection.js +877 -0
  58. package/core/registry.js +1 -1
  59. package/core/reporting.js +234 -52
  60. package/core/request-helpers.js +561 -0
  61. package/core/route-graph.js +1922 -0
  62. package/core/route-lists.js +2414 -0
  63. package/core/rust-macro-expansion.js +2113 -0
  64. package/core/rust-modules.js +185 -0
  65. package/core/search.js +121 -29
  66. package/core/shared.js +19 -1
  67. package/core/test-name-bindings.js +256 -0
  68. package/core/type-denotation.js +1161 -0
  69. package/core/type-references.js +3229 -0
  70. package/core/verify.js +3360 -672
  71. package/languages/adapter.js +31 -5
  72. package/languages/c-family.js +4061 -212
  73. package/languages/c-macro-invocations.js +380 -0
  74. package/languages/c-preprocessor.js +254 -0
  75. package/languages/csharp.js +1406 -49
  76. package/languages/go.js +1243 -70
  77. package/languages/index.js +728 -6
  78. package/languages/java.js +473 -61
  79. package/languages/javascript.js +1270 -197
  80. package/languages/lexical-scope.js +1280 -0
  81. package/languages/python.js +1557 -72
  82. package/languages/rust-macro-rules.js +1020 -0
  83. package/languages/rust-value-flow.js +416 -0
  84. package/languages/rust.js +2237 -115
  85. package/languages/utils.js +298 -4
  86. package/mcp/server.js +25 -11
  87. package/package.json +1 -1
@@ -0,0 +1,877 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Runtime reflection by NAME PATTERN (fix #363).
5
+ *
6
+ * A reflective member access names its target with a string. A literal string
7
+ * is an exact spelling; a string BUILT from literal fragments around a runtime
8
+ * part (`"_get_%s_permissions" % src`, f"_get_{p}", `"on" + evt`,
9
+ * `this[`handle${t}`]`, `getMethod("get" + n)`, `MethodByName("X" + y)`) is a
10
+ * pattern: literal fragments with wildcards. Patterns come from the AST of the
11
+ * name argument (plus one-hop local string bindings in the same scope); a name
12
+ * the pattern can spell is a possible runtime target when the reflective
13
+ * receiver could hold an instance of the candidate's class.
14
+ *
15
+ * Consumers:
16
+ * - deadcode withholds pattern-reachable candidates (counted per pattern);
17
+ * - findCallers (collectAccount) lists each reaching site as an unverified
18
+ * `reflection-pattern` caller, so impact/plan see the dependency.
19
+ * A pattern whose literal part names nothing specific (`"_%s" % x`, a bare
20
+ * parameter) stays a disclosed dynamic use, never a blanket withhold.
21
+ *
22
+ * Site extraction is syntax-only and runs in the same per-file parse as the
23
+ * computed-dispatch inventory (ast-analysis.js); the per-project matcher is
24
+ * memoized on the index and reset with it.
25
+ */
26
+
27
+ const { langTraits } = require('../languages');
28
+
29
+ const WILD = '\u0000*';
30
+ const MODULE_RECEIVER = Object.freeze({ kind: 'module' });
31
+ const CALL_NODE_TYPES = ['call', 'call_expression', 'invocation_expression', 'method_invocation'];
32
+ // Computed-member keys that build a name in place (`'on' + evt`, `\`h${t}\``).
33
+ const COMPUTED_PATTERN_KEYS = new Set(['binary_expression', 'template_string']);
34
+ const MAX_ALTERNATIVES = 8;
35
+ const MAX_RESOLVE_DEPTH = 3;
36
+ const EXPRESSION_CAP = 160;
37
+ // Letters/digits a wildcard pattern must spell before it identifies anything:
38
+ // `_*`, `*s` or `*` name every member and stay disclosed dynamic uses.
39
+ const MIN_SPECIFIC_CHARS = 2;
40
+
41
+ const STRING_NODES = new Set([
42
+ 'string', 'string_literal', 'raw_string_literal', 'verbatim_string_literal',
43
+ 'interpreted_string_literal', 'template_string', 'interpolated_string_expression',
44
+ ]);
45
+ const STRING_CONTENT_NODES = new Set([
46
+ 'string_content', 'string_fragment', 'string_literal_content',
47
+ 'interpreted_string_literal_content', 'raw_string_literal_content',
48
+ 'raw_string_content', 'verbatim_string_literal_content',
49
+ ]);
50
+ const INTERPOLATION_NODES = new Set([
51
+ 'interpolation', 'template_substitution',
52
+ ]);
53
+ const SCOPE_NODES = new Set([
54
+ 'function_declaration', 'generator_function_declaration',
55
+ 'function_expression', 'generator_function', 'arrow_function',
56
+ 'method_definition', 'function_definition', 'lambda',
57
+ 'method_declaration', 'func_literal', 'constructor_declaration',
58
+ 'lambda_expression', 'local_function_statement', 'anonymous_method_expression',
59
+ 'module', 'program', 'source_file', 'compilation_unit',
60
+ ]);
61
+ const CLASS_NODES = new Set([
62
+ 'class_definition', 'class_declaration', 'class', 'class_expression',
63
+ 'abstract_class_declaration', 'struct_declaration', 'record_declaration',
64
+ 'enum_declaration', 'interface_declaration',
65
+ ]);
66
+ // A non-arrow JS function rebinds `this`; arrows and class members keep it.
67
+ const THIS_REBINDING_FUNCTIONS = new Set([
68
+ 'function_declaration', 'function_expression', 'generator_function',
69
+ 'generator_function_declaration', 'function',
70
+ ]);
71
+
72
+ // ---------------------------------------------------------------------------
73
+ // String patterns
74
+ // ---------------------------------------------------------------------------
75
+
76
+ function _concat(a, b) {
77
+ const out = [];
78
+ for (const x of a) {
79
+ for (const y of b) {
80
+ out.push([...x, ...y]);
81
+ if (out.length > MAX_ALTERNATIVES) return [[WILD]];
82
+ }
83
+ }
84
+ return out;
85
+ }
86
+
87
+ function _expandFormat(alternatives, style) {
88
+ const re = style === 'brace'
89
+ ? /\{\{|\}\}|\{[^{}]*\}/g
90
+ : /%%|%(?:\([^)]*\)|\[\d+\])?[-#0 +]*(?:\d+|\*)?(?:\.(?:\d+|\*))?[A-Za-z]/g;
91
+ return alternatives.map(alt => {
92
+ const out = [];
93
+ for (const piece of alt) {
94
+ if (piece === WILD) { out.push(WILD); continue; }
95
+ let last = 0;
96
+ let m;
97
+ re.lastIndex = 0;
98
+ while ((m = re.exec(piece)) !== null) {
99
+ out.push(piece.slice(last, m.index));
100
+ if (m[0] === '%%') out.push('%');
101
+ else if (m[0] === '{{') out.push('{');
102
+ else if (m[0] === '}}') out.push('}');
103
+ else out.push(WILD);
104
+ last = m.index + m[0].length;
105
+ }
106
+ out.push(piece.slice(last));
107
+ }
108
+ return out;
109
+ });
110
+ }
111
+
112
+ function _unwrap(node) {
113
+ let cur = node;
114
+ for (let i = 0; cur && i < 4; i++) {
115
+ if (cur.type === 'argument' || cur.type === 'parenthesized_expression') {
116
+ cur = cur.namedChildren.find(c => c.type !== 'comment') || null;
117
+ } else break;
118
+ }
119
+ return cur;
120
+ }
121
+
122
+ function _stringLiteral(node) {
123
+ const pieces = [];
124
+ let sawContent = false;
125
+ for (const child of node.children || []) {
126
+ if (STRING_CONTENT_NODES.has(child.type)) {
127
+ pieces.push(child.text);
128
+ sawContent = true;
129
+ } else if (INTERPOLATION_NODES.has(child.type)) {
130
+ pieces.push(WILD);
131
+ } else if (child.type === 'escape_sequence') {
132
+ // An escaped character never spells an identifier fragment.
133
+ pieces.push('\\');
134
+ }
135
+ // Delimiters, prefixes and quote tokens carry no name text.
136
+ }
137
+ if (!sawContent && pieces.length === 0) {
138
+ // Grammars that keep simple literals as one token (`"abc"`, `'abc'`).
139
+ const raw = String(node.text || '');
140
+ const m = /^[A-Za-z@$]*(["'`])([\s\S]*)\1$/.exec(raw);
141
+ if (m) pieces.push(m[2]);
142
+ }
143
+ return [pieces];
144
+ }
145
+
146
+ /**
147
+ * Pattern alternatives of a string-valued expression, or null when the
148
+ * expression is not string-shaped. A runtime part becomes WILD.
149
+ * @param {object} node
150
+ * @param {object} ctx - { formatCalls: {callee: style}, resolve(identNode) }
151
+ */
152
+ function stringAlternatives(node, ctx, depth = 0) {
153
+ node = _unwrap(node);
154
+ if (!node || depth > 12) return null;
155
+ if (STRING_NODES.has(node.type)) return _stringLiteral(node);
156
+ switch (node.type) {
157
+ case 'concatenated_string': {
158
+ let acc = [[]];
159
+ for (const child of node.namedChildren) {
160
+ const part = stringAlternatives(child, ctx, depth + 1) || [[WILD]];
161
+ acc = _concat(acc, part);
162
+ }
163
+ return acc;
164
+ }
165
+ case 'binary_operator':
166
+ case 'binary_expression': {
167
+ const op = node.childForFieldName('operator')?.text ||
168
+ (node.children || []).find(c => !c.isNamed)?.text;
169
+ const left = node.childForFieldName('left') || node.namedChild(0);
170
+ const right = node.childForFieldName('right') || node.namedChild(1);
171
+ if (op === '+') {
172
+ const l = stringAlternatives(left, ctx, depth + 1);
173
+ const r = stringAlternatives(right, ctx, depth + 1);
174
+ if (!l && !r) return null;
175
+ return _concat(l || [[WILD]], r || [[WILD]]);
176
+ }
177
+ if (op === '%') {
178
+ // Python printf-style formatting: only a string left operand.
179
+ const l = _unwrap(left);
180
+ if (!l || !(STRING_NODES.has(l.type) || l.type === 'concatenated_string')) return null;
181
+ return _expandFormat(stringAlternatives(l, ctx, depth + 1), 'percent');
182
+ }
183
+ return null;
184
+ }
185
+ case 'call':
186
+ case 'call_expression':
187
+ case 'invocation_expression':
188
+ case 'method_invocation': {
189
+ const fn = node.childForFieldName('function') || node.childForFieldName('name');
190
+ const argsNode = node.childForFieldName('arguments') ||
191
+ (node.namedChildren || []).find(c => c.type === 'argument_list' || c.type === 'arguments');
192
+ const args = (argsNode?.namedChildren || []).filter(c => c.type !== 'comment');
193
+ // `"lit{}".format(x)` — a format method on a string receiver.
194
+ if (fn?.type === 'attribute' &&
195
+ fn.childForFieldName('attribute')?.text === 'format') {
196
+ const recv = _unwrap(fn.childForFieldName('object'));
197
+ if (recv && (STRING_NODES.has(recv.type) || recv.type === 'concatenated_string')) {
198
+ return _expandFormat(stringAlternatives(recv, ctx, depth + 1), 'brace');
199
+ }
200
+ return null;
201
+ }
202
+ // Formatting functions from the language vocabulary
203
+ // (fmt.Sprintf, String.format, string.Format).
204
+ let calleeText;
205
+ if (node.type === 'method_invocation') {
206
+ const obj = node.childForFieldName('object');
207
+ calleeText = `${obj ? obj.text + '.' : ''}${node.childForFieldName('name')?.text || ''}`;
208
+ } else {
209
+ calleeText = fn?.text || '';
210
+ }
211
+ const style = ctx.formatCalls && Object.prototype.hasOwnProperty.call(ctx.formatCalls, calleeText)
212
+ ? ctx.formatCalls[calleeText] : null;
213
+ if (style && args[0]) {
214
+ const fmt = stringAlternatives(args[0], ctx, depth + 1);
215
+ return fmt ? _expandFormat(fmt, style) : null;
216
+ }
217
+ return null;
218
+ }
219
+ case 'identifier': {
220
+ if (!ctx.resolve || (ctx.resolveDepth || 0) >= MAX_RESOLVE_DEPTH) return null;
221
+ const values = ctx.resolve(node);
222
+ if (!values || values.length === 0) return null;
223
+ let out = [];
224
+ for (const value of values) {
225
+ const alt = stringAlternatives(value,
226
+ { ...ctx, resolveDepth: (ctx.resolveDepth || 0) + 1 }, depth + 1);
227
+ if (!alt) return null; // one non-string binding: unknown
228
+ out.push(...alt);
229
+ if (out.length > MAX_ALTERNATIVES) return [[WILD]];
230
+ }
231
+ return out;
232
+ }
233
+ default:
234
+ return null;
235
+ }
236
+ }
237
+
238
+ /** Normalize one alternative: merged literals and wildcards; null if it
239
+ * cannot spell an identifier. */
240
+ function _normalize(alt) {
241
+ const out = [];
242
+ for (const piece of alt) {
243
+ if (piece === WILD) {
244
+ if (out[out.length - 1] !== WILD) out.push(WILD);
245
+ } else if (piece.length > 0) {
246
+ if (out.length > 0 && out[out.length - 1] !== WILD) out[out.length - 1] += piece;
247
+ else out.push(piece);
248
+ }
249
+ }
250
+ if (out.length === 0) return null; // the empty string names nothing
251
+ for (const piece of out) {
252
+ if (piece !== WILD && !/^[A-Za-z0-9_$]+$/.test(piece)) return null;
253
+ }
254
+ return out;
255
+ }
256
+
257
+ /** Glob string (`_get_*_permissions`) of a normalized alternative. */
258
+ function patternText(pieces) {
259
+ return pieces.map(p => (p === WILD ? '*' : p)).join('');
260
+ }
261
+
262
+ function _specific(pieces) {
263
+ let chars = 0;
264
+ for (const p of pieces) if (p !== WILD) chars += (p.match(/[A-Za-z0-9]/g) || []).length;
265
+ return chars >= MIN_SPECIFIC_CHARS;
266
+ }
267
+
268
+ /**
269
+ * Classify a name expression.
270
+ * @returns {{name?: string, patterns?: string[], dynamic: boolean}}
271
+ * name - one exact spelling; patterns - specific glob patterns (sorted);
272
+ * dynamic - some alternative is unknown or names anything.
273
+ */
274
+ function classifyNameExpression(node, ctx) {
275
+ const alts = stringAlternatives(node, ctx);
276
+ if (!alts) return { dynamic: true };
277
+ const exact = new Set();
278
+ const patterns = new Set();
279
+ let dynamic = false;
280
+ for (const raw of alts) {
281
+ const pieces = _normalize(raw);
282
+ if (!pieces) continue; // cannot spell a member name
283
+ if (!pieces.includes(WILD)) exact.add(pieces[0]);
284
+ else if (_specific(pieces)) patterns.add(patternText(pieces));
285
+ else dynamic = true;
286
+ }
287
+ if (exact.size === 0 && patterns.size === 0) return { dynamic: true };
288
+ if (exact.size === 1 && patterns.size === 0 && !dynamic) {
289
+ return { name: [...exact][0], dynamic: false };
290
+ }
291
+ // Several exact spellings behave as one-literal patterns.
292
+ return { patterns: [...exact, ...patterns].sort(), dynamic };
293
+ }
294
+
295
+ // ---------------------------------------------------------------------------
296
+ // Local string bindings (one hop, same scope)
297
+ // ---------------------------------------------------------------------------
298
+
299
+ function _enclosingScope(node) {
300
+ let cur = node.parent;
301
+ while (cur && !SCOPE_NODES.has(cur.type)) cur = cur.parent;
302
+ return cur;
303
+ }
304
+
305
+ function _bindingPairs(node) {
306
+ switch (node.type) {
307
+ case 'assignment': // Python
308
+ case 'assignment_expression': { // JS, C#
309
+ const left = node.childForFieldName('left');
310
+ const right = node.childForFieldName('right');
311
+ return left?.type === 'identifier' && right ? [[left.text, right]] : [];
312
+ }
313
+ case 'augmented_assignment':
314
+ case 'augmented_assignment_expression':
315
+ case 'compound_assignment_expr': {
316
+ const left = node.childForFieldName('left');
317
+ return left?.type === 'identifier' ? [[left.text, null]] : [];
318
+ }
319
+ case 'variable_declarator': { // JS, Java, C#
320
+ const name = node.childForFieldName('name') ||
321
+ node.namedChildren.find(c => c.type === 'identifier');
322
+ let value = node.childForFieldName('value');
323
+ if (!value) {
324
+ const named = node.namedChildren.filter(c => c !== name && c.type !== 'comment');
325
+ const clause = named.find(c => c.type === 'equals_value_clause');
326
+ value = clause ? clause.namedChild(0) : (named.length > 0 ? named[named.length - 1] : null);
327
+ if (value && (value.type === 'bracketed_argument_list' || value.type.endsWith('_type'))) value = null;
328
+ }
329
+ return name?.type === 'identifier' && value ? [[name.text, value]] : [];
330
+ }
331
+ case 'short_var_declaration': // Go
332
+ case 'assignment_statement': {
333
+ const left = node.childForFieldName('left');
334
+ const right = node.childForFieldName('right');
335
+ const ls = left?.namedChildren || [];
336
+ const rs = right?.namedChildren || [];
337
+ if (ls.length !== rs.length) {
338
+ return ls.filter(l => l.type === 'identifier').map(l => [l.text, null]);
339
+ }
340
+ return ls.map((l, i) => (l.type === 'identifier' ? [l.text, rs[i]] : null)).filter(Boolean);
341
+ }
342
+ case 'var_spec':
343
+ case 'const_spec': {
344
+ const names = node.namedChildren.filter(c => c.type === 'identifier');
345
+ const value = node.childForFieldName('value');
346
+ const rs = value?.namedChildren || [];
347
+ if (names.length !== rs.length) return names.map(n => [n.text, null]);
348
+ return names.map((n, i) => [n.text, rs[i]]);
349
+ }
350
+ default:
351
+ return [];
352
+ }
353
+ }
354
+
355
+ function makeResolver() {
356
+ const scopes = new Map();
357
+ const bindingsOf = scope => {
358
+ const key = `${scope.startIndex}:${scope.endIndex}`;
359
+ let map = scopes.get(key);
360
+ if (map) return map;
361
+ map = new Map();
362
+ scopes.set(key, map);
363
+ const visit = n => {
364
+ for (const child of n.namedChildren || []) {
365
+ // Nested callables own their own bindings.
366
+ if (SCOPE_NODES.has(child.type)) continue;
367
+ for (const [name, value] of _bindingPairs(child)) {
368
+ if (!map.has(name)) map.set(name, []);
369
+ map.get(name).push(value);
370
+ }
371
+ visit(child);
372
+ }
373
+ };
374
+ visit(scope.type === 'method_definition' || scope.type === 'function_definition' ||
375
+ scope.type === 'method_declaration'
376
+ ? (scope.childForFieldName('body') || scope) : scope);
377
+ return map;
378
+ };
379
+ return ident => {
380
+ const scope = _enclosingScope(ident);
381
+ if (!scope) return null;
382
+ const values = bindingsOf(scope).get(ident.text);
383
+ // A parameter/captured name (no binding here) or any non-value
384
+ // rebinding (augmented, tuple split) leaves the name unknown.
385
+ if (!values || values.some(v => !v)) return null;
386
+ return values;
387
+ };
388
+ }
389
+
390
+ // ---------------------------------------------------------------------------
391
+ // Receivers
392
+ // ---------------------------------------------------------------------------
393
+
394
+ function _enclosingClass(node, stopAtThisRebinding) {
395
+ let cur = node.parent;
396
+ while (cur) {
397
+ if (CLASS_NODES.has(cur.type)) return cur;
398
+ if (stopAtThisRebinding && (THIS_REBINDING_FUNCTIONS.has(cur.type) ||
399
+ // An object-literal method binds `this` to the literal.
400
+ (cur.type === 'method_definition' && cur.parent?.type === 'object'))) return null;
401
+ cur = cur.parent;
402
+ }
403
+ return null;
404
+ }
405
+
406
+ function _classInfo(classNode) {
407
+ const nameNode = classNode.childForFieldName('name') ||
408
+ classNode.namedChildren.find(c => c.type === 'identifier' || c.type === 'type_identifier');
409
+ if (!nameNode) return null;
410
+ return { name: nameNode.text, line: classNode.startPosition.row + 1 };
411
+ }
412
+
413
+ /** Is `ident` the self parameter of the method it appears in? */
414
+ function _selfParamClass(ident, selfNames) {
415
+ if (!selfNames || !selfNames.includes(ident.text)) return null;
416
+ let fn = ident.parent;
417
+ while (fn && fn.type !== 'function_definition') fn = fn.parent;
418
+ if (!fn) return null;
419
+ const params = fn.childForFieldName('parameters');
420
+ const first = params?.namedChildren?.find(c => c.type !== 'comment');
421
+ const firstName = first?.type === 'identifier' ? first.text
422
+ : first?.childForFieldName?.('name')?.text || null;
423
+ if (firstName !== ident.text) return null;
424
+ // Decorated methods sit in decorated_definition inside the class block.
425
+ let owner = fn.parent;
426
+ if (owner?.type === 'decorated_definition') owner = owner.parent;
427
+ if (owner?.type !== 'block' || owner.parent?.type !== 'class_definition') return null;
428
+ return _classInfo(owner.parent);
429
+ }
430
+
431
+ /**
432
+ * Receiver facts of a reflective access. Returns null (untyped: any class) or
433
+ * { kind: 'self', className, classLine } - self/this/getClass()/GetType();
434
+ * { kind: 'name', name } - an identifier that may name a class (resolved
435
+ * against the index at query time; anything else stays untyped);
436
+ * { kind: 'type', name } - a class literal (`Foo.class`, `typeof(Foo)`).
437
+ */
438
+ function classifyReceiver(node, language) {
439
+ node = _unwrap(node);
440
+ if (!node) return null;
441
+ const traits = langTraits(language) || {};
442
+ const api = traits.reflectionApi || {};
443
+ const selfNames = Array.isArray(traits.selfParam) ? traits.selfParam : [];
444
+ const selfOf = (anchor, stop) => {
445
+ const cls = _enclosingClass(anchor, stop);
446
+ const info = cls && _classInfo(cls);
447
+ return info ? { kind: 'self', className: info.name, classLine: info.line } : null;
448
+ };
449
+ if (node.type === 'this' || node.type === 'this_expression' ||
450
+ (node.type === 'identifier' && node.text === 'this' && selfNames.includes('this'))) {
451
+ return selfOf(node, true);
452
+ }
453
+ if (node.type === 'identifier') {
454
+ const info = _selfParamClass(node, selfNames);
455
+ if (info) return { kind: 'self', className: info.name, classLine: info.line };
456
+ return { kind: 'name', name: node.text };
457
+ }
458
+ // type(self) / self.__class__ / getClass() / this.GetType()
459
+ if (node.type === 'call' || node.type === 'method_invocation' || node.type === 'invocation_expression') {
460
+ const fn = node.childForFieldName('function') || node.childForFieldName('name');
461
+ const obj = node.type === 'method_invocation' ? node.childForFieldName('object') : null;
462
+ const argsNode = node.childForFieldName('arguments') ||
463
+ node.namedChildren.find(c => c.type === 'argument_list');
464
+ const args = (argsNode?.namedChildren || []).filter(c => c.type !== 'comment');
465
+ const fnName = fn?.type === 'member_access_expression'
466
+ ? fn.childForFieldName('name')?.text : fn?.text;
467
+ const fnObj = fn?.type === 'member_access_expression' ? fn.childForFieldName('expression') : obj;
468
+ if ((api.selfTypeCalls || []).includes(fnName) && args.length === 0 &&
469
+ (!fnObj || fnObj.type === 'this' || fnObj.type === 'this_expression')) {
470
+ return selfOf(node, true);
471
+ }
472
+ if (fnName === 'type' && args.length === 1 && args[0].type === 'identifier') {
473
+ const info = _selfParamClass(args[0], selfNames);
474
+ if (info) return { kind: 'self', className: info.name, classLine: info.line };
475
+ }
476
+ return null;
477
+ }
478
+ if (node.type === 'attribute' && node.childForFieldName('attribute')?.text === '__class__') {
479
+ const obj = node.childForFieldName('object');
480
+ const info = obj?.type === 'identifier' && _selfParamClass(obj, selfNames);
481
+ if (info) return { kind: 'self', className: info.name, classLine: info.line };
482
+ return null;
483
+ }
484
+ // Foo.class (Java) / typeof(Foo) (C#): certainly a type.
485
+ if ((api.typeLiteralNodes || []).includes(node.type)) {
486
+ const t = node.namedChildren.find(c => c.type !== 'comment');
487
+ const name = t ? String(t.text).replace(/<.*$/s, '').split('.').pop() : null;
488
+ // C# `typeof(Outcome<>)` / `typeof(Outcome<,>)`: the written arity
489
+ // selects one of the same-name types (fix #389).
490
+ let arity;
491
+ if (t?.type === 'generic_name') {
492
+ const list = t.namedChildren.find(c => c.type === 'type_argument_list');
493
+ const args = list ? list.namedChildren.filter(c => c.type !== 'comment') : [];
494
+ arity = args.length > 0 ? args.length
495
+ : list ? list.children.filter(c => c.type === ',').length + 1 : 0;
496
+ } else if (t?.type === 'identifier' && node.type === 'typeof_expression') {
497
+ arity = 0;
498
+ }
499
+ return name ? { kind: 'type', name, ...(arity !== undefined && { arity }) } : null;
500
+ }
501
+ return null;
502
+ }
503
+
504
+ // ---------------------------------------------------------------------------
505
+ // Site extraction
506
+ // ---------------------------------------------------------------------------
507
+
508
+ // Member-shaped callee node -> [member-name field, receiver field].
509
+ const MEMBER_CALLEE_FIELDS = {
510
+ selector_expression: ['field', 'operand'], // Go
511
+ member_expression: ['property', 'object'], // JS/TS
512
+ member_access_expression: ['name', 'expression'], // C#
513
+ attribute: ['attribute', 'object'], // Python
514
+ };
515
+
516
+ /** Callee text, member name and receiver of a call node (no arguments). */
517
+ function _calleeParts(node, wantMembers, wantQualified) {
518
+ if (node.type === 'method_invocation') {
519
+ if (!wantMembers) return null;
520
+ const name = node.childForFieldName('name')?.text || '';
521
+ return { calleeText: name, member: name, object: node.childForFieldName('object') };
522
+ }
523
+ const fn = node.childForFieldName('function') || node.namedChild(0);
524
+ if (!fn) return null;
525
+ if (fn.type === 'identifier') {
526
+ return { calleeText: fn.text || '', member: fn.text, object: null, bare: true };
527
+ }
528
+ if (!wantMembers) return wantQualified ? { calleeText: null, fn, member: null, object: null } : null;
529
+ const fields = MEMBER_CALLEE_FIELDS[fn.type];
530
+ if (!fields) return wantQualified ? { calleeText: null, fn, member: null, object: null } : null;
531
+ return {
532
+ calleeText: null,
533
+ fn,
534
+ member: fn.childForFieldName(fields[0])?.text || null,
535
+ object: fn.childForFieldName(fields[1]),
536
+ };
537
+ }
538
+
539
+ function _callArgs(node) {
540
+ const argsNode = node.childForFieldName('arguments') ||
541
+ (node.namedChildren || []).find(c =>
542
+ c.type === 'argument_list' || c.type === 'arguments');
543
+ return argsNode ? argsNode.namedChildren.filter(c => c.type !== 'comment') : null;
544
+ }
545
+
546
+ /**
547
+ * Reflection sites of one parsed file.
548
+ * Each site: { kind, line, expression, dynamic, name?, patterns?, receiver?,
549
+ * membersOnly? }.
550
+ */
551
+ function reflectionSitesInTree(tree, language, code = null) {
552
+ const traits = langTraits(language) || {};
553
+ const api = traits.reflectionApi;
554
+ if (!api || !tree) return [];
555
+ const resolve = makeResolver();
556
+ const ctx = { formatCalls: api.formatCalls || null, resolve };
557
+ const sites = [];
558
+ const push = (node, kind, nameNode, receiverNode, receiverFact) => {
559
+ const cls = classifyNameExpression(nameNode, ctx);
560
+ const expression = String(node.text || '');
561
+ sites.push({
562
+ kind,
563
+ line: node.startPosition.row + 1,
564
+ expression: expression.length > EXPRESSION_CAP
565
+ ? expression.slice(0, EXPRESSION_CAP) + '...' : expression,
566
+ ...(cls.name && { name: cls.name }),
567
+ ...(cls.patterns && { patterns: cls.patterns }),
568
+ dynamic: cls.dynamic,
569
+ ...(receiverFact ? { receiver: receiverFact }
570
+ : receiverNode && { receiver: classifyReceiver(receiverNode, language) }),
571
+ ...(api.membersOnly && { membersOnly: true }),
572
+ });
573
+ };
574
+ const ns = api.namespaceLookups;
575
+ // Candidate nodes are located natively: reflective calls from the
576
+ // vocabulary's spelling positions (descendantForIndex, walking up to the
577
+ // enclosing call), computed member keys by node type. No JS-side walk of
578
+ // the whole tree.
579
+ const hintPattern = api.hintPattern || null;
580
+ const useHints = code != null && hintPattern;
581
+ const types = [];
582
+ if (!useHints) {
583
+ types.push(...CALL_NODE_TYPES);
584
+ if (ns?.subscriptOf) types.push('subscript');
585
+ }
586
+ if (api.computedMembers) types.push('subscript_expression');
587
+ const wantMembers = (api.members || []).length > 0;
588
+ // Qualified function callees (`Reflect.get`) need the callee text of
589
+ // member-shaped calls; bare vocabularies (`getattr`) never do.
590
+ const wantQualified = (api.functions || []).some(rule => rule.qualified);
591
+ const visitNode = node => {
592
+ if (node.type === 'subscript_expression') {
593
+ // obj["on" + evt] / this[`handle${t}`] (JS/TS computed member
594
+ // access): only a key built in place from literal fragments
595
+ // counts; opaque keys stay the computed-dispatch disclosure.
596
+ const index = node.childForFieldName('index');
597
+ const inner = _unwrap(index);
598
+ if (!inner || !COMPUTED_PATTERN_KEYS.has(inner.type)) return;
599
+ const cls = classifyNameExpression(index, ctx);
600
+ if (cls.patterns && cls.patterns.length > 0) {
601
+ push(node, 'computed', index, node.childForFieldName('object'));
602
+ }
603
+ return;
604
+ }
605
+ if (node.type === 'subscript') {
606
+ // `globals()[name]`: a lookup in this file's module namespace.
607
+ const value = node.childForFieldName('value');
608
+ const key = node.childForFieldName('subscript');
609
+ if (value && key && ns.subscriptOf.test(String(value.text || '').replace(/\s+/g, ''))) {
610
+ push(node, 'namespace', key, null, MODULE_RECEIVER);
611
+ }
612
+ return;
613
+ }
614
+ const call = _calleeParts(node, wantMembers, wantQualified);
615
+ if (!call) return;
616
+ // Match on the callee spelling first; arguments are read only for a
617
+ // vocabulary hit (most calls in a hinted file are unrelated).
618
+ const calleeText = () => (call.calleeText ??= call.fn.text || '');
619
+ for (const rule of api.functions || []) {
620
+ if (!rule.callee.test(calleeText())) continue;
621
+ const args = _callArgs(node);
622
+ if (args && args[rule.nameArg]) {
623
+ push(node, rule.kind || calleeText().split('.').pop(),
624
+ args[rule.nameArg], args[rule.receiverArg]);
625
+ }
626
+ return;
627
+ }
628
+ if (call.member && call.object) {
629
+ for (const rule of api.members || []) {
630
+ if (!rule.member.test(call.member)) continue;
631
+ const args = _callArgs(node);
632
+ if (args && args[rule.nameArg]) push(node, call.member, args[rule.nameArg], call.object);
633
+ return;
634
+ }
635
+ }
636
+ // `eval(cmd)`: the receiver is this file's own top-level namespace,
637
+ // so even a fully dynamic name is bounded to it.
638
+ if (ns?.calls && call.bare && ns.calls.test(call.calleeText)) {
639
+ const args = _callArgs(node);
640
+ if (args && args[0]) push(node, call.calleeText, args[0], null, MODULE_RECEIVER);
641
+ }
642
+ };
643
+ try {
644
+ const candidates = [];
645
+ if (useHints) {
646
+ const seen = new Set();
647
+ const re = new RegExp(hintPattern.source, 'g');
648
+ let m;
649
+ while ((m = re.exec(code)) !== null) {
650
+ let node = tree.rootNode.descendantForIndex(m.index);
651
+ for (let i = 0; node && i < 6; i++, node = node.parent) {
652
+ if (!CALL_NODE_TYPES.includes(node.type)) continue;
653
+ if (!seen.has(node.startIndex)) { seen.add(node.startIndex); candidates.push(node); }
654
+ // `globals()[name]`: the lookup is the enclosing subscript.
655
+ const parent = node.parent;
656
+ if (ns?.subscriptOf && parent?.type === 'subscript' &&
657
+ !seen.has(-parent.startIndex - 1)) {
658
+ seen.add(-parent.startIndex - 1);
659
+ candidates.push(parent);
660
+ }
661
+ break;
662
+ }
663
+ }
664
+ }
665
+ if (types.length > 0) candidates.push(...tree.rootNode.descendantsOfType(types));
666
+ candidates.sort((a, b) => a.startIndex - b.startIndex || (a.endIndex - b.endIndex));
667
+ for (const node of candidates) visitNode(node);
668
+ } catch (_) { /* partial inventory is disclosed as-is */ }
669
+ return sites;
670
+ }
671
+
672
+ // ---------------------------------------------------------------------------
673
+ // Project matcher
674
+ // ---------------------------------------------------------------------------
675
+
676
+ function globMatches(pattern, name) {
677
+ const parts = pattern.split('*');
678
+ if (parts.length === 1) return pattern === name;
679
+ const first = parts[0];
680
+ const last = parts[parts.length - 1];
681
+ if (!name.startsWith(first) || !name.endsWith(last) ||
682
+ name.length < first.length + last.length) return false;
683
+ let pos = first.length;
684
+ const end = name.length - last.length;
685
+ for (let i = 1; i < parts.length - 1; i++) {
686
+ const at = name.indexOf(parts[i], pos);
687
+ if (at < 0 || at + parts[i].length > end) return false;
688
+ pos = at + parts[i].length;
689
+ }
690
+ return true;
691
+ }
692
+
693
+ function _longestFragment(pattern) {
694
+ let best = '';
695
+ for (const p of pattern.split('*')) if (p.length > best.length) best = p;
696
+ return best;
697
+ }
698
+
699
+ function _family(language) {
700
+ return langTraits(language)?.reflectionApi?.family || language;
701
+ }
702
+
703
+ /**
704
+ * Memoized project matcher over the reflection inventory: pattern entries
705
+ * grouped by their longest literal fragment, so a name probes only the
706
+ * fragments it contains (one Map lookup per substring length).
707
+ */
708
+ function reflectionPatternIndex(index) {
709
+ if (index._reflectionPatternIndex) return index._reflectionPatternIndex;
710
+ const { projectReflectionSites } = require('./ast-analysis');
711
+ const byFile = projectReflectionSites(index);
712
+ const byFragment = new Map();
713
+ const lengths = new Set();
714
+ let entryCount = 0;
715
+ const moduleScoped = new Map();
716
+ for (const [file, sites] of byFile) {
717
+ const fe = index.files.get(file);
718
+ if (!fe) continue;
719
+ for (const site of sites) {
720
+ if (site.receiver?.kind === 'module') {
721
+ // Bounded to this file's top-level names: a fully dynamic
722
+ // name reaches every one of them.
723
+ const patterns = site.dynamic ? ['*'] : (site.patterns || (site.name ? [site.name] : []));
724
+ for (const pattern of patterns) {
725
+ if (!moduleScoped.has(file)) moduleScoped.set(file, []);
726
+ moduleScoped.get(file).push({ pattern, file, relativePath: fe.relativePath,
727
+ language: fe.language, family: _family(fe.language), site });
728
+ }
729
+ continue;
730
+ }
731
+ if (!site.patterns) continue;
732
+ for (const pattern of site.patterns) {
733
+ const frag = _longestFragment(pattern);
734
+ if (!frag) continue;
735
+ lengths.add(frag.length);
736
+ if (!byFragment.has(frag)) byFragment.set(frag, []);
737
+ byFragment.get(frag).push({ pattern, file, relativePath: fe.relativePath,
738
+ language: fe.language, family: _family(fe.language), site });
739
+ entryCount++;
740
+ }
741
+ }
742
+ }
743
+ const result = { byFragment, lengths: [...lengths].sort((a, b) => a - b), entryCount, moduleScoped };
744
+ index._reflectionPatternIndex = result;
745
+ return result;
746
+ }
747
+
748
+ /** Pattern entries whose glob spells `name` (same language family as `language`). */
749
+ function patternEntriesFor(index, name, language, file = null) {
750
+ const pidx = reflectionPatternIndex(index);
751
+ if (!name) return [];
752
+ const out = [];
753
+ if (file && pidx.moduleScoped.has(file)) {
754
+ for (const entry of pidx.moduleScoped.get(file)) {
755
+ if (globMatches(entry.pattern, name)) out.push(entry);
756
+ }
757
+ }
758
+ if (pidx.entryCount === 0) return out;
759
+ const family = language ? _family(language) : null;
760
+ const seenFrag = new Set();
761
+ for (const len of pidx.lengths) {
762
+ if (len > name.length) break;
763
+ for (let i = 0; i + len <= name.length; i++) {
764
+ const frag = name.substr(i, len);
765
+ if (seenFrag.has(frag)) continue;
766
+ seenFrag.add(frag);
767
+ const entries = pidx.byFragment.get(frag);
768
+ if (!entries) continue;
769
+ for (const entry of entries) {
770
+ if (family && entry.family !== family) continue;
771
+ if (globMatches(entry.pattern, name)) out.push(entry);
772
+ }
773
+ }
774
+ }
775
+ return out;
776
+ }
777
+
778
+ function _classRefFor(index, name, file, line, arity) {
779
+ const ci = require('./class-identity');
780
+ const info = ci.classDefsNamed(index, name);
781
+ if (info.entries.length === 0) return null;
782
+ if (line != null) {
783
+ const hit = info.entries.find(e => e.def.file === file &&
784
+ e.def.startLine <= line && (e.def.endLine || e.def.startLine) >= line);
785
+ if (hit) return { name, key: hit.key, def: hit.def };
786
+ }
787
+ return ci.resolveClassRef(index, name, file, Number.isInteger(arity) ? { arity } : {});
788
+ }
789
+
790
+ function _reachesKey(index, fromRef, targetKey, targetName) {
791
+ const ci = require('./class-identity');
792
+ const visited = new Set();
793
+ const queue = [fromRef];
794
+ while (queue.length > 0) {
795
+ const cur = queue.shift();
796
+ const vk = cur.key || `n:${cur.name}`;
797
+ if (visited.has(vk) || visited.size > 256) continue;
798
+ visited.add(vk);
799
+ if (cur.key && cur.key === targetKey) return true;
800
+ if (!cur.key && cur.name === targetName && !cur.external) return true; // unresolved: maybe
801
+ if (cur.external) continue;
802
+ if (cur.key || cur.def) queue.push(...ci.parentRefsOf(index, cur));
803
+ }
804
+ return false;
805
+ }
806
+
807
+ /**
808
+ * Could the reflective receiver of `entry` hold an instance whose class has
809
+ * member `def`? Untyped receivers answer yes; self/type receivers answer yes
810
+ * for the same class, an ancestor or descendant, and (multiple inheritance /
811
+ * mixins) classes sharing a descendant. Unresolved identities answer yes.
812
+ */
813
+ function reflectionEntryReaches(index, entry, def) {
814
+ const site = entry.site;
815
+ if (site.membersOnly && !def.className && !def.receiver) return false;
816
+ const recv = site.receiver;
817
+ if (!recv) return true;
818
+ if (recv.kind === 'module') {
819
+ return def.file === entry.file && !def.className && !def.receiver;
820
+ }
821
+ let recvRef = null;
822
+ if (recv.kind === 'self') {
823
+ recvRef = _classRefFor(index, recv.className, entry.file, recv.classLine);
824
+ } else if (recv.kind === 'name') {
825
+ // An identifier naming a project class is a class receiver; any other
826
+ // identifier is an untyped value.
827
+ recvRef = _classRefFor(index, recv.name, entry.file, null);
828
+ if (!recvRef) return true;
829
+ } else if (recv.kind === 'type') {
830
+ recvRef = _classRefFor(index, recv.name, entry.file, null, recv.arity);
831
+ if (!recvRef) {
832
+ // An out-of-project type (`typeof(Convert)`): its members are not
833
+ // project code, but invoking a member it declares dispatches into
834
+ // project classes deriving from it (`typeof(IConvertible)`).
835
+ const { ownerDerivesFromExternal } = require('./contract-membership');
836
+ return ownerDerivesFromExternal(index, def, [recv.name]);
837
+ }
838
+ }
839
+ if (!recvRef || !recvRef.key) return true;
840
+ const ci = require('./class-identity');
841
+ const ownerRef = ci.ownerRefOf(index, def);
842
+ // A class-level receiver never reaches a free function.
843
+ if (!ownerRef) return false;
844
+ if (!ownerRef.key) return true;
845
+ if (ownerRef.key === recvRef.key) return true;
846
+ if (_reachesKey(index, recvRef, ownerRef.key, ownerRef.name)) return true; // inherited member
847
+ if (_reachesKey(index, ownerRef, recvRef.key, recvRef.name)) return true; // subclass member
848
+ return ci.shareDescendant(index, recvRef, [def]);
849
+ }
850
+
851
+ /** Reaching pattern entries for one definition (sorted file, line). */
852
+ function reflectionPatternReferences(index, def) {
853
+ const lang = index.files.get(def.file)?.language;
854
+ const entries = patternEntriesFor(index, def.name, lang, def.file)
855
+ .filter(entry => reflectionEntryReaches(index, entry, def));
856
+ entries.sort((a, b) => (a.relativePath < b.relativePath ? -1 : a.relativePath > b.relativePath ? 1 : 0) ||
857
+ a.site.line - b.site.line || (a.pattern < b.pattern ? -1 : a.pattern > b.pattern ? 1 : 0));
858
+ return entries;
859
+ }
860
+
861
+ function resetReflectionIndex(index) {
862
+ index._reflectionPatternIndex = null;
863
+ }
864
+
865
+ module.exports = {
866
+ WILD,
867
+ stringAlternatives,
868
+ classifyNameExpression,
869
+ classifyReceiver,
870
+ reflectionSitesInTree,
871
+ globMatches,
872
+ patternEntriesFor,
873
+ reflectionEntryReaches,
874
+ reflectionPatternReferences,
875
+ reflectionPatternIndex,
876
+ resetReflectionIndex,
877
+ };