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,1402 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
traverseTree,
|
|
5
|
+
traverseTreeCached,
|
|
6
|
+
nodeToLocation,
|
|
7
|
+
extractJSDocstring,
|
|
8
|
+
extractStringArg,
|
|
9
|
+
visitNameNodes,
|
|
10
|
+
sameNode,
|
|
11
|
+
} = require('./utils');
|
|
12
|
+
const { PARSE_OPTIONS, safeParse } = require('./index');
|
|
13
|
+
|
|
14
|
+
const TYPE_DECLARATIONS = new Map([
|
|
15
|
+
['class_declaration', 'class'],
|
|
16
|
+
['interface_declaration', 'interface'],
|
|
17
|
+
['struct_declaration', 'struct'],
|
|
18
|
+
['record_declaration', 'record'],
|
|
19
|
+
['enum_declaration', 'enum'],
|
|
20
|
+
]);
|
|
21
|
+
const IDENTIFIER_NODES = new Set(['identifier', 'generic_name']);
|
|
22
|
+
const METHOD_LIKE_NODES = new Set([
|
|
23
|
+
'method_declaration', 'constructor_declaration',
|
|
24
|
+
'destructor_declaration', 'operator_declaration',
|
|
25
|
+
'conversion_operator_declaration',
|
|
26
|
+
]);
|
|
27
|
+
const CONTROL_FLOW_KEYWORDS = new Set([
|
|
28
|
+
'if', 'for', 'foreach', 'while', 'switch', 'catch',
|
|
29
|
+
'using', 'lock', 'fixed',
|
|
30
|
+
]);
|
|
31
|
+
const CONVERT_RETURN_TYPES = new Map([
|
|
32
|
+
['ToBoolean', 'bool'],
|
|
33
|
+
['ToByte', 'byte'],
|
|
34
|
+
['ToSByte', 'sbyte'],
|
|
35
|
+
['ToInt16', 'short'],
|
|
36
|
+
['ToUInt16', 'ushort'],
|
|
37
|
+
['ToInt32', 'int'],
|
|
38
|
+
['ToUInt32', 'uint'],
|
|
39
|
+
['ToInt64', 'long'],
|
|
40
|
+
['ToUInt64', 'ulong'],
|
|
41
|
+
['ToSingle', 'float'],
|
|
42
|
+
['ToDouble', 'double'],
|
|
43
|
+
['ToDecimal', 'decimal'],
|
|
44
|
+
['ToChar', 'char'],
|
|
45
|
+
['ToDateTime', 'DateTime'],
|
|
46
|
+
['ToString', 'string'],
|
|
47
|
+
]);
|
|
48
|
+
|
|
49
|
+
function isControlFlowLocalArtifact(node) {
|
|
50
|
+
return node?.type === 'local_function_statement' &&
|
|
51
|
+
CONTROL_FLOW_KEYWORDS.has(node.childForFieldName('name')?.text);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function parseTree(parser, code) {
|
|
55
|
+
return safeParse(parser, code, undefined, PARSE_OPTIONS);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function namespaceOf(node, tree) {
|
|
59
|
+
for (let parent = node?.parent; parent; parent = parent.parent) {
|
|
60
|
+
if (parent.type === 'namespace_declaration') {
|
|
61
|
+
return parent.childForFieldName('name')?.text ||
|
|
62
|
+
parent.namedChildren.find(child =>
|
|
63
|
+
child.type === 'identifier' || child.type === 'qualified_name')?.text ||
|
|
64
|
+
null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
const fileScoped = (tree?.rootNode?.namedChildren || []).find(child =>
|
|
68
|
+
child.type === 'file_scoped_namespace_declaration');
|
|
69
|
+
return fileScoped?.childForFieldName('name')?.text ||
|
|
70
|
+
fileScoped?.namedChildren.find(child =>
|
|
71
|
+
child.type === 'identifier' || child.type === 'qualified_name')?.text ||
|
|
72
|
+
null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function modifiersOf(node) {
|
|
76
|
+
return (node.namedChildren || [])
|
|
77
|
+
.filter(child => child.type === 'modifier')
|
|
78
|
+
.map(child => child.text);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function attributeData(node) {
|
|
82
|
+
const attributes = [];
|
|
83
|
+
for (const list of (node.namedChildren || []).filter(child => child.type === 'attribute_list')) {
|
|
84
|
+
for (const attribute of list.namedChildren || []) {
|
|
85
|
+
if (attribute.type !== 'attribute') continue;
|
|
86
|
+
const nameNode = attribute.childForFieldName('name') || attribute.namedChild(0);
|
|
87
|
+
if (!nameNode) continue;
|
|
88
|
+
const args = attribute.namedChildren.find(child => child.type === 'attribute_argument_list');
|
|
89
|
+
const firstArg = args?.namedChildren.find(child => child.type === 'attribute_argument')
|
|
90
|
+
?.namedChild(0);
|
|
91
|
+
const stringArg = extractStringArg(firstArg);
|
|
92
|
+
attributes.push({
|
|
93
|
+
name: nameNode.text.replace(/Attribute$/, ''),
|
|
94
|
+
...(stringArg && {
|
|
95
|
+
arg: stringArg.value,
|
|
96
|
+
interp: stringArg.interp,
|
|
97
|
+
}),
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return attributes;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function structuredParams(paramsNode) {
|
|
105
|
+
if (!paramsNode) return [];
|
|
106
|
+
const params = [];
|
|
107
|
+
const recoveredParams = [];
|
|
108
|
+
// tree-sitter-c-sharp 0.23 exposes `params T[] name` as three siblings
|
|
109
|
+
// (`params` token, type node, identifier) rather than a named
|
|
110
|
+
// parameter_array node. Recover that compiler-significant shape before
|
|
111
|
+
// processing ordinary parameter nodes; losing it makes overload arity
|
|
112
|
+
// and normal-vs-expanded params resolution unsound.
|
|
113
|
+
for (let i = 0; i < paramsNode.childCount; i++) {
|
|
114
|
+
if (paramsNode.child(i).type !== 'params') continue;
|
|
115
|
+
let typeNode = null;
|
|
116
|
+
let nameNode = null;
|
|
117
|
+
for (let j = i + 1; j < paramsNode.childCount; j++) {
|
|
118
|
+
const child = paramsNode.child(j);
|
|
119
|
+
if (child.type === ',' || child.type === ')') break;
|
|
120
|
+
if (!child.isNamed) continue;
|
|
121
|
+
if (!typeNode) typeNode = child;
|
|
122
|
+
else {
|
|
123
|
+
nameNode = child;
|
|
124
|
+
break;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (nameNode) {
|
|
128
|
+
recoveredParams.push({
|
|
129
|
+
name: nameNode.text,
|
|
130
|
+
...(typeNode && { type: typeNode.text }),
|
|
131
|
+
rest: true,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
for (const param of paramsNode.namedChildren || []) {
|
|
136
|
+
if (param.type !== 'parameter' && param.type !== 'parameter_array') continue;
|
|
137
|
+
const nameNode = param.childForFieldName('name');
|
|
138
|
+
const typeNode = param.childForFieldName('type');
|
|
139
|
+
if (!nameNode) continue;
|
|
140
|
+
const info = { name: nameNode.text };
|
|
141
|
+
if (typeNode) info.type = typeNode.text;
|
|
142
|
+
if (modifiersOf(param).includes('this')) info.extensionReceiver = true;
|
|
143
|
+
if (param.type === 'parameter_array') info.rest = true;
|
|
144
|
+
const value = param.childForFieldName('value') ||
|
|
145
|
+
param.namedChildren.find(child => child !== nameNode && child !== typeNode &&
|
|
146
|
+
!['attribute_list', 'modifier'].includes(child.type));
|
|
147
|
+
if (value) {
|
|
148
|
+
info.default = value.text;
|
|
149
|
+
info.optional = true;
|
|
150
|
+
}
|
|
151
|
+
params.push(info);
|
|
152
|
+
}
|
|
153
|
+
// A params array is required to be the final declaration parameter.
|
|
154
|
+
params.push(...recoveredParams);
|
|
155
|
+
return params;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// `operator +` / `operator ==` name from the token following the anonymous
|
|
159
|
+
// `operator` keyword; conversion operators name their target type
|
|
160
|
+
// (`implicit operator int` → `operator int`), matching the C++ operator_name
|
|
161
|
+
// convention.
|
|
162
|
+
function operatorName(node) {
|
|
163
|
+
if (node.type === 'operator_declaration') {
|
|
164
|
+
for (let i = 0; i < node.childCount - 1; i++) {
|
|
165
|
+
if (node.child(i).type === 'operator') {
|
|
166
|
+
return `operator${node.child(i + 1).text}`;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
if (node.type === 'conversion_operator_declaration') {
|
|
172
|
+
const target = node.childForFieldName('type');
|
|
173
|
+
return target ? `operator ${target.text}` : null;
|
|
174
|
+
}
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function conversionKind(node) {
|
|
179
|
+
if (node.type !== 'conversion_operator_declaration') return null;
|
|
180
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
181
|
+
const type = node.child(i).type;
|
|
182
|
+
if (type === 'implicit' || type === 'explicit') return type;
|
|
183
|
+
}
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function memberFromNode(node, className, lines) {
|
|
188
|
+
if (!METHOD_LIKE_NODES.has(node.type)) return null;
|
|
189
|
+
const nameNode = node.childForFieldName('name');
|
|
190
|
+
const name = nameNode?.text ||
|
|
191
|
+
operatorName(node) ||
|
|
192
|
+
(node.type === 'constructor_declaration' ? className : null);
|
|
193
|
+
if (!name) return null;
|
|
194
|
+
const paramsNode = node.childForFieldName('parameters');
|
|
195
|
+
const returnNode = node.childForFieldName('returns') || node.childForFieldName('type');
|
|
196
|
+
const { startLine, endLine, indent } = nodeToLocation(node, lines);
|
|
197
|
+
const attrs = attributeData(node);
|
|
198
|
+
const modifiers = modifiersOf(node);
|
|
199
|
+
const isConstructor = node.type === 'constructor_declaration';
|
|
200
|
+
if (isConstructor) modifiers.push('constructor');
|
|
201
|
+
if (node.type === 'destructor_declaration') modifiers.push('destructor');
|
|
202
|
+
const conversion = conversionKind(node);
|
|
203
|
+
if (conversion) modifiers.push(conversion);
|
|
204
|
+
const explicitInterfaceNode = node.namedChildren.find(child =>
|
|
205
|
+
child.type === 'explicit_interface_specifier');
|
|
206
|
+
const explicitInterface = explicitInterfaceNode?.text
|
|
207
|
+
.replace(/\.$/, '').trim() || null;
|
|
208
|
+
const paramsStructured = structuredParams(paramsNode);
|
|
209
|
+
return {
|
|
210
|
+
name,
|
|
211
|
+
params: paramsNode ? paramsNode.text.replace(/^\(|\)$/g, '').trim() : '...',
|
|
212
|
+
paramsStructured,
|
|
213
|
+
returnType: isConstructor ? null : returnNode?.text || null,
|
|
214
|
+
startLine,
|
|
215
|
+
endLine,
|
|
216
|
+
indent,
|
|
217
|
+
modifiers,
|
|
218
|
+
memberType: isConstructor ? 'constructor' : 'method',
|
|
219
|
+
isMethod: true,
|
|
220
|
+
isConstructor,
|
|
221
|
+
className,
|
|
222
|
+
...(explicitInterface && { explicitInterface }),
|
|
223
|
+
isAsync: modifiers.includes('async'),
|
|
224
|
+
...(modifiers.includes('static') &&
|
|
225
|
+
paramsStructured[0]?.extensionReceiver && {
|
|
226
|
+
isExtensionMethod: true,
|
|
227
|
+
}),
|
|
228
|
+
docstring: extractJSDocstring(lines, startLine),
|
|
229
|
+
...(attrs.length > 0 && {
|
|
230
|
+
decorators: attrs.map(attr => attr.name),
|
|
231
|
+
attributesWithArgs: attrs,
|
|
232
|
+
}),
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function fieldMembers(node, lines) {
|
|
237
|
+
if (node.type !== 'field_declaration' && node.type !== 'event_field_declaration') return [];
|
|
238
|
+
const declaration = node.namedChildren.find(child => child.type === 'variable_declaration');
|
|
239
|
+
const typeNode = declaration?.childForFieldName('type');
|
|
240
|
+
const members = [];
|
|
241
|
+
for (const declarator of declaration?.namedChildren || []) {
|
|
242
|
+
if (declarator.type !== 'variable_declarator') continue;
|
|
243
|
+
const nameNode = declarator.childForFieldName('name');
|
|
244
|
+
if (!nameNode?.text) continue;
|
|
245
|
+
const { startLine, endLine, indent } = nodeToLocation(declarator, lines);
|
|
246
|
+
members.push({
|
|
247
|
+
name: nameNode.text,
|
|
248
|
+
startLine,
|
|
249
|
+
endLine,
|
|
250
|
+
indent,
|
|
251
|
+
modifiers: modifiersOf(node),
|
|
252
|
+
memberType: 'field',
|
|
253
|
+
fieldType: typeNode?.text || null,
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
return members;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function indexerMember(node, className, lines) {
|
|
260
|
+
if (node.type !== 'indexer_declaration') return null;
|
|
261
|
+
const paramsNode = node.childForFieldName('parameters');
|
|
262
|
+
const typeNode = node.childForFieldName('type');
|
|
263
|
+
const { startLine, endLine, indent } = nodeToLocation(node, lines);
|
|
264
|
+
return {
|
|
265
|
+
name: 'this[]',
|
|
266
|
+
params: paramsNode ? paramsNode.text.replace(/^\[|\]$/g, '').trim() : '...',
|
|
267
|
+
paramsStructured: structuredParams(paramsNode),
|
|
268
|
+
returnType: typeNode?.text || null,
|
|
269
|
+
startLine,
|
|
270
|
+
endLine,
|
|
271
|
+
indent,
|
|
272
|
+
modifiers: modifiersOf(node),
|
|
273
|
+
memberType: 'property',
|
|
274
|
+
isMethod: true,
|
|
275
|
+
className,
|
|
276
|
+
docstring: extractJSDocstring(lines, startLine),
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function propertyMember(node, lines) {
|
|
281
|
+
if (node.type !== 'property_declaration' && node.type !== 'event_declaration') return null;
|
|
282
|
+
const nameNode = node.childForFieldName('name');
|
|
283
|
+
const typeNode = node.childForFieldName('type');
|
|
284
|
+
// Conditional attributes inside a property can make tree-sitter recover a
|
|
285
|
+
// second, zero-width property fragment (`get { ... }` with a missing name).
|
|
286
|
+
// The real declaration is already indexed; reject the missing-node
|
|
287
|
+
// artifact instead of letting one invalid symbol discard the whole file.
|
|
288
|
+
if (!nameNode?.text) return null;
|
|
289
|
+
const { startLine, endLine, indent } = nodeToLocation(node, lines);
|
|
290
|
+
return {
|
|
291
|
+
name: nameNode.text,
|
|
292
|
+
startLine,
|
|
293
|
+
endLine,
|
|
294
|
+
indent,
|
|
295
|
+
modifiers: modifiersOf(node),
|
|
296
|
+
memberType: 'field',
|
|
297
|
+
fieldType: typeNode?.text || null,
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function baseHeadName(text) {
|
|
302
|
+
return text.replace(/<.*$/, '').split('.').pop().trim();
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Classify the base list into extends/implements. The C# grammar guarantees a
|
|
306
|
+
// class base precedes the interfaces, so only position 0 needs deciding:
|
|
307
|
+
// same-file declarations are ground truth, and the BCL-wide I-prefix
|
|
308
|
+
// convention (IDisposable, IList<T>) covers external names. Interfaces only
|
|
309
|
+
// ever extend; structs only ever implement.
|
|
310
|
+
function classifyBases(bases, type, fileTypeKinds) {
|
|
311
|
+
if (bases.length === 0) return {};
|
|
312
|
+
if (type === 'interface') return { extends: bases.join(', ') };
|
|
313
|
+
if (type === 'enum') return { extends: bases[0] };
|
|
314
|
+
let extendsBase = null;
|
|
315
|
+
let implementsList = bases;
|
|
316
|
+
if (type !== 'struct') {
|
|
317
|
+
const head = baseHeadName(bases[0]);
|
|
318
|
+
const declaredKind = fileTypeKinds.get(head);
|
|
319
|
+
const isInterface = declaredKind === 'interface' ||
|
|
320
|
+
(!declaredKind && /^I[A-Z]/.test(head));
|
|
321
|
+
if (!isInterface) {
|
|
322
|
+
extendsBase = bases[0];
|
|
323
|
+
implementsList = bases.slice(1);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
return {
|
|
327
|
+
...(extendsBase && { extends: extendsBase }),
|
|
328
|
+
...(implementsList.length > 0 && { implements: implementsList }),
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function findClasses(code, parser) {
|
|
333
|
+
const tree = parseTree(parser, code);
|
|
334
|
+
const lines = code.split('\n');
|
|
335
|
+
const classes = [];
|
|
336
|
+
// Same-file type kinds override the I-prefix convention when classifying
|
|
337
|
+
// base lists (a project class legitimately named IFoo stays `extends`).
|
|
338
|
+
const fileTypeKinds = new Map();
|
|
339
|
+
traverseTreeCached(tree.rootNode, node => {
|
|
340
|
+
const kind = TYPE_DECLARATIONS.get(node.type);
|
|
341
|
+
if (!kind) return true;
|
|
342
|
+
const kindName = node.childForFieldName('name')?.text;
|
|
343
|
+
if (kindName && !fileTypeKinds.has(kindName)) fileTypeKinds.set(kindName, kind);
|
|
344
|
+
return true;
|
|
345
|
+
});
|
|
346
|
+
traverseTreeCached(tree.rootNode, node => {
|
|
347
|
+
if (node.type === 'delegate_declaration') {
|
|
348
|
+
// A delegate declares an importable callable type, like a C
|
|
349
|
+
// function-pointer typedef.
|
|
350
|
+
const delegateName = node.childForFieldName('name');
|
|
351
|
+
if (delegateName) {
|
|
352
|
+
const { startLine, endLine, indent } = nodeToLocation(node, lines);
|
|
353
|
+
classes.push({
|
|
354
|
+
name: delegateName.text,
|
|
355
|
+
type: 'type',
|
|
356
|
+
startLine,
|
|
357
|
+
endLine,
|
|
358
|
+
indent,
|
|
359
|
+
modifiers: modifiersOf(node),
|
|
360
|
+
...(namespaceOf(node, tree) && { namespace: namespaceOf(node, tree) }),
|
|
361
|
+
members: [],
|
|
362
|
+
docstring: extractJSDocstring(lines, startLine),
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
return true;
|
|
366
|
+
}
|
|
367
|
+
const type = TYPE_DECLARATIONS.get(node.type);
|
|
368
|
+
if (!type) return true;
|
|
369
|
+
const nameNode = node.childForFieldName('name');
|
|
370
|
+
if (!nameNode) return true;
|
|
371
|
+
const body = node.childForFieldName('body') ||
|
|
372
|
+
node.namedChildren.find(child => child.type === 'declaration_list');
|
|
373
|
+
const members = [];
|
|
374
|
+
const memberNodes = [];
|
|
375
|
+
const collectMemberNodes = (container) => {
|
|
376
|
+
for (const child of container?.namedChildren || []) {
|
|
377
|
+
if (TYPE_DECLARATIONS.has(child.type)) continue;
|
|
378
|
+
if (child.type.startsWith('preproc_')) {
|
|
379
|
+
collectMemberNodes(child);
|
|
380
|
+
} else {
|
|
381
|
+
memberNodes.push(child);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
};
|
|
385
|
+
collectMemberNodes(body);
|
|
386
|
+
for (const child of memberNodes) {
|
|
387
|
+
if (TYPE_DECLARATIONS.has(child.type)) continue;
|
|
388
|
+
if (type === 'enum' && child.type === 'enum_member_declaration') {
|
|
389
|
+
const enumName = child.childForFieldName('name') ||
|
|
390
|
+
child.namedChildren.find(item => item.type === 'identifier');
|
|
391
|
+
if (enumName) {
|
|
392
|
+
const { startLine, endLine, indent } = nodeToLocation(child, lines);
|
|
393
|
+
members.push({
|
|
394
|
+
name: enumName.text,
|
|
395
|
+
startLine,
|
|
396
|
+
endLine,
|
|
397
|
+
indent,
|
|
398
|
+
modifiers: ['public', 'static'],
|
|
399
|
+
memberType: 'field',
|
|
400
|
+
fieldType: nameNode.text,
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
continue;
|
|
404
|
+
}
|
|
405
|
+
const method = memberFromNode(child, nameNode.text, lines);
|
|
406
|
+
if (method) members.push(method);
|
|
407
|
+
else {
|
|
408
|
+
const property = propertyMember(child, lines) ||
|
|
409
|
+
indexerMember(child, nameNode.text, lines);
|
|
410
|
+
if (property) members.push(property);
|
|
411
|
+
members.push(...fieldMembers(child, lines));
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
const baseList = node.namedChildren.find(child => child.type === 'base_list');
|
|
415
|
+
const bases = baseList?.namedChildren.map(child => child.text) || [];
|
|
416
|
+
const { startLine, endLine, indent } = nodeToLocation(node, lines);
|
|
417
|
+
const attrs = attributeData(node);
|
|
418
|
+
let enclosingType;
|
|
419
|
+
for (let parent = node.parent; parent; parent = parent.parent) {
|
|
420
|
+
if (!TYPE_DECLARATIONS.has(parent.type)) continue;
|
|
421
|
+
enclosingType = parent.childForFieldName('name')?.text;
|
|
422
|
+
if (enclosingType) break;
|
|
423
|
+
}
|
|
424
|
+
classes.push({
|
|
425
|
+
name: nameNode.text,
|
|
426
|
+
type,
|
|
427
|
+
startLine,
|
|
428
|
+
endLine,
|
|
429
|
+
indent,
|
|
430
|
+
modifiers: modifiersOf(node),
|
|
431
|
+
...(enclosingType && { enclosingType }),
|
|
432
|
+
...(namespaceOf(node, tree) && { namespace: namespaceOf(node, tree) }),
|
|
433
|
+
members,
|
|
434
|
+
...classifyBases(bases, type, fileTypeKinds),
|
|
435
|
+
docstring: extractJSDocstring(lines, startLine),
|
|
436
|
+
...(attrs.length > 0 && {
|
|
437
|
+
decorators: attrs.map(attr => attr.name),
|
|
438
|
+
attributesWithArgs: attrs,
|
|
439
|
+
}),
|
|
440
|
+
});
|
|
441
|
+
return true;
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
// tree-sitter-c-sharp can end a class node early after a malformed
|
|
445
|
+
// preprocessor branch while still recovering all following methods as
|
|
446
|
+
// method_declaration siblings under the namespace. Preserve those AST
|
|
447
|
+
// declarations by attaching an orphan to the nearest preceding type in
|
|
448
|
+
// the same namespace and at a shallower indentation. This is declaration
|
|
449
|
+
// recovery only—call extraction remains AST-derived.
|
|
450
|
+
traverseTreeCached(tree.rootNode, node => {
|
|
451
|
+
if (!METHOD_LIKE_NODES.has(node.type)) return true;
|
|
452
|
+
for (let parent = node.parent; parent; parent = parent.parent) {
|
|
453
|
+
if (TYPE_DECLARATIONS.has(parent.type)) return false;
|
|
454
|
+
}
|
|
455
|
+
const startLine = node.startPosition.row + 1;
|
|
456
|
+
const nodeNamespace = namespaceOf(node, tree);
|
|
457
|
+
const candidate = classes
|
|
458
|
+
.filter(type => type.startLine < startLine &&
|
|
459
|
+
(type.namespace || null) === (nodeNamespace || null) &&
|
|
460
|
+
type.indent < node.startPosition.column &&
|
|
461
|
+
type.type !== 'enum')
|
|
462
|
+
.sort((a, b) => b.startLine - a.startLine)[0];
|
|
463
|
+
if (!candidate) return false;
|
|
464
|
+
const member = memberFromNode(node, candidate.name, lines);
|
|
465
|
+
if (member && !candidate.members.some(existing =>
|
|
466
|
+
existing.startLine === member.startLine &&
|
|
467
|
+
existing.name === member.name)) {
|
|
468
|
+
candidate.members.push(member);
|
|
469
|
+
candidate.endLine = Math.max(candidate.endLine, member.endLine);
|
|
470
|
+
}
|
|
471
|
+
return false;
|
|
472
|
+
});
|
|
473
|
+
return classes;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function findFunctions(code, parser) {
|
|
477
|
+
const tree = parseTree(parser, code);
|
|
478
|
+
const lines = code.split('\n');
|
|
479
|
+
const functions = [];
|
|
480
|
+
traverseTreeCached(tree.rootNode, node => {
|
|
481
|
+
if (node.type !== 'local_function_statement') return true;
|
|
482
|
+
const nameNode = node.childForFieldName('name');
|
|
483
|
+
if (!nameNode) return true;
|
|
484
|
+
// Conditional-compilation recovery can make `else if (...)` look
|
|
485
|
+
// like a local function whose return type is `else` and name is
|
|
486
|
+
// `if`. C# keywords cannot be ordinary local-function identifiers;
|
|
487
|
+
// rejecting this parser artifact keeps enclosing-function ownership
|
|
488
|
+
// on the real constructor/method.
|
|
489
|
+
if (isControlFlowLocalArtifact(node)) {
|
|
490
|
+
return false;
|
|
491
|
+
}
|
|
492
|
+
const paramsNode = node.childForFieldName('parameters');
|
|
493
|
+
const returnNode = node.childForFieldName('returns') || node.childForFieldName('type');
|
|
494
|
+
const { startLine, endLine, indent } = nodeToLocation(node, lines);
|
|
495
|
+
const modifiers = modifiersOf(node);
|
|
496
|
+
functions.push({
|
|
497
|
+
name: nameNode.text,
|
|
498
|
+
params: paramsNode ? paramsNode.text.replace(/^\(|\)$/g, '').trim() : '...',
|
|
499
|
+
paramsStructured: structuredParams(paramsNode),
|
|
500
|
+
returnType: returnNode?.text || null,
|
|
501
|
+
startLine,
|
|
502
|
+
endLine,
|
|
503
|
+
indent,
|
|
504
|
+
modifiers,
|
|
505
|
+
isAsync: modifiers.includes('async'),
|
|
506
|
+
isNested: true,
|
|
507
|
+
docstring: extractJSDocstring(lines, startLine),
|
|
508
|
+
});
|
|
509
|
+
return false;
|
|
510
|
+
});
|
|
511
|
+
const topLevel = (tree.rootNode.namedChildren || []).filter(child =>
|
|
512
|
+
child.type === 'global_statement');
|
|
513
|
+
if (topLevel.length > 0) {
|
|
514
|
+
functions.push({
|
|
515
|
+
name: 'Main',
|
|
516
|
+
params: '',
|
|
517
|
+
paramsStructured: [],
|
|
518
|
+
returnType: null,
|
|
519
|
+
startLine: topLevel[0].startPosition.row + 1,
|
|
520
|
+
endLine: topLevel[topLevel.length - 1].endPosition.row + 1,
|
|
521
|
+
indent: topLevel[0].startPosition.column,
|
|
522
|
+
modifiers: ['static', 'top-level'],
|
|
523
|
+
namespace: namespaceOf(topLevel[0], tree) || undefined,
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
return functions;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function findStateObjects(code, parser) {
|
|
530
|
+
const tree = parseTree(parser, code);
|
|
531
|
+
const lines = code.split('\n');
|
|
532
|
+
const states = [];
|
|
533
|
+
traverseTreeCached(tree.rootNode, node => {
|
|
534
|
+
if (node.type !== 'global_statement') return true;
|
|
535
|
+
const declaration = node.namedChildren.find(child =>
|
|
536
|
+
child.type === 'local_declaration_statement')?.namedChild(0);
|
|
537
|
+
for (const declarator of declaration?.namedChildren || []) {
|
|
538
|
+
if (declarator.type !== 'variable_declarator') continue;
|
|
539
|
+
const nameNode = declarator.childForFieldName('name');
|
|
540
|
+
if (!nameNode) continue;
|
|
541
|
+
const { startLine, endLine, indent } = nodeToLocation(node, lines);
|
|
542
|
+
states.push({
|
|
543
|
+
name: nameNode.text,
|
|
544
|
+
startLine,
|
|
545
|
+
endLine,
|
|
546
|
+
indent,
|
|
547
|
+
modifiers: [],
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
return false;
|
|
551
|
+
});
|
|
552
|
+
return states;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function enclosingFunctionOf(node) {
|
|
556
|
+
for (let parent = node?.parent; parent; parent = parent.parent) {
|
|
557
|
+
if (parent.type === 'method_declaration' ||
|
|
558
|
+
parent.type === 'constructor_declaration' ||
|
|
559
|
+
parent.type === 'local_function_statement') {
|
|
560
|
+
if (isControlFlowLocalArtifact(parent)) continue;
|
|
561
|
+
const nameNode = parent.childForFieldName('name');
|
|
562
|
+
if (!nameNode) return null;
|
|
563
|
+
return {
|
|
564
|
+
name: nameNode.text,
|
|
565
|
+
startLine: parent.startPosition.row + 1,
|
|
566
|
+
endLine: parent.endPosition.row + 1,
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
if (parent.type === 'global_statement') {
|
|
570
|
+
return {
|
|
571
|
+
name: 'Main',
|
|
572
|
+
startLine: parent.startPosition.row + 1,
|
|
573
|
+
endLine: parent.endPosition.row + 1,
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
return null;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
function enclosingClassName(node) {
|
|
581
|
+
for (let parent = node?.parent; parent; parent = parent.parent) {
|
|
582
|
+
if (TYPE_DECLARATIONS.has(parent.type)) {
|
|
583
|
+
return parent.childForFieldName('name')?.text || null;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
return null;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
/** Whether a bare identifier is a field/property/event of the enclosing type. */
|
|
590
|
+
function enclosingTypeDeclaresMember(node, memberName) {
|
|
591
|
+
let typeNode = null;
|
|
592
|
+
for (let parent = node?.parent; parent; parent = parent.parent) {
|
|
593
|
+
if (TYPE_DECLARATIONS.has(parent.type)) {
|
|
594
|
+
typeNode = parent;
|
|
595
|
+
break;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
const body = typeNode?.childForFieldName('body') ||
|
|
599
|
+
typeNode?.namedChildren.find(child => child.type === 'declaration_list');
|
|
600
|
+
if (!body) return false;
|
|
601
|
+
const stack = [...(body.namedChildren || [])];
|
|
602
|
+
while (stack.length > 0) {
|
|
603
|
+
const current = stack.pop();
|
|
604
|
+
if (TYPE_DECLARATIONS.has(current.type)) continue;
|
|
605
|
+
if (current.type === 'property_declaration' ||
|
|
606
|
+
current.type === 'event_declaration') {
|
|
607
|
+
if (current.childForFieldName('name')?.text === memberName) return true;
|
|
608
|
+
continue;
|
|
609
|
+
}
|
|
610
|
+
if (current.type === 'field_declaration' ||
|
|
611
|
+
current.type === 'event_field_declaration') {
|
|
612
|
+
for (const child of current.namedChildren || []) {
|
|
613
|
+
const variables = child.type === 'variable_declaration'
|
|
614
|
+
? child.namedChildren : [child];
|
|
615
|
+
if (variables.some(variable =>
|
|
616
|
+
variable.type === 'variable_declarator' &&
|
|
617
|
+
variable.childForFieldName('name')?.text === memberName)) {
|
|
618
|
+
return true;
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
continue;
|
|
622
|
+
}
|
|
623
|
+
// Preprocessor containers can wrap real declarations. Descend through
|
|
624
|
+
// those and the type body, but never into methods/accessors where a
|
|
625
|
+
// same-named local would not make the receiver a class member.
|
|
626
|
+
if (current.type.startsWith('preproc_') || current.type === 'declaration_list') {
|
|
627
|
+
stack.push(...(current.namedChildren || []));
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
return false;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
const CALLABLE_SCOPE_NODES = new Set([
|
|
634
|
+
'method_declaration', 'constructor_declaration', 'destructor_declaration',
|
|
635
|
+
'operator_declaration', 'conversion_operator_declaration',
|
|
636
|
+
'local_function_statement', 'accessor_declaration',
|
|
637
|
+
]);
|
|
638
|
+
|
|
639
|
+
function variableScopeKey(node) {
|
|
640
|
+
for (let parent = node?.parent; parent; parent = parent.parent) {
|
|
641
|
+
if (CALLABLE_SCOPE_NODES.has(parent.type)) {
|
|
642
|
+
if (isControlFlowLocalArtifact(parent)) continue;
|
|
643
|
+
return parent.startPosition.row + 1;
|
|
644
|
+
}
|
|
645
|
+
if (parent.type === 'global_statement') return 'global';
|
|
646
|
+
}
|
|
647
|
+
return 'global';
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
function buildVariableTypes(tree, parser) {
|
|
651
|
+
const byScope = new Map([['global', new Map()]]);
|
|
652
|
+
const conflictsByScope = new Map([['global', new Set()]]);
|
|
653
|
+
const scopeStack = [];
|
|
654
|
+
const setType = (scope, name, type) => {
|
|
655
|
+
if (!name || !type) return;
|
|
656
|
+
if (!conflictsByScope.has(scope)) conflictsByScope.set(scope, new Set());
|
|
657
|
+
const conflicts = conflictsByScope.get(scope);
|
|
658
|
+
if (conflicts.has(name)) return;
|
|
659
|
+
const types = byScope.get(scope);
|
|
660
|
+
const previous = types.get(name);
|
|
661
|
+
if (previous && previous !== type) {
|
|
662
|
+
types.delete(name);
|
|
663
|
+
conflicts.add(name);
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
types.set(name, type);
|
|
667
|
+
};
|
|
668
|
+
traverseTree(tree.rootNode, node => {
|
|
669
|
+
if (CALLABLE_SCOPE_NODES.has(node.type) &&
|
|
670
|
+
!isControlFlowLocalArtifact(node)) {
|
|
671
|
+
const key = node.startPosition.row + 1;
|
|
672
|
+
scopeStack.push(key);
|
|
673
|
+
if (!byScope.has(key)) byScope.set(key, new Map());
|
|
674
|
+
if (!conflictsByScope.has(key)) conflictsByScope.set(key, new Set());
|
|
675
|
+
}
|
|
676
|
+
const currentKey = scopeStack[scopeStack.length - 1] || 'global';
|
|
677
|
+
if (isControlFlowLocalArtifact(node)) {
|
|
678
|
+
const paramsNode = node.childForFieldName('parameters');
|
|
679
|
+
const raw = paramsNode?.text;
|
|
680
|
+
if (raw?.startsWith('(') && raw.endsWith(')')) {
|
|
681
|
+
const expression = raw.slice(1, -1);
|
|
682
|
+
const synthetic =
|
|
683
|
+
`class __UcnRecovery { bool __Call() => ${expression}; }`;
|
|
684
|
+
const recovered = safeParse(
|
|
685
|
+
parser, synthetic, undefined, PARSE_OPTIONS);
|
|
686
|
+
traverseTree(recovered.rootNode, recoveredNode => {
|
|
687
|
+
if (recoveredNode.type !== 'declaration_pattern' &&
|
|
688
|
+
recoveredNode.type !== 'declaration_expression') {
|
|
689
|
+
return true;
|
|
690
|
+
}
|
|
691
|
+
const name =
|
|
692
|
+
recoveredNode.childForFieldName('name')?.text ||
|
|
693
|
+
recoveredNode.namedChildren.at(-1)?.text;
|
|
694
|
+
const type =
|
|
695
|
+
recoveredNode.childForFieldName('type')?.text ||
|
|
696
|
+
recoveredNode.namedChild(0)?.text;
|
|
697
|
+
setType(currentKey, name, type);
|
|
698
|
+
return true;
|
|
699
|
+
});
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
if (node.type === 'parameter') {
|
|
703
|
+
let artifactParameter = false;
|
|
704
|
+
for (let parent = node.parent; parent; parent = parent.parent) {
|
|
705
|
+
if (isControlFlowLocalArtifact(parent)) {
|
|
706
|
+
artifactParameter = true;
|
|
707
|
+
break;
|
|
708
|
+
}
|
|
709
|
+
if (CALLABLE_SCOPE_NODES.has(parent.type)) break;
|
|
710
|
+
}
|
|
711
|
+
if (artifactParameter) return true;
|
|
712
|
+
const name = node.childForFieldName('name')?.text;
|
|
713
|
+
const type = node.childForFieldName('type')?.text;
|
|
714
|
+
setType(currentKey, name, type);
|
|
715
|
+
} else if (node.type === 'declaration_pattern' ||
|
|
716
|
+
node.type === 'declaration_expression') {
|
|
717
|
+
const name = node.childForFieldName('name')?.text ||
|
|
718
|
+
node.namedChildren.at(-1)?.text;
|
|
719
|
+
const type = node.childForFieldName('type')?.text ||
|
|
720
|
+
node.namedChild(0)?.text;
|
|
721
|
+
setType(currentKey, name, type);
|
|
722
|
+
} else if (node.type === 'variable_declaration') {
|
|
723
|
+
// Class fields have their own declared-field receiver path; do not
|
|
724
|
+
// leak them into the top-level-program local scope.
|
|
725
|
+
if (scopeStack.length === 0) {
|
|
726
|
+
let inGlobal = false;
|
|
727
|
+
for (let parent = node.parent; parent; parent = parent.parent) {
|
|
728
|
+
if (parent.type === 'global_statement') {
|
|
729
|
+
inGlobal = true;
|
|
730
|
+
break;
|
|
731
|
+
}
|
|
732
|
+
if (TYPE_DECLARATIONS.has(parent.type)) break;
|
|
733
|
+
}
|
|
734
|
+
if (!inGlobal) return true;
|
|
735
|
+
}
|
|
736
|
+
const typeNode = node.childForFieldName('type');
|
|
737
|
+
// A disabled preprocessor branch can make a switch label plus
|
|
738
|
+
// following invocation look like `case <declarator>`. `case` is
|
|
739
|
+
// not a type, so it must never overwrite a real parameter/local
|
|
740
|
+
// receiver type (for example `JsonWriter writer`).
|
|
741
|
+
if (typeNode?.text === 'case') return true;
|
|
742
|
+
for (const declarator of node.namedChildren || []) {
|
|
743
|
+
if (declarator.type !== 'variable_declarator') continue;
|
|
744
|
+
const name = declarator.childForFieldName('name')?.text;
|
|
745
|
+
const value = declarator.childForFieldName('value') ||
|
|
746
|
+
declarator.namedChildren.find(child => child.type === 'object_creation_expression');
|
|
747
|
+
const dynamicType = value?.type === 'object_creation_expression'
|
|
748
|
+
? value.childForFieldName('type')?.text : null;
|
|
749
|
+
const type = dynamicType || (typeNode?.text !== 'var' ? typeNode?.text : null);
|
|
750
|
+
setType(currentKey, name, type);
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
return true;
|
|
754
|
+
}, {
|
|
755
|
+
onLeave(node) {
|
|
756
|
+
if (CALLABLE_SCOPE_NODES.has(node.type) &&
|
|
757
|
+
!isControlFlowLocalArtifact(node)) {
|
|
758
|
+
scopeStack.pop();
|
|
759
|
+
}
|
|
760
|
+
},
|
|
761
|
+
});
|
|
762
|
+
return byScope;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
function normalizeReceiverType(raw) {
|
|
766
|
+
if (!raw) return null;
|
|
767
|
+
let value = String(raw).trim().replace(/\?$/, '');
|
|
768
|
+
if (value.endsWith('[]')) return { name: 'Array', namespace: 'System' };
|
|
769
|
+
value = value.replace(/^global::/, '').replace(/::/g, '.');
|
|
770
|
+
const match = value.match(/^([A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*)\s*(?:<.*>)?$/s);
|
|
771
|
+
if (!match) return null;
|
|
772
|
+
const parts = match[1].split('.');
|
|
773
|
+
return {
|
|
774
|
+
name: parts.pop(),
|
|
775
|
+
...(parts.length > 0 && { namespace: parts.join('.') }),
|
|
776
|
+
};
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
function literalReceiverType(node) {
|
|
780
|
+
if (!node) return null;
|
|
781
|
+
if (node.type === 'string_literal' ||
|
|
782
|
+
node.type === 'verbatim_string_literal' ||
|
|
783
|
+
node.type === 'interpolated_string_expression') {
|
|
784
|
+
return { name: 'string', namespace: 'System' };
|
|
785
|
+
}
|
|
786
|
+
if (node.type === 'character_literal') {
|
|
787
|
+
return { name: 'char', namespace: 'System' };
|
|
788
|
+
}
|
|
789
|
+
return null;
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
function unwrapReceiverNode(node) {
|
|
793
|
+
let current = node;
|
|
794
|
+
while (current && current.namedChildCount === 1 &&
|
|
795
|
+
(current.type === 'parenthesized_expression' ||
|
|
796
|
+
current.type === 'postfix_unary_expression')) {
|
|
797
|
+
current = current.namedChild(0);
|
|
798
|
+
}
|
|
799
|
+
return current;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
/**
|
|
803
|
+
* Preserve compiler-visible receiver types which do not come from a local
|
|
804
|
+
* declaration. Casts are especially important in C#: `((IList)value).Add()`
|
|
805
|
+
* performs lookup on IList, not on every project method named Add.
|
|
806
|
+
*/
|
|
807
|
+
function receiverTypeFromNode(node, variableTypes) {
|
|
808
|
+
const current = unwrapReceiverNode(node);
|
|
809
|
+
if (!current) return null;
|
|
810
|
+
const literal = literalReceiverType(current);
|
|
811
|
+
if (literal) return literal;
|
|
812
|
+
if (current.type === 'cast_expression') {
|
|
813
|
+
return normalizeReceiverType(current.childForFieldName('type')?.text);
|
|
814
|
+
}
|
|
815
|
+
if (current.type === 'object_creation_expression') {
|
|
816
|
+
return normalizeReceiverType(current.childForFieldName('type')?.text);
|
|
817
|
+
}
|
|
818
|
+
if (current.type === 'identifier') {
|
|
819
|
+
return normalizeReceiverType(variableTypes?.get(current.text));
|
|
820
|
+
}
|
|
821
|
+
return null;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
function receiverCastIsThis(node) {
|
|
825
|
+
const current = unwrapReceiverNode(node);
|
|
826
|
+
if (current?.type !== 'cast_expression') return false;
|
|
827
|
+
const value = unwrapReceiverNode(current.childForFieldName('value') ||
|
|
828
|
+
current.namedChildren[current.namedChildCount - 1]);
|
|
829
|
+
return value?.text === 'this' || value?.text === 'base';
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
/**
|
|
833
|
+
* Decompose a receiver into a declared root plus a member path. Query-time
|
|
834
|
+
* resolution walks the indexed field/property types one hop at a time. This
|
|
835
|
+
* covers `_list!.CopyTo()`, `_resolver.LoadedSchemas.Add()`, and conditional
|
|
836
|
+
* access without guessing a final runtime type in the parser.
|
|
837
|
+
*/
|
|
838
|
+
function receiverFieldPath(node, variableTypes, enclosingClass) {
|
|
839
|
+
const current = unwrapReceiverNode(node);
|
|
840
|
+
if (!current) return null;
|
|
841
|
+
if (current.type === 'this_expression' || current.type === 'base_expression' ||
|
|
842
|
+
current.text === 'this' || current.text === 'base') {
|
|
843
|
+
return enclosingClass
|
|
844
|
+
? { root: current.text, fields: [], rootType: enclosingClass }
|
|
845
|
+
: null;
|
|
846
|
+
}
|
|
847
|
+
if (current.type === 'identifier') {
|
|
848
|
+
const declared = normalizeReceiverType(variableTypes?.get(current.text));
|
|
849
|
+
if (declared) {
|
|
850
|
+
return {
|
|
851
|
+
root: current.text,
|
|
852
|
+
fields: [],
|
|
853
|
+
rootType: declared.name,
|
|
854
|
+
...(declared.namespace && { rootNamespace: declared.namespace }),
|
|
855
|
+
};
|
|
856
|
+
}
|
|
857
|
+
return {
|
|
858
|
+
root: 'this',
|
|
859
|
+
fields: [current.text],
|
|
860
|
+
...(enclosingClass && { rootType: enclosingClass }),
|
|
861
|
+
};
|
|
862
|
+
}
|
|
863
|
+
if (current.type === 'member_access_expression') {
|
|
864
|
+
const expression = current.childForFieldName('expression') || current.namedChild(0);
|
|
865
|
+
const member = current.childForFieldName('name') ||
|
|
866
|
+
current.namedChildren[current.namedChildCount - 1];
|
|
867
|
+
const path = receiverFieldPath(expression, variableTypes, enclosingClass);
|
|
868
|
+
return path && member
|
|
869
|
+
? { ...path, fields: [...path.fields, member.text] }
|
|
870
|
+
: null;
|
|
871
|
+
}
|
|
872
|
+
if (current.type === 'conditional_access_expression') {
|
|
873
|
+
const expression = current.childForFieldName('condition') || current.namedChild(0);
|
|
874
|
+
const binding = current.namedChildren.find(child =>
|
|
875
|
+
child.type === 'member_binding_expression');
|
|
876
|
+
const member = binding?.childForFieldName('name') || binding?.namedChild(0);
|
|
877
|
+
const path = receiverFieldPath(expression, variableTypes, enclosingClass);
|
|
878
|
+
return path && member
|
|
879
|
+
? { ...path, fields: [...path.fields, member.text] }
|
|
880
|
+
: null;
|
|
881
|
+
}
|
|
882
|
+
return null;
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
function invocationIdentity(node) {
|
|
886
|
+
if (!node) return {};
|
|
887
|
+
if (node.type === 'identifier' || node.type === 'generic_name') {
|
|
888
|
+
return { name: node.type === 'generic_name' ? node.namedChild(0)?.text : node.text,
|
|
889
|
+
nameNode: node, isMethod: false };
|
|
890
|
+
}
|
|
891
|
+
if (node.type === 'member_access_expression' || node.type === 'member_binding_expression') {
|
|
892
|
+
const nameNode = node.childForFieldName('name') ||
|
|
893
|
+
node.namedChildren[node.namedChildCount - 1];
|
|
894
|
+
const expression = node.childForFieldName('expression') || node.namedChild(0);
|
|
895
|
+
return {
|
|
896
|
+
name: nameNode?.type === 'generic_name' ? nameNode.namedChild(0)?.text : nameNode?.text,
|
|
897
|
+
nameNode,
|
|
898
|
+
receiver: expression?.text,
|
|
899
|
+
isMethod: true,
|
|
900
|
+
};
|
|
901
|
+
}
|
|
902
|
+
if (node.type === 'conditional_access_expression') {
|
|
903
|
+
const expression = node.childForFieldName('condition') ||
|
|
904
|
+
node.namedChild(0);
|
|
905
|
+
const binding = node.namedChildren.find(child =>
|
|
906
|
+
child.type === 'member_binding_expression');
|
|
907
|
+
const nameNode = binding?.childForFieldName('name') ||
|
|
908
|
+
binding?.namedChild(0);
|
|
909
|
+
return {
|
|
910
|
+
name: nameNode?.type === 'generic_name'
|
|
911
|
+
? nameNode.namedChild(0)?.text : nameNode?.text,
|
|
912
|
+
nameNode,
|
|
913
|
+
receiver: expression?.text,
|
|
914
|
+
isMethod: true,
|
|
915
|
+
};
|
|
916
|
+
}
|
|
917
|
+
return {};
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
function staticMemberPath(node, variableTypes) {
|
|
921
|
+
if (!node) return null;
|
|
922
|
+
if (node.type === 'identifier') {
|
|
923
|
+
const type = variableTypes?.get(node.text);
|
|
924
|
+
if (type) return [type];
|
|
925
|
+
return /^[A-Z]/.test(node.text) ? [node.text] : null;
|
|
926
|
+
}
|
|
927
|
+
if (node.type === 'member_access_expression') {
|
|
928
|
+
const expression = node.childForFieldName('expression') ||
|
|
929
|
+
node.namedChild(0);
|
|
930
|
+
const member = node.childForFieldName('name') ||
|
|
931
|
+
node.namedChildren[node.namedChildCount - 1];
|
|
932
|
+
const path = staticMemberPath(expression, variableTypes);
|
|
933
|
+
return path && member ? [...path, member.text] : null;
|
|
934
|
+
}
|
|
935
|
+
if (node.type === 'element_access_expression') {
|
|
936
|
+
const expression = node.childForFieldName('expression') ||
|
|
937
|
+
node.namedChild(0);
|
|
938
|
+
const path = staticMemberPath(expression, variableTypes);
|
|
939
|
+
return path ? [...path, '[]'] : null;
|
|
940
|
+
}
|
|
941
|
+
if (node.type === 'conditional_access_expression') {
|
|
942
|
+
const expression = node.childForFieldName('condition') ||
|
|
943
|
+
node.namedChild(0);
|
|
944
|
+
const binding = node.namedChildren.find(child =>
|
|
945
|
+
child.type === 'member_binding_expression');
|
|
946
|
+
const member = binding?.childForFieldName('name') ||
|
|
947
|
+
binding?.namedChild(0);
|
|
948
|
+
const path = staticMemberPath(expression, variableTypes);
|
|
949
|
+
return path && member ? [...path, member.text] : null;
|
|
950
|
+
}
|
|
951
|
+
if (node.type === 'parenthesized_expression' &&
|
|
952
|
+
node.namedChildCount === 1) {
|
|
953
|
+
return staticMemberPath(node.namedChild(0), variableTypes);
|
|
954
|
+
}
|
|
955
|
+
return null;
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
function staticArgKind(node, variableTypes) {
|
|
959
|
+
if (!node) return 'expr';
|
|
960
|
+
switch (node.type) {
|
|
961
|
+
case 'string_literal':
|
|
962
|
+
case 'verbatim_string_literal':
|
|
963
|
+
case 'interpolated_string_expression':
|
|
964
|
+
return 'string';
|
|
965
|
+
case 'character_literal':
|
|
966
|
+
return 'char';
|
|
967
|
+
case 'integer_literal':
|
|
968
|
+
return /[lL]$/.test(node.text) ? 'long' : 'int';
|
|
969
|
+
case 'real_literal':
|
|
970
|
+
return /[fF]$/.test(node.text) ? 'float' : 'double';
|
|
971
|
+
case 'boolean_literal':
|
|
972
|
+
return 'boolean';
|
|
973
|
+
case 'null_literal':
|
|
974
|
+
return 'null';
|
|
975
|
+
case 'object_creation_expression': {
|
|
976
|
+
const type = node.childForFieldName('type');
|
|
977
|
+
return type ? `new:${type.text}` : 'expr';
|
|
978
|
+
}
|
|
979
|
+
case 'array_creation_expression': {
|
|
980
|
+
const type = node.childForFieldName('type');
|
|
981
|
+
return type ? `type:${type.text}` : 'expr';
|
|
982
|
+
}
|
|
983
|
+
case 'cast_expression': {
|
|
984
|
+
const type = node.childForFieldName('type');
|
|
985
|
+
return type ? `cast:${type.text}` : 'expr';
|
|
986
|
+
}
|
|
987
|
+
case 'identifier': {
|
|
988
|
+
const type = variableTypes?.get(node.text);
|
|
989
|
+
return type ? `type:${type}` : 'expr';
|
|
990
|
+
}
|
|
991
|
+
case 'member_access_expression': {
|
|
992
|
+
const path = staticMemberPath(node, variableTypes);
|
|
993
|
+
if (path?.length > 1) {
|
|
994
|
+
return `fieldpath:${path.map(encodeURIComponent).join('|')}`;
|
|
995
|
+
}
|
|
996
|
+
const owner = node.childForFieldName('expression') || node.namedChild(0);
|
|
997
|
+
const member = node.childForFieldName('name') ||
|
|
998
|
+
node.namedChildren[node.namedChildCount - 1];
|
|
999
|
+
if (!owner || !member) return 'expr';
|
|
1000
|
+
const ownerType = owner.type === 'identifier'
|
|
1001
|
+
? variableTypes?.get(owner.text)
|
|
1002
|
+
: null;
|
|
1003
|
+
return `field:${ownerType || owner.text}:${member.text}`;
|
|
1004
|
+
}
|
|
1005
|
+
case 'invocation_expression': {
|
|
1006
|
+
const identity = invocationIdentity(node.childForFieldName('function'));
|
|
1007
|
+
if (identity.name === 'GetValueOrDefault' && identity.receiver) {
|
|
1008
|
+
const rawType = variableTypes?.get(identity.receiver);
|
|
1009
|
+
if (rawType?.trim().endsWith('?')) {
|
|
1010
|
+
return `type:${rawType.trim().slice(0, -1)}`;
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
if (identity.name === 'ToString') return 'type:string';
|
|
1014
|
+
if (identity.receiver === 'Convert' &&
|
|
1015
|
+
CONVERT_RETURN_TYPES.has(identity.name)) {
|
|
1016
|
+
return `type:${CONVERT_RETURN_TYPES.get(identity.name)}`;
|
|
1017
|
+
}
|
|
1018
|
+
if (identity.receiver) {
|
|
1019
|
+
const receiverType = variableTypes?.get(identity.receiver) ||
|
|
1020
|
+
identity.receiver;
|
|
1021
|
+
const argsNode = node.childForFieldName('arguments') ||
|
|
1022
|
+
node.namedChildren.find(child =>
|
|
1023
|
+
child.type === 'argument_list');
|
|
1024
|
+
const kinds = (argsNode?.namedChildren || [])
|
|
1025
|
+
.filter(child => child.type === 'argument')
|
|
1026
|
+
.map(argument =>
|
|
1027
|
+
staticArgKind(argument.namedChild(0), variableTypes));
|
|
1028
|
+
return `callshape:${encodeURIComponent(receiverType)}|` +
|
|
1029
|
+
`${encodeURIComponent(identity.name)}|` +
|
|
1030
|
+
kinds.map(encodeURIComponent).join(',');
|
|
1031
|
+
}
|
|
1032
|
+
return 'expr';
|
|
1033
|
+
}
|
|
1034
|
+
case 'conditional_access_expression': {
|
|
1035
|
+
const path = staticMemberPath(node, variableTypes);
|
|
1036
|
+
if (path?.length > 1) {
|
|
1037
|
+
return `fieldpath:${path.map(encodeURIComponent).join('|')}`;
|
|
1038
|
+
}
|
|
1039
|
+
const owner = node.namedChild(0);
|
|
1040
|
+
const binding = node.namedChildren.find(child =>
|
|
1041
|
+
child.type === 'member_binding_expression');
|
|
1042
|
+
const member = binding?.childForFieldName('name') ||
|
|
1043
|
+
binding?.namedChild(0);
|
|
1044
|
+
if (!owner || !member) return 'expr';
|
|
1045
|
+
const ownerType = owner.type === 'identifier'
|
|
1046
|
+
? variableTypes?.get(owner.text)
|
|
1047
|
+
: null;
|
|
1048
|
+
return `field:${ownerType || owner.text}:${member.text}`;
|
|
1049
|
+
}
|
|
1050
|
+
case 'conditional_expression': {
|
|
1051
|
+
const consequence = node.childForFieldName('consequence');
|
|
1052
|
+
const alternative = node.childForFieldName('alternative');
|
|
1053
|
+
const left = staticArgKind(consequence, variableTypes);
|
|
1054
|
+
const right = staticArgKind(alternative, variableTypes);
|
|
1055
|
+
if (left === right) return left;
|
|
1056
|
+
const typed = kind => /^(?:new|cast|type):(.+)$/.exec(kind)?.[1] || null;
|
|
1057
|
+
const leftType = typed(left);
|
|
1058
|
+
const rightType = typed(right);
|
|
1059
|
+
if (left === 'null' && rightType) return `type:${rightType}`;
|
|
1060
|
+
if (right === 'null' && leftType) return `type:${leftType}`;
|
|
1061
|
+
if (leftType && rightType &&
|
|
1062
|
+
leftType.replace(/\?$/, '') === rightType.replace(/\?$/, '')) {
|
|
1063
|
+
const nullable = leftType.endsWith('?') ? leftType : rightType;
|
|
1064
|
+
return `type:${nullable}`;
|
|
1065
|
+
}
|
|
1066
|
+
return 'expr';
|
|
1067
|
+
}
|
|
1068
|
+
case 'parenthesized_expression':
|
|
1069
|
+
return node.namedChildCount === 1
|
|
1070
|
+
? staticArgKind(node.namedChild(0), variableTypes)
|
|
1071
|
+
: 'expr';
|
|
1072
|
+
case 'prefix_unary_expression':
|
|
1073
|
+
return node.namedChildCount === 1
|
|
1074
|
+
? staticArgKind(node.namedChild(0), variableTypes)
|
|
1075
|
+
: 'expr';
|
|
1076
|
+
case 'lambda_expression':
|
|
1077
|
+
case 'anonymous_method_expression':
|
|
1078
|
+
return 'lambda';
|
|
1079
|
+
default:
|
|
1080
|
+
return 'expr';
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
function callArgs(node, variableTypes) {
|
|
1085
|
+
const argsNode = node.childForFieldName('arguments') ||
|
|
1086
|
+
node.namedChildren.find(child => child.type === 'argument_list');
|
|
1087
|
+
const args = (argsNode?.namedChildren || []).filter(child => child.type === 'argument');
|
|
1088
|
+
const argKinds = args.map(argument =>
|
|
1089
|
+
staticArgKind(argument.namedChild(0), variableTypes));
|
|
1090
|
+
return {
|
|
1091
|
+
argCount: args.length,
|
|
1092
|
+
...(argKinds.some(kind => kind !== 'expr') && { argKinds }),
|
|
1093
|
+
firstArg: args[0]?.namedChild(0),
|
|
1094
|
+
args,
|
|
1095
|
+
};
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
function assignmentTargetOf(callNode) {
|
|
1099
|
+
let value = callNode;
|
|
1100
|
+
let assignedUnwrap = false;
|
|
1101
|
+
while (value.parent && (value.parent.type === 'await_expression' ||
|
|
1102
|
+
value.parent.type === 'parenthesized_expression')) {
|
|
1103
|
+
if (value.parent.type === 'await_expression') assignedUnwrap = true;
|
|
1104
|
+
value = value.parent;
|
|
1105
|
+
}
|
|
1106
|
+
const parent = value.parent;
|
|
1107
|
+
if (parent?.type === 'variable_declarator') {
|
|
1108
|
+
const name = parent.childForFieldName('name');
|
|
1109
|
+
return name ? { assignedTo: name.text, assignedUnwrap } : null;
|
|
1110
|
+
}
|
|
1111
|
+
if (parent?.type === 'assignment_expression') {
|
|
1112
|
+
const left = parent.childForFieldName('left') || parent.namedChild(0);
|
|
1113
|
+
return left?.type === 'identifier'
|
|
1114
|
+
? { assignedTo: left.text, assignedUnwrap }
|
|
1115
|
+
: null;
|
|
1116
|
+
}
|
|
1117
|
+
return null;
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
function findCallsInCode(code, parser) {
|
|
1121
|
+
const tree = parseTree(parser, code);
|
|
1122
|
+
const variableTypesByScope = buildVariableTypes(tree, parser);
|
|
1123
|
+
const calls = [];
|
|
1124
|
+
traverseTree(tree.rootNode, node => {
|
|
1125
|
+
const variableTypes = variableTypesByScope.get(variableScopeKey(node)) ||
|
|
1126
|
+
variableTypesByScope.get('global');
|
|
1127
|
+
if (isControlFlowLocalArtifact(node)) {
|
|
1128
|
+
const paramsNode = node.childForFieldName('parameters');
|
|
1129
|
+
const raw = paramsNode?.text;
|
|
1130
|
+
if (raw?.startsWith('(') && raw.endsWith(')')) {
|
|
1131
|
+
const expression = raw.slice(1, -1);
|
|
1132
|
+
const synthetic = `class __UcnRecovery { bool __Call() => ${expression}; }`;
|
|
1133
|
+
const recovered = findCallsInCode(synthetic, parser);
|
|
1134
|
+
for (const call of recovered) {
|
|
1135
|
+
calls.push({
|
|
1136
|
+
...call,
|
|
1137
|
+
line: node.startPosition.row + call.line,
|
|
1138
|
+
enclosingFunction: enclosingFunctionOf(node),
|
|
1139
|
+
});
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
// Keep traversing the artifact's real block; its body still
|
|
1143
|
+
// contains valid invocation AST nodes.
|
|
1144
|
+
return true;
|
|
1145
|
+
}
|
|
1146
|
+
if (node.type === 'invocation_expression') {
|
|
1147
|
+
const identity = invocationIdentity(node.childForFieldName('function'));
|
|
1148
|
+
if (!identity.name) return true;
|
|
1149
|
+
const args = callArgs(node, variableTypes);
|
|
1150
|
+
const first = extractStringArg(args.firstArg);
|
|
1151
|
+
const receiverRoot = identity.receiver?.split('.')[0];
|
|
1152
|
+
const functionNode = node.childForFieldName('function');
|
|
1153
|
+
const receiverNode = functionNode?.type === 'member_access_expression'
|
|
1154
|
+
? functionNode.childForFieldName('expression') || functionNode.namedChild(0)
|
|
1155
|
+
: functionNode?.type === 'conditional_access_expression'
|
|
1156
|
+
? functionNode.childForFieldName('condition') || functionNode.namedChild(0)
|
|
1157
|
+
: null;
|
|
1158
|
+
const receiverTypeInfo = receiverTypeFromNode(receiverNode, variableTypes) ||
|
|
1159
|
+
normalizeReceiverType(receiverRoot && variableTypes.get(receiverRoot));
|
|
1160
|
+
const receiverType = receiverTypeInfo?.name;
|
|
1161
|
+
const unwrappedReceiverNode = unwrapReceiverNode(receiverNode);
|
|
1162
|
+
const receiverCastThis = receiverCastIsThis(receiverNode);
|
|
1163
|
+
const receiverIsTypeQualified = !!(identity.isMethod &&
|
|
1164
|
+
unwrappedReceiverNode?.type === 'identifier' &&
|
|
1165
|
+
/^[A-Z]/.test(identity.receiver || '') &&
|
|
1166
|
+
!variableTypes.has(identity.receiver) &&
|
|
1167
|
+
!enclosingTypeDeclaresMember(node, identity.receiver));
|
|
1168
|
+
const currentNamespace = namespaceOf(node, tree);
|
|
1169
|
+
let receiverCall = null;
|
|
1170
|
+
let receiverCallIsMethod = false;
|
|
1171
|
+
let receiverCallLine = null;
|
|
1172
|
+
let receiverCallReceiver = null;
|
|
1173
|
+
if (receiverNode?.type === 'invocation_expression') {
|
|
1174
|
+
const producer = invocationIdentity(receiverNode.childForFieldName('function'));
|
|
1175
|
+
if (producer.name) {
|
|
1176
|
+
receiverCall = producer.name;
|
|
1177
|
+
receiverCallIsMethod = producer.isMethod;
|
|
1178
|
+
receiverCallLine = producer.nameNode?.startPosition.row + 1 ||
|
|
1179
|
+
receiverNode.startPosition.row + 1;
|
|
1180
|
+
receiverCallReceiver = producer.receiver;
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
const assignment = assignmentTargetOf(node);
|
|
1184
|
+
let fieldRoot, fieldName, fieldNames, fieldRootType, fieldRootNamespace;
|
|
1185
|
+
if (identity.isMethod && !receiverType && identity.receiver &&
|
|
1186
|
+
!receiverIsTypeQualified) {
|
|
1187
|
+
const fieldPath = receiverFieldPath(
|
|
1188
|
+
receiverNode, variableTypes, enclosingClassName(node));
|
|
1189
|
+
if (fieldPath?.fields.length) {
|
|
1190
|
+
fieldRoot = fieldPath.root;
|
|
1191
|
+
fieldNames = fieldPath.fields;
|
|
1192
|
+
fieldName = fieldNames[fieldNames.length - 1];
|
|
1193
|
+
fieldRootType = fieldPath.rootType;
|
|
1194
|
+
fieldRootNamespace = fieldPath.rootNamespace || currentNamespace;
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
calls.push({
|
|
1198
|
+
name: identity.name,
|
|
1199
|
+
line: identity.nameNode?.startPosition.row + 1 || node.startPosition.row + 1,
|
|
1200
|
+
isMethod: identity.isMethod,
|
|
1201
|
+
...(identity.receiver && { receiver: identity.receiver }),
|
|
1202
|
+
...(receiverIsTypeQualified && { receiverIsTypeQualified: true }),
|
|
1203
|
+
...(receiverType && { receiverType }),
|
|
1204
|
+
...(receiverCastThis && { receiverCastThis: true }),
|
|
1205
|
+
...(receiverType && receiverTypeInfo.namespace && {
|
|
1206
|
+
receiverTypeNamespace: receiverTypeInfo.namespace,
|
|
1207
|
+
}),
|
|
1208
|
+
...(fieldName && {
|
|
1209
|
+
receiverRoot: fieldRoot,
|
|
1210
|
+
receiverField: fieldName,
|
|
1211
|
+
receiverFields: fieldNames,
|
|
1212
|
+
...(fieldRootType && { receiverRootType: fieldRootType }),
|
|
1213
|
+
...(fieldRootNamespace && { receiverRootNamespace: fieldRootNamespace }),
|
|
1214
|
+
}),
|
|
1215
|
+
...(receiverCall && { receiverCall }),
|
|
1216
|
+
...(receiverCallIsMethod && { receiverCallIsMethod: true }),
|
|
1217
|
+
...(receiverCallLine && { receiverCallLine }),
|
|
1218
|
+
...(receiverCallReceiver && { receiverCallReceiver }),
|
|
1219
|
+
...(assignment?.assignedTo && { assignedTo: assignment.assignedTo }),
|
|
1220
|
+
...(assignment?.assignedUnwrap && { assignedUnwrap: true }),
|
|
1221
|
+
argCount: args.argCount,
|
|
1222
|
+
...(args.argKinds && { argKinds: args.argKinds }),
|
|
1223
|
+
enclosingFunction: enclosingFunctionOf(node),
|
|
1224
|
+
...(first && {
|
|
1225
|
+
firstStringArg: first.value,
|
|
1226
|
+
firstStringArgInterp: first.interp,
|
|
1227
|
+
}),
|
|
1228
|
+
});
|
|
1229
|
+
// Minimal API registrations and other framework callbacks pass
|
|
1230
|
+
// method groups as arguments (`app.MapGet("/x", Handle)`). Keep
|
|
1231
|
+
// those references in the same call cache so entrypoint detection
|
|
1232
|
+
// and ordinary caller analysis share one AST-derived record.
|
|
1233
|
+
for (const argument of args.args.slice(1)) {
|
|
1234
|
+
const value = argument.namedChild(0);
|
|
1235
|
+
if (!value) continue;
|
|
1236
|
+
let callbackName = null;
|
|
1237
|
+
let callbackReceiver = null;
|
|
1238
|
+
if (value.type === 'identifier') {
|
|
1239
|
+
callbackName = value.text;
|
|
1240
|
+
} else if (value.type === 'member_access_expression') {
|
|
1241
|
+
const callback = invocationIdentity(value);
|
|
1242
|
+
callbackName = callback.name;
|
|
1243
|
+
callbackReceiver = callback.receiver;
|
|
1244
|
+
}
|
|
1245
|
+
if (!callbackName) continue;
|
|
1246
|
+
calls.push({
|
|
1247
|
+
name: callbackName,
|
|
1248
|
+
line: value.startPosition.row + 1,
|
|
1249
|
+
isMethod: !!callbackReceiver,
|
|
1250
|
+
...(callbackReceiver && { receiver: callbackReceiver }),
|
|
1251
|
+
isFunctionReference: true,
|
|
1252
|
+
isPotentialCallback: true,
|
|
1253
|
+
enclosingFunction: enclosingFunctionOf(node),
|
|
1254
|
+
});
|
|
1255
|
+
}
|
|
1256
|
+
return true;
|
|
1257
|
+
}
|
|
1258
|
+
if (node.type === 'object_creation_expression' ||
|
|
1259
|
+
node.type === 'implicit_object_creation_expression') {
|
|
1260
|
+
const typeNode = node.childForFieldName('type');
|
|
1261
|
+
if (!typeNode) return true;
|
|
1262
|
+
const args = callArgs(node, variableTypes);
|
|
1263
|
+
const raw = typeNode.text.replace(/<.*>$/, '');
|
|
1264
|
+
const name = raw.split('.').pop();
|
|
1265
|
+
calls.push({
|
|
1266
|
+
name,
|
|
1267
|
+
line: typeNode.startPosition.row + 1,
|
|
1268
|
+
isMethod: false,
|
|
1269
|
+
isConstructor: true,
|
|
1270
|
+
argCount: args.argCount,
|
|
1271
|
+
...(args.argKinds && { argKinds: args.argKinds }),
|
|
1272
|
+
enclosingFunction: enclosingFunctionOf(node),
|
|
1273
|
+
});
|
|
1274
|
+
}
|
|
1275
|
+
return true;
|
|
1276
|
+
});
|
|
1277
|
+
return calls;
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
function findImportsInCode(code, parser) {
|
|
1281
|
+
const tree = parseTree(parser, code);
|
|
1282
|
+
const imports = [];
|
|
1283
|
+
traverseTreeCached(tree.rootNode, node => {
|
|
1284
|
+
if (node.type !== 'using_directive') return true;
|
|
1285
|
+
const nameNode = node.childForFieldName('name');
|
|
1286
|
+
const named = node.namedChildren || [];
|
|
1287
|
+
const moduleNode = named[named.length - 1];
|
|
1288
|
+
if (!moduleNode) return false;
|
|
1289
|
+
imports.push({
|
|
1290
|
+
module: moduleNode.text,
|
|
1291
|
+
names: nameNode ? [nameNode.text] : ['*'],
|
|
1292
|
+
type: 'using',
|
|
1293
|
+
line: node.startPosition.row + 1,
|
|
1294
|
+
...(node.text.trimStart().startsWith('global using ') && { global: true }),
|
|
1295
|
+
});
|
|
1296
|
+
return false;
|
|
1297
|
+
});
|
|
1298
|
+
return imports;
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
function findUsagesInCode(code, name, parser, existingTree) {
|
|
1302
|
+
const tree = existingTree || parseTree(parser, code);
|
|
1303
|
+
const usages = [];
|
|
1304
|
+
visitNameNodes(tree, code, name, node => {
|
|
1305
|
+
if (!IDENTIFIER_NODES.has(node.type) || node.text !== name) return;
|
|
1306
|
+
let usageType = 'reference';
|
|
1307
|
+
const parent = node.parent;
|
|
1308
|
+
if (parent) {
|
|
1309
|
+
if ((parent.type === 'method_declaration' ||
|
|
1310
|
+
parent.type === 'constructor_declaration' ||
|
|
1311
|
+
TYPE_DECLARATIONS.has(parent.type) ||
|
|
1312
|
+
parent.type === 'parameter' ||
|
|
1313
|
+
parent.type === 'variable_declarator') &&
|
|
1314
|
+
(sameNode(parent.childForFieldName('name'), node))) {
|
|
1315
|
+
usageType = 'definition';
|
|
1316
|
+
} else if (parent.type === 'invocation_expression' ||
|
|
1317
|
+
parent.parent?.type === 'invocation_expression') {
|
|
1318
|
+
usageType = 'call';
|
|
1319
|
+
} else if (parent.type === 'using_directive') {
|
|
1320
|
+
usageType = 'import';
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
usages.push({
|
|
1324
|
+
line: node.startPosition.row + 1,
|
|
1325
|
+
column: node.startPosition.column,
|
|
1326
|
+
usageType,
|
|
1327
|
+
});
|
|
1328
|
+
});
|
|
1329
|
+
return usages;
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
function getEntryPointKind(symbol) {
|
|
1333
|
+
const decorators = new Set(symbol.decorators || []);
|
|
1334
|
+
if (symbol.name === 'Main' && symbol.modifiers?.includes('static')) return 'main';
|
|
1335
|
+
if (decorators.has('Fact') || decorators.has('Theory') ||
|
|
1336
|
+
decorators.has('Test') || decorators.has('TestMethod')) return 'test';
|
|
1337
|
+
if ([...decorators].some(name => /^(Http(Get|Post|Put|Delete|Patch)|Route|ApiController)$/.test(name))) {
|
|
1338
|
+
return 'framework';
|
|
1339
|
+
}
|
|
1340
|
+
return null;
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
function isEntryPoint(symbol) {
|
|
1344
|
+
return getEntryPointKind(symbol) !== null;
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
// Stable BCL receiver identities used only after AST/declaration typing has
|
|
1348
|
+
// proved the static receiver type. Interfaces are included intentionally:
|
|
1349
|
+
// core still checks whether the pinned project type implements the interface
|
|
1350
|
+
// before excluding, so virtual dispatch remains visible when it is possible.
|
|
1351
|
+
const CSHARP_PLATFORM_RECEIVER_TYPES = new Set([
|
|
1352
|
+
'List', 'Dictionary', 'HashSet', 'Queue', 'Stack',
|
|
1353
|
+
'IEnumerable', 'ICollection', 'IList', 'IDictionary',
|
|
1354
|
+
'IReadOnlyCollection', 'IReadOnlyList', 'IReadOnlyDictionary',
|
|
1355
|
+
'BinaryReader', 'BinaryWriter', 'TextReader', 'TextWriter',
|
|
1356
|
+
'StringReader', 'StringWriter',
|
|
1357
|
+
'Type', 'MemberInfo', 'FieldInfo', 'PropertyInfo', 'MethodInfo',
|
|
1358
|
+
]);
|
|
1359
|
+
|
|
1360
|
+
function isPlatformConcreteCall(receiverType, _methodName) {
|
|
1361
|
+
const normalized = normalizeReceiverType(receiverType);
|
|
1362
|
+
return !!normalized && CSHARP_PLATFORM_RECEIVER_TYPES.has(normalized.name);
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
function parse(code, parser) {
|
|
1366
|
+
const tree = parseTree(parser, code);
|
|
1367
|
+
return {
|
|
1368
|
+
language: 'csharp',
|
|
1369
|
+
totalLines: code.length === 0 ? 0 : code.split('\n').length,
|
|
1370
|
+
functions: findFunctions(code, parser),
|
|
1371
|
+
classes: findClasses(code, parser),
|
|
1372
|
+
stateObjects: findStateObjects(code, parser),
|
|
1373
|
+
imports: findImportsInCode(code, parser),
|
|
1374
|
+
exports: findExportsInCodeShallow(code, parser),
|
|
1375
|
+
...(tree.rootNode.hasError && { parseRecovery: true }),
|
|
1376
|
+
};
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
function findExportsInCodeShallow(code, parser) {
|
|
1380
|
+
const classes = findClasses(code, parser);
|
|
1381
|
+
const exports = [];
|
|
1382
|
+
for (const cls of classes) {
|
|
1383
|
+
if (cls.modifiers.includes('public')) {
|
|
1384
|
+
exports.push({ name: cls.name, type: 'export', line: cls.startLine });
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
return exports;
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
module.exports = {
|
|
1391
|
+
findFunctions,
|
|
1392
|
+
findClasses,
|
|
1393
|
+
findStateObjects,
|
|
1394
|
+
findCallsInCode,
|
|
1395
|
+
findImportsInCode,
|
|
1396
|
+
findExportsInCode: findExportsInCodeShallow,
|
|
1397
|
+
findUsagesInCode,
|
|
1398
|
+
isPlatformConcreteCall,
|
|
1399
|
+
isEntryPoint,
|
|
1400
|
+
getEntryPointKind,
|
|
1401
|
+
parse,
|
|
1402
|
+
};
|