ucn 4.2.3 → 5.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/.claude/skills/ucn/SKILL.md +89 -77
  2. package/.claude/skills/ucn/references/commands.md +62 -68
  3. package/.claude/skills/ucn/references/trust-contract.md +31 -6
  4. package/README.md +438 -305
  5. package/assets/demo.svg +31 -0
  6. package/cli/index.js +430 -1385
  7. package/core/account.js +144 -34
  8. package/core/analysis.js +182 -72
  9. package/core/ast-analysis.js +279 -0
  10. package/core/bridge.js +205 -24
  11. package/core/brief.js +27 -58
  12. package/core/build-worker.js +21 -140
  13. package/core/cache.js +513 -11
  14. package/core/callers.js +4920 -456
  15. package/core/check.js +13 -4
  16. package/core/command-contracts.js +402 -0
  17. package/core/compilation-database.js +276 -0
  18. package/core/confidence.js +4 -1
  19. package/core/deadcode.js +397 -19
  20. package/core/discovery.js +359 -46
  21. package/core/entrypoints.js +195 -41
  22. package/core/execute.js +887 -81
  23. package/core/graph-build.js +162 -7
  24. package/core/graph.js +53 -77
  25. package/core/imports.js +65 -6
  26. package/core/index-ir.js +138 -0
  27. package/core/ir.js +195 -0
  28. package/core/output/analysis.js +212 -22
  29. package/core/output/brief.js +23 -0
  30. package/core/output/check.js +4 -0
  31. package/core/output/doctor.js +37 -6
  32. package/core/output/endpoints.js +5 -2
  33. package/core/output/extraction.js +24 -12
  34. package/core/output/find.js +141 -36
  35. package/core/output/graph.js +11 -5
  36. package/core/output/public.js +462 -0
  37. package/core/output/refactoring.js +42 -10
  38. package/core/output/reporting.js +97 -20
  39. package/core/output/search.js +24 -16
  40. package/core/output/shared.js +22 -1
  41. package/core/output/tracing.js +30 -15
  42. package/core/output-budget.js +295 -0
  43. package/core/output.js +1 -0
  44. package/core/parallel-build.js +44 -11
  45. package/core/parser.js +3 -3
  46. package/core/project.js +384 -187
  47. package/core/public-command.js +47 -0
  48. package/core/registry.js +247 -117
  49. package/core/reporting.js +312 -290
  50. package/core/search.js +317 -185
  51. package/core/semantic-provider.js +110 -0
  52. package/core/stacktrace.js +25 -0
  53. package/core/tracing.js +101 -51
  54. package/core/trust-matrix.js +19 -40
  55. package/core/verify.js +534 -37
  56. package/languages/adapter.js +218 -0
  57. package/languages/c-family.js +2791 -0
  58. package/languages/c.js +3 -0
  59. package/languages/cpp.js +3 -0
  60. package/languages/csharp.js +1402 -0
  61. package/languages/go.js +60 -21
  62. package/languages/html.js +2 -2
  63. package/languages/index.js +85 -7
  64. package/languages/java.js +396 -13
  65. package/languages/javascript.js +199 -19
  66. package/languages/python.js +964 -22
  67. package/languages/rust.js +1317 -152
  68. package/languages/utils.js +40 -3
  69. package/mcp/server.js +254 -636
  70. package/package.json +39 -22
  71. package/eslint.config.js +0 -43
  72. package/jsconfig.json +0 -10
