codedeep-mcp 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +177 -0
  3. package/dist/config.js +223 -0
  4. package/dist/git/analyzer.js +177 -0
  5. package/dist/git/git-service.js +568 -0
  6. package/dist/git/head-watcher.js +113 -0
  7. package/dist/git/runner.js +204 -0
  8. package/dist/index.js +138 -0
  9. package/dist/indexer/code-index.js +1801 -0
  10. package/dist/indexer/complexity.js +633 -0
  11. package/dist/indexer/extractor.js +354 -0
  12. package/dist/indexer/languages/cpp.js +934 -0
  13. package/dist/indexer/languages/csharp.js +854 -0
  14. package/dist/indexer/languages/dart.js +777 -0
  15. package/dist/indexer/languages/go.js +665 -0
  16. package/dist/indexer/languages/java.js +507 -0
  17. package/dist/indexer/languages/kotlin.js +709 -0
  18. package/dist/indexer/languages/objc.js +397 -0
  19. package/dist/indexer/languages/php.js +771 -0
  20. package/dist/indexer/languages/python.js +455 -0
  21. package/dist/indexer/languages/ruby.js +697 -0
  22. package/dist/indexer/languages/rust.js +754 -0
  23. package/dist/indexer/languages/swift.js +691 -0
  24. package/dist/indexer/languages/typescript.js +485 -0
  25. package/dist/indexer/parser.js +175 -0
  26. package/dist/indexer/pipeline.js +342 -0
  27. package/dist/indexer/scanner.js +279 -0
  28. package/dist/indexer/watcher.js +353 -0
  29. package/dist/logger.js +16 -0
  30. package/dist/server.js +170 -0
  31. package/dist/tools/common.js +207 -0
  32. package/dist/tools/find-references.js +224 -0
  33. package/dist/tools/find-symbol.js +94 -0
  34. package/dist/tools/get-context.js +370 -0
  35. package/dist/tools/impact.js +218 -0
  36. package/dist/tools/overview.js +482 -0
  37. package/dist/tools/search-structure.js +303 -0
  38. package/dist/types.js +61 -0
  39. package/grammars/tree-sitter-c.wasm +0 -0
  40. package/grammars/tree-sitter-c_sharp.wasm +0 -0
  41. package/grammars/tree-sitter-cpp.wasm +0 -0
  42. package/grammars/tree-sitter-dart.wasm +0 -0
  43. package/grammars/tree-sitter-go.wasm +0 -0
  44. package/grammars/tree-sitter-java.wasm +0 -0
  45. package/grammars/tree-sitter-javascript.wasm +0 -0
  46. package/grammars/tree-sitter-kotlin.wasm +0 -0
  47. package/grammars/tree-sitter-objc.wasm +0 -0
  48. package/grammars/tree-sitter-php.wasm +0 -0
  49. package/grammars/tree-sitter-python.wasm +0 -0
  50. package/grammars/tree-sitter-ruby.wasm +0 -0
  51. package/grammars/tree-sitter-rust.wasm +0 -0
  52. package/grammars/tree-sitter-swift.wasm +0 -0
  53. package/grammars/tree-sitter-tsx.wasm +0 -0
  54. package/grammars/tree-sitter-typescript.wasm +0 -0
  55. package/package.json +67 -0
