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.
- package/LICENSE +21 -0
- package/README.md +177 -0
- package/dist/config.js +223 -0
- package/dist/git/analyzer.js +177 -0
- package/dist/git/git-service.js +568 -0
- package/dist/git/head-watcher.js +113 -0
- package/dist/git/runner.js +204 -0
- package/dist/index.js +138 -0
- package/dist/indexer/code-index.js +1801 -0
- package/dist/indexer/complexity.js +633 -0
- package/dist/indexer/extractor.js +354 -0
- package/dist/indexer/languages/cpp.js +934 -0
- package/dist/indexer/languages/csharp.js +854 -0
- package/dist/indexer/languages/dart.js +777 -0
- package/dist/indexer/languages/go.js +665 -0
- package/dist/indexer/languages/java.js +507 -0
- package/dist/indexer/languages/kotlin.js +709 -0
- package/dist/indexer/languages/objc.js +397 -0
- package/dist/indexer/languages/php.js +771 -0
- package/dist/indexer/languages/python.js +455 -0
- package/dist/indexer/languages/ruby.js +697 -0
- package/dist/indexer/languages/rust.js +754 -0
- package/dist/indexer/languages/swift.js +691 -0
- package/dist/indexer/languages/typescript.js +485 -0
- package/dist/indexer/parser.js +175 -0
- package/dist/indexer/pipeline.js +342 -0
- package/dist/indexer/scanner.js +279 -0
- package/dist/indexer/watcher.js +353 -0
- package/dist/logger.js +16 -0
- package/dist/server.js +170 -0
- package/dist/tools/common.js +207 -0
- package/dist/tools/find-references.js +224 -0
- package/dist/tools/find-symbol.js +94 -0
- package/dist/tools/get-context.js +370 -0
- package/dist/tools/impact.js +218 -0
- package/dist/tools/overview.js +482 -0
- package/dist/tools/search-structure.js +303 -0
- package/dist/types.js +61 -0
- package/grammars/tree-sitter-c.wasm +0 -0
- package/grammars/tree-sitter-c_sharp.wasm +0 -0
- package/grammars/tree-sitter-cpp.wasm +0 -0
- package/grammars/tree-sitter-dart.wasm +0 -0
- package/grammars/tree-sitter-go.wasm +0 -0
- package/grammars/tree-sitter-java.wasm +0 -0
- package/grammars/tree-sitter-javascript.wasm +0 -0
- package/grammars/tree-sitter-kotlin.wasm +0 -0
- package/grammars/tree-sitter-objc.wasm +0 -0
- package/grammars/tree-sitter-php.wasm +0 -0
- package/grammars/tree-sitter-python.wasm +0 -0
- package/grammars/tree-sitter-ruby.wasm +0 -0
- package/grammars/tree-sitter-rust.wasm +0 -0
- package/grammars/tree-sitter-swift.wasm +0 -0
- package/grammars/tree-sitter-tsx.wasm +0 -0
- package/grammars/tree-sitter-typescript.wasm +0 -0
- 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
|
+
}
|