ucn 4.2.3 → 5.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/.claude/skills/ucn/SKILL.md +89 -77
  2. package/.claude/skills/ucn/references/commands.md +62 -68
  3. package/.claude/skills/ucn/references/trust-contract.md +31 -6
  4. package/README.md +438 -305
  5. package/assets/demo.svg +31 -0
  6. package/cli/index.js +430 -1385
  7. package/core/account.js +144 -34
  8. package/core/analysis.js +182 -72
  9. package/core/ast-analysis.js +279 -0
  10. package/core/bridge.js +205 -24
  11. package/core/brief.js +27 -58
  12. package/core/build-worker.js +21 -140
  13. package/core/cache.js +513 -11
  14. package/core/callers.js +4920 -456
  15. package/core/check.js +13 -4
  16. package/core/command-contracts.js +402 -0
  17. package/core/compilation-database.js +276 -0
  18. package/core/confidence.js +4 -1
  19. package/core/deadcode.js +397 -19
  20. package/core/discovery.js +359 -46
  21. package/core/entrypoints.js +195 -41
  22. package/core/execute.js +887 -81
  23. package/core/graph-build.js +162 -7
  24. package/core/graph.js +53 -77
  25. package/core/imports.js +65 -6
  26. package/core/index-ir.js +138 -0
  27. package/core/ir.js +195 -0
  28. package/core/output/analysis.js +212 -22
  29. package/core/output/brief.js +23 -0
  30. package/core/output/check.js +4 -0
  31. package/core/output/doctor.js +37 -6
  32. package/core/output/endpoints.js +5 -2
  33. package/core/output/extraction.js +24 -12
  34. package/core/output/find.js +141 -36
  35. package/core/output/graph.js +11 -5
  36. package/core/output/public.js +462 -0
  37. package/core/output/refactoring.js +42 -10
  38. package/core/output/reporting.js +97 -20
  39. package/core/output/search.js +24 -16
  40. package/core/output/shared.js +22 -1
  41. package/core/output/tracing.js +30 -15
  42. package/core/output-budget.js +295 -0
  43. package/core/output.js +1 -0
  44. package/core/parallel-build.js +44 -11
  45. package/core/parser.js +3 -3
  46. package/core/project.js +384 -187
  47. package/core/public-command.js +47 -0
  48. package/core/registry.js +247 -117
  49. package/core/reporting.js +312 -290
  50. package/core/search.js +317 -185
  51. package/core/semantic-provider.js +110 -0
  52. package/core/stacktrace.js +25 -0
  53. package/core/tracing.js +101 -51
  54. package/core/trust-matrix.js +19 -40
  55. package/core/verify.js +534 -37
  56. package/languages/adapter.js +218 -0
  57. package/languages/c-family.js +2791 -0
  58. package/languages/c.js +3 -0
  59. package/languages/cpp.js +3 -0
  60. package/languages/csharp.js +1402 -0
  61. package/languages/go.js +60 -21
  62. package/languages/html.js +2 -2
  63. package/languages/index.js +85 -7
  64. package/languages/java.js +396 -13
  65. package/languages/javascript.js +199 -19
  66. package/languages/python.js +964 -22
  67. package/languages/rust.js +1317 -152
  68. package/languages/utils.js +40 -3
  69. package/mcp/server.js +254 -636
  70. package/package.json +39 -22
  71. package/eslint.config.js +0 -43
  72. package/jsconfig.json +0 -10
@@ -0,0 +1,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
+ };