@@ -0,0 +1,854 @@
1
+ import { IMPORT_NAMESPACE, RECEIVER_OPAQUE } from '../../types.js';
2
+ import { SIGNATURE_DISPLAY_CAP, commentDocLine, isTrailingComment, normalizeSignature, resolveCalls, symbolId, } from '../extractor.js';
3
+ import { cFamilyBooleanOperatorKind, computeComplexity, isCFamilyBooleanOperator } from '../complexity.js';
4
+ // Type-declaration node types → the SymbolKind they map to. Doubles as the
5
+ // "is this a type declaration" test during body iteration. struct/record →
6
+ // class (constructable, member-bearing); delegate → type (a named
7
+ // function-type); enum → enum (members NOT extracted, the universal rule).
8
+ const TYPE_KIND = {
9
+ class_declaration: 'class',
10
+ struct_declaration: 'class',
11
+ record_declaration: 'class',
12
+ interface_declaration: 'interface',
13
+ enum_declaration: 'enum',
14
+ delegate_declaration: 'type',
15
+ };
16
+ // Nested `local_function_statement`s create their own scope — their calls must
17
+ // NOT attribute to an enclosing body. (Inert without a decorator selector, but
18
+ // documents intent; CSHARP_SKIP_TYPES is derived from it so the prune is real.)
19
+ const CSHARP_FUNCTION_BODY_SKIP_TYPES = new Set(['local_function_statement']);
20
+ // walkCalls skip set: nested funcs (own scope, from FUNCTION_BODY_SKIP) PLUS
21
+ // `attribute_list`. C# attribute arguments must be constant expressions, but
22
+ // `[Foo(Bar())]` still parses a REAL invocation_expression inside the leading
23
+ // `attribute_list` child, which the body/module-root walks would otherwise emit
24
+ // as a spurious `calls` ref. Skipping `attribute_list` drops those (the Dart
25
+ // `annotation` rule). Lambdas (`lambda_expression`) are DESCENDED (the
26
+ // Go/Kotlin/Dart rule). Type declarations are deliberately NOT skipped: every
27
+ // member initializer owns a per-binding PendingBody, so the module-root re-walk
28
+ // only re-touches already-seen call nodes (deduped by the engine's seen-set).
29
+ const CSHARP_SKIP_TYPES = new Set([
30
+ ...CSHARP_FUNCTION_BODY_SKIP_TYPES,
31
+ 'attribute_list',
32
+ ]);
33
+ // `new Foo()` resolves to a 'class'-kind symbol. C# construction `new Foo()`
34
+ // exposes a plain `identifier` type (NOT Java's distinct `type_identifier`), so
35
+ // it is recognized by CALL-NODE type (constructorSelectorTypes below) rather
36
+ // than callee type, and routed through constructorKinds. Bare invocation calls
37
+ // `Foo()` are then Java-precise: they resolve ONLY against the enclosing class's
38
+ // methods (bareCallsBindToEnclosingClass), never against a same-named class — so
39
+ // bareCallableKinds is EMPTY (a bare call colliding with a class name can't
40
+ // produce a wrong edge, and a `new Foo()` colliding with an enclosing method
41
+ // can't either).
42
+ const CSHARP_BARE_CALLABLE_KINDS = new Set();
43
+ const CSHARP_CONSTRUCTOR_KINDS = new Set(['class']);
44
+ const CSHARP_CONSTRUCTOR_SELECTORS = new Set(['object_creation_expression']);
45
+ // C# names that parse as bare calls but never resolve to a local symbol and
46
+ // would otherwise flood the name-keyed reference store. `nameof(x)` is an
47
+ // invocation_expression with an `identifier` callee. `typeof`/`sizeof`/`default`
48
+ // are their own expression nodes (not calls), so they need no entry. Suppressed
49
+ // ONLY when unresolved (a file-local function shadowing the name keeps its refs).
50
+ // Start tiny; extend after dogfood measurement.
51
+ const CSHARP_IGNORED_BARE_CALLEES = new Set(['nameof']);
52
+ // ── call selectors ─────────────────────────────────────────────────────────
53
+ // Callee of an `invocation_expression` = its `function:` field: an `identifier`
54
+ // for bare calls, a `generic_name` for bare generic calls (unwrapped to the
55
+ // inner identifier), or a member/conditional access for member calls.
56
+ function csharpCallCallee(node) {
57
+ const fn = node.childForFieldName('function');
58
+ if (!fn)
59
+ return null;
60
+ if (fn.type === 'generic_name')
61
+ return childOfType(fn, 'identifier');
62
+ return fn;
63
+ }
64
+ // Callee of an `object_creation_expression` (`new Foo()`) = the simple-name
65
+ // identifier of its `type:` (identifier→itself, generic_name→inner id). A
66
+ // `qualified_name` type (`new A.B()`, cross-namespace) is dropped — its
67
+ // final segment routinely collides with a same-named in-repo type and same-file
68
+ // qualified construction is rare.
69
+ function csharpObjectCreationCallee(node) {
70
+ const t = node.childForFieldName('type');
71
+ if (!t)
72
+ return null;
73
+ if (t.type === 'identifier')
74
+ return t;
75
+ if (t.type === 'generic_name')
76
+ return childOfType(t, 'identifier');
77
+ return null;
78
+ }
79
+ const CSHARP_SELECTORS = [
80
+ { nodeType: 'invocation_expression', getCallee: csharpCallCallee },
81
+ { nodeType: 'object_creation_expression', getCallee: csharpObjectCreationCallee },
82
+ ];
83
+ // The called member name from a member_access/binding `name:` (identifier or a
84
+ // generic_name like `obj.Method<int>()` — unwrapped to its identifier).
85
+ function propertyName(nameNode) {
86
+ if (!nameNode)
87
+ return null;
88
+ if (nameNode.type === 'identifier')
89
+ return nameNode.text;
90
+ if (nameNode.type === 'generic_name')
91
+ return childOfType(nameNode, 'identifier')?.text ?? null;
92
+ return null;
93
+ }
94
+ // Reduces a `member_access_expression` (`obj.M()`, `this.M()`, `C.Static()`) or
95
+ // a `conditional_access_expression` (`a?.M()`) callee to {receiver, property}.
96
+ // `this`/`base` are ANONYMOUS tokens (not *_expression nodes): `this` →
97
+ // self-call; `base` → null (super-like, skipped, the Java rule). A chained
98
+ // `a.b.c()` has a member_access receiver → RECEIVER_OPAQUE (findable by name,
99
+ // never resolved). A computed/non-name property emits nothing.
100
+ function csharpMemberCallInfo(callee) {
101
+ if (callee.type === 'member_access_expression') {
102
+ const expr = callee.childForFieldName('expression');
103
+ const prop = propertyName(callee.childForFieldName('name'));
104
+ if (!expr || !prop)
105
+ return null;
106
+ if (expr.type === 'this')
107
+ return { receiver: 'this', property: prop, isSelf: true };
108
+ if (expr.type === 'base')
109
+ return null;
110
+ if (expr.type === 'identifier')
111
+ return { receiver: expr.text, property: prop, isSelf: false };
112
+ return { receiver: RECEIVER_OPAQUE, property: prop, isSelf: false };
113
+ }
114
+ if (callee.type === 'conditional_access_expression') {
115
+ const cond = callee.childForFieldName('condition');
116
+ const binding = childOfType(callee, 'member_binding_expression');
117
+ const prop = binding ? propertyName(binding.childForFieldName('name')) : null;
118
+ if (!cond || !prop)
119
+ return null;
120
+ if (cond.type === 'this')
121
+ return { receiver: 'this', property: prop, isSelf: true };
122
+ if (cond.type === 'base')
123
+ return null;
124
+ if (cond.type === 'identifier')
125
+ return { receiver: cond.text, property: prop, isSelf: false };
126
+ return { receiver: RECEIVER_OPAQUE, property: prop, isSelf: false };
127
+ }
128
+ return null;
129
+ }
130
+ // Dominant C# LINQ/collection/string method names (>=4 chars) suppressed when a
131
+ // member call to them is unresolved — capturing chained `.Where().Select()`
132
+ // calls otherwise floods the name-keyed store. Domain method names are
133
+ // deliberately absent. <=3-char names are gated downstream by
134
+ // SHORT_NAME_THRESHOLD.
135
+ const CSHARP_IGNORED_MEMBER_CALLEES = new Set([
136
+ 'Where', 'Select', 'SelectMany', 'OrderBy', 'OrderByDescending', 'ThenBy',
137
+ 'GroupBy', 'Aggregate', 'First', 'FirstOrDefault', 'Last', 'LastOrDefault',
138
+ 'Single', 'SingleOrDefault', 'Count', 'Average', 'Distinct',
139
+ 'Take', 'Skip', 'Contains', 'ContainsKey', 'ContainsValue',
140
+ 'ToList', 'ToArray', 'ToDictionary', 'ToHashSet', 'AsEnumerable',
141
+ 'ForEach', 'Reverse', 'Concat', 'Union', 'Intersect', 'Except',
142
+ 'ToString', 'Equals', 'GetHashCode', 'GetType', 'Substring', 'IndexOf',
143
+ 'Replace', 'Trim', 'Split', 'StartsWith', 'EndsWith', 'ToLower', 'ToUpper',
144
+ 'Append', 'Remove', 'Clear', 'TryGetValue', 'ConfigureAwait', 'GetEnumerator',
145
+ ]);
146
+ // ── complexity (cyclomatic + cognitive) — pinned EXACT to SonarC# ───────────
147
+ // (SonarAnalyzer.CSharp's CSharpCyclomaticComplexityMetric / CSharpCognitive-
148
+ // ComplexityMetric, run as a per-method oracle; see the project docs' "C# Complexity
149
+ // Rules"). Both metrics MEASURED against the real analyzer.
150
+ // Cyclomatic decision nodes (each +1). Booleans/`??`, `??=`, and the constant-
151
+ // `case` discriminator route through csharpCyclomaticExtra (shared node types).
152
+ // MEASURED: a `switch_expression_arm` counts for EVERY arm (incl `_`/pattern arms),
153
+ // `conditional_access_expression` (`?.` AND `?[`) ALWAYS counts (no Dart property-
154
+ // vs-call split), pattern combinators `and`/`or` count per-operator, `not` does not.
155
+ const CSHARP_DECISION_NODE_TYPES = new Set([
156
+ 'if_statement',
157
+ 'for_statement', 'foreach_statement', 'while_statement', 'do_statement',
158
+ 'conditional_expression', // ternary
159
+ 'switch_expression_arm', // every arm (incl `_`/pattern arms)
160
+ 'and_pattern', 'or_pattern', // pattern combinators (NOT negated_pattern)
161
+ 'conditional_access_expression', // `?.` / `?[` — always +1
162
+ ]);
163
+ // A switch-STATEMENT `switch_section` that SonarC# cyclomatic counts (Roslyn's
164
+ // CaseSwitchLabel): a plain constant `case <const>:` OR a bare discard `case _:`
165
+ // (both measured +1). The discriminant is a direct `constant_pattern` or `discard`
166
+ // child with NO `when_clause` (a `when` guard promotes the label to a
167
+ // CasePatternSwitchLabel → NOT counted). EXCLUDED: a `constant_pattern` that wraps a
168
+ // `tuple_expression` (`case (1,2):` is a positional pattern, NOT a compile-time
169
+ // constant — a tuple literal can never be const, so SonarC# does not count it);
170
+ // pattern cases (`declaration_pattern`/`relational_pattern`/`or_pattern`/…) and
171
+ // `default`. (`case 1 or 2:` is an or_pattern section → not a constant case here,
172
+ // but its `or` counts via CSHARP_DECISION_NODE_TYPES.) Switch EXPRESSIONS differ —
173
+ // every `switch_expression_arm` counts (in the node set above).
174
+ function csharpIsConstantCase(node) {
175
+ let hasConstant = false;
176
+ for (const c of node.namedChildren) {
177
+ if (c.type === 'when_clause')
178
+ return false; // a guard → CasePatternSwitchLabel
179
+ if (c.type === 'discard')
180
+ hasConstant = true; // bare `case _:`
181
+ // a `constant_pattern` wrapping a tuple is a positional pattern, not a constant.
182
+ if (c.type === 'constant_pattern' && c.namedChild(0)?.type !== 'tuple_expression')
183
+ hasConstant = true;
184
+ }
185
+ return hasConstant;
186
+ }
187
+ // Extra cyclomatic +1s beyond the node set: `&&`/`||`/`??` (one binary_expression,
188
+ // read via the shared C-family token helper — SonarC# counts all three), the
189
+ // null-coalescing assignment `??=` (shares assignment_expression with `=`/`+=`),
190
+ // and a constant switch `case`.
191
+ function csharpCyclomaticExtra(node) {
192
+ if (isCFamilyBooleanOperator(node))
193
+ return true; // && || ??
194
+ if (node.type === 'assignment_expression')
195
+ return node.childForFieldName('operator')?.type === '??='; // `.type` (= the token), like the other readers
196
+ if (node.type === 'switch_section')
197
+ return csharpIsConstantCase(node);
198
+ return false;
199
+ }
200
+ // COGNITIVE boolean-run kind: `&&`/`||` (binary_expression, via the shared C-family
201
+ // reader — but NOT `??`, which is cyclomatic-only, the expected cyc/cog divergence)
202
+ // AND the pattern combinators `and`/`or` (and_pattern/or_pattern, their own nodes),
203
+ // so both fold into the SAME TREE-SCOPED run (booleanByTreeParent). `not`
204
+ // (negated_pattern) → null (uncounted).
205
+ function csharpCognitiveBooleanKind(node) {
206
+ const op = cFamilyBooleanOperatorKind(node); // '&&' / '||' / '??' (binary_expression) or null
207
+ if (op !== null)
208
+ return op === '??' ? null : op; // `??` is cog-free
209
+ if (node.type === 'and_pattern')
210
+ return 'and';
211
+ if (node.type === 'or_pattern')
212
+ return 'or';
213
+ return null;
214
+ }
215
+ // Direct-recursion self-call (SonarC# counts it +1 cognitive, like gocognit) —
216
+ // but ONLY when the bare-IDENTIFIER callee name AND the ARGUMENT COUNT both match
217
+ // the enclosing method. SonarC#'s CSharpCognitiveComplexityMetric checks
218
+ // IdentifierName + arg-count == the method's parameter count; a `Foo(2 args)` call
219
+ // inside `Foo(3 params)` is OVERLOAD FORWARDING (ubiquitous in C#), NOT recursion —
220
+ // a name-only check over-counts it heavily (measured: ~150 false +1s on Polly). So
221
+ // match the call's `argument` count against the enclosing declaration's `parameter`
222
+ // count. `this.Foo()` (member_access) and `Foo<T>()` (generic_name) are not bare
223
+ // identifiers → not self-calls. The declaration is the body's owner: for a method
224
+ // the body (block / arrow_expression_clause) is a child of method_declaration; a
225
+ // constructor's body IS the declaration (it carries `parameters` itself), so check
226
+ // the body first, then its parent.
227
+ function csharpCountChildren(node, type) {
228
+ if (!node)
229
+ return 0;
230
+ let n = 0;
231
+ for (const c of node.namedChildren)
232
+ if (c.type === type)
233
+ n++;
234
+ return n;
235
+ }
236
+ function csharpIsSelfCall(callNode, body, sym) {
237
+ const fn = callNode.childForFieldName('function');
238
+ if (fn?.type !== 'identifier' || fn.text !== sym.name)
239
+ return false;
240
+ // Recursion only applies to a REAL method body — a `block` or an arrow
241
+ // `arrow_expression_clause`. A constructor's synthesized body is the whole
242
+ // `constructor_declaration`, or (primary ctor) a `parameter_list` / `base_list`;
243
+ // those have no resolvable owning-method parameter list (the type decl's params
244
+ // are a POSITIONAL child, not a `parameters` field → paramCount would mis-resolve
245
+ // to 0 and match any 0-arg call), and a constructor can't bare-self-recurse anyway.
246
+ if (body.type !== 'block' && body.type !== 'arrow_expression_clause')
247
+ return false;
248
+ const decl = body.childForFieldName('parameters') ? body : body.parent;
249
+ const params = decl?.childForFieldName('parameters') ?? null;
250
+ // A `params T[] x` parameter is NOT wrapped in a `parameter` node (a grammar
251
+ // quirk) — it flattens to a trailing `array_type` + `identifier` directly under
252
+ // the parameter_list, so add the bare `identifier` (only a params param leaks one).
253
+ const paramCount = csharpCountChildren(params, 'parameter') + csharpCountChildren(params, 'identifier');
254
+ const argCount = csharpCountChildren(callNode.childForFieldName('arguments'), 'argument');
255
+ return paramCount === argCount;
256
+ }
257
+ // Complexity body boundary: skip ONLY `attribute_list`. codedeep-mcp measures each member's
258
+ // BODY (its PendingBody), not the declaration's attribute_lists (which sit OUTSIDE
259
+ // the body for top-level members anyway); SonarC# walks the whole declaration and
260
+ // DOES count control flow in attribute arguments — but that is a degenerate case
261
+ // (valid C# attribute args are compile-time constants, so a ternary/`&&`/switch
262
+ // there is near-zero in real code: 0 cases across Newtonsoft.Json+Polly), a SAFE
263
+ // documented under-count from the body boundary. The skip keeps a body-INTERNAL
264
+ // attribute (on a local fn / lambda) consistent with that boundary. (A SEPARATE set
265
+ // from CSHARP_SKIP_TYPES, the resolveCalls boundary, which DOES prune local functions
266
+ // from call attribution.) local_function_statement and lambda_expression are
267
+ // DELIBERATELY ABSENT (descended) so they ROLL INTO the enclosing member with a
268
+ // nesting bump — SonarC#'s per-member model for a NON-static local fn / lambda. A
269
+ // STATIC local function is scored separately by SonarC# but rolled in here (no codedeep-mcp
270
+ // symbol exists for it) — the documented per-symbol-model divergence.
271
+ const CSHARP_COMPLEXITY_SKIP_TYPES = new Set(['attribute_list']);
272
+ // Cognitive config (SonarC# S3776 — sonar-java-shaped: field-based `if`, contained
273
+ // `catch`). MEASURED EXACT. Two non-default shapes the oracle forced: booleans are
274
+ // TREE-SCOPED (booleanByTreeParent, like the SonarQube Dart model — NOT sonar-java's source-order),
275
+ // and `goto`/`goto case` is a SURCHARGE (+1+nesting) → surchargeTypes.
276
+ const CSHARP_COGNITIVE_OPTIONS = {
277
+ ifType: 'if_statement',
278
+ conditionField: 'condition',
279
+ consequenceField: 'consequence',
280
+ alternativeField: 'alternative', // Java-style: else/else-if held directly (no else_clause wrapper)
281
+ loopTypes: new Set(['for_statement', 'foreach_statement', 'while_statement', 'do_statement']),
282
+ switchTypes: new Set(['switch_statement', 'switch_expression']), // whole-switch +1 (stmt + expr)
283
+ ternaryType: 'conditional_expression',
284
+ catchType: 'catch_clause', // contains its `body:` block → the generic catch branch
285
+ surchargeTypes: new Set(['goto_statement']), // `goto`/`goto case`: +1+nesting
286
+ nestOnlyTypes: new Set(['lambda_expression', 'local_function_statement']), // roll in, nest +0
287
+ labeledJumpTypes: new Set(), // C# break/continue are unlabeled; goto is a surcharge (above)
288
+ hasLabel: () => false,
289
+ booleanOperatorKind: csharpCognitiveBooleanKind,
290
+ // TREE-SCOPED boolean runs (like the SonarQube Dart model, NOT sonar-java's source-order) — a
291
+ // `&&`/`||` (or pattern `and`/`or`) counts iff its operator kind differs from its
292
+ // nearest logical ancestor (skipping parens). MEASURED EXACT: `a && b && (c||d) &&
293
+ // (e||f)` = cog 3 (one &&-spine + two ||s), NOT source-order's 4 — the slice's
294
+ // surprise. The `parenthesizedType` SET below skips BOTH a parenthesized EXPRESSION
295
+ // (`(c||d)`) and a parenthesized PATTERN (`(int and >0)`) so a same-kind combinator
296
+ // grouped by parens stays ONE run (`is (A and B) and C` = cog 2). See the project docs' "C# Complexity Rules".
297
+ booleanByTreeParent: true,
298
+ // A SET (not a single string) so the tree-scoped ancestor walk treats BOTH a
299
+ // parenthesized EXPRESSION (`(c||d)`) and a parenthesized PATTERN (`(int and >0)`)
300
+ // as transparent — a same-kind combinator grouped by parens stays one run.
301
+ parenthesizedType: new Set(['parenthesized_expression', 'parenthesized_pattern']),
302
+ recursion: {
303
+ callType: 'invocation_expression',
304
+ isSelfCall: csharpIsSelfCall,
305
+ eligibleKinds: new Set(['method']),
306
+ oncePerSymbol: true, // SonarC# adds +1 once per recursive method, not per call-site
307
+ },
308
+ // NO: elseClauseType / conditionFromNamedChildren / collectionIfType / tryType /
309
+ // initField / nestElseBody / loopBodyField / flatIncrement / positional-if knobs —
310
+ // C# is field-based and sonar-java-shaped.
311
+ };
312
+ export function extractCSharp(tree, content, fileInfo) {
313
+ const ctx = {
314
+ content,
315
+ fileInfo,
316
+ occurrences: new Map(),
317
+ symbols: [],
318
+ imports: [],
319
+ bodies: [],
320
+ typeStats: new Map(),
321
+ };
322
+ extractMembers(ctx, tree.rootNode.namedChildren, '');
323
+ // Same-named types collide on the simple-name FQN, so resolving through them
324
+ // first-wins would bind to the WRONG type — EXCEPT an all-`partial`,
325
+ // single-group set is the same logical type and SHOULD merge (methodsByClass
326
+ // first-wins is then correct). Flag a name when it has >1 decl and they are
327
+ // NOT all partial, OR they span more than one group (different namespace /
328
+ // enclosing type / generic arity → distinct types sharing the simple name).
329
+ const ambiguousTypeNames = new Set();
330
+ for (const [name, st] of ctx.typeStats) {
331
+ if (st.total > 1 && (!st.allPartial || st.groups.size > 1))
332
+ ambiguousTypeNames.add(name);
333
+ }
334
+ const references = resolveCalls(ctx.bodies, tree.rootNode, ctx.symbols, fileInfo, CSHARP_SELECTORS, CSHARP_SKIP_TYPES, CSHARP_FUNCTION_BODY_SKIP_TYPES, csharpMemberCallInfo, {
335
+ // Bare `Foo()` is an implicit-`this` method call → resolves ONLY against
336
+ // the enclosing class (bareCallableKinds empty, Java-precise). `new Foo()`
337
+ // (object_creation_expression — a constructorSelector) routes through
338
+ // constructorKinds to the 'class'-kind symbol, never the enclosing class,
339
+ // so a `new Foo()` can't mis-bind to a same-named enclosing method.
340
+ bareCallsBindToEnclosingClass: true,
341
+ bareCallableKinds: CSHARP_BARE_CALLABLE_KINDS,
342
+ constructorKinds: CSHARP_CONSTRUCTOR_KINDS,
343
+ constructorSelectorTypes: CSHARP_CONSTRUCTOR_SELECTORS,
344
+ ambiguousClassNames: ambiguousTypeNames,
345
+ ignoredBareCallees: CSHARP_IGNORED_BARE_CALLEES,
346
+ ignoredMemberCallees: CSHARP_IGNORED_MEMBER_CALLEES,
347
+ });
348
+ // Cyclomatic + cognitive complexity (SonarC#-pinned), computed while the tree
349
+ // is alive (the Dart/Kotlin call-site pattern). Uses its OWN skip set (local fns
350
+ // + lambdas roll into the enclosing member — SonarC#'s per-member model), not
351
+ // CSHARP_SKIP_TYPES.
352
+ computeComplexity(ctx.bodies, ctx.symbols, {
353
+ decisionNodeTypes: CSHARP_DECISION_NODE_TYPES,
354
+ extraDecisionPredicate: csharpCyclomaticExtra,
355
+ skipTypes: CSHARP_COMPLEXITY_SKIP_TYPES,
356
+ cognitive: CSHARP_COGNITIVE_OPTIONS,
357
+ });
358
+ return { symbols: ctx.symbols, references, imports: ctx.imports };
359
+ }
360
+ // Processes a list of compilation_unit / namespace-body children, threading the
361
+ // current namespace qualifier. A `file_scoped_namespace_declaration` updates the
362
+ // qualifier for all FOLLOWING siblings; a block `namespace_declaration` recurses
363
+ // into its own body with the joined qualifier. Namespaces are NOT symbols — C#
364
+ // FQNs are file-path based, so a per-file module symbol would be pure noise; the
365
+ // namespace path only disambiguates hashed ids (via the qualifier).
366
+ function extractMembers(ctx, children, nsQualifier) {
367
+ let ns = nsQualifier;
368
+ for (const child of children) {
369
+ switch (child.type) {
370
+ case 'using_directive':
371
+ extractImport(ctx, child);
372
+ break;
373
+ case 'file_scoped_namespace_declaration': {
374
+ const name = child.childForFieldName('name')?.text;
375
+ if (name)
376
+ ns = joinQualifier(nsQualifier, name);
377
+ break;
378
+ }
379
+ case 'namespace_declaration': {
380
+ const name = child.childForFieldName('name')?.text ?? '';
381
+ const body = child.childForFieldName('body');
382
+ if (body)
383
+ extractMembers(ctx, body.namedChildren, joinQualifier(ns, name));
384
+ break;
385
+ }
386
+ // global_statement (top-level statements / Program.cs), extern_alias,
387
+ // comments — no symbols. Calls inside top-level statements still attribute
388
+ // to module scope via the module-root walk.
389
+ default:
390
+ if (TYPE_KIND[child.type] !== undefined) {
391
+ extractType(ctx, child, ns, true, true, false);
392
+ }
393
+ break;
394
+ }
395
+ }
396
+ }
397
+ // A type declaration (class/struct/record/interface/enum/delegate). Recurses
398
+ // through the body for nested types (simple-name FQN; the namespace+enclosing
399
+ // chain folds into the hashed qualifier only — the Java/Kotlin rule) and members.
400
+ function extractType(ctx, decl, parentQualifier, containerExported, isTopLevelType, containerIsInterface) {
401
+ const name = decl.childForFieldName('name')?.text;
402
+ if (!name)
403
+ return;
404
+ const kind = TYPE_KIND[decl.type];
405
+ const mods = findModifierTexts(decl);
406
+ // A type nested directly in an interface is implicitly public (like interface
407
+ // members), so containerIsInterface gates exportedness the same way.
408
+ const exported = csharpExported(mods, containerExported, containerIsInterface, isTopLevelType);
409
+ // Record partial-aware type stats (all type-kinds share the simple-name FQN
410
+ // namespace). The "group" key (qualifying context + generic arity) lets only
411
+ // genuine same-namespace/same-arity partials merge: `Foo` vs `Foo<T>` (arity)
412
+ // and `N1.Foo` vs `N2.Foo` (namespace) get distinct groups → flagged ambiguous.
413
+ const isPartial = mods.has('partial');
414
+ const group = `${parentQualifier}\0${genericArity(decl)}`;
415
+ const st = ctx.typeStats.get(name) ?? { total: 0, allPartial: true, groups: new Set() };
416
+ st.total += 1;
417
+ st.allPartial = st.allPartial && isPartial;
418
+ st.groups.add(group);
419
+ ctx.typeStats.set(name, st);
420
+ ctx.symbols.push(makeCSharpSymbol(ctx, decl, csharpSig(ctx, decl), kind, name, topFqn(ctx, name), exported, csharpDoc(decl), parentQualifier));
421
+ // delegate: a single-line named function-type, no body. enum: members are
422
+ // enum constants (NOT extracted, the universal rule).
423
+ if (decl.type === 'delegate_declaration' || decl.type === 'enum_declaration')
424
+ return;
425
+ const memberQualifier = joinQualifier(parentQualifier, name);
426
+ const isInterface = decl.type === 'interface_declaration';
427
+ // Primary constructor (`class D(int x)` / `record R(int X)` / struct). Records
428
+ // turn positional params into public init-only PROPERTIES; class/struct
429
+ // primary-ctor params are NOT members (implicit captures), but both
430
+ // synthesize a 'constructor' owning the param defaults + base-initializer
431
+ // args (`: Base(Make(x))`).
432
+ if (decl.type === 'record_declaration' ||
433
+ decl.type === 'class_declaration' ||
434
+ decl.type === 'struct_declaration') {
435
+ extractPrimaryConstructor(ctx, decl, name, memberQualifier, exported, decl.type === 'record_declaration');
436
+ }
437
+ const body = decl.childForFieldName('body'); // declaration_list
438
+ if (!body)
439
+ return;
440
+ for (const member of body.namedChildren) {
441
+ if (TYPE_KIND[member.type] !== undefined) {
442
+ extractType(ctx, member, memberQualifier, exported, false, isInterface);
443
+ }
444
+ else {
445
+ extractMember(ctx, member, name, memberQualifier, exported, isInterface);
446
+ }
447
+ }
448
+ }
449
+ // A class/struct/record/interface body member. Routes to callable, property,
450
+ // field, or event handling. Nested type decls are handled by the caller.
451
+ function extractMember(ctx, member, className, qualifier, containerExported, inInterface) {
452
+ const doc = csharpDoc(member);
453
+ const mods = findModifierTexts(member);
454
+ const exported = csharpExported(mods, containerExported, inInterface, false);
455
+ switch (member.type) {
456
+ case 'method_declaration': {
457
+ const name = member.childForFieldName('name')?.text;
458
+ if (!name)
459
+ return;
460
+ // An extension method (`static T M(this string s)`) is methods-apart: its
461
+ // FQN keys on the receiver-param type so `s.M()` resolves (Kotlin/Swift/
462
+ // Dart rule), BUT it is a STATIC method with no implicit `this`, so a bare
463
+ // call inside its body binds to the CONTAINER class's static methods — the
464
+ // PendingBody className stays `className`, not the receiver type.
465
+ const fqnClass = extensionReceiverName(member) ?? className;
466
+ extractCallable(ctx, member, name, fqnClass, className, qualifier, exported, doc, member.childForFieldName('body'));
467
+ return;
468
+ }
469
+ case 'constructor_declaration': {
470
+ // Named 'constructor' (TS convention); the WHOLE decl is the body so the
471
+ // `: base(...)`/`: this(...)` initializer args and param defaults attribute
472
+ // here. `new C()` refs bind to the CLASS symbol via constructorKinds.
473
+ extractCallable(ctx, member, 'constructor', className, className, qualifier, exported, doc, member);
474
+ return;
475
+ }
476
+ case 'operator_declaration': {
477
+ const op = member.childForFieldName('operator')?.text;
478
+ if (!op)
479
+ return;
480
+ extractCallable(ctx, member, op, className, className, qualifier, exported, doc, member.childForFieldName('body'));
481
+ return;
482
+ }
483
+ case 'conversion_operator_declaration': {
484
+ // `public static implicit operator int(C c) => ...` — no `name:` field;
485
+ // name it `operator <target type>` (the `type:` field). The whole decl is
486
+ // the body so the conversion expression's calls attribute here.
487
+ const target = simpleTypeName(member.childForFieldName('type'));
488
+ extractCallable(ctx, member, `operator ${target ?? '?'}`, className, className, qualifier, exported, doc, member);
489
+ return;
490
+ }
491
+ case 'destructor_declaration': {
492
+ // `~C() { ... }` finalizer → method 'finalize' (its `name:` field repeats
493
+ // the class name, the constructor problem). Owns its body's calls.
494
+ extractCallable(ctx, member, 'finalize', className, className, qualifier, exported, doc, member.childForFieldName('body'));
495
+ return;
496
+ }
497
+ case 'indexer_declaration': {
498
+ // `this[int i]` → a 'method' named 'this[]'; the WHOLE decl is the body so
499
+ // the get/set accessor (or arrow value) calls attribute here.
500
+ extractCallable(ctx, member, 'this[]', className, className, qualifier, exported, doc, member);
501
+ return;
502
+ }
503
+ case 'property_declaration': {
504
+ const name = member.childForFieldName('name')?.text;
505
+ if (!name)
506
+ return;
507
+ const sym = makeCSharpSymbol(ctx, member, csharpSig(ctx, member), 'variable', name, memberFqn(ctx, className, name), exported, doc, qualifier);
508
+ ctx.symbols.push(sym);
509
+ // The whole property owns its accessor bodies, arrow `value:`, and `= init`.
510
+ ctx.bodies.push({ symbolId: sym.id, body: member, className });
511
+ return;
512
+ }
513
+ case 'event_declaration': {
514
+ // Explicit event with add/remove accessors → a 'variable'; the whole decl
515
+ // owns the accessor calls.
516
+ const name = member.childForFieldName('name')?.text;
517
+ if (!name)
518
+ return;
519
+ const sym = makeCSharpSymbol(ctx, member, csharpSig(ctx, member), 'variable', name, memberFqn(ctx, className, name), exported, doc, qualifier);
520
+ ctx.symbols.push(sym);
521
+ ctx.bodies.push({ symbolId: sym.id, body: member, className });
522
+ return;
523
+ }
524
+ case 'field_declaration':
525
+ case 'event_field_declaration':
526
+ extractFieldLike(ctx, member, className, qualifier, exported, doc);
527
+ return;
528
+ // destructor/conversion-operator/static-constructor-without-name and stray
529
+ // tokens — no symbol (static ctors arrive as constructor_declaration above).
530
+ default:
531
+ return;
532
+ }
533
+ }
534
+ // A method / constructor / operator / indexer → a 'method' symbol. `fqnClass`
535
+ // keys the FQN/methods-apart lookup (the receiver type for an extension method,
536
+ // else the enclosing class); `bodyClass` is the PendingBody className for
537
+ // bare/self-call resolution inside the body (always the enclosing class — the
538
+ // two differ only for extension methods). `bodyNode` is the `body:` field for
539
+ // methods/operators, or the WHOLE declaration for constructors/indexers (so
540
+ // initializer/accessor calls attribute here); abstract/interface members pass
541
+ // null and own no PendingBody.
542
+ function extractCallable(ctx, decl, name, fqnClass, bodyClass, qualifier, exported, doc, bodyNode) {
543
+ const sym = makeCSharpSymbol(ctx, decl, csharpSig(ctx, decl), 'method', name, memberFqn(ctx, fqnClass, name), exported, doc, qualifier);
544
+ ctx.symbols.push(sym);
545
+ if (bodyNode)
546
+ ctx.bodies.push({ symbolId: sym.id, body: bodyNode, className: bodyClass });
547
+ }
548
+ // field_declaration / event_field_declaration → one 'variable' per
549
+ // variable_declarator (`int a = 1, b;` carries several). Each declarator with an
550
+ // initializer owns a PendingBody on itself (the Java/Dart per-binding rule).
551
+ function extractFieldLike(ctx, member, className, qualifier, exported, doc) {
552
+ const varDecl = childOfType(member, 'variable_declaration');
553
+ if (!varDecl)
554
+ return;
555
+ // The `value:` initializer lives on each variable_declarator, not on the
556
+ // field node, so csharpSig can't cut it — build "<modifiers> <type>
557
+ // <name>[, <name>]" explicitly to keep initializers out of the signature
558
+ // (matching the property path; the id still hashes this normalized form).
559
+ const signature = fieldSignature(ctx, member, varDecl);
560
+ for (const declarator of varDecl.namedChildren) {
561
+ if (declarator.type !== 'variable_declarator')
562
+ continue;
563
+ const name = declarator.childForFieldName('name')?.text;
564
+ if (!name)
565
+ continue;
566
+ const sym = makeCSharpSymbol(ctx, member, signature, 'variable', name, memberFqn(ctx, className, name), exported, doc, qualifier);
567
+ ctx.symbols.push(sym);
568
+ // The declarator carries any `= initializer` — own its calls per-binding.
569
+ ctx.bodies.push({ symbolId: sym.id, body: declarator, className });
570
+ }
571
+ }
572
+ // The "<modifiers> <type> <name>[, <name>]" signature of a field /
573
+ // event_field_declaration, with all `= initializer` text dropped.
574
+ function fieldSignature(ctx, member, varDecl) {
575
+ const type = varDecl.childForFieldName('type');
576
+ const head = type
577
+ ? ctx.content.slice(signatureStart(member), type.endIndex)
578
+ : ctx.content.slice(signatureStart(member), varDecl.startIndex);
579
+ const names = varDecl.namedChildren
580
+ .filter((c) => c.type === 'variable_declarator')
581
+ .map((d) => d.childForFieldName('name')?.text)
582
+ .filter((n) => Boolean(n));
583
+ return normalizeSignature(`${head} ${names.join(', ')}`);
584
+ }
585
+ // A primary constructor on a record / class / struct (`record R(int X)`,
586
+ // `class D(int x) : Base(Make(x))`). For RECORDS the positional params are
587
+ // public init-only PROPERTIES (`emitProperties`); for class/struct they are
588
+ // implicit captures, not members. Either way a SINGLE 'constructor' is
589
+ // synthesized when there are params, owning the parameter_list (default-arg
590
+ // calls) AND the base_list (`: Base(Make(x))` initializer-arg calls). No phantom
591
+ // for a parameterless type.
592
+ function extractPrimaryConstructor(ctx, decl, className, qualifier, containerExported, emitProperties) {
593
+ const plist = childOfType(decl, 'parameter_list');
594
+ if (!plist)
595
+ return;
596
+ const params = plist.namedChildren.filter((c) => c.type === 'parameter');
597
+ let hasParam = false;
598
+ for (const param of params) {
599
+ const name = param.childForFieldName('name')?.text;
600
+ if (!name)
601
+ continue;
602
+ hasParam = true;
603
+ if (!emitProperties)
604
+ continue;
605
+ ctx.symbols.push(makeCSharpSymbol(ctx, param, normalizeSignature(ctx.content.slice(param.startIndex, param.endIndex)), 'variable', name, memberFqn(ctx, className, name), containerExported, null, qualifier));
606
+ }
607
+ if (!hasParam)
608
+ return;
609
+ const sym = makeCSharpSymbol(ctx, plist, normalizeSignature(`constructor${ctx.content.slice(plist.startIndex, plist.endIndex)}`), 'method', 'constructor', memberFqn(ctx, className, 'constructor'), containerExported, null, qualifier);
610
+ ctx.symbols.push(sym);
611
+ ctx.bodies.push({ symbolId: sym.id, body: plist, className });
612
+ // `: Base(Make(x))` base-initializer args run at construction — own their calls.
613
+ const baseList = childOfType(decl, 'base_list');
614
+ if (baseList)
615
+ ctx.bodies.push({ symbolId: sym.id, body: baseList, className });
616
+ }
617
+ // `using X.Y;` / `using static X.Y;` / `global using X.Y;` → namespace import
618
+ // (IMPORT_NAMESPACE — these widen scope over a whole namespace). `using A = X.Y;`
619
+ // → an alias import binding A to the last segment. Low cross-file value (C#
620
+ // namespaces don't map to indexed files, no directory carve-out — the Rust/
621
+ // Kotlin framing).
622
+ function extractImport(ctx, node) {
623
+ const aliasNode = node.childForFieldName('name'); // present only for `A = X.Y`
624
+ let pathNode = childOfType(node, 'qualified_name');
625
+ if (!pathNode) {
626
+ // The path is a bare `identifier` (`using System;`) or, for an unqualified
627
+ // generic alias (`using F = List<int>;`), a `generic_name` — both distinct
628
+ // from the alias `name:` identifier.
629
+ pathNode =
630
+ node.namedChildren.find((c) => (c.type === 'identifier' || c.type === 'generic_name') && c.id !== aliasNode?.id) ?? null;
631
+ }
632
+ if (!pathNode)
633
+ return;
634
+ const line = node.startPosition.row + 1;
635
+ if (aliasNode) {
636
+ // `using F = N.C<int>;` — the bound name is the base type (`C`), not the
637
+ // generic instantiation text; simpleTypeName strips type arguments.
638
+ const last = simpleTypeName(pathNode);
639
+ const source = pathNode.type === 'qualified_name' ? pathNode.childForFieldName('qualifier')?.text ?? '' : '';
640
+ if (!last)
641
+ return;
642
+ const imported = { name: last, alias: aliasNode.text };
643
+ ctx.imports.push({ file: ctx.fileInfo.path, sourceModule: source, importedNames: [imported], line });
644
+ return;
645
+ }
646
+ ctx.imports.push({
647
+ file: ctx.fileInfo.path,
648
+ sourceModule: pathNode.text,
649
+ importedNames: [{ name: IMPORT_NAMESPACE, kind: 'namespace' }],
650
+ line,
651
+ });
652
+ }
653
+ // ── helpers ──────────────────────────────────────────────────────────────
654
+ // Each `modifier` is a separate named child whose `.text` is the keyword
655
+ // ('public'/'static'/'partial'/'this'/…). Absent entirely on modifier-less
656
+ // declarations.
657
+ function findModifierTexts(decl) {
658
+ const out = new Set();
659
+ for (const c of decl.namedChildren) {
660
+ if (c.type === 'modifier')
661
+ out.add(c.text);
662
+ }
663
+ return out;
664
+ }
665
+ // C#'s member default is PRIVATE (absent modifier ≠ public, unlike Kotlin/
666
+ // Swift), so the "no private keyword" heuristic fails. Rule:
667
+ // • a `private` modifier (incl. `private protected`) is never exported;
668
+ // • interface members default public (the Java rule);
669
+ // • a top-level type defaults to internal → exported (internal-as-exported
670
+ // preserves cross-file member-call recall; no dir→package carve-out);
671
+ // • everything else (member / nested type) needs an explicit
672
+ // public/protected/internal modifier (default private → not exported).
673
+ // Members AND-in their container's exportedness via the caller.
674
+ function csharpExported(mods, containerExported, inInterface, isTopLevelType) {
675
+ if (!containerExported)
676
+ return false;
677
+ if (mods.has('private'))
678
+ return false;
679
+ if (inInterface)
680
+ return true;
681
+ if (isTopLevelType)
682
+ return true;
683
+ return mods.has('public') || mods.has('protected') || mods.has('internal');
684
+ }
685
+ // An extension method's receiver type: its FIRST parameter carries a `this`
686
+ // modifier; the receiver's simple type name keys the method apart. Returns null
687
+ // for a non-extension method.
688
+ function extensionReceiverName(method) {
689
+ const plist = method.childForFieldName('parameters');
690
+ if (!plist)
691
+ return null;
692
+ const first = plist.namedChildren.find((c) => c.type === 'parameter');
693
+ if (!first)
694
+ return null;
695
+ const isExtension = first.namedChildren.some((c) => c.type === 'modifier' && c.text === 'this');
696
+ if (!isExtension)
697
+ return null;
698
+ return simpleTypeName(first.childForFieldName('type'));
699
+ }
700
+ // Simple name of a type node (predefined `int`/`string`, `identifier`,
701
+ // `generic_name`→base, `qualified_name`→last segment, `nullable_type`→inner).
702
+ // Returns null for non-nominal types (array/tuple/pointer/function) — the caller
703
+ // then keeps an extension method on its container class. The qualified_name case
704
+ // RECURSES into its `name:` segment, which is itself a `generic_name` for a
705
+ // fully-qualified generic (`System...List<int>` → `List`, not `List<int>`).
706
+ function simpleTypeName(t) {
707
+ if (!t)
708
+ return null;
709
+ if (t.type === 'nullable_type')
710
+ return simpleTypeName(t.namedChildren[0] ?? null);
711
+ if (t.type === 'identifier' || t.type === 'predefined_type')
712
+ return t.text;
713
+ if (t.type === 'generic_name')
714
+ return childOfType(t, 'identifier')?.text ?? null;
715
+ if (t.type === 'qualified_name')
716
+ return simpleTypeName(t.childForFieldName('name'));
717
+ return null;
718
+ }
719
+ // Generic arity = the number of type parameters in a type declaration's
720
+ // `type_parameter_list` (0 when non-generic). `Foo` and `Foo<T>` share the
721
+ // simple name `Foo` but are DISTINCT types, so arity disambiguates them.
722
+ function genericArity(decl) {
723
+ const tpl = childOfType(decl, 'type_parameter_list');
724
+ if (!tpl)
725
+ return 0;
726
+ return tpl.namedChildren.filter((c) => c.type === 'type_parameter').length;
727
+ }
728
+ // Signature = source from the first non-attribute token (attributes excluded —
729
+ // the Java annotation rationale: `[Attr(...)]` blocks blow the 120-char cap and
730
+ // collide overload ids) to the body / accessors / property-value / ctor
731
+ // initializer, with a trailing `;` stripped (bodiless interface/abstract
732
+ // members, delegates, bodiless records). Feeds symbolId hashing; the stored copy
733
+ // is capped by makeCSharpSymbol.
734
+ function csharpSig(ctx, node) {
735
+ const start = signatureStart(node);
736
+ let end = node.endIndex;
737
+ for (const field of ['body', 'accessors', 'value']) {
738
+ const c = node.childForFieldName(field);
739
+ if (c)
740
+ end = Math.min(end, c.startIndex);
741
+ }
742
+ const init = node.namedChildren.find((c) => c.type === 'constructor_initializer');
743
+ if (init)
744
+ end = Math.min(end, init.startIndex);
745
+ let sig = normalizeSignature(ctx.content.slice(start, end));
746
+ if (sig.endsWith(';'))
747
+ sig = sig.slice(0, -1).trimEnd();
748
+ return sig;
749
+ }
750
+ // Past the leading `attribute_list` children (they sit before the modifiers/
751
+ // keyword); the keyword itself is anonymous, so we can't address it by named
752
+ // child — instead start right after the last leading attribute_list.
753
+ function signatureStart(decl) {
754
+ let start = decl.startIndex;
755
+ for (const c of decl.namedChildren) {
756
+ if (c.type === 'attribute_list')
757
+ start = c.endIndex;
758
+ else
759
+ break;
760
+ }
761
+ return start;
762
+ }
763
+ // Doc = the immediately-preceding `///` XML-doc block (consecutive `///` are
764
+ // SEPARATE `comment` nodes → contiguous-block walk, first content line) or a
765
+ // single `/** */`. Plain `//` and `/* */` are NOT docs. Attributes live INSIDE
766
+ // the declaration (a leading child), so they don't break adjacency — no
767
+ // Rust-style sibling skip.
768
+ function csharpDoc(decl) {
769
+ const nearest = decl.previousNamedSibling;
770
+ if (!nearest || nearest.type !== 'comment')
771
+ return null;
772
+ if (nearest.endPosition.row !== decl.startPosition.row - 1)
773
+ return null; // adjacency
774
+ if (isTrailingComment(nearest))
775
+ return null;
776
+ const text = nearest.text;
777
+ if (text.startsWith('/**'))
778
+ return commentDocLine(text);
779
+ if (!text.startsWith('///'))
780
+ return null;
781
+ const chain = [nearest];
782
+ for (;;) {
783
+ const bottom = chain[chain.length - 1];
784
+ const prev = bottom.previousNamedSibling;
785
+ if (!prev ||
786
+ prev.type !== 'comment' ||
787
+ !prev.text.startsWith('///') ||
788
+ prev.endPosition.row !== bottom.startPosition.row - 1 ||
789
+ isTrailingComment(prev)) {
790
+ break;
791
+ }
792
+ chain.push(prev);
793
+ }
794
+ chain.reverse();
795
+ for (const comment of chain) {
796
+ const line = csharpDocLine(comment.text);
797
+ if (line)
798
+ return line;
799
+ }
800
+ return null;
801
+ }
802
+ // First content line of a `///`/`/**` comment, with a leading XML doc tag
803
+ // (`<summary>` etc.) and its closing tag stripped for a cleaner one-liner. A
804
+ // tag-only line (`/// <summary>`) strips to empty → null, so the block walk
805
+ // skips it and continues to the first line carrying real content.
806
+ function csharpDocLine(text) {
807
+ const line = commentDocLine(text);
808
+ if (!line)
809
+ return null;
810
+ const stripped = line
811
+ .replace(/^<\s*[A-Za-z]+[^>]*>\s*/, '')
812
+ .replace(/\s*<\/\s*[A-Za-z]+\s*>\s*$/, '')
813
+ .trim();
814
+ return stripped || null;
815
+ }
816
+ function topFqn(ctx, name) {
817
+ return `${ctx.fileInfo.path}:${name}`;
818
+ }
819
+ function memberFqn(ctx, className, name) {
820
+ return className ? `${ctx.fileInfo.path}:${className}.${name}` : `${ctx.fileInfo.path}:${name}`;
821
+ }
822
+ // First direct named child of one of the given types (or null).
823
+ function childOfType(node, ...types) {
824
+ return node.namedChildren.find((c) => types.includes(c.type)) ?? null;
825
+ }
826
+ // Namespace path / enclosing-type chain only disambiguate hashed ids — they
827
+ // never reach FQN parsing — so any unique join works.
828
+ function joinQualifier(a, b) {
829
+ if (!a)
830
+ return b;
831
+ if (!b)
832
+ return a;
833
+ return `${a}.${b}`;
834
+ }
835
+ function makeCSharpSymbol(ctx, node, signature, kind, name, fqn, exported, doc, qualifier = '') {
836
+ const key = `${name}\0${kind}\0${signature}\0${qualifier}`;
837
+ const n = (ctx.occurrences.get(key) ?? 0) + 1;
838
+ ctx.occurrences.set(key, n);
839
+ const effectiveQualifier = n === 1 ? qualifier : `${qualifier}#${n}`;
840
+ return {
841
+ // The id hashes the FULL signature; only the stored copy is capped.
842
+ id: symbolId(ctx.fileInfo.path, name, kind, signature, effectiveQualifier),
843
+ name,
844
+ fqn,
845
+ kind,
846
+ file: ctx.fileInfo.path,
847
+ startLine: node.startPosition.row + 1,
848
+ endLine: node.endPosition.row + 1,
849
+ signature: signature.slice(0, SIGNATURE_DISPLAY_CAP),
850
+ doc,
851
+ exported,
852
+ language: ctx.fileInfo.language,
853
+ };
854
+ }