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.
- package/.claude/skills/ucn/SKILL.md +89 -77
- package/.claude/skills/ucn/references/commands.md +62 -68
- package/.claude/skills/ucn/references/trust-contract.md +31 -6
- package/README.md +438 -305
- package/assets/demo.svg +31 -0
- package/cli/index.js +430 -1385
- package/core/account.js +144 -34
- package/core/analysis.js +182 -72
- package/core/ast-analysis.js +279 -0
- package/core/bridge.js +205 -24
- package/core/brief.js +27 -58
- package/core/build-worker.js +21 -140
- package/core/cache.js +513 -11
- package/core/callers.js +4920 -456
- package/core/check.js +13 -4
- package/core/command-contracts.js +402 -0
- package/core/compilation-database.js +276 -0
- package/core/confidence.js +4 -1
- package/core/deadcode.js +397 -19
- package/core/discovery.js +359 -46
- package/core/entrypoints.js +195 -41
- package/core/execute.js +887 -81
- package/core/graph-build.js +162 -7
- package/core/graph.js +53 -77
- package/core/imports.js +65 -6
- package/core/index-ir.js +138 -0
- package/core/ir.js +195 -0
- package/core/output/analysis.js +212 -22
- package/core/output/brief.js +23 -0
- package/core/output/check.js +4 -0
- package/core/output/doctor.js +37 -6
- package/core/output/endpoints.js +5 -2
- package/core/output/extraction.js +24 -12
- package/core/output/find.js +141 -36
- package/core/output/graph.js +11 -5
- package/core/output/public.js +462 -0
- package/core/output/refactoring.js +42 -10
- package/core/output/reporting.js +97 -20
- package/core/output/search.js +24 -16
- package/core/output/shared.js +22 -1
- package/core/output/tracing.js +30 -15
- package/core/output-budget.js +295 -0
- package/core/output.js +1 -0
- package/core/parallel-build.js +44 -11
- package/core/parser.js +3 -3
- package/core/project.js +384 -187
- package/core/public-command.js +47 -0
- package/core/registry.js +247 -117
- package/core/reporting.js +312 -290
- package/core/search.js +317 -185
- package/core/semantic-provider.js +110 -0
- package/core/stacktrace.js +25 -0
- package/core/tracing.js +101 -51
- package/core/trust-matrix.js +19 -40
- package/core/verify.js +534 -37
- package/languages/adapter.js +218 -0
- package/languages/c-family.js +2791 -0
- package/languages/c.js +3 -0
- package/languages/cpp.js +3 -0
- package/languages/csharp.js +1402 -0
- package/languages/go.js +60 -21
- package/languages/html.js +2 -2
- package/languages/index.js +85 -7
- package/languages/java.js +396 -13
- package/languages/javascript.js +199 -19
- package/languages/python.js +964 -22
- package/languages/rust.js +1317 -152
- package/languages/utils.js +40 -3
- package/mcp/server.js +254 -636
- package/package.json +39 -22
- package/eslint.config.js +0 -43
- 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
|
+
};
|