@@ -0,0 +1,2791 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Shared AST extraction for C and C++.
5
+ *
6
+ * This module deliberately works from tree-sitter node kinds and fields. Text
7
+ * is read only from nodes already identified by the grammar; there is no regex
8
+ * source fallback.
9
+ */
10
+
11
+ const {
12
+ traverseTree,
13
+ traverseTreeCached,
14
+ nodeToLocation,
15
+ extractJSDocstring,
16
+ visitNameNodes,
17
+ sameNode,
18
+ extractStringArg,
19
+ } = require('./utils');
20
+ const { createHash } = require('crypto');
21
+ const { isMainThread } = require('worker_threads');
22
+ const { PARSE_OPTIONS, safeParse } = require('./index');
23
+
24
+ const TYPE_NODES = new Set([
25
+ 'primitive_type', 'type_identifier', 'sized_type_specifier',
26
+ 'qualified_identifier', 'template_type', 'auto', 'decltype',
27
+ ]);
28
+ const CLASS_NODES = new Set([
29
+ 'class_specifier', 'struct_specifier', 'union_specifier', 'enum_specifier',
30
+ ]);
31
+ const FUNCTION_CONTAINERS = new Set(['function_definition', 'declaration', 'field_declaration']);
32
+ const IDENTIFIER_NODES = new Set([
33
+ 'identifier', 'field_identifier', 'type_identifier', 'namespace_identifier',
34
+ 'operator_name', 'destructor_name',
35
+ ]);
36
+
37
+ // Attribute-macro parse recovery. Export/visibility macros (`TS_PUBLIC extern
38
+ // void (*fp)(void *);`, `API int f(int);`) are consumed by the grammar as the
39
+ // declaration's TYPE, displacing the real return type into an ERROR node — and
40
+ // for function-pointer declarators, displacing the real name into a parameter.
41
+ // Blanking the macro token with spaces preserves every byte offset, so one
42
+ // whole-file re-parse yields correct positions for all extractors. Errored
43
+ // files pay the full recovery scan; a clean file still needs one cached walk
44
+ // because `class API Name` is a grammar-valid (but semantically wrong)
45
+ // function-definition shape.
46
+ const DECLARATION_NODES = new Set([
47
+ 'declaration', 'function_definition', 'field_declaration', 'type_definition',
48
+ ]);
49
+ // An identifier node can only carry one of these texts through a mis-parse —
50
+ // they are reserved words in both C and C++.
51
+ const RESERVED_TYPE_KEYWORDS = new Set([
52
+ 'void', 'int', 'char', 'float', 'double', 'long', 'short',
53
+ 'signed', 'unsigned', 'bool', '_Bool',
54
+ ]);
55
+
56
+ function hasMissingChild(node) {
57
+ for (let i = 0; i < node.childCount; i++) {
58
+ if (node.child(i).isMissing) return true;
59
+ }
60
+ return false;
61
+ }
62
+
63
+ function classAttributeShape(node) {
64
+ if (node.type !== 'function_definition') return null;
65
+ const typeNode = node.childForFieldName('type');
66
+ const declaratorNode = node.childForFieldName('declarator');
67
+ if (!['class_specifier', 'struct_specifier', 'union_specifier']
68
+ .includes(typeNode?.type) ||
69
+ typeNode.childForFieldName('body') ||
70
+ declaratorNode?.type !== 'identifier' ||
71
+ node.childForFieldName('body')?.type !== 'compound_statement') {
72
+ return null;
73
+ }
74
+ return typeNode.childForFieldName('name') || null;
75
+ }
76
+
77
+ function isMacroToken(text) {
78
+ const value = String(text || '');
79
+ if (value.length < 2) return false;
80
+ let hasLetter = false;
81
+ for (const character of value) {
82
+ if (character >= 'A' && character <= 'Z') {
83
+ hasLetter = true;
84
+ continue;
85
+ }
86
+ if ((character >= '0' && character <= '9') ||
87
+ character === '_') {
88
+ continue;
89
+ }
90
+ return false;
91
+ }
92
+ return hasLetter;
93
+ }
94
+
95
+ function macroInvocationRange(code, identifier) {
96
+ if (!identifier || !isMacroToken(identifier.text)) return null;
97
+ let cursor = identifier.endIndex;
98
+ while (cursor < code.length &&
99
+ (code[cursor] === ' ' || code[cursor] === '\t')) {
100
+ cursor++;
101
+ }
102
+ if (code[cursor] !== '(') return null;
103
+ let depth = 0;
104
+ let quote = null;
105
+ let escaped = false;
106
+ for (let index = cursor; index < code.length; index++) {
107
+ const character = code[index];
108
+ if (quote) {
109
+ if (escaped) escaped = false;
110
+ else if (character === '\\') escaped = true;
111
+ else if (character === quote) quote = null;
112
+ continue;
113
+ }
114
+ if (character === '"' || character === "'") {
115
+ quote = character;
116
+ continue;
117
+ }
118
+ if (character === '(') depth++;
119
+ else if (character === ')' && --depth === 0) {
120
+ return [identifier.startIndex, index + 1];
121
+ }
122
+ if (character === '\n' && depth === 0) break;
123
+ }
124
+ return null;
125
+ }
126
+
127
+ function isStandaloneMacroLine(code, identifier) {
128
+ if (!identifier || !isMacroToken(identifier.text)) return false;
129
+ const lineStart = code.lastIndexOf('\n', identifier.startIndex - 1) + 1;
130
+ const lineEndAt = code.indexOf('\n', identifier.endIndex);
131
+ const lineEnd = lineEndAt < 0 ? code.length : lineEndAt;
132
+ return code.slice(lineStart, lineEnd).trim() === identifier.text;
133
+ }
134
+
135
+ function errorMacroRanges(node, code) {
136
+ const ranges = [];
137
+ if (node.type === 'ERROR') {
138
+ const named = node.namedChildren || [];
139
+ for (const child of named) {
140
+ if (['class_specifier', 'struct_specifier',
141
+ 'union_specifier'].includes(child.type)) {
142
+ const name = child.childForFieldName('name');
143
+ const invocation = macroInvocationRange(code, name);
144
+ if (invocation) ranges.push(invocation);
145
+ continue;
146
+ }
147
+ if (!IDENTIFIER_NODES.has(child.type)) continue;
148
+ const invocation = macroInvocationRange(code, child);
149
+ if (invocation) ranges.push(invocation);
150
+ else if (isStandaloneMacroLine(code, child)) {
151
+ ranges.push([child.startIndex, child.endIndex]);
152
+ } else if (child === named[0] &&
153
+ child.startPosition.row === node.startPosition.row &&
154
+ isMacroToken(child.text)) {
155
+ // Prefix before a declaration fragment inside one ERROR:
156
+ // `FMT_EXPORT template <...> class X`.
157
+ ranges.push([child.startIndex, child.endIndex]);
158
+ }
159
+ }
160
+ }
161
+ if (node.type === 'function_definition' && node.hasError) {
162
+ const type = node.childForFieldName('type');
163
+ const declarator = node.childForFieldName('declarator');
164
+ if (type && isStandaloneMacroLine(code, type) &&
165
+ declarator &&
166
+ declarator.startPosition.row > type.startPosition.row) {
167
+ // A standalone namespace/opening macro followed by a declaration
168
+ // can be swallowed as the return type of one enormous malformed
169
+ // function. It cannot be a real multi-line C++ return type.
170
+ ranges.push([type.startIndex, type.endIndex]);
171
+ }
172
+ }
173
+ // Prefix macro before a template declaration:
174
+ // `FMT_EXPORT template <typename T> class X`. The grammar represents the
175
+ // prefix plus template head as an erroneous template_function, followed
176
+ // by the class fragment and its body.
177
+ if (node.type === 'template_function' && node.hasError) {
178
+ const macro = node.childForFieldName('name') || node.namedChild(0);
179
+ if (macro && isMacroToken(macro.text) &&
180
+ (node.namedChildren || []).some(child =>
181
+ child.type === 'ERROR' &&
182
+ child.text.trim() === 'template')) {
183
+ ranges.push([macro.startIndex, macro.endIndex]);
184
+ }
185
+ }
186
+ return ranges;
187
+ }
188
+
189
+ function macroTypeRanges(tree, code) {
190
+ const ranges = [];
191
+ const treeHasError = tree.rootNode.hasError;
192
+ traverseTree(tree.rootNode, node => {
193
+ ranges.push(...errorMacroRanges(node, code));
194
+ const classAttribute = classAttributeShape(node);
195
+ if (!node.hasError && !classAttribute) {
196
+ // In an errored tree a clean subtree cannot hide a recovery
197
+ // candidate. A wholly clean tree still needs one traversal for
198
+ // grammar-valid `class API Name` misparses.
199
+ return treeHasError ? false : true;
200
+ }
201
+ if (!DECLARATION_NODES.has(node.type)) return true;
202
+ const typeNode = node.childForFieldName('type');
203
+ const declaratorNode = node.childForFieldName('declarator');
204
+ const directErrorNode = (node.namedChildren || [])
205
+ .find(child => child.type === 'ERROR');
206
+ if (node.type === 'function_definition' && typeNode &&
207
+ directErrorNode?.namedChildCount === 1 &&
208
+ directErrorNode.namedChild(0)?.type === 'identifier' &&
209
+ !RESERVED_TYPE_KEYWORDS.has(
210
+ directErrorNode.namedChild(0).text) &&
211
+ functionDeclarator(node)) {
212
+ ranges.push([
213
+ directErrorNode.namedChild(0).startIndex,
214
+ directErrorNode.namedChild(0).endIndex,
215
+ ]);
216
+ return true;
217
+ }
218
+ // `class API Widget { ... }` is parsed as a malformed function:
219
+ // type=`class API`, declarator=`Widget`, body=`{...}`. The bodyless
220
+ // class-specifier plus a bare declarator cannot be a valid function
221
+ // declaration, so the specifier's name is proven to be a class
222
+ // attribute/visibility macro. Blank only that token; the reparse
223
+ // recovers the real class and all member ownership.
224
+ if (classAttribute) {
225
+ ranges.push([classAttribute.startIndex, classAttribute.endIndex]);
226
+ return true;
227
+ }
228
+ // Calling-convention/export macro between a builtin return type and
229
+ // function name (`int CJSON_CDECL main(void)`) splits into a missing-;
230
+ // declaration plus a malformed function_definition on the SAME line.
231
+ // The declarator identifier is the macro token in that AST shape.
232
+ const next = node.nextNamedSibling;
233
+ if (typeNode && declaratorNode?.type === 'identifier' &&
234
+ hasMissingChild(node) &&
235
+ next?.type === 'function_definition' &&
236
+ next.startPosition.row === node.startPosition.row) {
237
+ ranges.push([declaratorNode.startIndex, declaratorNode.endIndex]);
238
+ return true;
239
+ }
240
+ if (!typeNode || typeNode.type !== 'type_identifier') return true;
241
+ const directError = !!directErrorNode;
242
+ const identity = declaratorIdentity(functionDeclarator(node));
243
+ // `MACRO Type::Type(...) : Base{...} { ... }` is parsed as a
244
+ // declaration whose initializer consumes the base brace and whose
245
+ // real body becomes a sibling compound_statement. A qualified
246
+ // constructor cannot have a return type, so an all-caps type token is
247
+ // compiler-proven decoration and may be blanked safely.
248
+ const qualifiedConstructorMacro = node.type === 'declaration' &&
249
+ node.hasError && isMacroToken(typeNode.text) &&
250
+ identity?.className &&
251
+ (identity.name === identity.className ||
252
+ identity.name === `~${identity.className}`);
253
+ // Stacked attribute macros (`A B extern void (*fp)(void *);`) split
254
+ // into a fragment declaration [type_identifier, identifier,
255
+ // MISSING ';'] plus a clean tail — no ERROR node, so the evidence is
256
+ // the missing semicolon on a bare two-identifier fragment. Blanking
257
+ // the type re-joins the pair (the next round handles the inner
258
+ // macro) and removes the phantom state var the fragment indexed.
259
+ const bareFragment = declaratorNode && declaratorNode.type === 'identifier' &&
260
+ node.namedChildCount === 2 && hasMissingChild(node);
261
+ if (directError || bareFragment || qualifiedConstructorMacro ||
262
+ RESERVED_TYPE_KEYWORDS.has(identity?.name)) {
263
+ ranges.push([typeNode.startIndex, typeNode.endIndex]);
264
+ }
265
+ return true;
266
+ });
267
+ return [...new Map(ranges.map(range => [
268
+ `${range[0]}:${range[1]}`, range,
269
+ ])).values()];
270
+ }
271
+
272
+ function countParseErrors(node) {
273
+ let count = node.type === 'ERROR' ? 1 : 0;
274
+ for (let i = 0; i < node.childCount; i++) {
275
+ const child = node.child(i);
276
+ if (child.isMissing) count++;
277
+ else if (child.hasError || child.type === 'ERROR') count += countParseErrors(child);
278
+ }
279
+ return count;
280
+ }
281
+
282
+ // original code → blanked code (null = no recovery applies). The extractors
283
+ // each re-parse the same file content; the memo makes recovery detection a
284
+ // one-time cost per file content.
285
+ const RECOVERY_MEMO_MAX = 8;
286
+ const recoveryMemo = new Map();
287
+ // Whether the selected tree differs from the literal-source parse. A recovered
288
+ // tree can be completely error-free, so `tree.rootNode.hasError` alone cannot
289
+ // disclose that conditional-compilation or attribute recovery was required.
290
+ const recoveryAppliedMemo = new Map();
291
+ const recoveryAppliedByTree = new WeakMap();
292
+ // A selected conditional tree may be derived from an attribute-normalized
293
+ // all-source view. Keep that byte/line-preserving source with the selected
294
+ // native tree so secondary extraction does not reintroduce the declaration
295
+ // macros that recovery already proved were syntactic adapters.
296
+ const allSourceRecoveryByTree = new WeakMap();
297
+ // C/C++ usage, test, and consistency queries revisit the same files across
298
+ // separate operations. A recovered tree is immutable, so retain a bounded
299
+ // content-addressed LRU per grammar instead of reparsing it for every symbol.
300
+ // Hash keys avoid retaining a second copy of every source string. The byte
301
+ // budget is based on source size (native tree size is not exposed); eviction
302
+ // explicitly releases native trees rather than waiting for N-API finalizers.
303
+ const TREE_CACHE_MAX_ENTRIES = 128;
304
+ const TREE_CACHE_MAX_SOURCE_BYTES = 32 * 1024 * 1024;
305
+ const treeCacheByParser = new WeakMap();
306
+ // Secondary all-source AST for a selected conditional tree. Weak keys tie its
307
+ // lifetime to the bounded primary-tree LRU, so repeated extractors and agent
308
+ // queries pay one additional parse per recovered file instead of one per
309
+ // symbol. This is critical for template-heavy C++ headers.
310
+ const allSourceTreeBySelected = new WeakMap();
311
+ // Replacement-list nodes are opaque leaves in the C grammars. Usage queries
312
+ // used to traverse an entire (sometimes 100k-node) header for every requested
313
+ // name merely to rediscover the same handful of macro definitions. Trees are
314
+ // immutable, so retain the exact AST node set without retaining dead trees.
315
+ const macroDefinitionsByTree = new WeakMap();
316
+
317
+ function macroDefinitionNodes(tree) {
318
+ const cached = macroDefinitionsByTree.get(tree);
319
+ if (cached) return cached;
320
+ const definitions = [];
321
+ traverseTreeCached(tree.rootNode, node => {
322
+ if (node.type === 'preproc_function_def' || node.type === 'preproc_def') {
323
+ definitions.push(node);
324
+ return false;
325
+ }
326
+ return true;
327
+ });
328
+ macroDefinitionsByTree.set(tree, definitions);
329
+ return definitions;
330
+ }
331
+
332
+ function treeCacheKey(code) {
333
+ return `${code.length}:${createHash('sha256').update(code).digest('base64url')}`;
334
+ }
335
+
336
+ function cachedCFamilyTree(parser, key) {
337
+ const cache = treeCacheByParser.get(parser);
338
+ const entry = cache?.entries.get(key);
339
+ if (!entry) return null;
340
+ cache.entries.delete(key);
341
+ cache.entries.set(key, entry);
342
+ return entry.tree;
343
+ }
344
+
345
+ function cacheCFamilyTree(parser, key, tree, sourceBytes) {
346
+ let cache = treeCacheByParser.get(parser);
347
+ if (!cache) {
348
+ cache = { entries: new Map(), sourceBytes: 0 };
349
+ treeCacheByParser.set(parser, cache);
350
+ }
351
+ const previous = cache.entries.get(key);
352
+ if (previous) {
353
+ cache.sourceBytes -= previous.sourceBytes;
354
+ if (previous.tree !== tree) previous.tree.delete?.();
355
+ cache.entries.delete(key);
356
+ }
357
+ cache.entries.set(key, { tree, sourceBytes });
358
+ cache.sourceBytes += sourceBytes;
359
+ while (cache.entries.size > TREE_CACHE_MAX_ENTRIES ||
360
+ cache.sourceBytes > TREE_CACHE_MAX_SOURCE_BYTES) {
361
+ const oldestKey = cache.entries.keys().next().value;
362
+ const oldest = cache.entries.get(oldestKey);
363
+ cache.entries.delete(oldestKey);
364
+ cache.sourceBytes -= oldest.sourceBytes;
365
+ if (oldest.tree !== tree) oldest.tree.delete?.();
366
+ }
367
+ }
368
+
369
+ function releaseCFamilyTree(parser, code, tree) {
370
+ const cache = treeCacheByParser.get(parser);
371
+ if (cache) {
372
+ const key = treeCacheKey(code);
373
+ const entry = cache.entries.get(key);
374
+ if (entry?.tree === tree) {
375
+ cache.entries.delete(key);
376
+ cache.sourceBytes -= entry.sourceBytes;
377
+ }
378
+ }
379
+ const allSource = allSourceTreeBySelected.get(tree);
380
+ allSourceTreeBySelected.delete(tree);
381
+ // Build workers are short-lived and terminate immediately after handing
382
+ // immutable IR back to the parent. Dropping ownership here lets their
383
+ // isolate reclaim all native trees in one teardown; eagerly walking and
384
+ // deleting every tree serialized worker completion and cost >10% cold
385
+ // throughput on fmt. The main process is long-lived, so direct/sequential
386
+ // indexing still releases native memory deterministically.
387
+ if (isMainThread) {
388
+ allSource?.delete?.();
389
+ tree?.delete?.();
390
+ }
391
+ }
392
+
393
+ function blankLine(line) {
394
+ return line.replace(/[^\r\n]/g, ' ');
395
+ }
396
+
397
+ function preprocessorDirective(line) {
398
+ const content = line.replace(/[\r\n]+$/, '');
399
+ const match = content.match(/^\s*#\s*(if|ifdef|ifndef|elif|else|endif)\b(.*)$/);
400
+ if (!match) return null;
401
+ let condition = match[2].trim();
402
+ const defined = condition.match(/^defined\s*(?:\(\s*([A-Za-z_]\w*)\s*\)|([A-Za-z_]\w*))$/);
403
+ if (defined) condition = defined[1] || defined[2];
404
+ return { kind: match[1], condition };
405
+ }
406
+
407
+ /**
408
+ * Produce a bounded set of coherent preprocessor configurations while
409
+ * preserving every byte and line offset. Tree-sitter models directives but
410
+ * cannot represent braces whose opening and closing tokens live in separate
411
+ * `#ifdef` regions. Parsing one concrete configuration is the same structural
412
+ * view a compiler gets after preprocessing; trying several configurations
413
+ * avoids silently swallowing the declarations that follow the malformed
414
+ * region. This is syntax recovery, not a text-based symbol extractor.
415
+ *
416
+ * The search is bounded twice: by how many feature keys may vary (below), and
417
+ * by source size. Each configuration is a whole-file reparse, so the sweep
418
+ * costs O(configurations x file size) in simultaneously-live native ASTs.
419
+ * tree-sitter 0.21 does not expose Tree#delete, which means a synchronous
420
+ * build cannot reclaim those trees until V8 runs their finalizers. Large
421
+ * amalgamated/generated sources are therefore kept on the literal AST view;
422
+ * unlike ordinary translation units, sweeping them multiplies memory without
423
+ * a reliable way to release it during the build.
424
+ */
425
+ const CONDITIONAL_RECOVERY_MAX_BYTES = 256 * 1024;
426
+
427
+ function conditionalRecoverySources(code) {
428
+ if (Buffer.byteLength(code) > CONDITIONAL_RECOVERY_MAX_BYTES) return [];
429
+ const lines = code.match(/.*(?:\r\n|\n|\r|$)/g)?.filter(Boolean) || [];
430
+ const directives = lines.map(preprocessorDirective);
431
+ const keys = [];
432
+ for (const directive of directives) {
433
+ if (!directive || !['if', 'ifdef', 'ifndef', 'elif'].includes(directive.kind)) {
434
+ continue;
435
+ }
436
+ const condition = directive.condition;
437
+ if (!condition || condition === '0' || condition === '1') continue;
438
+ if (!keys.includes(condition)) keys.push(condition);
439
+ }
440
+ if (!directives.some(Boolean)) return [];
441
+
442
+ const assignments = [];
443
+ const addAssignment = values => {
444
+ const key = keys.map(name => values.get(name) ? '1' : '0').join('');
445
+ if (!assignments.some(entry => entry.key === key)) {
446
+ assignments.push({ key, values });
447
+ }
448
+ };
449
+ addAssignment(new Map(keys.map(key => [key, true])));
450
+ addAssignment(new Map(keys.map(key => [key, false])));
451
+ // Mixed configurations matter when two independent feature gates jointly
452
+ // shape a declaration. Bound the search so pathological generated headers
453
+ // do not create exponential parse work.
454
+ for (const key of keys.slice(0, 6)) {
455
+ addAssignment(new Map(keys.map(name => [name, name === key])));
456
+ addAssignment(new Map(keys.map(name => [name, name !== key])));
457
+ }
458
+
459
+ const evaluate = (condition, values) => {
460
+ if (condition === '0') return false;
461
+ if (condition === '1') return true;
462
+ return values.get(condition) ?? true;
463
+ };
464
+ const sources = [];
465
+ for (const { values } of assignments) {
466
+ const stack = [];
467
+ let active = true;
468
+ const selected = [];
469
+ let valid = true;
470
+ for (let index = 0; index < lines.length; index++) {
471
+ const line = lines[index];
472
+ const directive = directives[index];
473
+ if (!directive) {
474
+ selected.push(active ? line : blankLine(line));
475
+ continue;
476
+ }
477
+ selected.push(blankLine(line));
478
+ if (directive.kind === 'if' || directive.kind === 'ifdef' ||
479
+ directive.kind === 'ifndef') {
480
+ let branch = evaluate(directive.condition, values);
481
+ if (directive.kind === 'ifndef') branch = !branch;
482
+ const frame = {
483
+ parentActive: active,
484
+ branchTaken: branch,
485
+ };
486
+ stack.push(frame);
487
+ active = frame.parentActive && branch;
488
+ } else if (directive.kind === 'elif') {
489
+ const frame = stack[stack.length - 1];
490
+ if (!frame) { valid = false; break; }
491
+ const branch = !frame.branchTaken &&
492
+ evaluate(directive.condition, values);
493
+ frame.branchTaken ||= branch;
494
+ active = frame.parentActive && branch;
495
+ } else if (directive.kind === 'else') {
496
+ const frame = stack[stack.length - 1];
497
+ if (!frame) { valid = false; break; }
498
+ const branch = !frame.branchTaken;
499
+ frame.branchTaken = true;
500
+ active = frame.parentActive && branch;
501
+ } else {
502
+ const frame = stack.pop();
503
+ if (!frame) { valid = false; break; }
504
+ active = frame.parentActive;
505
+ }
506
+ }
507
+ if (!valid || stack.length > 0) continue;
508
+ const source = selected.join('');
509
+ if (!sources.includes(source)) sources.push(source);
510
+ }
511
+ return sources;
512
+ }
513
+
514
+ function treeStructureScore(tree) {
515
+ let declarations = 0;
516
+ let calls = 0;
517
+ const declarationTypes = new Set([
518
+ 'function_definition', 'class_specifier', 'struct_specifier',
519
+ 'union_specifier', 'enum_specifier', 'type_definition',
520
+ ]);
521
+ const cursor = tree.walk();
522
+ let entered = true;
523
+ while (entered) {
524
+ const type = cursor.nodeType;
525
+ if (declarationTypes.has(type)) declarations++;
526
+ else if (type === 'call_expression') calls++;
527
+ if (cursor.gotoFirstChild()) continue;
528
+ while (!cursor.gotoNextSibling()) {
529
+ if (!cursor.gotoParent()) {
530
+ entered = false;
531
+ break;
532
+ }
533
+ }
534
+ }
535
+ cursor.delete?.();
536
+ return declarations * 1000 + calls;
537
+ }
538
+
539
+ function parseTree(parser, code) {
540
+ const cacheKey = treeCacheKey(code);
541
+ const cached = cachedCFamilyTree(parser, cacheKey);
542
+ if (cached) return cached;
543
+ const tree = safeParse(parser, code, undefined, PARSE_OPTIONS);
544
+ if (recoveryMemo.has(code) && !recoveryAppliedMemo.get(code)) {
545
+ const blanked = recoveryMemo.get(code);
546
+ const selected = blanked === null
547
+ ? tree : safeParse(parser, blanked, undefined, PARSE_OPTIONS);
548
+ if (selected !== tree) tree.delete?.();
549
+ allSourceRecoveryByTree.set(selected, null);
550
+ recoveryAppliedByTree.set(selected, selected.rootNode.hasError);
551
+ cacheCFamilyTree(parser, cacheKey, selected, Buffer.byteLength(code));
552
+ return selected;
553
+ }
554
+ const initialRanges = macroTypeRanges(tree, code);
555
+ if (!tree.rootNode.hasError && initialRanges.length === 0) {
556
+ recoveryMemo.set(code, null);
557
+ recoveryAppliedMemo.set(code, false);
558
+ allSourceRecoveryByTree.set(tree, null);
559
+ recoveryAppliedByTree.set(tree, false);
560
+ if (recoveryMemo.size > RECOVERY_MEMO_MAX) {
561
+ const oldest = recoveryMemo.keys().next().value;
562
+ recoveryMemo.delete(oldest);
563
+ recoveryAppliedMemo.delete(oldest);
564
+ }
565
+ cacheCFamilyTree(parser, cacheKey, tree, Buffer.byteLength(code));
566
+ return tree;
567
+ }
568
+ // Non-worsening rounds may continue (blanking a stacked macro can turn a
569
+ // MISSING token into an ERROR before the next round clears it), but a
570
+ // recovery is only ACCEPTED when it strictly improved on the original.
571
+ let current = code;
572
+ let workingTree = tree;
573
+ let best = null;
574
+ let bestCode = null;
575
+ let bestErrors = countParseErrors(tree.rootNode);
576
+ const originalHasError = tree.rootNode.hasError;
577
+ const liveTrees = new Set([tree]);
578
+ const releaseTree = candidate => {
579
+ if (!candidate || !liveTrees.has(candidate)) return;
580
+ liveTrees.delete(candidate);
581
+ candidate.delete?.();
582
+ };
583
+ for (let attempt = 0; attempt < 8; attempt++) {
584
+ const ranges = attempt === 0
585
+ ? initialRanges : macroTypeRanges(workingTree, current);
586
+ if (ranges.length === 0) break;
587
+ let next = current;
588
+ for (const [start, end] of ranges) {
589
+ next = next.slice(0, start) + ' '.repeat(end - start) + next.slice(end);
590
+ }
591
+ const candidate = safeParse(parser, next, undefined, PARSE_OPTIONS);
592
+ liveTrees.add(candidate);
593
+ const errors = countParseErrors(candidate.rootNode);
594
+ if (errors > bestErrors) {
595
+ releaseTree(candidate);
596
+ break;
597
+ }
598
+ const previousWorking = workingTree;
599
+ current = next;
600
+ workingTree = candidate;
601
+ if (errors < bestErrors ||
602
+ (!originalHasError && attempt === 0 &&
603
+ errors === bestErrors)) {
604
+ const previousBest = best;
605
+ best = candidate;
606
+ bestCode = next;
607
+ bestErrors = errors;
608
+ if (previousBest && previousBest !== tree) {
609
+ releaseTree(previousBest);
610
+ }
611
+ }
612
+ if (previousWorking !== tree && previousWorking !== best) {
613
+ releaseTree(previousWorking);
614
+ }
615
+ if (!candidate.rootNode.hasError) break;
616
+ }
617
+ if (workingTree !== tree && workingTree !== best) releaseTree(workingTree);
618
+ const attributeSelected = best || tree;
619
+ const attributeSource = bestCode || code;
620
+ let selectedErrors = countParseErrors(attributeSelected.rootNode);
621
+ let selectedScore = null;
622
+ let conditionalApplied = false;
623
+ // Conditional branches may contain matching braces separated across two
624
+ // directives. Parse coherent feature configurations and prefer fewer
625
+ // syntax errors, then the richest declaration/call view.
626
+ if (attributeSelected.rootNode.hasError) {
627
+ for (const candidateSource of conditionalRecoverySources(attributeSource)) {
628
+ const candidate = safeParse(parser, candidateSource, undefined, PARSE_OPTIONS);
629
+ liveTrees.add(candidate);
630
+ const errors = countParseErrors(candidate.rootNode);
631
+ let score = null;
632
+ let improves = errors < selectedErrors;
633
+ if (errors === selectedErrors) {
634
+ if (selectedScore == null) {
635
+ selectedScore = treeStructureScore(best || attributeSelected);
636
+ }
637
+ score = treeStructureScore(candidate);
638
+ improves = score > selectedScore;
639
+ }
640
+ if (improves) {
641
+ const previousBest = best;
642
+ best = candidate;
643
+ bestCode = candidateSource;
644
+ selectedErrors = errors;
645
+ // A strictly lower error count resets the tie baseline; defer
646
+ // its structural walk until a later equal-error candidate.
647
+ selectedScore = score;
648
+ conditionalApplied = true;
649
+ if (previousBest && previousBest !== tree) {
650
+ releaseTree(previousBest);
651
+ }
652
+ } else {
653
+ releaseTree(candidate);
654
+ }
655
+ }
656
+ }
657
+ recoveryMemo.set(code, best ? bestCode : null);
658
+ // Deterministic attribute/visibility macro normalization is treated like
659
+ // ordinary parser adaptation once it yields a clean tree. Conditional
660
+ // configuration selection is inherently partial and must remain visible.
661
+ recoveryAppliedMemo.set(code, conditionalApplied);
662
+ if (recoveryMemo.size > RECOVERY_MEMO_MAX) {
663
+ const oldest = recoveryMemo.keys().next().value;
664
+ recoveryMemo.delete(oldest);
665
+ recoveryAppliedMemo.delete(oldest);
666
+ }
667
+ const selected = best || tree;
668
+ allSourceRecoveryByTree.set(
669
+ selected,
670
+ conditionalApplied && attributeSource !== code ? attributeSource :
671
+ conditionalApplied ? code : null,
672
+ );
673
+ recoveryAppliedByTree.set(
674
+ selected,
675
+ conditionalApplied || selected.rootNode.hasError,
676
+ );
677
+ for (const parsed of liveTrees) {
678
+ if (parsed !== selected) releaseTree(parsed);
679
+ }
680
+ cacheCFamilyTree(parser, cacheKey, selected, Buffer.byteLength(code));
681
+ return selected;
682
+ }
683
+
684
+ function parseRecoveryApplied(code, tree) {
685
+ return recoveryAppliedByTree.get(tree) ??
686
+ (recoveryAppliedMemo.get(code) || tree.rootNode.hasError);
687
+ }
688
+
689
+ /**
690
+ * Return the literal-source AST when parseTree selected a concrete
691
+ * preprocessor configuration. The selected tree is the best view for
692
+ * ownership and type evidence, but it cannot represent declarations/calls in
693
+ * mutually-exclusive branches. The literal tree still contains many of
694
+ * those nodes as structurally valid children of preprocessor nodes. Querying
695
+ * both gives C/C++ the same all-source inventory contract as grep without
696
+ * pretending the branch-only call sites are active in the selected build.
697
+ */
698
+ function literalRecoveryTree(parser, code, selected) {
699
+ const allSource = allSourceRecoveryByTree.get(selected);
700
+ if (!parseRecoveryApplied(code, selected) || !allSource) return null;
701
+ const cached = allSourceTreeBySelected.get(selected);
702
+ if (cached) return cached;
703
+ const literal = safeParse(
704
+ parser,
705
+ allSource,
706
+ undefined,
707
+ PARSE_OPTIONS,
708
+ );
709
+ allSourceTreeBySelected.set(selected, literal);
710
+ return literal;
711
+ }
712
+
713
+ function mergeExtracted(primary, secondary, keyOf) {
714
+ const merged = [...primary];
715
+ const seen = new Set(primary.map(keyOf));
716
+ for (const item of secondary) {
717
+ const key = keyOf(item);
718
+ if (seen.has(key)) continue;
719
+ seen.add(key);
720
+ merged.push(item);
721
+ }
722
+ // Secondary recovery trees may contribute an earlier source item after a
723
+ // later primary item. Preserve the public source-order contract instead
724
+ // of exposing merge history through callers, usages, or JSON output.
725
+ return merged.sort((a, b) =>
726
+ ((a.line ?? a.startLine ?? 0) - (b.line ?? b.startLine ?? 0)) ||
727
+ ((a.column ?? a.startColumn ?? 0) - (b.column ?? b.startColumn ?? 0)) ||
728
+ ((a.callStart ?? 0) - (b.callStart ?? 0)));
729
+ }
730
+
731
+ function unwrapDeclarator(node) {
732
+ let current = node;
733
+ const seen = new Set();
734
+ while (current && !seen.has(current.id)) {
735
+ seen.add(current.id);
736
+ if (current.type === 'function_declarator') return current;
737
+ const next = current.childForFieldName('declarator');
738
+ if (next) {
739
+ current = next;
740
+ continue;
741
+ }
742
+ for (const child of current.namedChildren || []) {
743
+ if (child.type === 'function_declarator' ||
744
+ child.type.endsWith('_declarator') ||
745
+ child.type === 'qualified_identifier') {
746
+ current = child;
747
+ break;
748
+ }
749
+ }
750
+ if (current === node || !current) break;
751
+ }
752
+ return null;
753
+ }
754
+
755
+ function functionDeclarator(node) {
756
+ if (!node) return null;
757
+ if (node.type === 'function_declarator' ||
758
+ node.type === 'operator_cast') return node;
759
+ if (node.type === 'template_declaration') {
760
+ const declaration = node.childForFieldName('declaration') ||
761
+ node.namedChildren.find(child =>
762
+ FUNCTION_CONTAINERS.has(child.type) ||
763
+ child.type === 'operator_cast');
764
+ return declaration ? functionDeclarator(declaration) : null;
765
+ }
766
+ const direct = node.childForFieldName('declarator');
767
+ if (direct?.type === 'operator_cast') return direct;
768
+ const unwrapped = unwrapDeclarator(direct);
769
+ if (unwrapped) return unwrapped;
770
+ for (const child of node.namedChildren || []) {
771
+ if (child.type === 'operator_cast') return child;
772
+ const found = unwrapDeclarator(child);
773
+ if (found) return found;
774
+ }
775
+ return null;
776
+ }
777
+
778
+ function canonicalCallableName(raw) {
779
+ const text = String(raw || '').trim();
780
+ if (!text.startsWith('operator')) return text;
781
+ const rest = text.slice('operator'.length).trim();
782
+ if (!rest) return 'operator';
783
+ // Symbolic operator tokens, multi-character alternatives first. This set
784
+ // must stay in lockstep with `canonicalOperatorName` in
785
+ // eval/oracles/clangd-oracle.js — the eval pins UCN definitions by
786
+ // oracle-listed name, so a token missing HERE mis-names the definition
787
+ // (fmt's `operator++` landed in the conversion branch as "operator ++")
788
+ // and a token missing THERE truncates the oracle's name.
789
+ if (/^(?:\(\)|\[\]|<=>|<<=?|>>=?|->\*?|\+\+|--|&&|\|\||,|[+\-*/%<>=!&|^~]=?)$/
790
+ .test(rest)) {
791
+ return `operator${rest}`;
792
+ }
793
+ // Allocation operators keep their array suffix; user-defined literals are
794
+ // named by their suffix.
795
+ const wordForm = rest.match(/^(new|delete)\s*(\[\s*\])?$/);
796
+ if (wordForm) return `operator ${wordForm[1]}${wordForm[2] ? '[]' : ''}`;
797
+ const literal = rest.match(/^""\s*(_[A-Za-z0-9_]*)/);
798
+ if (literal) return `operator""${literal[1]}`;
799
+ // Conversion operators are named by their destination type. Template
800
+ // arguments are instantiation detail, not source-level callable identity.
801
+ const destination = rest.replace(/\s*\(\).*/s, '')
802
+ .replace(/<.*>$/s, '').replace(/\s+/g, ' ').trim();
803
+ return destination ? `operator ${destination}` : 'operator';
804
+ }
805
+
806
+ function parameterListOf(node) {
807
+ if (!node) return null;
808
+ const direct = node.childForFieldName('parameters');
809
+ if (direct) return direct;
810
+ for (const child of node.namedChildren || []) {
811
+ if (child.type === 'parameter_list') return child;
812
+ const nested = parameterListOf(child);
813
+ if (nested) return nested;
814
+ }
815
+ return null;
816
+ }
817
+
818
+ function declaratorIdentity(declarator) {
819
+ if (!declarator) return {};
820
+ if (declarator.type === 'operator_cast') {
821
+ const destination = declarator.childForFieldName('type') ||
822
+ declarator.namedChildren.find(child => TYPE_NODES.has(child.type));
823
+ if (!destination) return {};
824
+ return {
825
+ name: canonicalCallableName(`operator ${destination.text}`),
826
+ nameNode: declarator,
827
+ conversionType: typeName(destination),
828
+ };
829
+ }
830
+ let node = declarator.childForFieldName('declarator') || declarator;
831
+ while (node && (node.type.endsWith('_declarator') ||
832
+ node.type === 'parenthesized_declarator')) {
833
+ const next = node.childForFieldName('declarator');
834
+ if (!next) break;
835
+ node = next;
836
+ }
837
+ if (!node) return {};
838
+ if (node.type === 'qualified_identifier') {
839
+ const nameNode = node.childForFieldName('name') ||
840
+ node.namedChildren[node.namedChildCount - 1];
841
+ const scopeNode = node.childForFieldName('scope') || node.namedChild(0);
842
+ return {
843
+ name: canonicalCallableName(nameNode?.text),
844
+ className: scopeNode?.text?.split('::').pop(),
845
+ nameNode,
846
+ };
847
+ }
848
+ if (IDENTIFIER_NODES.has(node.type)) {
849
+ return { name: canonicalCallableName(node.text), nameNode: node };
850
+ }
851
+ const named = node.namedChildren || [];
852
+ for (let i = named.length - 1; i >= 0; i--) {
853
+ const candidate = declaratorIdentity(named[i]);
854
+ if (candidate.name) return candidate;
855
+ }
856
+ return {};
857
+ }
858
+
859
+ function enclosingClass(node) {
860
+ for (let parent = node?.parent; parent; parent = parent.parent) {
861
+ if (CLASS_NODES.has(parent.type)) {
862
+ return classIdentity(parent);
863
+ }
864
+ }
865
+ return null;
866
+ }
867
+
868
+ function enclosingClassName(node) {
869
+ const identity = enclosingClass(node);
870
+ return identity?.ownerName || identity?.name || null;
871
+ }
872
+
873
+ function enclosingTypeScope(node) {
874
+ const identity = enclosingClass(node);
875
+ if (!identity?.node) return {};
876
+ return {
877
+ enclosingType: identity.ownerName || identity.name,
878
+ lexicalScopeStartLine: identity.node.startPosition.row + 1,
879
+ lexicalScopeEndLine: identity.node.endPosition.row + 1,
880
+ };
881
+ }
882
+
883
+ function enclosingNamespace(node) {
884
+ const parts = [];
885
+ for (let parent = node?.parent; parent; parent = parent.parent) {
886
+ if (parent.type !== 'namespace_definition') continue;
887
+ const nameNode = parent.childForFieldName('name') ||
888
+ (parent.namedChildren || []).find(child =>
889
+ child.type === 'namespace_identifier' ||
890
+ child.type === 'identifier' ||
891
+ child.type === 'nested_namespace_specifier');
892
+ if (nameNode?.text) parts.unshift(nameNode.text.replace(/\s+/g, ''));
893
+ }
894
+ return parts.length > 0 ? parts.join('::') : null;
895
+ }
896
+
897
+ function cLanguageLinkage(node) {
898
+ for (let current = node; current; current = current.parent) {
899
+ if (current.type === 'linkage_specification' &&
900
+ /^extern\s+"C"/.test(current.text.trim())) {
901
+ return 'c';
902
+ }
903
+ if (current.type === 'translation_unit') break;
904
+ }
905
+ return null;
906
+ }
907
+
908
+ function classIdentity(node) {
909
+ let nameNode = node.childForFieldName('name');
910
+ if (!nameNode && node.parent?.type === 'type_definition') {
911
+ nameNode = node.parent.childForFieldName('declarator');
912
+ }
913
+ if (!nameNode) return null;
914
+ if (nameNode.type === 'template_type') {
915
+ const baseNode = nameNode.childForFieldName('name') ||
916
+ (nameNode.namedChildren || []).find(child =>
917
+ child.type === 'type_identifier' ||
918
+ child.type === 'identifier');
919
+ if (baseNode?.text) {
920
+ return {
921
+ name: baseNode.text,
922
+ ownerName: nameNode.text,
923
+ node,
924
+ nameNode: baseNode,
925
+ };
926
+ }
927
+ }
928
+ return { name: nameNode.text, ownerName: nameNode.text, node, nameNode };
929
+ }
930
+
931
+ function modifiersOf(node, extra = []) {
932
+ const modifiers = new Set(extra);
933
+ for (const child of node.children || []) {
934
+ if (child.type === 'virtual') modifiers.add('virtual');
935
+ if (child.type === 'storage_class_specifier' ||
936
+ child.type === 'type_qualifier' ||
937
+ child.type === 'virtual_specifier' ||
938
+ child.type === 'access_specifier') {
939
+ modifiers.add(child.text);
940
+ }
941
+ }
942
+ // `override`/`final` live under the function_declarator rather than as
943
+ // direct children of the definition. Preserve those explicit C++
944
+ // virtual-dispatch facts without scraping declaration text.
945
+ const stack = [...(node.namedChildren || [])];
946
+ while (stack.length > 0) {
947
+ const child = stack.pop();
948
+ if (child.type === 'virtual_specifier') modifiers.add(child.text);
949
+ else stack.push(...(child.namedChildren || []));
950
+ }
951
+ return [...modifiers];
952
+ }
953
+
954
+ function isTemplateDependentCallable(node) {
955
+ // A callable can be dependent either because it has its own template
956
+ // declaration or because it is a member of a class template. Keep this as
957
+ // a boolean semantic fact; evaluating requires/enable_if expressions is a
958
+ // compiler job, but knowing that overload selection depends on template
959
+ // substitution lets the caller contract explain the uncertainty honestly.
960
+ for (let parent = node?.parent; parent; parent = parent.parent) {
961
+ if (parent.type === 'template_declaration') return true;
962
+ if (parent.type === 'translation_unit') break;
963
+ }
964
+ return false;
965
+ }
966
+
967
+ // Full type text for a NAMED parameter: the parameter's own text with the
968
+ // name removed — `const char *s` → `const char *`, `int **pp` → `int **`,
969
+ // `void (*cb)(int)` → `void (*)(int)`. Reading around the identifier keeps
970
+ // qualifiers, pointer levels, array suffixes, and function-pointer shapes
971
+ // without re-deriving declarator grammar. Default values (C++ optional
972
+ // params) are cut before the removal.
973
+ function paramTypeText(param, identity) {
974
+ if (!identity.nameNode) return null;
975
+ const base = param.startIndex;
976
+ const defaultValue = param.childForFieldName('default_value');
977
+ const end = defaultValue ? defaultValue.startIndex : param.endIndex;
978
+ if (identity.nameNode.startIndex < base || identity.nameNode.endIndex > end) return null;
979
+ const text = param.text.slice(0, end - base);
980
+ const typeText = (text.slice(0, identity.nameNode.startIndex - base) +
981
+ text.slice(identity.nameNode.endIndex - base))
982
+ .replace(/\s+/g, ' ')
983
+ .replace(/\s*=\s*$/, '')
984
+ .trim();
985
+ return typeText || null;
986
+ }
987
+
988
+ function structuredParams(paramsNode) {
989
+ if (!paramsNode) return [];
990
+ const result = [];
991
+ for (const param of paramsNode.namedChildren || []) {
992
+ if (param.type !== 'parameter_declaration' &&
993
+ param.type !== 'optional_parameter_declaration' &&
994
+ param.type !== 'variadic_parameter_declaration' &&
995
+ param.type !== 'variadic_parameter') continue;
996
+ const declarator = param.childForFieldName('declarator');
997
+ const identity = declaratorIdentity(declarator);
998
+ const typeNode = param.childForFieldName('type') ||
999
+ param.namedChildren.find(child => TYPE_NODES.has(child.type));
1000
+ // Unnamed parameters (`int f(size_t)`, `int f(void *)`) display as
1001
+ // their type text alone — the type must not double as both name and
1002
+ // annotation, and `void *` must not collapse into the `(void)` form.
1003
+ const info = {
1004
+ name: identity.name || param.text.replace(/\s+/g, ' ').trim(),
1005
+ };
1006
+ if (typeNode && identity.name) {
1007
+ info.type = paramTypeText(param, identity) || typeNode.text;
1008
+ }
1009
+ if (param.type === 'optional_parameter_declaration') info.optional = true;
1010
+ let declaratorCursor = declarator;
1011
+ let variadicDeclarator = false;
1012
+ const seenDeclarators = new Set();
1013
+ while (declaratorCursor && !seenDeclarators.has(declaratorCursor.id)) {
1014
+ seenDeclarators.add(declaratorCursor.id);
1015
+ if (declaratorCursor.type === 'variadic_declarator') {
1016
+ variadicDeclarator = true;
1017
+ break;
1018
+ }
1019
+ declaratorCursor = declaratorCursor.childForFieldName('declarator') ||
1020
+ (declaratorCursor.namedChildren || []).find(child =>
1021
+ child.type.endsWith('_declarator'));
1022
+ }
1023
+ if (param.type === 'variadic_parameter_declaration' ||
1024
+ param.type === 'variadic_parameter' || variadicDeclarator) {
1025
+ info.rest = true;
1026
+ }
1027
+ result.push(info);
1028
+ }
1029
+ // tree-sitter-c/cpp represents a bare C-style `...` as anonymous
1030
+ // punctuation rather than a named variadic parameter node. Preserve that
1031
+ // tail explicitly so nominal arity pruning accepts calls beyond the fixed
1032
+ // prefix (`void log(const char*, ...)`) instead of excluding every real
1033
+ // variadic call.
1034
+ if (!result.some(param => param.rest) &&
1035
+ /(?:\(|,)\s*\.\.\.\s*\)$/.test(paramsNode.text)) {
1036
+ result.push({ name: '...', rest: true });
1037
+ }
1038
+ if (result.length === 1 && result[0].name === 'void') return [];
1039
+ return result;
1040
+ }
1041
+
1042
+ function skipLexicalRegion(code, index) {
1043
+ if (code.startsWith('//', index)) {
1044
+ const newline = code.indexOf('\n', index + 2);
1045
+ return newline < 0 ? code.length : newline;
1046
+ }
1047
+ if (code.startsWith('/*', index)) {
1048
+ const end = code.indexOf('*/', index + 2);
1049
+ return end < 0 ? code.length : end + 2;
1050
+ }
1051
+ if (code.startsWith('R"', index)) {
1052
+ const open = code.indexOf('(', index + 2);
1053
+ if (open >= 0 && open - (index + 2) <= 16) {
1054
+ const delimiter = code.slice(index + 2, open);
1055
+ const close = code.indexOf(`)${delimiter}"`, open + 1);
1056
+ if (close >= 0) return close + delimiter.length + 2;
1057
+ }
1058
+ }
1059
+ const quote = code[index];
1060
+ if (quote !== '"' && quote !== "'") return null;
1061
+ for (let cursor = index + 1; cursor < code.length; cursor++) {
1062
+ if (code[cursor] === '\\') cursor++;
1063
+ else if (code[cursor] === quote) return cursor + 1;
1064
+ }
1065
+ return code.length;
1066
+ }
1067
+
1068
+ function balancedTokenEnd(code, openIndex, openToken, closeToken) {
1069
+ let depth = 0;
1070
+ for (let index = openIndex; index < code.length; index++) {
1071
+ const skipped = skipLexicalRegion(code, index);
1072
+ if (skipped != null) {
1073
+ index = skipped - 1;
1074
+ continue;
1075
+ }
1076
+ if (code[index] === openToken) depth++;
1077
+ else if (code[index] === closeToken && --depth === 0) return index + 1;
1078
+ }
1079
+ return null;
1080
+ }
1081
+
1082
+ function cppConstructorBodyOpen(code, paramsEnd) {
1083
+ let inInitializers = false;
1084
+ let initializerComplete = false;
1085
+ for (let index = paramsEnd; index < code.length; index++) {
1086
+ const skipped = skipLexicalRegion(code, index);
1087
+ if (skipped != null) {
1088
+ index = skipped - 1;
1089
+ continue;
1090
+ }
1091
+ const character = code[index];
1092
+ if (!inInitializers) {
1093
+ if (character === ':') {
1094
+ inInitializers = true;
1095
+ initializerComplete = false;
1096
+ } else if (character === '{') {
1097
+ return index;
1098
+ } else if (character === ';' || character === '=') {
1099
+ return null;
1100
+ }
1101
+ continue;
1102
+ }
1103
+ if (/\s/.test(character)) continue;
1104
+ if (character === ',') {
1105
+ initializerComplete = false;
1106
+ continue;
1107
+ }
1108
+ if (character === '{' && initializerComplete) return index;
1109
+ if (character === '(' || character === '{') {
1110
+ const end = balancedTokenEnd(
1111
+ code, index, character, character === '(' ? ')' : '}');
1112
+ if (end == null) return null;
1113
+ index = end - 1;
1114
+ initializerComplete = true;
1115
+ continue;
1116
+ }
1117
+ // After a complete mem-initializer, the only legal top-level tokens
1118
+ // are a comma or the function body's opening brace. Attributes and
1119
+ // comments were consumed above; ordinary identifier characters here
1120
+ // belong to the next mem-initializer's name.
1121
+ }
1122
+ return null;
1123
+ }
1124
+
1125
+ function functionRangeEnd(code, node, paramsNode, isConstructor, mode) {
1126
+ if (node.type !== 'function_definition') return null;
1127
+ const astBody = node.childForFieldName('body');
1128
+ let open = astBody?.startIndex;
1129
+ if (mode === 'cpp' && isConstructor && paramsNode) {
1130
+ open = cppConstructorBodyOpen(code, paramsNode.endIndex) ?? open;
1131
+ }
1132
+ if (open == null || code[open] !== '{') return null;
1133
+ return balancedTokenEnd(code, open, '{', '}');
1134
+ }
1135
+
1136
+ function lineNumberAtIndex(lineStarts, index) {
1137
+ let low = 0;
1138
+ let high = lineStarts.length;
1139
+ while (low + 1 < high) {
1140
+ const mid = (low + high) >> 1;
1141
+ if (lineStarts[mid] <= index) low = mid;
1142
+ else high = mid;
1143
+ }
1144
+ return low + 1;
1145
+ }
1146
+
1147
+ function returnTypeOf(node) {
1148
+ const findTrailing = current => {
1149
+ if (!current) return null;
1150
+ if (current.type === 'trailing_return_type') {
1151
+ const descriptor = (current.namedChildren || []).find(child =>
1152
+ child.type === 'type_descriptor') || current.namedChild(0);
1153
+ const type = descriptor?.childForFieldName('type') || descriptor;
1154
+ return type?.text || null;
1155
+ }
1156
+ for (const child of current.namedChildren || []) {
1157
+ const found = findTrailing(child);
1158
+ if (found) return found;
1159
+ }
1160
+ return null;
1161
+ };
1162
+ const trailing = findTrailing(node.childForFieldName('declarator'));
1163
+ if (trailing) return trailing;
1164
+ const typeNode = node.childForFieldName('type') ||
1165
+ node.namedChildren.find(child => TYPE_NODES.has(child.type));
1166
+ if (!typeNode) return null;
1167
+ // Pointer declarators wrapping the function declarator belong to the
1168
+ // RETURN type: `char *dup(...)` returns `char *`, and the pointer
1169
+ // variable `void *(*fp)(size_t)` yields `void *` when called. The walk
1170
+ // stops at the function/parenthesized declarator — inner pointers are
1171
+ // the function-pointer itself, not the return type.
1172
+ let stars = 0;
1173
+ let current = node.childForFieldName('declarator');
1174
+ const seen = new Set();
1175
+ while (current && !seen.has(current.id)) {
1176
+ seen.add(current.id);
1177
+ if (current.type === 'function_declarator' ||
1178
+ current.type === 'parenthesized_declarator') break;
1179
+ if (current.type === 'pointer_declarator') stars++;
1180
+ current = current.childForFieldName('declarator') ||
1181
+ (current.namedChildren || []).find(child => child.type.endsWith('_declarator'));
1182
+ }
1183
+ return stars > 0 ? `${typeNode.text} ${'*'.repeat(stars)}` : typeNode.text;
1184
+ }
1185
+
1186
+ function memberFromNode(node, className, access, lines, mode) {
1187
+ const declarator = functionDeclarator(node);
1188
+ if (!declarator) return null;
1189
+ const identity = declaratorIdentity(declarator);
1190
+ if (!identity.name) return null;
1191
+ const paramsNode = parameterListOf(declarator);
1192
+ const { startLine, endLine, indent } = nodeToLocation(node, lines);
1193
+ const isConstructor = mode === 'cpp' &&
1194
+ (identity.name === className || identity.name === `~${className}`);
1195
+ const modifiers = modifiersOf(node, access ? [access] : []);
1196
+ if (isConstructor && identity.name.startsWith('~')) modifiers.push('destructor');
1197
+ return {
1198
+ name: identity.name,
1199
+ params: paramsNode ? paramsNode.text.replace(/^\(|\)$/g, '').trim() : '...',
1200
+ paramsStructured: structuredParams(paramsNode),
1201
+ returnType: isConstructor ? null :
1202
+ (identity.conversionType || returnTypeOf(node)),
1203
+ startLine,
1204
+ endLine,
1205
+ ...(identity.nameNode?.startPosition.row + 1 !== startLine && {
1206
+ nameLine: identity.nameNode.startPosition.row + 1,
1207
+ }),
1208
+ indent,
1209
+ modifiers,
1210
+ memberType: isConstructor ? 'constructor' : 'method',
1211
+ isMethod: true,
1212
+ isConstructor,
1213
+ ...(enclosingNamespace(node) && {
1214
+ namespace: enclosingNamespace(node),
1215
+ }),
1216
+ className,
1217
+ ...(mode === 'cpp' && isTemplateDependentCallable(node) && {
1218
+ templateDependent: true,
1219
+ }),
1220
+ ...(mode === 'cpp' && cLanguageLinkage(node) && {
1221
+ linkage: cLanguageLinkage(node),
1222
+ }),
1223
+ ...(node.type !== 'function_definition' && { isSignature: true }),
1224
+ docstring: extractJSDocstring(lines, startLine),
1225
+ };
1226
+ }
1227
+
1228
+ function fieldMembers(node, access, lines) {
1229
+ if (node.type !== 'field_declaration') return [];
1230
+ if (functionDeclarator(node)) return [];
1231
+ const typeNode = node.childForFieldName('type') ||
1232
+ node.namedChildren.find(child => TYPE_NODES.has(child.type));
1233
+ const fields = [];
1234
+ for (const child of node.namedChildren || []) {
1235
+ if (!IDENTIFIER_NODES.has(child.type) && child.type !== 'field_declarator') continue;
1236
+ const identity = declaratorIdentity(child);
1237
+ if (!identity.name || identity.name === typeNode?.text) continue;
1238
+ const { startLine, endLine, indent } = nodeToLocation(child, lines);
1239
+ fields.push({
1240
+ name: identity.name,
1241
+ startLine,
1242
+ endLine,
1243
+ indent,
1244
+ modifiers: access ? [access] : [],
1245
+ memberType: 'field',
1246
+ fieldType: typeNode?.text || null,
1247
+ });
1248
+ }
1249
+ return fields;
1250
+ }
1251
+
1252
+ function classMembers(node, lines, mode) {
1253
+ const identity = classIdentity(node);
1254
+ if (!identity) return [];
1255
+ const body = node.childForFieldName('body');
1256
+ if (!body) return [];
1257
+ let access = node.type === 'class_specifier' ? 'private' : 'public';
1258
+ const members = [];
1259
+ for (const child of body.namedChildren || []) {
1260
+ // Enumerators are declarations in an enum body, not ordinary C/C++
1261
+ // field_declaration nodes. Index them as named constant members so
1262
+ // they remain navigable across find/search/usages.
1263
+ if (node.type === 'enum_specifier' && child.type === 'enumerator') {
1264
+ const nameNode = child.childForFieldName('name') ||
1265
+ (child.namedChildren || []).find(candidate =>
1266
+ candidate.type === 'identifier');
1267
+ if (nameNode?.text) {
1268
+ const { startLine, endLine, indent } = nodeToLocation(nameNode, lines);
1269
+ members.push({
1270
+ name: nameNode.text,
1271
+ startLine,
1272
+ endLine,
1273
+ indent,
1274
+ modifiers: ['public'],
1275
+ memberType: 'field',
1276
+ fieldType: identity.ownerName || identity.name,
1277
+ });
1278
+ }
1279
+ continue;
1280
+ }
1281
+ if (child.type === 'access_specifier') {
1282
+ access = child.text;
1283
+ continue;
1284
+ }
1285
+ if (CLASS_NODES.has(child.type)) continue;
1286
+ const member = memberFromNode(
1287
+ child, identity.ownerName || identity.name, access, lines, mode);
1288
+ if (member) members.push(member);
1289
+ else members.push(...fieldMembers(child, access, lines));
1290
+ }
1291
+ return members;
1292
+ }
1293
+
1294
+ function typedefEntries(node, lines) {
1295
+ // `typedef void (*cb)(int);` / `typedef int myint;` / `typedef struct A B;`
1296
+ // declare importable type names. Anonymous specifiers
1297
+ // (`typedef struct { … } Point;`) are named through classIdentity's
1298
+ // type_definition fallback, so only alias-style declarators are added here.
1299
+ const inner = node.childForFieldName('type');
1300
+ const innerIsClass = inner && CLASS_NODES.has(inner.type);
1301
+ const innerClassName = innerIsClass ? inner.childForFieldName('name')?.text : null;
1302
+ const entries = [];
1303
+ for (const child of node.namedChildren || []) {
1304
+ if (inner && sameNode(child, inner)) continue;
1305
+ if (child.type === 'type_qualifier' || child.type === 'storage_class_specifier') continue;
1306
+ const identity = declaratorIdentity(child);
1307
+ if (!identity.name || RESERVED_TYPE_KEYWORDS.has(identity.name)) continue;
1308
+ if (innerIsClass && (!innerClassName || innerClassName === identity.name)) continue;
1309
+ const { startLine, endLine, indent } = nodeToLocation(child, lines);
1310
+ // A function-pointer typedef aliases a function shape, not the return
1311
+ // type — record no aliasOf for it.
1312
+ const aliasOf = unwrapDeclarator(child)
1313
+ ? null
1314
+ : (innerIsClass ? innerClassName : typeName(inner));
1315
+ entries.push({
1316
+ name: identity.name,
1317
+ type: 'type',
1318
+ startLine,
1319
+ endLine,
1320
+ ...(identity.nameNode?.startPosition.row + 1 !== startLine && {
1321
+ nameLine: identity.nameNode.startPosition.row + 1,
1322
+ }),
1323
+ indent,
1324
+ modifiers: ['public'],
1325
+ members: [],
1326
+ ...enclosingTypeScope(node),
1327
+ ...(enclosingNamespace(node) && {
1328
+ namespace: enclosingNamespace(node),
1329
+ }),
1330
+ ...(aliasOf && aliasOf !== identity.name && { aliasOf }),
1331
+ docstring: extractJSDocstring(lines, startLine),
1332
+ });
1333
+ }
1334
+ return entries;
1335
+ }
1336
+
1337
+ function findClassesInTree(code, tree, mode, sourceLines = null) {
1338
+ const lines = sourceLines || code.split('\n');
1339
+ const classes = [];
1340
+ const seen = new Set();
1341
+ // Names with a bodied definition in this file: bodyless occurrences of
1342
+ // the same name (forward declarations, `struct S` in a parameter or
1343
+ // field type position) are references to it, never second definitions.
1344
+ const bodiedNames = new Set();
1345
+ const bodylessEntries = new Set();
1346
+ const forwardDeclared = new Set();
1347
+ traverseTreeCached(tree.rootNode, node => {
1348
+ if (mode === 'cpp' && node.type === 'alias_declaration') {
1349
+ const nameNode = node.childForFieldName('name') ||
1350
+ node.namedChildren.find(child =>
1351
+ child.type === 'type_identifier' ||
1352
+ child.type === 'identifier');
1353
+ const valueNode = node.childForFieldName('type') ||
1354
+ node.namedChildren.find(child =>
1355
+ child !== nameNode &&
1356
+ (child.type === 'type_descriptor' ||
1357
+ TYPE_NODES.has(child.type)));
1358
+ if (!nameNode?.text) return false;
1359
+ const { startLine, endLine, indent } =
1360
+ nodeToLocation(node, lines);
1361
+ classes.push({
1362
+ name: nameNode.text,
1363
+ type: 'type',
1364
+ startLine,
1365
+ endLine,
1366
+ indent,
1367
+ modifiers: ['public'],
1368
+ members: [],
1369
+ ...enclosingTypeScope(node),
1370
+ ...(enclosingNamespace(node) && {
1371
+ namespace: enclosingNamespace(node),
1372
+ }),
1373
+ ...(valueNode?.text && { aliasOf: valueNode.text }),
1374
+ docstring: extractJSDocstring(lines, startLine),
1375
+ });
1376
+ return false;
1377
+ }
1378
+ if (node.type === 'type_definition') {
1379
+ classes.push(...typedefEntries(node, lines));
1380
+ return true; // descend — the inner specifier may be a named class
1381
+ }
1382
+ if (!CLASS_NODES.has(node.type)) return true;
1383
+ const identity = classIdentity(node);
1384
+ if (!identity?.name) return true;
1385
+ const hasBody = !!node.childForFieldName('body');
1386
+ if (hasBody) {
1387
+ bodiedNames.add(identity.name);
1388
+ } else {
1389
+ if (bodiedNames.has(identity.name)) return true;
1390
+ // A bodyless specifier is only a DECLARATION at declaration
1391
+ // level (`struct S;`); in a type position (`void f(struct S *)`)
1392
+ // it is a reference. One entry per opaque forward-declared name.
1393
+ const parentType = node.parent?.type;
1394
+ if (parentType !== 'translation_unit' && parentType !== 'declaration_list') return true;
1395
+ if (forwardDeclared.has(identity.name)) return true;
1396
+ forwardDeclared.add(identity.name);
1397
+ }
1398
+ const key = `${node.startIndex}:${identity.name}`;
1399
+ if (seen.has(key)) return false;
1400
+ seen.add(key);
1401
+ const { startLine, endLine, indent } = nodeToLocation(node, lines);
1402
+ let type = node.type.replace('_specifier', '');
1403
+ if (type === 'union') type = 'type';
1404
+ const baseClause = node.namedChildren.find(child => child.type === 'base_class_clause');
1405
+ const bases = baseClause
1406
+ ? baseClause.namedChildren
1407
+ .filter(child => child.type !== 'access_specifier')
1408
+ .map(child => child.text)
1409
+ : [];
1410
+ const modifiers = modifiersOf(node);
1411
+ if (mode === 'c' || node.type !== 'class_specifier' || modifiers.includes('public')) {
1412
+ modifiers.push('public');
1413
+ }
1414
+ const entry = {
1415
+ name: identity.name,
1416
+ ...(identity.ownerName !== identity.name && {
1417
+ specialization: identity.ownerName,
1418
+ }),
1419
+ type,
1420
+ startLine,
1421
+ endLine,
1422
+ ...(identity.nameNode?.startPosition.row + 1 !== startLine && {
1423
+ nameLine: identity.nameNode.startPosition.row + 1,
1424
+ }),
1425
+ indent,
1426
+ modifiers: [...new Set(modifiers)],
1427
+ members: classMembers(node, lines, mode),
1428
+ ...(enclosingNamespace(node) && {
1429
+ namespace: enclosingNamespace(node),
1430
+ }),
1431
+ ...(bases.length > 0 && { extends: bases.join(', ') }),
1432
+ docstring: extractJSDocstring(lines, startLine),
1433
+ };
1434
+ classes.push(entry);
1435
+ if (!hasBody) bodylessEntries.add(entry);
1436
+ return true;
1437
+ });
1438
+ // A forward declaration can precede its body. Filtering after the single
1439
+ // traversal preserves the old "body wins" result without paying a full
1440
+ // preliminary tree walk merely to discover future bodied names.
1441
+ return classes.filter(entry =>
1442
+ !bodylessEntries.has(entry) || !bodiedNames.has(entry.name));
1443
+ }
1444
+
1445
+ function findClasses(code, parser, mode) {
1446
+ const tree = parseTree(parser, code);
1447
+ const primary = findClassesInTree(code, tree, mode);
1448
+ const literal = literalRecoveryTree(parser, code, tree);
1449
+ if (!literal) return primary;
1450
+ try {
1451
+ return mergeExtracted(
1452
+ primary,
1453
+ findClassesInTree(code, literal, mode),
1454
+ item => `${item.name}:${item.startLine}:${item.type}:${item.namespace || ''}`,
1455
+ );
1456
+ } finally { /* cached with the selected tree */ }
1457
+ }
1458
+
1459
+ function findFunctionsInTree(code, tree, mode, sourceLines = null) {
1460
+ const lines = sourceLines || code.split('\n');
1461
+ const lineStarts = [0];
1462
+ for (let index = 0; index < code.length; index++) {
1463
+ if (code.charCodeAt(index) === 10) lineStarts.push(index + 1);
1464
+ }
1465
+ const functions = [];
1466
+ const seen = new Set();
1467
+ const variableTypes = mode === 'cpp' ? buildVariableTypes(tree) : null;
1468
+ traverseTreeCached(tree.rootNode, node => {
1469
+ if (!FUNCTION_CONTAINERS.has(node.type)) return true;
1470
+ const declarator = functionDeclarator(node);
1471
+ if (!declarator) return true;
1472
+ const owner = enclosingClass(node);
1473
+ const identity = declaratorIdentity(declarator);
1474
+ if (!identity.name) return true;
1475
+ if (owner && !identity.className) return false;
1476
+ const key = `${node.startIndex}:${identity.name}`;
1477
+ if (seen.has(key)) return false;
1478
+ seen.add(key);
1479
+ const paramsNode = parameterListOf(declarator);
1480
+ const location = nodeToLocation(node, lines);
1481
+ const startLine = location.startLine;
1482
+ const indent = location.indent;
1483
+ const isConstructor = mode === 'cpp' && !!identity.className &&
1484
+ (identity.name === identity.className || identity.name === `~${identity.className}`);
1485
+ const lexicalEnd = functionRangeEnd(
1486
+ code, node, paramsNode, isConstructor, mode);
1487
+ const endLine = lexicalEnd == null
1488
+ ? location.endLine
1489
+ : lineNumberAtIndex(lineStarts, Math.max(0, lexicalEnd - 1));
1490
+ const modifiers = modifiersOf(node);
1491
+ if (!modifiers.includes('static')) modifiers.push('export');
1492
+ const returnedConcreteType = mode === 'cpp' && variableTypes
1493
+ ? inferredAutoReturnType(node, variableTypes)
1494
+ : null;
1495
+ functions.push({
1496
+ name: identity.name,
1497
+ params: paramsNode ? paramsNode.text.replace(/^\(|\)$/g, '').trim() : '...',
1498
+ paramsStructured: structuredParams(paramsNode),
1499
+ returnType: isConstructor ? null :
1500
+ (identity.conversionType || returnTypeOf(node)),
1501
+ startLine,
1502
+ endLine,
1503
+ ...(identity.nameNode?.startPosition.row + 1 !== startLine && {
1504
+ nameLine: identity.nameNode.startPosition.row + 1,
1505
+ }),
1506
+ indent,
1507
+ modifiers,
1508
+ ...(enclosingNamespace(node) && {
1509
+ namespace: enclosingNamespace(node),
1510
+ }),
1511
+ ...(identity.className && {
1512
+ className: identity.className,
1513
+ receiver: identity.className,
1514
+ isMethod: true,
1515
+ }),
1516
+ ...(mode === 'cpp' && isTemplateDependentCallable(node) && {
1517
+ templateDependent: true,
1518
+ }),
1519
+ ...(returnedConcreteType && { returnedConcreteType }),
1520
+ ...(mode === 'cpp' && cLanguageLinkage(node) && {
1521
+ linkage: cLanguageLinkage(node),
1522
+ }),
1523
+ ...(isConstructor && { isConstructor: true }),
1524
+ ...(node.type !== 'function_definition' && { isSignature: true }),
1525
+ docstring: extractJSDocstring(lines, startLine),
1526
+ });
1527
+ return false;
1528
+ });
1529
+ return functions;
1530
+ }
1531
+
1532
+ /**
1533
+ * C++ `auto` return deduction is compiler-exact when every return statement
1534
+ * yields a local whose declared/inferred type agrees. This is intentionally
1535
+ * narrower than expression type inference; unknown or mixed returns abstain.
1536
+ */
1537
+ function inferredAutoReturnType(functionNode, variableTypes) {
1538
+ const declared = returnTypeOf(functionNode);
1539
+ if (!/^auto\b/.test(String(declared || '').trim())) return null;
1540
+ const body = functionNode.childForFieldName('body');
1541
+ if (!body) return null;
1542
+ const autoBindings = [];
1543
+ traverseTree(body, node => {
1544
+ if (node !== body &&
1545
+ (node.type === 'function_definition' ||
1546
+ node.type === 'lambda_expression')) return false;
1547
+ if (node !== body && CLASS_NODES.has(node.type)) return false;
1548
+ if (node.type !== 'declaration') return true;
1549
+ const typeNode = node.childForFieldName('type') ||
1550
+ (node.namedChildren || []).find(child => TYPE_NODES.has(child.type));
1551
+ if (typeName(typeNode) !== 'auto') return true;
1552
+ const scope = variableBindingScope(node);
1553
+ for (const declarator of variableDeclarators(node)) {
1554
+ const identity = declaratorIdentity(declarator);
1555
+ const value = declarator.childForFieldName('value');
1556
+ if (!identity.name || value?.type !== 'call_expression') continue;
1557
+ const callee = callIdentity(value.childForFieldName('function'));
1558
+ if (!callee.name) continue;
1559
+ autoBindings.push({
1560
+ name: identity.name,
1561
+ type: callee.name,
1562
+ declaredAt: declarator.startIndex,
1563
+ scopeStart: scope.startIndex,
1564
+ scopeEnd: scope.endIndex,
1565
+ });
1566
+ }
1567
+ return true;
1568
+ });
1569
+ const autoTypeAt = (name, node) => autoBindings
1570
+ .filter(binding => binding.name === name &&
1571
+ binding.scopeStart <= node.startIndex &&
1572
+ node.startIndex < binding.scopeEnd &&
1573
+ binding.declaredAt <= node.startIndex)
1574
+ .sort((left, right) =>
1575
+ (left.scopeEnd - left.scopeStart) -
1576
+ (right.scopeEnd - right.scopeStart) ||
1577
+ right.declaredAt - left.declaredAt)[0]?.type;
1578
+ const types = [];
1579
+ let incomplete = false;
1580
+ const stack = [body];
1581
+ while (stack.length > 0) {
1582
+ const node = stack.pop();
1583
+ if (node !== body &&
1584
+ (node.type === 'function_definition' ||
1585
+ node.type === 'lambda_expression')) continue;
1586
+ if (node !== body && CLASS_NODES.has(node.type)) continue;
1587
+ if (node.type === 'return_statement') {
1588
+ let value = node.namedChild(0);
1589
+ while (value?.type === 'parenthesized_expression') {
1590
+ value = value.namedChild(0);
1591
+ }
1592
+ const type = value?.type === 'identifier'
1593
+ ? (variableTypes.get(value.text, node) ||
1594
+ autoTypeAt(value.text, node))
1595
+ : null;
1596
+ if (type) types.push(type);
1597
+ else incomplete = true;
1598
+ continue;
1599
+ }
1600
+ for (let index = node.namedChildCount - 1; index >= 0; index--) {
1601
+ stack.push(node.namedChild(index));
1602
+ }
1603
+ }
1604
+ return !incomplete && types.length > 0 && new Set(types).size === 1
1605
+ ? types[0] : null;
1606
+ }
1607
+
1608
+ function findFunctions(code, parser, mode) {
1609
+ const tree = parseTree(parser, code);
1610
+ const primary = findFunctionsInTree(code, tree, mode);
1611
+ const literal = literalRecoveryTree(parser, code, tree);
1612
+ if (!literal) return primary;
1613
+ try {
1614
+ return mergeExtracted(
1615
+ primary,
1616
+ findFunctionsInTree(code, literal, mode),
1617
+ item => `${item.name}:${item.startLine}:${item.className || ''}:${item.isSignature ? 1 : 0}`,
1618
+ );
1619
+ } finally { /* cached with the selected tree */ }
1620
+ }
1621
+
1622
+ function findStateObjectsInTree(tree, lines) {
1623
+ const states = [];
1624
+ traverseTreeCached(tree.rootNode, node => {
1625
+ if (node.type !== 'declaration') return true;
1626
+ if (functionDeclarator(node)) return false;
1627
+ // A top-level declaration may be wrapped in one or more preprocessor
1628
+ // condition nodes. Treat those wrappers as transparent: both arms of
1629
+ // an #if/#else remain part of the source inventory even though only
1630
+ // one arm can exist in any particular build configuration.
1631
+ let scope = node.parent;
1632
+ while (scope && /^preproc_/.test(scope.type)) scope = scope.parent;
1633
+ if (scope?.type !== 'translation_unit' && scope?.type !== 'declaration_list') return false;
1634
+ for (const child of node.namedChildren || []) {
1635
+ const identity = declaratorIdentity(child);
1636
+ if (!identity.name || TYPE_NODES.has(child.type)) continue;
1637
+ const { startLine, endLine, indent } = nodeToLocation(child, lines);
1638
+ states.push({
1639
+ name: identity.name,
1640
+ startLine,
1641
+ endLine,
1642
+ indent,
1643
+ modifiers: modifiersOf(node),
1644
+ });
1645
+ }
1646
+ return false;
1647
+ });
1648
+ return states;
1649
+ }
1650
+
1651
+ function findStateObjects(code, parser) {
1652
+ const tree = parseTree(parser, code);
1653
+ const lines = code.split('\n');
1654
+ const primary = findStateObjectsInTree(tree, lines);
1655
+ const literal = literalRecoveryTree(parser, code, tree);
1656
+ return literal ? mergeExtracted(primary,
1657
+ findStateObjectsInTree(literal, lines),
1658
+ item => `${item.name}:${item.startLine}`) : primary;
1659
+ }
1660
+
1661
+ function findMacrosInTree(tree, lines) {
1662
+ const macros = [];
1663
+ for (const node of macroDefinitionNodes(tree)) {
1664
+ const nameNode = node.childForFieldName('name') ||
1665
+ (node.namedChildren || []).find(child => child.type === 'identifier');
1666
+ if (!nameNode) continue;
1667
+ const paramsNode = node.childForFieldName('parameters') ||
1668
+ (node.namedChildren || []).find(child => child.type === 'preproc_params');
1669
+ const { startLine, indent } = nodeToLocation(node, lines);
1670
+ // Preprocessor nodes include their terminating newline, so a node
1671
+ // ending at column zero belongs to the preceding physical line.
1672
+ const endLine = Math.max(startLine,
1673
+ node.endPosition.row + (node.endPosition.column > 0 ? 1 : 0));
1674
+ macros.push({
1675
+ name: nameNode.text,
1676
+ startLine,
1677
+ endLine,
1678
+ indent,
1679
+ params: paramsNode ? paramsNode.text.replace(/^\(|\)$/g, '').trim() : undefined,
1680
+ paramsStructured: paramsNode
1681
+ ? (paramsNode.namedChildren || [])
1682
+ .filter(child => child.type === 'identifier')
1683
+ .map(child => ({ name: child.text }))
1684
+ : undefined,
1685
+ modifiers: [],
1686
+ functionLike: node.type === 'preproc_function_def',
1687
+ docstring: extractJSDocstring(lines, startLine),
1688
+ });
1689
+ }
1690
+ return macros;
1691
+ }
1692
+
1693
+ function findMacros(code, parser) {
1694
+ const tree = parseTree(parser, code);
1695
+ const lines = code.split('\n');
1696
+ const primary = findMacrosInTree(tree, lines);
1697
+ const literal = literalRecoveryTree(parser, code, tree);
1698
+ return literal ? mergeExtracted(primary,
1699
+ findMacrosInTree(literal, lines),
1700
+ item => `${item.name}:${item.startLine}:${item.functionLike ? 1 : 0}`) : primary;
1701
+ }
1702
+
1703
+ function enclosingFunctionOf(node) {
1704
+ for (let parent = node?.parent; parent; parent = parent.parent) {
1705
+ if (parent.type === 'function_definition') {
1706
+ const identity = declaratorIdentity(functionDeclarator(parent));
1707
+ return identity.name ? {
1708
+ name: identity.name,
1709
+ startLine: parent.startPosition.row + 1,
1710
+ endLine: parent.endPosition.row + 1,
1711
+ ...(identity.className && {
1712
+ className: identity.className,
1713
+ }),
1714
+ } : null;
1715
+ }
1716
+ }
1717
+ return null;
1718
+ }
1719
+
1720
+ function typeName(node) {
1721
+ if (!node) return null;
1722
+ let text = node.text;
1723
+ text = text.replace(/\b(const|volatile|struct|class|typename)\b/g, '').trim();
1724
+ text = text.replace(/[*&]+/g, '').trim();
1725
+ // Strip template arguments before splitting namespace qualifiers.
1726
+ // `dynamic_store<fmt::context<Char>>` contains `::` inside its template
1727
+ // arguments; splitting first produced the bogus receiver type
1728
+ // `context>` and discarded otherwise compiler-visible ownership.
1729
+ const templateStart = text.indexOf('<');
1730
+ if (templateStart >= 0) text = text.slice(0, templateStart).trim();
1731
+ const parts = text.split(/::/);
1732
+ return parts[parts.length - 1].trim() || null;
1733
+ }
1734
+
1735
+ function variableBindingScope(node) {
1736
+ const parameter = node.type === 'parameter_declaration';
1737
+ for (let parent = node.parent; parent; parent = parent.parent) {
1738
+ if (parameter && parent.type === 'function_definition') return parent;
1739
+ if (!parameter && (parent.type === 'compound_statement' ||
1740
+ parent.type === 'translation_unit')) {
1741
+ return parent;
1742
+ }
1743
+ }
1744
+ return treeRoot(node);
1745
+ }
1746
+
1747
+ function treeRoot(node) {
1748
+ let current = node;
1749
+ while (current?.parent) current = current.parent;
1750
+ return current;
1751
+ }
1752
+
1753
+ function variableDeclarators(node) {
1754
+ if (node.type === 'parameter_declaration') {
1755
+ const declarator = node.childForFieldName('declarator');
1756
+ return declarator ? [declarator] : [];
1757
+ }
1758
+ // An initializer can contain a call expression whose nested syntax looks
1759
+ // declarator-like to the generic recursive probe. The declaration itself
1760
+ // is still unequivocally a value binding (`T value = factory()`), so keep
1761
+ // its init_declarator before asking whether the outer node is callable.
1762
+ const initialized = (node.namedChildren || []).filter(child =>
1763
+ child.type === 'init_declarator');
1764
+ if (initialized.length > 0) return initialized;
1765
+ if (functionDeclarator(node)) return [];
1766
+ return (node.namedChildren || []).filter(child =>
1767
+ child.type !== 'attribute_specifier' &&
1768
+ !TYPE_NODES.has(child.type));
1769
+ }
1770
+
1771
+ /**
1772
+ * Scope- and position-aware declared-type bindings.
1773
+ *
1774
+ * A file-global Map is unsound for C/C++: a later `auto specs` in an
1775
+ * unrelated function used to overwrite an earlier `format_specs specs`
1776
+ * parameter, changing every receiver in the file. Bindings are instead
1777
+ * selected from scopes containing the use, with the nearest scope and latest
1778
+ * preceding declaration winning. `auto` deliberately contributes no static
1779
+ * type; assigned-call return flow handles it separately.
1780
+ */
1781
+ function buildVariableTypes(tree) {
1782
+ const bindings = [];
1783
+ const fieldTypes = new Map();
1784
+ // tree-sitter must preserve C++'s most-vexing-parse ambiguity and can
1785
+ // represent `T value(factory())` as a block-scope function declarator.
1786
+ // A later `value.method()` use proves that spelling denotes an object in
1787
+ // compiling code: a function declaration cannot be a member receiver.
1788
+ // Record only those use-proven direct initializers, never every ambiguous
1789
+ // block declaration.
1790
+ const memberReceiverUses = [];
1791
+ const ambiguousDirectInitializers = [];
1792
+ const addBindings = (node, type, pointeeType, scope, declarators) => {
1793
+ for (const declarator of declarators) {
1794
+ const identity = declaratorIdentity(declarator);
1795
+ if (!identity.name) continue;
1796
+ bindings.push({
1797
+ name: identity.name,
1798
+ type,
1799
+ ...(pointeeType && { pointeeType }),
1800
+ declaredAt: node.type === 'parameter_declaration'
1801
+ ? scope.startIndex : declarator.startIndex,
1802
+ scopeStart: scope.startIndex,
1803
+ scopeEnd: scope.endIndex,
1804
+ });
1805
+ }
1806
+ };
1807
+ traverseTree(tree.rootNode, node => {
1808
+ if (node.type === 'field_expression') {
1809
+ const argument = node.childForFieldName('argument') || node.namedChild(0);
1810
+ if (argument?.type === 'identifier') {
1811
+ memberReceiverUses.push({ name: argument.text, at: argument.startIndex });
1812
+ }
1813
+ return true;
1814
+ }
1815
+ if (node.type === 'field_declaration' && !functionDeclarator(node)) {
1816
+ const owner = enclosingClassName(node);
1817
+ const typeNode = node.childForFieldName('type') ||
1818
+ node.namedChildren.find(child => TYPE_NODES.has(child.type));
1819
+ const fieldType = typeName(typeNode);
1820
+ if (!owner || !fieldType) return true;
1821
+ for (const child of node.namedChildren || []) {
1822
+ if (!IDENTIFIER_NODES.has(child.type) &&
1823
+ child.type !== 'field_declarator') continue;
1824
+ const identity = declaratorIdentity(child);
1825
+ if (identity.name && identity.name !== typeNode?.text) {
1826
+ fieldTypes.set(`${owner}.${identity.name}`, fieldType);
1827
+ }
1828
+ }
1829
+ return true;
1830
+ }
1831
+ if (node.type !== 'parameter_declaration' && node.type !== 'declaration') {
1832
+ return true;
1833
+ }
1834
+ const typeNode = node.childForFieldName('type') ||
1835
+ (node.namedChildren || []).find(child => TYPE_NODES.has(child.type));
1836
+ const type = typeName(typeNode);
1837
+ if (!type || type === 'auto') return true;
1838
+ const pointeeType = (() => {
1839
+ const raw = String(typeNode?.text || '')
1840
+ .replace(/\b(const|volatile|class|struct|typename)\b/g, '')
1841
+ .trim();
1842
+ const match = raw.match(
1843
+ /^(?:std\s*::\s*)?(?:unique_ptr|shared_ptr|auto_ptr)\s*<\s*(.+)\s*>$/s);
1844
+ if (!match) return undefined;
1845
+ let depth = 0;
1846
+ for (const character of match[1]) {
1847
+ if (character === '<') depth++;
1848
+ else if (character === '>') depth--;
1849
+ else if (character === ',' && depth === 0) return undefined;
1850
+ }
1851
+ return typeName({ text: match[1] }) || undefined;
1852
+ })();
1853
+ const scope = variableBindingScope(node);
1854
+ const declarators = variableDeclarators(node);
1855
+ if (declarators.length === 0 && node.type === 'declaration' &&
1856
+ node.parent?.type === 'compound_statement') {
1857
+ const direct = node.childForFieldName('declarator');
1858
+ const identity = direct?.type === 'function_declarator'
1859
+ ? declaratorIdentity(direct) : null;
1860
+ if (identity?.name) {
1861
+ ambiguousDirectInitializers.push({
1862
+ node, type, pointeeType, scope, direct, name: identity.name,
1863
+ });
1864
+ }
1865
+ return true;
1866
+ }
1867
+ addBindings(node, type, pointeeType, scope, declarators);
1868
+ return true;
1869
+ });
1870
+ for (const candidate of ambiguousDirectInitializers) {
1871
+ if (memberReceiverUses.some(use =>
1872
+ use.name === candidate.name &&
1873
+ use.at > candidate.direct.endIndex &&
1874
+ candidate.scope.startIndex <= use.at &&
1875
+ use.at < candidate.scope.endIndex)) {
1876
+ addBindings(candidate.node, candidate.type, candidate.pointeeType,
1877
+ candidate.scope, [candidate.direct]);
1878
+ }
1879
+ }
1880
+ const resolveBinding = (name, atNode) => {
1881
+ if (!name || !atNode) return undefined;
1882
+ const at = atNode.startIndex;
1883
+ const candidates = bindings.filter(binding =>
1884
+ binding.name === name &&
1885
+ binding.scopeStart <= at && at < binding.scopeEnd &&
1886
+ binding.declaredAt <= at);
1887
+ candidates.sort((a, b) => {
1888
+ const aSpan = a.scopeEnd - a.scopeStart;
1889
+ const bSpan = b.scopeEnd - b.scopeStart;
1890
+ return aSpan - bSpan || b.declaredAt - a.declaredAt;
1891
+ });
1892
+ return candidates[0];
1893
+ };
1894
+ return {
1895
+ get: (name, atNode) => resolveBinding(name, atNode)?.type,
1896
+ getPointee: (name, atNode) =>
1897
+ resolveBinding(name, atNode)?.pointeeType,
1898
+ has: (name, atNode) => resolveBinding(name, atNode) !== undefined,
1899
+ fieldTypes,
1900
+ };
1901
+ }
1902
+
1903
+ function stringLiteralKind(node) {
1904
+ const text = node?.text || '';
1905
+ if (text.startsWith('L"') || text.startsWith('LR"')) {
1906
+ return 'string:wchar_t';
1907
+ }
1908
+ if (text.startsWith('u8"') || text.startsWith('u8R"')) {
1909
+ return 'string:char8_t';
1910
+ }
1911
+ if (text.startsWith('u"') || text.startsWith('uR"')) {
1912
+ return 'string:char16_t';
1913
+ }
1914
+ if (text.startsWith('U"') || text.startsWith('UR"')) {
1915
+ return 'string:char32_t';
1916
+ }
1917
+ return 'string:char';
1918
+ }
1919
+
1920
+ function literalPrefixKind(node, base) {
1921
+ const text = node?.text || '';
1922
+ if (text.startsWith('L')) return `${base}:wchar_t`;
1923
+ if (text.startsWith('u8')) return `${base}:char8_t`;
1924
+ if (text.startsWith('u')) return `${base}:char16_t`;
1925
+ if (text.startsWith('U')) return `${base}:char32_t`;
1926
+ return `${base}:char`;
1927
+ }
1928
+
1929
+ /**
1930
+ * Compiler-visible argument shape for conservative C++ overload pruning.
1931
+ *
1932
+ * Every branch starts from an AST-classified expression node. Text is used
1933
+ * only to distinguish literal prefixes/suffixes or recover the type spelling
1934
+ * carried by that node; unknown expressions deliberately stay `expr`.
1935
+ */
1936
+ function staticArgumentKind(node, variableTypes) {
1937
+ if (!node) return 'expr';
1938
+ if (node.type === 'parenthesized_expression') {
1939
+ return staticArgumentKind(node.namedChild(0), variableTypes);
1940
+ }
1941
+ if (node.type === 'string_literal' || node.type === 'raw_string_literal') {
1942
+ return stringLiteralKind(node);
1943
+ }
1944
+ if (node.type === 'concatenated_string') {
1945
+ const parts = (node.namedChildren || [])
1946
+ .filter(child => child.type === 'string_literal' ||
1947
+ child.type === 'raw_string_literal')
1948
+ .map(stringLiteralKind);
1949
+ return parts.length > 0 && parts.every(kind => kind === parts[0])
1950
+ ? parts[0] : 'expr';
1951
+ }
1952
+ if (node.type === 'char_literal') {
1953
+ return literalPrefixKind(node, 'char');
1954
+ }
1955
+ if (node.type === 'number_literal') {
1956
+ const text = node.text || '';
1957
+ const floating = text.includes('.') ||
1958
+ /[pP][+-]?[0-9]/.test(text) ||
1959
+ /[eE][+-]?[0-9]/.test(text) ||
1960
+ /[fF]$/.test(text);
1961
+ return floating ? 'number:floating' : 'number:integer';
1962
+ }
1963
+ if (node.type === 'true' || node.type === 'false') return 'bool';
1964
+ if (node.type === 'null' || node.type === 'nullptr') return 'null';
1965
+ if (node.type === 'identifier') {
1966
+ const type = variableTypes?.get(node.text, node);
1967
+ return type ? `type:${type}` : 'expr';
1968
+ }
1969
+ if (node.type === 'compound_literal_expression') {
1970
+ const typeNode = node.childForFieldName('type') ||
1971
+ (node.namedChildren || []).find(child =>
1972
+ TYPE_NODES.has(child.type) || child.type === 'type_descriptor');
1973
+ const type = typeName(typeNode);
1974
+ return type ? `type:${type}` : 'expr';
1975
+ }
1976
+ if (node.type === 'new_expression') {
1977
+ const typeNode = node.childForFieldName('type') ||
1978
+ (node.namedChildren || []).find(child =>
1979
+ TYPE_NODES.has(child.type) || child.type === 'type_descriptor');
1980
+ const type = typeName(typeNode);
1981
+ return type ? `type:${type}` : 'expr';
1982
+ }
1983
+ if (node.type === 'cast_expression') {
1984
+ const typeNode = node.childForFieldName('type') ||
1985
+ (node.namedChildren || []).find(child =>
1986
+ TYPE_NODES.has(child.type) || child.type === 'type_descriptor');
1987
+ const type = typeName(typeNode);
1988
+ return type ? `type:${type}` : 'expr';
1989
+ }
1990
+ if (node.type === 'call_expression') {
1991
+ const fnNode = node.childForFieldName('function');
1992
+ const identity = callIdentity(fnNode);
1993
+ if (identity.name === 'static_cast' || identity.name === 'dynamic_cast' ||
1994
+ identity.name === 'const_cast' || identity.name === 'reinterpret_cast') {
1995
+ const typeNode = fnNode?.childForFieldName('type') ||
1996
+ (fnNode?.namedChildren || []).find(child =>
1997
+ child.type === 'type_descriptor' || TYPE_NODES.has(child.type));
1998
+ const type = typeName(typeNode);
1999
+ if (type) return `type:${type}`;
2000
+ }
2001
+ return identity.name ? `call:${identity.name}` : 'expr';
2002
+ }
2003
+ return 'expr';
2004
+ }
2005
+
2006
+ function callArguments(node, variableTypes) {
2007
+ const args = node.childForFieldName('arguments');
2008
+ if (!args) return { argCount: 0, argKinds: [] };
2009
+ const values = args.namedChildren.filter(child => !child.type.endsWith('comment'));
2010
+ return {
2011
+ argCount: values.length,
2012
+ argKinds: values.map(value => staticArgumentKind(value, variableTypes)),
2013
+ firstArg: values[0],
2014
+ };
2015
+ }
2016
+
2017
+ function callIdentity(fnNode) {
2018
+ if (!fnNode) return {};
2019
+ if (fnNode.type === 'type_descriptor') {
2020
+ return callIdentity(fnNode.childForFieldName('type') ||
2021
+ fnNode.namedChild(0));
2022
+ }
2023
+ if (fnNode.type === 'parenthesized_expression') {
2024
+ return callIdentity(fnNode.namedChild(0));
2025
+ }
2026
+ if (IDENTIFIER_NODES.has(fnNode.type)) {
2027
+ return { name: fnNode.text, nameNode: fnNode, isMethod: false };
2028
+ }
2029
+ if (fnNode.type === 'field_expression') {
2030
+ const nameNode = fnNode.childForFieldName('field') ||
2031
+ fnNode.namedChildren[fnNode.namedChildCount - 1];
2032
+ const receiverNode = fnNode.childForFieldName('argument') || fnNode.namedChild(0);
2033
+ return {
2034
+ name: nameNode?.text,
2035
+ nameNode,
2036
+ isMethod: true,
2037
+ receiver: receiverNode?.text,
2038
+ receiverNode,
2039
+ pointerAccess: (fnNode.children || []).some(child =>
2040
+ child.type === '->'),
2041
+ };
2042
+ }
2043
+ if (fnNode.type === 'qualified_identifier') {
2044
+ const rawNameNode = fnNode.childForFieldName('name') ||
2045
+ fnNode.namedChildren[fnNode.namedChildCount - 1];
2046
+ const scopeNode = fnNode.childForFieldName('scope') || fnNode.namedChild(0);
2047
+ const globalQualified = fnNode.text.startsWith('::') &&
2048
+ (!scopeNode || sameNode(scopeNode, rawNameNode));
2049
+ if (globalQualified) {
2050
+ return {
2051
+ name: rawNameNode?.text,
2052
+ nameNode: rawNameNode,
2053
+ isMethod: false,
2054
+ isPathCall: true,
2055
+ globalQualified: true,
2056
+ };
2057
+ }
2058
+ const nested = rawNameNode?.type === 'template_function' ||
2059
+ rawNameNode?.type === 'qualified_identifier'
2060
+ ? callIdentity(rawNameNode) : null;
2061
+ return {
2062
+ name: nested?.name || rawNameNode?.text,
2063
+ nameNode: nested?.nameNode || rawNameNode,
2064
+ isMethod: true,
2065
+ receiver: nested?.receiver
2066
+ ? `${scopeNode?.text}::${nested.receiver}`
2067
+ : scopeNode?.text,
2068
+ isPathCall: true,
2069
+ ...(nested?.explicitTemplateCall && { explicitTemplateCall: true }),
2070
+ };
2071
+ }
2072
+ if (fnNode.type === 'template_function' || fnNode.type === 'template_type') {
2073
+ const nested = callIdentity(
2074
+ fnNode.childForFieldName('name') || fnNode.namedChild(0));
2075
+ return {
2076
+ ...nested,
2077
+ explicitTemplateCall: true,
2078
+ };
2079
+ }
2080
+ return {};
2081
+ }
2082
+
2083
+ function compileTimeOnlyContext(node) {
2084
+ for (let current = node?.parent; current; current = current.parent) {
2085
+ // `decltype(f())` forms a compile-time dependency but never executes
2086
+ // `f`. Keep it visible to impact analysis without presenting it as a
2087
+ // runtime caller. Stop at the nearest callable so an outer declaration
2088
+ // cannot accidentally classify calls inside a nested function body.
2089
+ if (current.type === 'decltype') return 'decltype';
2090
+ if (current.type === 'function_definition' ||
2091
+ current.type === 'lambda_expression') return null;
2092
+ }
2093
+ return null;
2094
+ }
2095
+
2096
+ function recoveredExplicitCallOperator(node, variableTypes) {
2097
+ if (node?.type === 'ERROR') {
2098
+ const operatorNode = (node.namedChildren || []).find(child =>
2099
+ child.type === 'operator_name' && child.text === 'operator()');
2100
+ const tokens = new Set((node.children || [])
2101
+ .filter(child => !child.isNamed).map(child => child.type));
2102
+ const named = node.namedChildren || [];
2103
+ const operatorIndex = named.indexOf(operatorNode);
2104
+ const templateType = named[operatorIndex + 1];
2105
+ const hasTemplateType = TYPE_NODES.has(templateType?.type) ||
2106
+ templateType?.type === 'type_descriptor';
2107
+ if (!operatorNode || !hasTemplateType ||
2108
+ !tokens.has('<') || !tokens.has('>') ||
2109
+ !tokens.has('(') || !tokens.has(')')) return null;
2110
+ const values = named.slice(operatorIndex + 2);
2111
+ return {
2112
+ name: 'operator()',
2113
+ line: operatorNode.startPosition.row + 1,
2114
+ column: operatorNode.startPosition.column,
2115
+ isMethod: false,
2116
+ explicitTemplateCall: true,
2117
+ argCount: values.length,
2118
+ argKinds: values.map(value =>
2119
+ staticArgumentKind(value, variableTypes)),
2120
+ enclosingFunction: enclosingFunctionOf(node),
2121
+ };
2122
+ }
2123
+ if (node?.type !== 'binary_expression') return null;
2124
+ const left = node.childForFieldName('left') || node.namedChild(0);
2125
+ const right = node.childForFieldName('right') ||
2126
+ node.namedChildren[node.namedChildCount - 1];
2127
+ if (left?.type !== 'call_expression' || !right) return null;
2128
+ const functionNode = left.childForFieldName('function');
2129
+ const argumentNode = left.childForFieldName('arguments');
2130
+ if (functionNode?.type !== 'identifier' ||
2131
+ functionNode.text !== 'operator' ||
2132
+ (argumentNode?.namedChildCount || 0) !== 0) return null;
2133
+ const errorNode = (node.namedChildren || []).find(
2134
+ child => child.type === 'ERROR');
2135
+ const hasTemplateType = (errorNode?.namedChildren || []).some(child =>
2136
+ TYPE_NODES.has(child.type) || child.type === 'type_descriptor');
2137
+ const errorTokens = new Set((errorNode?.children || [])
2138
+ .filter(child => !child.isNamed).map(child => child.type));
2139
+ const binaryTokens = new Set((node.children || [])
2140
+ .filter(child => !child.isNamed).map(child => child.type));
2141
+ // tree-sitter-cpp 0.23 recovers `operator()<T>(value)` as
2142
+ // `(operator()) < ERROR[T>( value`. This exact AST recovery shape is
2143
+ // stronger than a text fallback and prevents the call from disappearing.
2144
+ if (!hasTemplateType || !binaryTokens.has('<') ||
2145
+ !errorTokens.has('>') || !errorTokens.has('(')) return null;
2146
+ const values = commaExpressionValues(right);
2147
+ return {
2148
+ name: 'operator()',
2149
+ line: functionNode.startPosition.row + 1,
2150
+ column: functionNode.startPosition.column,
2151
+ isMethod: false,
2152
+ explicitTemplateCall: true,
2153
+ argCount: values.length,
2154
+ argKinds: values.map(value =>
2155
+ staticArgumentKind(value, variableTypes)),
2156
+ enclosingFunction: enclosingFunctionOf(node),
2157
+ };
2158
+ }
2159
+
2160
+ function commaExpressionValues(node) {
2161
+ if (!node) return [];
2162
+ if (node.type !== 'comma_expression') return [node];
2163
+ const left = node.childForFieldName('left') || node.namedChild(0);
2164
+ const right = node.childForFieldName('right') || node.namedChild(1);
2165
+ return [...commaExpressionValues(left), ...commaExpressionValues(right)];
2166
+ }
2167
+
2168
+ function assignmentTargetOf(callNode) {
2169
+ let value = callNode;
2170
+ let parent = value.parent;
2171
+ while (parent?.type === 'parenthesized_expression') {
2172
+ value = parent;
2173
+ parent = parent.parent;
2174
+ }
2175
+ if (parent?.type === 'init_declarator' &&
2176
+ sameNode(parent.childForFieldName('value'), value)) {
2177
+ const identity = declaratorIdentity(parent.childForFieldName('declarator'));
2178
+ return identity.name || null;
2179
+ }
2180
+ if (parent?.type === 'assignment_expression' &&
2181
+ sameNode(parent.childForFieldName('right'), value)) {
2182
+ const left = parent.childForFieldName('left');
2183
+ return left?.type === 'identifier' ? left.text : null;
2184
+ }
2185
+ return null;
2186
+ }
2187
+
2188
+ function fieldReceiverPath(node) {
2189
+ if (!node) return null;
2190
+ if (node.type === 'parenthesized_expression') {
2191
+ return fieldReceiverPath(node.namedChild(0));
2192
+ }
2193
+ if (node.type === 'identifier' || node.type === 'this') {
2194
+ return { root: node.text, fields: [] };
2195
+ }
2196
+ if (node.type !== 'field_expression') return null;
2197
+ const argument = node.childForFieldName('argument') || node.namedChild(0);
2198
+ const field = node.childForFieldName('field') ||
2199
+ node.namedChildren[node.namedChildCount - 1];
2200
+ const base = fieldReceiverPath(argument);
2201
+ if (!base || !field?.text) return null;
2202
+ return { root: base.root, fields: [...base.fields, field.text] };
2203
+ }
2204
+
2205
+ function findCallsInTree(code, parser, _options = {}, existingTree = null,
2206
+ includeMacroBodies = true) {
2207
+ const tree = existingTree || parseTree(parser, code);
2208
+ const variableTypes = buildVariableTypes(tree);
2209
+ const fieldTypes = variableTypes.fieldTypes;
2210
+ const calls = [];
2211
+ traverseTree(tree.rootNode, node => {
2212
+ const recoveredOperator = recoveredExplicitCallOperator(
2213
+ node, variableTypes);
2214
+ if (recoveredOperator) {
2215
+ calls.push(recoveredOperator);
2216
+ return true;
2217
+ }
2218
+ if (node.type === 'call_expression') {
2219
+ if (recoveredExplicitCallOperator(node.parent, variableTypes)) {
2220
+ return true;
2221
+ }
2222
+ const functionNode = node.childForFieldName('function');
2223
+ const identity = callIdentity(functionNode);
2224
+ if (!identity.name) return true;
2225
+ const args = callArguments(node, variableTypes);
2226
+ const first = extractStringArg(args.firstArg);
2227
+ const enclosingFunction = enclosingFunctionOf(node);
2228
+ const owner = enclosingClassName(node) ||
2229
+ enclosingFunction?.className;
2230
+ const receiverNode = identity.receiverNode ||
2231
+ (functionNode?.type === 'field_expression'
2232
+ ? functionNode.childForFieldName('argument') || functionNode.namedChild(0)
2233
+ : null);
2234
+ let receiverCall;
2235
+ let receiverCallIsMethod = false;
2236
+ let receiverCallReceiver;
2237
+ let receiverCallLine;
2238
+ let receiverCallStart;
2239
+ let receiverCallEnd;
2240
+ if (receiverNode?.type === 'call_expression') {
2241
+ const producerNode = receiverNode.childForFieldName('function');
2242
+ const producer = callIdentity(producerNode);
2243
+ if (producer.name) {
2244
+ receiverCall = producer.name;
2245
+ receiverCallIsMethod = producer.isMethod;
2246
+ receiverCallReceiver = producer.isPathCall
2247
+ ? producer.receiver : undefined;
2248
+ receiverCallLine = producer.nameNode?.startPosition.row + 1 ||
2249
+ receiverNode.startPosition.row + 1;
2250
+ receiverCallStart = receiverNode.startIndex;
2251
+ receiverCallEnd = receiverNode.endIndex;
2252
+ }
2253
+ }
2254
+ const receiverPath = fieldReceiverPath(receiverNode);
2255
+ let receiverRoot = receiverPath?.root;
2256
+ let receiverFields = receiverPath?.fields || [];
2257
+ let receiverRootType = receiverRoot
2258
+ ? ((identity.pointerAccess && receiverFields.length === 0
2259
+ ? variableTypes.getPointee(receiverRoot, node) : undefined) ||
2260
+ variableTypes.get(receiverRoot, node))
2261
+ : undefined;
2262
+ // A bare field inside a member function is implicitly rooted at
2263
+ // `this`; keep that field path so declared-field resolution can
2264
+ // type it without mistaking it for an unrelated local.
2265
+ if (owner && receiverNode?.type === 'identifier' &&
2266
+ !receiverRootType &&
2267
+ (fieldTypes.has(`${owner}.${receiverNode.text}`) ||
2268
+ !variableTypes.get(receiverNode.text, node))) {
2269
+ receiverRoot = 'this';
2270
+ receiverFields = [receiverNode.text];
2271
+ receiverRootType = owner;
2272
+ }
2273
+ if (receiverRoot === 'this' && !receiverRootType) {
2274
+ receiverRootType = owner || undefined;
2275
+ }
2276
+ const directReceiverType = receiverPath &&
2277
+ receiverFields.length === 0 && receiverRoot
2278
+ ? ((identity.pointerAccess
2279
+ ? variableTypes.getPointee(receiverRoot, node) : undefined) ||
2280
+ variableTypes.get(receiverRoot, node))
2281
+ : undefined;
2282
+ const assignedTo = assignmentTargetOf(node);
2283
+ const compileTimeOnly = compileTimeOnlyContext(node);
2284
+ calls.push({
2285
+ name: identity.name,
2286
+ line: identity.nameNode?.startPosition.row + 1 || node.startPosition.row + 1,
2287
+ column: identity.nameNode?.startPosition.column,
2288
+ callStart: node.startIndex,
2289
+ callEnd: node.endIndex,
2290
+ isMethod: identity.isMethod,
2291
+ ...(identity.receiver && { receiver: identity.receiver }),
2292
+ ...(identity.isPathCall && { isPathCall: true }),
2293
+ ...(identity.globalQualified && { globalQualified: true }),
2294
+ ...(identity.explicitTemplateCall && {
2295
+ explicitTemplateCall: true,
2296
+ }),
2297
+ ...(compileTimeOnly && { compileTimeOnly }),
2298
+ ...(directReceiverType && { receiverType: directReceiverType }),
2299
+ ...(receiverCall && {
2300
+ receiverCall,
2301
+ receiverIsChainRoot: true,
2302
+ receiverCallLine,
2303
+ receiverCallStart,
2304
+ receiverCallEnd,
2305
+ ...(receiverCallIsMethod && {
2306
+ receiverCallIsMethod: true,
2307
+ }),
2308
+ ...(receiverCallReceiver && { receiverCallReceiver }),
2309
+ }),
2310
+ ...(receiverFields.length > 0 && receiverRoot && {
2311
+ receiverRoot,
2312
+ receiverField: receiverFields[0],
2313
+ receiverFields,
2314
+ ...(receiverRootType && { receiverRootType }),
2315
+ }),
2316
+ ...(assignedTo && { assignedTo }),
2317
+ argCount: args.argCount,
2318
+ argKinds: args.argKinds,
2319
+ enclosingFunction,
2320
+ ...(first && {
2321
+ firstStringArg: first.value,
2322
+ firstStringArgInterp: first.interp,
2323
+ }),
2324
+ });
2325
+ return true;
2326
+ }
2327
+ if (node.type === 'cast_expression') {
2328
+ // tree-sitter-cpp parses parenthesized function-template
2329
+ // invocation (`(PrintSmartPointer<T>)(p, os, 0)`) as a cast whose
2330
+ // "type" is the template callable. This is still an AST-proven
2331
+ // callable expression: require the exact type-descriptor +
2332
+ // parenthesized-value shape and recover its base/path identity.
2333
+ const typeNode = node.childForFieldName('type');
2334
+ const valueNode = node.childForFieldName('value');
2335
+ if (typeNode?.type === 'type_descriptor' &&
2336
+ valueNode?.type === 'parenthesized_expression') {
2337
+ const identity = callIdentity(typeNode);
2338
+ if (identity.name) {
2339
+ const expression = valueNode.namedChild(0);
2340
+ const values = commaExpressionValues(expression);
2341
+ calls.push({
2342
+ name: identity.name,
2343
+ line: identity.nameNode?.startPosition.row + 1 ||
2344
+ node.startPosition.row + 1,
2345
+ column: identity.nameNode?.startPosition.column,
2346
+ isMethod: identity.isMethod,
2347
+ ...(identity.receiver && { receiver: identity.receiver }),
2348
+ ...(identity.isPathCall && { isPathCall: true }),
2349
+ argCount: values.length,
2350
+ argKinds: values.map(value =>
2351
+ staticArgumentKind(value, variableTypes)),
2352
+ enclosingFunction: enclosingFunctionOf(node),
2353
+ });
2354
+ return false;
2355
+ }
2356
+ }
2357
+ }
2358
+ if (node.type === 'new_expression') {
2359
+ const typeNode = node.childForFieldName('type') ||
2360
+ node.namedChildren.find(child => child.type === 'type_identifier' ||
2361
+ child.type === 'qualified_identifier');
2362
+ if (!typeNode) return true;
2363
+ const args = callArguments(node, variableTypes);
2364
+ const name = typeName(typeNode);
2365
+ if (name) {
2366
+ calls.push({
2367
+ name,
2368
+ line: typeNode.startPosition.row + 1,
2369
+ column: typeNode.startPosition.column,
2370
+ isMethod: false,
2371
+ isConstructor: true,
2372
+ argCount: args.argCount,
2373
+ argKinds: args.argKinds,
2374
+ enclosingFunction: enclosingFunctionOf(node),
2375
+ });
2376
+ }
2377
+ }
2378
+ return true;
2379
+ });
2380
+ if (includeMacroBodies) calls.push(...findMacroBodyCalls(tree, code, parser));
2381
+ return calls;
2382
+ }
2383
+
2384
+ function callIdentityKey(call) {
2385
+ return [
2386
+ call.name,
2387
+ call.line,
2388
+ call.column ?? '',
2389
+ call.callStart ?? '',
2390
+ call.callEnd ?? '',
2391
+ call.isMethod ? 1 : 0,
2392
+ call.isConstructor ? 1 : 0,
2393
+ call.inMacroBody ? 1 : 0,
2394
+ ].join(':');
2395
+ }
2396
+
2397
+ function attributeCallsToLexicalFunctions(calls, functions) {
2398
+ const bodies = functions.filter(fn => !fn.isSignature)
2399
+ .sort((a, b) =>
2400
+ ((a.endLine - a.startLine) - (b.endLine - b.startLine)) ||
2401
+ b.startLine - a.startLine);
2402
+ for (const call of calls) {
2403
+ const owner = bodies.find(fn =>
2404
+ fn.startLine <= call.line && call.line <= fn.endLine);
2405
+ if (!owner) continue;
2406
+ call.enclosingFunction = {
2407
+ name: owner.name,
2408
+ startLine: owner.startLine,
2409
+ endLine: owner.endLine,
2410
+ ...(owner.className && { className: owner.className }),
2411
+ };
2412
+ }
2413
+ return calls;
2414
+ }
2415
+
2416
+ function findCallsInCode(code, parser, options = {}, existingTree = null,
2417
+ includeMacroBodies = true, mode = 'cpp') {
2418
+ // Synthetic macro-body parses and other explicit trees are already exact
2419
+ // views supplied by the caller. Only whole-file extraction participates
2420
+ // in preprocessor-configuration conservation.
2421
+ if (existingTree) {
2422
+ const calls = findCallsInTree(
2423
+ code, parser, options, existingTree, includeMacroBodies);
2424
+ const functions = findFunctionsInTree(code, existingTree, mode);
2425
+ return attributeCallsToLexicalFunctions(calls, functions);
2426
+ }
2427
+ const tree = parseTree(parser, code);
2428
+ const primary = findCallsInTree(
2429
+ code, parser, options, tree, includeMacroBodies);
2430
+ const literal = literalRecoveryTree(parser, code, tree);
2431
+ if (!literal) {
2432
+ return attributeCallsToLexicalFunctions(
2433
+ primary, findFunctionsInTree(code, tree, mode));
2434
+ }
2435
+ try {
2436
+ const literalCalls = findCallsInTree(
2437
+ code, parser, options, literal, includeMacroBodies)
2438
+ .map(call => ({ ...call, configurationVariant: true }));
2439
+ // Selected-configuration facts retain their stronger evidence. Only
2440
+ // literal-only sites carry configurationVariant and are therefore
2441
+ // routed to the visible unverified tier by callers.js.
2442
+ const calls = mergeExtracted(primary, literalCalls, callIdentityKey);
2443
+ const functions = mergeExtracted(
2444
+ findFunctionsInTree(code, tree, mode),
2445
+ findFunctionsInTree(code, literal, mode),
2446
+ item => `${item.name}:${item.startLine}:${item.className || ''}:${item.isSignature ? 1 : 0}`,
2447
+ );
2448
+ return attributeCallsToLexicalFunctions(calls, functions);
2449
+ } finally { /* cached with the selected tree */ }
2450
+ }
2451
+
2452
+ /**
2453
+ * Parse C/C++ replacement lists as executable syntax without treating the
2454
+ * preprocessor's opaque `preproc_arg` token as text evidence. Tree-sitter does
2455
+ * not descend into replacement lists, so wrap each AST-identified value in a
2456
+ * synthetic function body and parse it with the same grammar. The wrapper and
2457
+ * line-splice blanking preserve a deterministic mapping back to source.
2458
+ *
2459
+ * Calls through macro parameters (`#define APPLY(fn, x) fn(x)`) are retained
2460
+ * for conservation but marked as lexical parameter dispatch; they must never
2461
+ * resolve to an unrelated project function that happens to share `fn`'s name.
2462
+ */
2463
+ function findMacroBodyCalls(tree, code, parser, onlyName = null) {
2464
+ const calls = [];
2465
+ for (const node of macroDefinitionNodes(tree)) {
2466
+ const nameNode = node.childForFieldName('name') ||
2467
+ (node.namedChildren || []).find(child => child.type === 'identifier');
2468
+ const valueNode = node.childForFieldName('value') ||
2469
+ (node.namedChildren || []).find(child => child.type === 'preproc_arg');
2470
+ if (!nameNode || !valueNode || !valueNode.text.trim()) continue;
2471
+ // Usage queries ask about one identifier. Avoid reparsing every macro
2472
+ // in every scanned file; the cheap prefilter only skips replacement
2473
+ // lists that cannot possibly produce the requested AST name.
2474
+ if (onlyName && !valueNode.text.includes(onlyName)) continue;
2475
+ const paramsNode = node.childForFieldName('parameters') ||
2476
+ (node.namedChildren || []).find(child => child.type === 'preproc_params');
2477
+ const parameters = new Set((paramsNode?.namedChildren || [])
2478
+ .filter(child => child.type === 'identifier')
2479
+ .map(child => child.text));
2480
+ // Replace only the continuation backslash. Keeping the newline and
2481
+ // every other byte makes source-line/column and span remapping exact.
2482
+ const body = valueNode.text.replace(/\\(?=\r?\n)/g, ' ');
2483
+ const prefix = 'void __ucn_macro_body__(void) {\n';
2484
+ const synthetic = `${prefix}${body}\n;}`;
2485
+ // Synthetic replacement-list wrappers are one-shot parse inputs. Do
2486
+ // not put hundreds of tiny trees in the full-source recovery LRU:
2487
+ // that evicted large project headers and made every later agent query
2488
+ // recover them again. Extract the immutable records, then explicitly
2489
+ // release this native tree.
2490
+ const nestedTree = safeParse(parser, synthetic, undefined, PARSE_OPTIONS);
2491
+ let nested;
2492
+ try {
2493
+ nested = findCallsInCode(synthetic, parser, {}, nestedTree, false);
2494
+ } finally {
2495
+ nestedTree.delete?.();
2496
+ }
2497
+ for (const call of nested) {
2498
+ if (call.line < 2) continue;
2499
+ const line = valueNode.startPosition.row + call.line - 1;
2500
+ const column = call.line === 2
2501
+ ? valueNode.startPosition.column + (call.column || 0)
2502
+ : call.column;
2503
+ const callStart = call.callStart == null
2504
+ ? undefined
2505
+ : valueNode.startIndex + call.callStart - prefix.length;
2506
+ const callEnd = call.callEnd == null
2507
+ ? undefined
2508
+ : valueNode.startIndex + call.callEnd - prefix.length;
2509
+ calls.push({
2510
+ ...call,
2511
+ line,
2512
+ column,
2513
+ ...(callStart != null && callStart >= valueNode.startIndex && {
2514
+ callStart,
2515
+ }),
2516
+ ...(callEnd != null && callEnd >= valueNode.startIndex && {
2517
+ callEnd,
2518
+ }),
2519
+ inMacroBody: true,
2520
+ ...(parameters.has(call.name) && { macroParameter: true }),
2521
+ enclosingFunction: {
2522
+ name: nameNode.text,
2523
+ startLine: node.startPosition.row + 1,
2524
+ endLine: node.endPosition.row + 1,
2525
+ isMacro: true,
2526
+ },
2527
+ });
2528
+ }
2529
+ }
2530
+ return calls;
2531
+ }
2532
+
2533
+ function findImportsInTree(code, tree) {
2534
+ const imports = [];
2535
+ traverseTreeCached(tree.rootNode, node => {
2536
+ if (node.type !== 'preproc_include') return true;
2537
+ const pathNode = node.childForFieldName('path') || node.namedChild(0);
2538
+ if (!pathNode) return false;
2539
+ const system = pathNode.type === 'system_lib_string';
2540
+ const raw = pathNode.text.replace(/^["<]|[">]$/g, '');
2541
+ imports.push({
2542
+ module: system ? raw : (raw.startsWith('.') ? raw : `./${raw}`),
2543
+ names: ['*'],
2544
+ type: system ? 'system-include' : 'include',
2545
+ line: node.startPosition.row + 1,
2546
+ });
2547
+ return false;
2548
+ });
2549
+ return imports;
2550
+ }
2551
+
2552
+ function findImportsInCode(code, parser) {
2553
+ const tree = parseTree(parser, code);
2554
+ const primary = findImportsInTree(code, tree);
2555
+ const literal = literalRecoveryTree(parser, code, tree);
2556
+ if (!literal) return primary;
2557
+ try {
2558
+ return mergeExtracted(
2559
+ primary,
2560
+ findImportsInTree(code, literal),
2561
+ item => `${item.module}:${item.line}:${item.type}`,
2562
+ );
2563
+ } finally { /* cached with the selected tree */ }
2564
+ }
2565
+
2566
+ function findUsagesInCode(code, name, parser, existingTree) {
2567
+ // Usage is the raw literal-name inventory. The literal C/C++ tree retains
2568
+ // identifiers from every preprocessor branch and is sufficient for
2569
+ // occurrence kind/line classification; symbol ownership still comes from
2570
+ // the recovered index. Reusing ProjectIndex's raw tree avoids replaying
2571
+ // expensive conditional recovery for every queried name.
2572
+ const tree = existingTree ||
2573
+ safeParse(parser, code, undefined, PARSE_OPTIONS);
2574
+ const usages = [];
2575
+ const seenUsages = new Set();
2576
+ const addUsage = usage => {
2577
+ const key = `${usage.line}:${usage.column ?? ''}:${usage.usageType}:${usage.receiver || ''}`;
2578
+ if (seenUsages.has(key)) return;
2579
+ seenUsages.add(key);
2580
+ usages.push(usage);
2581
+ };
2582
+ const collectTreeUsages = sourceTree => visitNameNodes(sourceTree, code, name, node => {
2583
+ if (!IDENTIFIER_NODES.has(node.type) || node.text !== name) return;
2584
+ let usageType = 'reference';
2585
+ let receiver = null;
2586
+ const parent = node.parent;
2587
+ if (parent) {
2588
+ const call = parent.type === 'call_expression'
2589
+ ? parent
2590
+ : parent.parent?.type === 'call_expression' ? parent.parent : null;
2591
+ // Only the callable's terminal name is a call usage. The previous
2592
+ // descendant test also matched receiver identifiers, classifying
2593
+ // `copy.descriptor()` as a call to a free function named `copy`.
2594
+ // callIdentity already unwraps qualified/template/parenthesized
2595
+ // callees while preserving the exact terminal name node.
2596
+ const calledIdentity = call
2597
+ ? callIdentity(call.childForFieldName('function')) : null;
2598
+ if (calledIdentity?.nameNode &&
2599
+ sameNode(calledIdentity.nameNode, node)) {
2600
+ usageType = 'call';
2601
+ } else if ((parent.type === 'function_declarator' ||
2602
+ parent.type === 'parameter_declaration') &&
2603
+ (sameNode(parent.childForFieldName('declarator'), node) ||
2604
+ sameNode(parent.childForFieldName('name'), node))) {
2605
+ usageType = 'definition';
2606
+ } else if (CLASS_NODES.has(parent.type) &&
2607
+ sameNode(parent.childForFieldName('name'), node)) {
2608
+ const body = parent.childForFieldName('body');
2609
+ const declaration = parent.parent;
2610
+ const opaqueForwardDeclaration =
2611
+ ['translation_unit', 'declaration_list']
2612
+ .includes(declaration?.type) ||
2613
+ (declaration?.type === 'declaration' &&
2614
+ (declaration.namedChildren || []).length === 1);
2615
+ // `struct S *value` names an existing tag; only a bodied
2616
+ // declaration or a standalone `struct S;` introduces it.
2617
+ usageType = body || opaqueForwardDeclaration
2618
+ ? 'definition' : 'reference';
2619
+ } else if (parent.type === 'preproc_include') {
2620
+ usageType = 'import';
2621
+ }
2622
+ if (parent.type === 'qualified_identifier' &&
2623
+ sameNode(parent.childForFieldName('name'), node)) {
2624
+ receiver = typeName(parent.childForFieldName('scope') ||
2625
+ parent.namedChild(0));
2626
+ } else if (parent.type === 'field_expression' &&
2627
+ sameNode(parent.childForFieldName('field'), node)) {
2628
+ const argument = parent.childForFieldName('argument') ||
2629
+ parent.namedChild(0);
2630
+ receiver = argument?.text || null;
2631
+ }
2632
+ }
2633
+ // A bare name lexically inside a class method participates in C++
2634
+ // member lookup on that class. This is ownership evidence for test
2635
+ // discovery and usage presentation; receiver-qualified forms above
2636
+ // retain their explicit receiver instead.
2637
+ if (!receiver) receiver = enclosingClassName(node);
2638
+ addUsage({
2639
+ line: node.startPosition.row + 1,
2640
+ column: node.startPosition.column,
2641
+ usageType,
2642
+ ...(receiver && { receiver }),
2643
+ });
2644
+ });
2645
+ collectTreeUsages(tree);
2646
+ // Replacement lists are opaque preproc_arg nodes in the C grammars. The
2647
+ // call extractor reparses those AST-proven regions; surface the resulting
2648
+ // call usages here as well so callers/callees, usages, and tests share one
2649
+ // semantic fact set.
2650
+ const seenCalls = new Set(usages
2651
+ .filter(usage => usage.usageType === 'call')
2652
+ .map(usage => `${usage.line}:${usage.column ?? ''}`));
2653
+ const macroCalls = findMacroBodyCalls(tree, code, parser, name);
2654
+ for (const call of macroCalls) {
2655
+ if (call.name !== name) continue;
2656
+ const key = `${call.line}:${call.column ?? ''}`;
2657
+ if (seenCalls.has(key)) continue;
2658
+ seenCalls.add(key);
2659
+ addUsage({
2660
+ line: call.line,
2661
+ column: call.column,
2662
+ usageType: 'call',
2663
+ ...(call.receiver && { receiver: call.receiver }),
2664
+ ...(call.macroParameter && { macroParameter: true }),
2665
+ });
2666
+ }
2667
+ return usages;
2668
+ }
2669
+
2670
+ function getEntryPointKind(symbol) {
2671
+ if (symbol.name === 'main' || symbol.name === 'WinMain' ||
2672
+ symbol.name === 'wWinMain' || symbol.name === 'DllMain') return 'main';
2673
+ if (/^(test_|Test|TEST_)/.test(symbol.name)) return 'test';
2674
+ return null;
2675
+ }
2676
+
2677
+ function isEntryPoint(symbol) {
2678
+ return getEntryPointKind(symbol) !== null;
2679
+ }
2680
+
2681
+ function parse(code, parser, mode, options = {}) {
2682
+ const tree = parseTree(parser, code);
2683
+ const literal = literalRecoveryTree(parser, code, tree);
2684
+ try {
2685
+ const lines = code.split('\n');
2686
+ const functions = literal
2687
+ ? mergeExtracted(
2688
+ findFunctionsInTree(code, tree, mode, lines),
2689
+ findFunctionsInTree(code, literal, mode, lines),
2690
+ item => `${item.name}:${item.startLine}:${item.className || ''}:${item.isSignature ? 1 : 0}`,
2691
+ )
2692
+ : findFunctionsInTree(code, tree, mode, lines);
2693
+ const classes = literal
2694
+ ? mergeExtracted(
2695
+ findClassesInTree(code, tree, mode, lines),
2696
+ findClassesInTree(code, literal, mode, lines),
2697
+ item => `${item.name}:${item.startLine}:${item.type}:${item.namespace || ''}`,
2698
+ )
2699
+ : findClassesInTree(code, tree, mode, lines);
2700
+ const imports = literal
2701
+ ? mergeExtracted(
2702
+ findImportsInTree(code, tree),
2703
+ findImportsInTree(code, literal),
2704
+ item => `${item.module}:${item.line}:${item.type}`,
2705
+ )
2706
+ : findImportsInTree(code, tree);
2707
+ const primaryCalls = findCallsInTree(code, parser, {}, tree, true);
2708
+ const calls = literal
2709
+ ? mergeExtracted(
2710
+ primaryCalls,
2711
+ findCallsInTree(code, parser, {}, literal, true)
2712
+ .map(call => ({ ...call, configurationVariant: true })),
2713
+ callIdentityKey,
2714
+ )
2715
+ : primaryCalls;
2716
+ attributeCallsToLexicalFunctions(calls, functions);
2717
+ const result = {
2718
+ language: mode,
2719
+ totalLines: code.length === 0 ? 0 : lines.length,
2720
+ functions,
2721
+ classes,
2722
+ stateObjects: literal
2723
+ ? mergeExtracted(findStateObjectsInTree(tree, lines),
2724
+ findStateObjectsInTree(literal, lines),
2725
+ item => `${item.name}:${item.startLine}`)
2726
+ : findStateObjectsInTree(tree, lines),
2727
+ macros: literal
2728
+ ? mergeExtracted(findMacrosInTree(tree, lines),
2729
+ findMacrosInTree(literal, lines),
2730
+ item => `${item.name}:${item.startLine}:${item.functionLike ? 1 : 0}`)
2731
+ : findMacrosInTree(tree, lines),
2732
+ imports,
2733
+ exports: [
2734
+ ...functions
2735
+ .filter(fn => !fn.modifiers.includes('static'))
2736
+ .map(fn => ({ name: fn.name, type: 'export', line: fn.startLine })),
2737
+ ...classes.map(cls => ({ name: cls.name, type: 'export', line: cls.startLine })),
2738
+ ],
2739
+ ...(parseRecoveryApplied(code, tree) && { parseRecovery: true }),
2740
+ };
2741
+ // Adapter-only full-analysis fact: keep the public parse result shape
2742
+ // stable while avoiding a second pair of whole-tree call walks during
2743
+ // indexing.
2744
+ Object.defineProperty(result, 'calls', {
2745
+ value: calls,
2746
+ enumerable: false,
2747
+ configurable: true,
2748
+ });
2749
+ return result;
2750
+ } finally {
2751
+ if (options.releaseAnalysisTree) {
2752
+ releaseCFamilyTree(parser, code, tree);
2753
+ }
2754
+ }
2755
+ }
2756
+
2757
+ function findExportsInCodeShallow(code, parser, mode) {
2758
+ const functions = findFunctions(code, parser, mode);
2759
+ const classes = findClasses(code, parser, mode);
2760
+ return [
2761
+ ...functions
2762
+ .filter(fn => !fn.modifiers.includes('static'))
2763
+ .map(fn => ({ name: fn.name, type: 'export', line: fn.startLine })),
2764
+ ...classes.map(cls => ({ name: cls.name, type: 'export', line: cls.startLine })),
2765
+ ];
2766
+ }
2767
+
2768
+ function createCFamilyLanguage(mode) {
2769
+ return {
2770
+ parseProvidesAnalysisFacts: true,
2771
+ findFunctions: (code, parser) => findFunctions(code, parser, mode),
2772
+ findClasses: (code, parser) => findClasses(code, parser, mode),
2773
+ findStateObjects,
2774
+ findMacros,
2775
+ findCallsInCode: (code, parser, options, existingTree, includeMacroBodies) =>
2776
+ findCallsInCode(code, parser, options, existingTree, includeMacroBodies, mode),
2777
+ findImportsInCode,
2778
+ findExportsInCode: (code, parser) => findExportsInCodeShallow(code, parser, mode),
2779
+ findUsagesInCode,
2780
+ isEntryPoint,
2781
+ getEntryPointKind,
2782
+ parse: (code, parser, options) => parse(code, parser, mode, options),
2783
+ };
2784
+ }
2785
+
2786
+ module.exports = {
2787
+ createCFamilyLanguage,
2788
+ // Deterministic test seams for the recovery memory/order contracts.
2789
+ conditionalRecoverySources,
2790
+ mergeExtracted,
2791
+ };