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,777 @@
|
|
|
1
|
+
import { IMPORT_NAMESPACE, RECEIVER_OPAQUE } from '../../types.js';
|
|
2
|
+
import { SIGNATURE_DISPLAY_CAP, collectAmbiguousTypeNames, commentDocLine, isTrailingComment, normalizeSignature, resolveCalls, symbolId, } from '../extractor.js';
|
|
3
|
+
import { computeComplexity } from '../complexity.js';
|
|
4
|
+
// Nested `local_function_declaration`s create their own scope — their calls
|
|
5
|
+
// must NOT attribute to an enclosing body, so they're pruned from the body walk
|
|
6
|
+
// (and aren't extracted, the top-level + member-only rule). `function_expression`
|
|
7
|
+
// (closures, `(e) => f(e)`) is deliberately ABSENT: a closure can't be a symbol,
|
|
8
|
+
// so calls inside `items.forEach((e) => f(e))` attribute to the enclosing body
|
|
9
|
+
// (the Go func_literal / Java lambda / Kotlin lambda rule, not the TS arrow rule).
|
|
10
|
+
const DART_FUNCTION_BODY_SKIP_TYPES = new Set(['local_function_declaration']);
|
|
11
|
+
// walkCalls skip set: nested funcs (own scope, above) PLUS `annotation` — Dart
|
|
12
|
+
// annotation arguments must be const expressions, but `@Foo(bar())` still parses
|
|
13
|
+
// a REAL call_expression inside the `annotation` node (a direct child of the
|
|
14
|
+
// declaration or class_member), which the body and module-root walks would
|
|
15
|
+
// otherwise emit as a spurious `calls` ref. Skipping `annotation` drops those.
|
|
16
|
+
const DART_SKIP_TYPES = new Set(['local_function_declaration', 'annotation']);
|
|
17
|
+
// A bare `identifier` callee binds to free functions AND classes. 'class' is
|
|
18
|
+
// here — the inverse of Go/Rust — because Dart construction is shape-identical
|
|
19
|
+
// to a call (`Circle(3)` has no `new` and no distinct construction node, exactly
|
|
20
|
+
// like Swift/Kotlin), so `bareCallableKinds` is the ONLY lever to resolve
|
|
21
|
+
// `Circle(3)` to its type. Accepted error class: a bare call colliding with a
|
|
22
|
+
// same-named type resolves to the type, which for Dart `Type(...)` is
|
|
23
|
+
// construction by convention. The enclosing-class fallback runs FIRST
|
|
24
|
+
// (bareCallsBindToEnclosingClass), so an implicit-this method call beats a
|
|
25
|
+
// same-named type — which keeps 'class' safe. (Bare callee is the engine-default
|
|
26
|
+
// `identifier`, so no plainCalleeType override — unlike Swift.)
|
|
27
|
+
const DART_BARE_CALLABLE_KINDS = new Set(['function', 'class']);
|
|
28
|
+
// Kinds sharing the simple-name FQN namespace — duplicates among these are
|
|
29
|
+
// excluded from extract-time resolution. (class/mixin→class, enum→enum,
|
|
30
|
+
// typedef→type.) Extensions are NOT symbols, so two extensions of one type never
|
|
31
|
+
// make that type look ambiguous.
|
|
32
|
+
const DART_TYPE_KINDS = new Set(['class', 'enum', 'type']);
|
|
33
|
+
// Dart stdlib globals / scalar conversions that parse as bare calls and would
|
|
34
|
+
// otherwise flood the name-keyed reference store. Suppressed ONLY when
|
|
35
|
+
// unresolved (a file-local function shadowing the name keeps its refs). Scalar
|
|
36
|
+
// conversions (`int.parse`, `String(x)` is not a thing — Dart has no scalar
|
|
37
|
+
// conversion calls) are minimal here. Start small; extend after dogfood.
|
|
38
|
+
const DART_IGNORED_BARE_CALLEES = new Set([
|
|
39
|
+
'print', 'identical', 'identityHashCode', 'assert',
|
|
40
|
+
'min', 'max', 'pow', 'sqrt',
|
|
41
|
+
'jsonEncode', 'jsonDecode',
|
|
42
|
+
]);
|
|
43
|
+
// Signature node types under a `method_signature` (bodied member) or a bodiless
|
|
44
|
+
// `declaration` member.
|
|
45
|
+
const DART_SIG_TYPES = [
|
|
46
|
+
'function_signature',
|
|
47
|
+
'getter_signature',
|
|
48
|
+
'setter_signature',
|
|
49
|
+
'operator_signature',
|
|
50
|
+
'constructor_signature',
|
|
51
|
+
'constant_constructor_signature',
|
|
52
|
+
'factory_constructor_signature',
|
|
53
|
+
'redirecting_factory_constructor_signature',
|
|
54
|
+
];
|
|
55
|
+
// The subset that are constructors → method named 'constructor' (unnamed) or the
|
|
56
|
+
// named-ctor segment.
|
|
57
|
+
const DART_CTOR_SIG_TYPES = new Set([
|
|
58
|
+
'constructor_signature',
|
|
59
|
+
'constant_constructor_signature',
|
|
60
|
+
'factory_constructor_signature',
|
|
61
|
+
'redirecting_factory_constructor_signature',
|
|
62
|
+
]);
|
|
63
|
+
// Callee of a `call_expression` = its `function:` field (identifier for
|
|
64
|
+
// bare/construction calls, member_expression for member/static/named-ctor calls).
|
|
65
|
+
//
|
|
66
|
+
// Grammar quirk recovery: an inline arrow closure whose body is itself a call —
|
|
67
|
+
// `(x) => f(x)` (bare) or `() => obj.m()` (member) — mis-parses as `((x) => f)(x)`,
|
|
68
|
+
// i.e. the trailing call args bind to the CLOSURE, leaving the real callee as the
|
|
69
|
+
// closure's arrow-body tail. `unwrapClosureTail` descends a function_expression to
|
|
70
|
+
// that tail. For a BARE-call body the function_expression sits directly in the
|
|
71
|
+
// call's `function:` slot (handled here); for a MEMBER-call body the slot is a
|
|
72
|
+
// member_expression whose `object:` is the function_expression (handled in
|
|
73
|
+
// dartMemberCallInfo, which unwraps the object). Inline arrow closures are
|
|
74
|
+
// ubiquitous in Dart (`.map`, `.where`, Flutter callbacks), so recovering both
|
|
75
|
+
// matters. NOT recoverable: a COMPOUND arrow body (`(e) => cond && f(e)`) — the
|
|
76
|
+
// misparse strips f's args, so that call is genuinely destroyed; the tail is a
|
|
77
|
+
// non-callee node and yields no ref (a documented recall gap, never a false edge).
|
|
78
|
+
function unwrapClosureTail(node) {
|
|
79
|
+
let n = node;
|
|
80
|
+
while (n?.type === 'function_expression') {
|
|
81
|
+
const body = n.childForFieldName('body'); // function_expression_body
|
|
82
|
+
// The arrow body expr is the LAST child (after the `=>` token). Use child()
|
|
83
|
+
// not namedChildren — for `() => this` the body expr is the anonymous `this`
|
|
84
|
+
// token, which namedChildren omits (so the self-receiver would be lost).
|
|
85
|
+
const last = body && body.childCount > 0 ? body.child(body.childCount - 1) : null;
|
|
86
|
+
n = last && last.type !== '=>' ? last : null;
|
|
87
|
+
}
|
|
88
|
+
return n;
|
|
89
|
+
}
|
|
90
|
+
function dartCallCallee(node) {
|
|
91
|
+
return unwrapClosureTail(node.childForFieldName('function'));
|
|
92
|
+
}
|
|
93
|
+
const DART_SELECTORS = [
|
|
94
|
+
{ nodeType: 'call_expression', getCallee: dartCallCallee },
|
|
95
|
+
// Cascade method-calls (`obj..a()..b()`). The node IS its own "callee": it
|
|
96
|
+
// carries the property but its receiver is a SIBLING of the enclosing
|
|
97
|
+
// cascade_section, reached via parent navigation in dartMemberCallInfo.
|
|
98
|
+
// Field-assignment cascades (`obj..x = 1`) use `cascade_selector` + `=`, NOT
|
|
99
|
+
// `cascade_call_expression`, so they emit nothing (correct — not a call).
|
|
100
|
+
{ nodeType: 'cascade_call_expression', getCallee: (n) => n },
|
|
101
|
+
];
|
|
102
|
+
// Peels transparent receiver wrappers — null-assertion `a!` (null_assertion_
|
|
103
|
+
// expression) and parens `(a)` (parenthesized_expression) — so `a!.x()` / `(a).x()`
|
|
104
|
+
// resolve like `a.x()`. The operand is the lone non-punctuation child, found by
|
|
105
|
+
// scanning ALL children (NOT firstNamedChild): `this`/`super` are ANONYMOUS tokens,
|
|
106
|
+
// so firstNamedChild misses them — skipping the wrapper's own `(`/`)`/`!` punctuation
|
|
107
|
+
// and comments instead recovers them, so `(this).x()` self-resolves and `(super).x()`
|
|
108
|
+
// hits the super-drop (rather than leaking an opaque ref past it). A genuine chain
|
|
109
|
+
// (`a.b().c()`) is not a wrapper → stays intact → opaque.
|
|
110
|
+
function unwrapDartReceiver(node) {
|
|
111
|
+
let n = node;
|
|
112
|
+
while (n && (n.type === 'null_assertion_expression' || n.type === 'parenthesized_expression')) {
|
|
113
|
+
let inner = null;
|
|
114
|
+
for (let i = 0; i < n.childCount; i++) {
|
|
115
|
+
const c = n.child(i);
|
|
116
|
+
if (!c || c.type === '(' || c.type === ')' || c.type === '!' || c.type === 'comment')
|
|
117
|
+
continue;
|
|
118
|
+
inner = c;
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
if (!inner)
|
|
122
|
+
break;
|
|
123
|
+
n = inner;
|
|
124
|
+
}
|
|
125
|
+
return n;
|
|
126
|
+
}
|
|
127
|
+
// Reduces a member-expression (`a.m()`), null-aware member call (`a?.m()` —
|
|
128
|
+
// `null_aware_member_expression`, the dominant Dart null-safety call shape, which
|
|
129
|
+
// shares object/property fields), OR cascade-call callee to {receiver, property}.
|
|
130
|
+
// `this` is a fixed token (a `this` node), decided here like Swift/Kotlin/Python.
|
|
131
|
+
// A non-null `a!.m()` or parenthesized `(a).m()` receiver is unwrapped to its token
|
|
132
|
+
// too. A chained/computed receiver (`a.b().c()`, `list[0].run()`) carries
|
|
133
|
+
// RECEIVER_OPAQUE: findable by name but never resolved. `super.m()` / `super..m()`
|
|
134
|
+
// and computed/non-identifier property names emit nothing.
|
|
135
|
+
function dartMemberCallInfo(callee) {
|
|
136
|
+
if (callee.type === 'member_expression' || callee.type === 'null_aware_member_expression') {
|
|
137
|
+
// unwrapClosureTail recovers the `() => obj.m()` arrow-closure misparse; then
|
|
138
|
+
// peel `!`/parens wrappers to recover a resolvable single-identifier receiver.
|
|
139
|
+
const object = unwrapDartReceiver(unwrapClosureTail(callee.childForFieldName('object')));
|
|
140
|
+
const property = callee.childForFieldName('property');
|
|
141
|
+
if (property?.type !== 'identifier')
|
|
142
|
+
return null;
|
|
143
|
+
if (object?.type === 'this')
|
|
144
|
+
return { receiver: 'this', property: property.text, isSelf: true };
|
|
145
|
+
if (object?.type === 'identifier')
|
|
146
|
+
return { receiver: object.text, property: property.text, isSelf: false };
|
|
147
|
+
if (object?.type === 'super')
|
|
148
|
+
return null; // parent-class call, skipped (the TS/Java rule)
|
|
149
|
+
return { receiver: RECEIVER_OPAQUE, property: property.text, isSelf: false };
|
|
150
|
+
}
|
|
151
|
+
if (callee.type === 'cascade_call_expression') {
|
|
152
|
+
const property = callee.childForFieldName('property');
|
|
153
|
+
if (property?.type !== 'identifier')
|
|
154
|
+
return null;
|
|
155
|
+
// Receiver = the cascade target: the sibling immediately before THIS chain's
|
|
156
|
+
// first `cascade_section`. Walk back over the CONTIGUOUS run of cascade_sections
|
|
157
|
+
// from the current one (so `f(a..x(), b..y())` correctly keeps `b` for `..y()`
|
|
158
|
+
// rather than the host's globally-first target `a`). previousSibling is used
|
|
159
|
+
// (not namedChildren) because the `this` target is an ANONYMOUS token.
|
|
160
|
+
const section = callee.parent;
|
|
161
|
+
if (section?.type !== 'cascade_section')
|
|
162
|
+
return null;
|
|
163
|
+
// Skip comments while navigating siblings — `obj /*c*/ ..m()` and
|
|
164
|
+
// `obj..m() /*c*/ ..n()` would otherwise land the target/walk on a comment node.
|
|
165
|
+
const prevNonComment = (x) => {
|
|
166
|
+
let p = x.previousSibling;
|
|
167
|
+
while (p && p.type === 'comment')
|
|
168
|
+
p = p.previousSibling;
|
|
169
|
+
return p;
|
|
170
|
+
};
|
|
171
|
+
let first = section;
|
|
172
|
+
while (prevNonComment(first)?.type === 'cascade_section')
|
|
173
|
+
first = prevNonComment(first);
|
|
174
|
+
const target = unwrapDartReceiver(prevNonComment(first));
|
|
175
|
+
if (target?.type === 'this')
|
|
176
|
+
return { receiver: 'this', property: property.text, isSelf: true };
|
|
177
|
+
if (target?.type === 'identifier')
|
|
178
|
+
return { receiver: target.text, property: property.text, isSelf: false };
|
|
179
|
+
if (target?.type === 'super')
|
|
180
|
+
return null; // `super..m()` parent-class call (the member-form rule)
|
|
181
|
+
// construction/chained/computed cascade target → opaque (findable, unresolved).
|
|
182
|
+
return { receiver: RECEIVER_OPAQUE, property: property.text, isSelf: false };
|
|
183
|
+
}
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
// Dominant Dart Iterable/collection/string/Future method names (>=4 chars)
|
|
187
|
+
// suppressed when a member call to them is unresolved — capturing chained
|
|
188
|
+
// `.where().map().toList()` calls otherwise floods the name-keyed store. Domain
|
|
189
|
+
// method names are deliberately absent. <=3-char names (`.map`) are gated
|
|
190
|
+
// downstream by SHORT_NAME_THRESHOLD.
|
|
191
|
+
const DART_IGNORED_MEMBER_CALLEES = new Set([
|
|
192
|
+
'where', 'expand', 'reduce', 'fold', 'forEach', 'firstWhere', 'lastWhere',
|
|
193
|
+
'singleWhere', 'every', 'contains', 'containsKey', 'containsValue', 'elementAt',
|
|
194
|
+
'toList', 'toSet', 'toString', 'join', 'skip', 'take', 'takeWhile',
|
|
195
|
+
'skipWhile', 'followedBy', 'whereType', 'cast', 'asMap', 'indexOf',
|
|
196
|
+
'sublist', 'insert', 'remove', 'removeAt', 'removeWhere', 'removeLast',
|
|
197
|
+
'clear', 'sort', 'shuffle', 'addAll', 'getRange',
|
|
198
|
+
'substring', 'replaceAll', 'replaceFirst', 'split', 'trim', 'startsWith',
|
|
199
|
+
'endsWith', 'padLeft', 'padRight', 'toLowerCase', 'toUpperCase', 'then',
|
|
200
|
+
'catchError', 'whenComplete', 'listen', 'cancel', 'noSuchMethod',
|
|
201
|
+
]);
|
|
202
|
+
// ── complexity (cyclomatic S1541 + cognitive S3776), pinned for behavioral
|
|
203
|
+
// compatibility with SonarQube's Dart rules, per the published Cognitive Complexity
|
|
204
|
+
// whitepaper and the public S1541/S3776 rule definitions. ──
|
|
205
|
+
// CYCLOMATIC decision nodes (+1 each). if + collection-`if` (`if_element`); ternary;
|
|
206
|
+
// all loops (C-`for`/`for-in`/`await for` all parse as `for_statement`; `while`/`do`)
|
|
207
|
+
// + collection-`for` (`for_element`); each switch-STATEMENT `case` AND switch-
|
|
208
|
+
// EXPRESSION arm (incl the `_` wildcard arm — `switch_*_default`/the container add
|
|
209
|
+
// nothing); and the per-OPERATOR null-aware/boolean nodes `&&`/`||`
|
|
210
|
+
// (logical_and/or_expression), `??` (if_null_expression — note `??` is cyclomatic
|
|
211
|
+
// but FREE cognitively), `?.` (null_aware_member_expression). `??=` is added by
|
|
212
|
+
// dartCyclomaticExtra; `?..` (a cascade, not a null_aware_member_expression) is not.
|
|
213
|
+
const DART_DECISION_NODE_TYPES = new Set([
|
|
214
|
+
'if_statement',
|
|
215
|
+
'if_element',
|
|
216
|
+
'conditional_expression',
|
|
217
|
+
'for_statement',
|
|
218
|
+
'for_element',
|
|
219
|
+
'while_statement',
|
|
220
|
+
'do_statement',
|
|
221
|
+
'switch_statement_case',
|
|
222
|
+
'switch_expression_case',
|
|
223
|
+
'logical_and_expression',
|
|
224
|
+
'logical_or_expression',
|
|
225
|
+
'if_null_expression',
|
|
226
|
+
'null_aware_index_expression', // `a?[i]` null-aware index access (read) — always +1
|
|
227
|
+
// `null_aware_member_expression` (`?.`) is NOT here — its counting is context-
|
|
228
|
+
// dependent (property access counts, method-call callee does not), handled in
|
|
229
|
+
// dartCyclomaticExtra.
|
|
230
|
+
]);
|
|
231
|
+
// Two cyclomatic decisions a flat node-type set can't express:
|
|
232
|
+
// 1. `??=` shares `assignment_expression` with `=`/`+=`/etc. (told apart by the
|
|
233
|
+
// `operator:` field text).
|
|
234
|
+
// 2. `?.` (`null_aware_member_expression`) counts as a null-aware PROPERTY ACCESS
|
|
235
|
+
// (`a?.length` → +1) but NOT as the callee of a null-aware INVOCATION
|
|
236
|
+
// (`a?.m()` → +0) — measured EXACT (`a?.length` = cyc 2, `a?.m()` = cyc 1,
|
|
237
|
+
// `a?.b?.c()` = cyc 2: the `?.b` property counts, the `?.c()` call does not).
|
|
238
|
+
// Detected by whether the node is the `function:` child of a `call_expression`.
|
|
239
|
+
// 3. A null-aware WRITE `a?.b = …` / `a?[i] = …` (incl compound `+=`) parses as an
|
|
240
|
+
// `assignable_expression` whose trailing null-aware selector is an ANONYMOUS
|
|
241
|
+
// token — `?.` for a property write, a bare `?` (before `[`) for an index write
|
|
242
|
+
// (the read forms are a null_aware_member_expression / null_aware_index_expression
|
|
243
|
+
// above). The cyclomatic DFS visits only NAMED children, so the token is counted
|
|
244
|
+
// here via its parent. One null-aware selector per assignable_expression level
|
|
245
|
+
// (a deeper `a?.b?.c =` nests the inner read).
|
|
246
|
+
// True when a `null_aware_member_expression` is the callee of a (possibly generic)
|
|
247
|
+
// null-aware invocation `a?.m(...)` / `a?.m<T>(...)` — those don't count, while a
|
|
248
|
+
// bare null-aware property access `a?.x` does. A generic call wraps the callee in an
|
|
249
|
+
// `instantiation_expression` (`function: instantiation_expression{ function: a?.m,
|
|
250
|
+
// type_arguments: <T> }`) before the `call_expression`, so step through it.
|
|
251
|
+
function dartNullAwareMemberIsCallee(node) {
|
|
252
|
+
let cur = node;
|
|
253
|
+
let parent = cur.parent;
|
|
254
|
+
if (parent?.type === 'instantiation_expression' &&
|
|
255
|
+
parent.childForFieldName('function')?.id === cur.id) {
|
|
256
|
+
cur = parent;
|
|
257
|
+
parent = cur.parent;
|
|
258
|
+
}
|
|
259
|
+
return parent?.type === 'call_expression' && parent.childForFieldName('function')?.id === cur.id;
|
|
260
|
+
}
|
|
261
|
+
function dartCyclomaticExtra(node) {
|
|
262
|
+
switch (node.type) {
|
|
263
|
+
case 'assignment_expression':
|
|
264
|
+
return node.childForFieldName('operator')?.text === '??='; // null-aware compound assign
|
|
265
|
+
case 'null_aware_member_expression':
|
|
266
|
+
return !dartNullAwareMemberIsCallee(node); // access `a?.x` → +1; call `a?.m()` → 0
|
|
267
|
+
case 'assignable_expression': // null-aware WRITE `a?.b = …` / `a?[i] = …` (anonymous `?.`/`?` token)
|
|
268
|
+
return node.children.some((c) => c?.type === '?.' || c?.type === '?');
|
|
269
|
+
case 'spread_element': // null-aware spread `...?x` (token `...?`); plain `...x` is not a decision
|
|
270
|
+
return node.children.some((c) => c?.type === '...?');
|
|
271
|
+
default:
|
|
272
|
+
return false;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
// COGNITIVE boolean-run reader: `&&`/`||` count (their own distinct nodes), while `??`
|
|
276
|
+
// (if_null_expression) is FREE cognitively — the expected cyc/cog divergence (cyclomatic
|
|
277
|
+
// counts `??`). Counted TREE-SCOPED (booleanByTreeParent): a `&&`/`||` adds +1 iff its
|
|
278
|
+
// nearest logical ancestor (skipping parens) is a different kind — the SonarQube Dart model,
|
|
279
|
+
// distinct from sonar-java's source-order flatten and SonarJS's `&&`-only runs.
|
|
280
|
+
function dartCognitiveBooleanKind(node) {
|
|
281
|
+
if (node.type === 'logical_and_expression')
|
|
282
|
+
return '&&';
|
|
283
|
+
if (node.type === 'logical_or_expression')
|
|
284
|
+
return '||';
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
287
|
+
// Complexity body boundary — skip ONLY `annotation` (its args are const expressions,
|
|
288
|
+
// not executable: `@Foo(c ? a : b)` must not count). `local_function_declaration` and
|
|
289
|
+
// `function_expression` (closures) are DELIBERATELY ABSENT (so descended): the SonarQube
|
|
290
|
+
// Dart model rolls a local fn / lambda's control flow INTO the enclosing member with a nesting
|
|
291
|
+
// bump (measured: a member with a local-fn/lambda `if` reads cyc 2 / cog 2 with ONE
|
|
292
|
+
// per-member message). This is a SEPARATE set from DART_SKIP_TYPES (the resolveCalls
|
|
293
|
+
// boundary, which DOES prune local functions from call attribution) — complexity and
|
|
294
|
+
// call-graph have different boundaries here, both correct for their purpose.
|
|
295
|
+
const DART_COMPLEXITY_SKIP_TYPES = new Set(['annotation']);
|
|
296
|
+
// Cognitive config (SonarQube Dart cognitive rule S3776). Grammar + algorithm shapes forced:
|
|
297
|
+
// the `if_statement` condition/pattern/`when`-guard are POSITIONAL (no condition field) →
|
|
298
|
+
// conditionFromNamedChildren; catch bodies are SIBLINGS of `catch_clause` → the `tryType`
|
|
299
|
+
// handler; collection-`if` (`if_element`) charges its `else` like a statement if →
|
|
300
|
+
// collectionIfType (NOT a switch); `&&`/`||` runs are TREE-SCOPED (a kind-change vs the
|
|
301
|
+
// logical ancestor, distinct from sonar-java/SonarJS) → booleanByTreeParent.
|
|
302
|
+
const DART_COGNITIVE_OPTIONS = {
|
|
303
|
+
ifType: 'if_statement',
|
|
304
|
+
collectionIfType: 'if_element', // collection-if charges its else (`[if(b)1 else 2]` = cog 2)
|
|
305
|
+
conditionFromNamedChildren: true, // positional condition/pattern/`when`-guard (booleans + guards count)
|
|
306
|
+
conditionField: '__dart_unused__', // sentinel — conditionFromNamedChildren replaces the field walk
|
|
307
|
+
consequenceField: 'consequence',
|
|
308
|
+
alternativeField: 'alternative',
|
|
309
|
+
loopTypes: new Set(['for_statement', 'for_element', 'while_statement', 'do_statement']),
|
|
310
|
+
// No loopBodyField: the SonarQube Dart rule nests the WHOLE loop (a ternary in a `for` init reads
|
|
311
|
+
// cog 3 — the header IS bumped, unlike sonar-python). Bump-all-children is correct here.
|
|
312
|
+
switchTypes: new Set(['switch_statement', 'switch_expression']), // whole-switch +1 (stmt AND expr)
|
|
313
|
+
ternaryType: 'conditional_expression',
|
|
314
|
+
tryType: { node: 'try_statement', bodyField: 'body', catchBodyType: 'block' }, // flat sibling catch bodies
|
|
315
|
+
catchType: '__dart_no_catch__', // sentinel — tryType handles every catch (incl binding-less `on E {}`)
|
|
316
|
+
nestOnlyTypes: new Set(['function_expression', 'local_function_declaration']), // closures + local fns roll in (+0, nest)
|
|
317
|
+
labeledJumpTypes: new Set(['break_statement', 'continue_statement']),
|
|
318
|
+
hasLabel: (n) => n.namedChildren.some((c) => c.type === 'identifier'), // labeled = a positional identifier child
|
|
319
|
+
booleanOperatorKind: dartCognitiveBooleanKind,
|
|
320
|
+
booleanByTreeParent: true, // tree-scoped runs (a logical op counts iff != its logical-ancestor kind)
|
|
321
|
+
parenthesizedType: 'parenthesized_expression', // skipped when finding the logical ancestor
|
|
322
|
+
// No recursion (a self-call adds 0 cognitively — measured). No initField (for-init isn't a distinct if-init).
|
|
323
|
+
};
|
|
324
|
+
export function extractDart(tree, content, fileInfo) {
|
|
325
|
+
const ctx = {
|
|
326
|
+
content,
|
|
327
|
+
fileInfo,
|
|
328
|
+
occurrences: new Map(),
|
|
329
|
+
symbols: [],
|
|
330
|
+
imports: [],
|
|
331
|
+
bodies: [],
|
|
332
|
+
};
|
|
333
|
+
extractTopLevel(ctx, tree.rootNode);
|
|
334
|
+
// Same-name types in one file are invalid Dart, so this only fires on broken
|
|
335
|
+
// parses — where refusing resolution beats binding through a half-parsed type.
|
|
336
|
+
const ambiguousTypeNames = collectAmbiguousTypeNames(ctx.symbols, DART_TYPE_KINDS);
|
|
337
|
+
const references = resolveCalls(ctx.bodies, tree.rootNode, ctx.symbols, fileInfo, DART_SELECTORS, DART_SKIP_TYPES, DART_FUNCTION_BODY_SKIP_TYPES, dartMemberCallInfo, {
|
|
338
|
+
// Bare/construction callee is the engine-default `identifier` — no
|
|
339
|
+
// plainCalleeType override. Dart allows implicit-this bare method calls
|
|
340
|
+
// (`m(){ helper() }` → this.helper()), so a bare call resolves against the
|
|
341
|
+
// enclosing class first.
|
|
342
|
+
bareCallsBindToEnclosingClass: true,
|
|
343
|
+
bareCallableKinds: DART_BARE_CALLABLE_KINDS,
|
|
344
|
+
// No constructorKinds: construction has no distinct node; it resolves as a
|
|
345
|
+
// bare call to a 'class'-kind symbol via bareCallableKinds.
|
|
346
|
+
ambiguousClassNames: ambiguousTypeNames,
|
|
347
|
+
ignoredBareCallees: DART_IGNORED_BARE_CALLEES,
|
|
348
|
+
ignoredMemberCallees: DART_IGNORED_MEMBER_CALLEES,
|
|
349
|
+
});
|
|
350
|
+
// Cyclomatic + cognitive complexity, computed while the tree is alive (the
|
|
351
|
+
// Go/Kotlin call-site pattern). Uses its OWN skip set (local fns + closures roll
|
|
352
|
+
// into the enclosing member — the SonarQube Dart per-member model), not DART_SKIP_TYPES.
|
|
353
|
+
computeComplexity(ctx.bodies, ctx.symbols, {
|
|
354
|
+
decisionNodeTypes: DART_DECISION_NODE_TYPES,
|
|
355
|
+
extraDecisionPredicate: dartCyclomaticExtra,
|
|
356
|
+
skipTypes: DART_COMPLEXITY_SKIP_TYPES,
|
|
357
|
+
cognitive: DART_COGNITIVE_OPTIONS,
|
|
358
|
+
});
|
|
359
|
+
return { symbols: ctx.symbols, references, imports: ctx.imports };
|
|
360
|
+
}
|
|
361
|
+
// Top-level source_file items. containerExported is true (the file is the
|
|
362
|
+
// module surface); qualifier is empty.
|
|
363
|
+
function extractTopLevel(ctx, root) {
|
|
364
|
+
for (const child of root.namedChildren) {
|
|
365
|
+
const doc = dartDoc(child);
|
|
366
|
+
switch (child.type) {
|
|
367
|
+
case 'import_or_export':
|
|
368
|
+
extractImport(ctx, child);
|
|
369
|
+
break;
|
|
370
|
+
case 'class_declaration':
|
|
371
|
+
case 'mixin_declaration':
|
|
372
|
+
extractClass(ctx, child, doc, '', true);
|
|
373
|
+
break;
|
|
374
|
+
case 'extension_declaration':
|
|
375
|
+
extractExtension(ctx, child, '', true);
|
|
376
|
+
break;
|
|
377
|
+
case 'extension_type_declaration':
|
|
378
|
+
extractExtensionType(ctx, child, doc, '', true);
|
|
379
|
+
break;
|
|
380
|
+
case 'enum_declaration':
|
|
381
|
+
extractEnum(ctx, child, doc, '', true);
|
|
382
|
+
break;
|
|
383
|
+
case 'type_alias':
|
|
384
|
+
extractTypeAlias(ctx, child, doc, '', true);
|
|
385
|
+
break;
|
|
386
|
+
// function / getter / setter, plus their `external` interop forms (dart:ffi,
|
|
387
|
+
// dart:js_interop), all carry a `signature:` field with a `name:`.
|
|
388
|
+
case 'function_declaration':
|
|
389
|
+
case 'external_function_declaration':
|
|
390
|
+
case 'getter_declaration':
|
|
391
|
+
case 'setter_declaration':
|
|
392
|
+
case 'external_getter_declaration':
|
|
393
|
+
case 'external_setter_declaration':
|
|
394
|
+
extractTopLevelFunction(ctx, child, doc, '', true);
|
|
395
|
+
break;
|
|
396
|
+
case 'top_level_variable_declaration':
|
|
397
|
+
case 'external_variable_declaration':
|
|
398
|
+
extractVariableDecl(ctx, child, doc, undefined, '', true);
|
|
399
|
+
break;
|
|
400
|
+
// library_name, part_directive, part_of_directive, comments, ERROR — no symbols.
|
|
401
|
+
default:
|
|
402
|
+
break;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
// class / mixin → 'class' kind (one shared handler — both have a `name` field and
|
|
407
|
+
// a `class_body`). Dart has no nested type declarations, so there is no recursion
|
|
408
|
+
// into nested classes (only local functions, which aren't members).
|
|
409
|
+
function extractClass(ctx, decl, doc, parentQualifier, containerExported) {
|
|
410
|
+
const name = decl.childForFieldName('name')?.text;
|
|
411
|
+
if (!name)
|
|
412
|
+
return;
|
|
413
|
+
const exported = containerExported && !isPrivate(name);
|
|
414
|
+
ctx.symbols.push(makeDartSymbol(ctx, decl, dartSig(ctx, decl), 'class', name, topFqn(ctx, name), exported, doc, parentQualifier));
|
|
415
|
+
const body = decl.childForFieldName('body'); // class_body
|
|
416
|
+
if (body)
|
|
417
|
+
extractMemberBody(ctx, body, name, joinQualifier(parentQualifier, name), exported);
|
|
418
|
+
}
|
|
419
|
+
// enum → 'enum' kind. enum_constant cases are NOT extracted (the TS/Java/Go/Rust/
|
|
420
|
+
// Swift/Kotlin enum-member rule); enhanced-enum members after the `;` (methods,
|
|
421
|
+
// fields, constructors) ARE — keyed on the enum name. Enum-constant constructor
|
|
422
|
+
// arguments (`earth(5.9)`) run at enum init but have no symbol owner, so their
|
|
423
|
+
// calls fall to the module-root walk (a documented minor recall gap).
|
|
424
|
+
function extractEnum(ctx, decl, doc, parentQualifier, containerExported) {
|
|
425
|
+
const name = decl.childForFieldName('name')?.text;
|
|
426
|
+
if (!name)
|
|
427
|
+
return;
|
|
428
|
+
const exported = containerExported && !isPrivate(name);
|
|
429
|
+
ctx.symbols.push(makeDartSymbol(ctx, decl, dartSig(ctx, decl), 'enum', name, topFqn(ctx, name), exported, doc, parentQualifier));
|
|
430
|
+
const body = decl.childForFieldName('body'); // enum_body
|
|
431
|
+
if (body)
|
|
432
|
+
extractMemberBody(ctx, body, name, joinQualifier(parentQualifier, name), exported);
|
|
433
|
+
}
|
|
434
|
+
// `extension Name on Type { ... }` — not a symbol. Its members key on the
|
|
435
|
+
// EXTENDED type (`file:Type.member`), merging into the same methodsByClass[Type]
|
|
436
|
+
// as the type's own methods (the Rust impl-merge / Go-receiver / Swift-extension
|
|
437
|
+
// pattern), so `self.m()` here and `obj.m()` elsewhere both resolve. Anonymous
|
|
438
|
+
// extensions (`extension on Type`) are kept — they still key on the on-type.
|
|
439
|
+
function extractExtension(ctx, decl, parentQualifier, containerExported) {
|
|
440
|
+
const onType = extensionOnTypeName(decl);
|
|
441
|
+
if (!onType)
|
|
442
|
+
return; // non-nominal on-type (function/record type) — no key
|
|
443
|
+
const extName = decl.childForFieldName('name')?.text;
|
|
444
|
+
// An extension's members are exported per the EXTENSION's own visibility (the
|
|
445
|
+
// leading-underscore on its NAME — the Swift/Kotlin rule), NOT the extended
|
|
446
|
+
// type's: a `private` extension makes its members library-private regardless of
|
|
447
|
+
// the on-type. An anonymous extension (no name) is public. Members AND-in their
|
|
448
|
+
// own underscore.
|
|
449
|
+
const exported = containerExported && !(extName !== undefined && isPrivate(extName));
|
|
450
|
+
const body = decl.childForFieldName('body'); // extension_body
|
|
451
|
+
if (body)
|
|
452
|
+
extractMemberBody(ctx, body, onType, joinQualifier(parentQualifier, extName ?? onType), exported);
|
|
453
|
+
}
|
|
454
|
+
// A class_body / extension_body / enum_body. Each member is a `class_member`
|
|
455
|
+
// wrapper (enum_body also carries leading `enum_constant`s, skipped). The
|
|
456
|
+
// payload inside class_member is either `method_declaration` (bodied) or a
|
|
457
|
+
// bodiless `declaration`.
|
|
458
|
+
function extractMemberBody(ctx, body, className, qualifier, containerExported) {
|
|
459
|
+
for (const member of body.namedChildren) {
|
|
460
|
+
if (member.type !== 'class_member')
|
|
461
|
+
continue; // enum_constant, comments, ERROR
|
|
462
|
+
const doc = dartDoc(member);
|
|
463
|
+
const payload = childOfType(member, 'method_declaration', 'declaration');
|
|
464
|
+
if (!payload)
|
|
465
|
+
continue;
|
|
466
|
+
dispatchMember(ctx, payload, doc, className, qualifier, containerExported);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
// Route a member payload (method_declaration | declaration) to method, ctor, or
|
|
470
|
+
// field handling.
|
|
471
|
+
function dispatchMember(ctx, payload, doc, className, qualifier, containerExported) {
|
|
472
|
+
if (payload.type === 'method_declaration') {
|
|
473
|
+
const sig = payload.childForFieldName('signature'); // method_signature
|
|
474
|
+
const inner = sig ? childOfType(sig, ...DART_SIG_TYPES) : null;
|
|
475
|
+
const body = payload.childForFieldName('body');
|
|
476
|
+
if (inner)
|
|
477
|
+
handleCallable(ctx, inner, payload, body, doc, className, qualifier, containerExported);
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
// bodiless `declaration`: abstract method / getter / setter / ctor, or a field.
|
|
481
|
+
const inner = childOfType(payload, ...DART_SIG_TYPES);
|
|
482
|
+
if (inner) {
|
|
483
|
+
handleCallable(ctx, inner, payload, null, doc, className, qualifier, containerExported);
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
extractVariableDecl(ctx, payload, doc, className, qualifier, containerExported);
|
|
487
|
+
}
|
|
488
|
+
// A method / getter / setter / operator / constructor signature.
|
|
489
|
+
function handleCallable(ctx, inner, payload, body, doc, className, qualifier, containerExported) {
|
|
490
|
+
if (DART_CTOR_SIG_TYPES.has(inner.type)) {
|
|
491
|
+
// A return-type-less method (`f() => g();`) parses as `constructor_signature`
|
|
492
|
+
// — the classic Dart ctor/method parse ambiguity. A REAL constructor always
|
|
493
|
+
// leads with the class name (`A()` / `A.named()`); factories likewise. If the
|
|
494
|
+
// first name segment isn't the class name, it's actually a method, so fall
|
|
495
|
+
// through to the method branch (childForFieldName('name') yields its name).
|
|
496
|
+
const isFactory = inner.type === 'factory_constructor_signature' ||
|
|
497
|
+
inner.type === 'redirecting_factory_constructor_signature';
|
|
498
|
+
const firstName = inner.namedChildren.find((c) => c.type === 'identifier')?.text;
|
|
499
|
+
if (isFactory || firstName === className) {
|
|
500
|
+
extractConstructor(ctx, inner, payload, doc, className, qualifier, containerExported);
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
const name = inner.type === 'operator_signature'
|
|
505
|
+
? inner.childForFieldName('operator')?.text
|
|
506
|
+
: inner.childForFieldName('name')?.text;
|
|
507
|
+
if (!name)
|
|
508
|
+
return;
|
|
509
|
+
const exported = containerExported && !isPrivate(name);
|
|
510
|
+
const sym = makeDartSymbol(ctx, payload, dartSig(ctx, payload), 'method', name, memberFqn(ctx, className, name), exported, doc, qualifier);
|
|
511
|
+
ctx.symbols.push(sym);
|
|
512
|
+
// Abstract members (no body) populate methodsByClass for resolution but own no
|
|
513
|
+
// PendingBody.
|
|
514
|
+
if (body)
|
|
515
|
+
ctx.bodies.push({ symbolId: sym.id, body, className });
|
|
516
|
+
}
|
|
517
|
+
// A constructor (generative / const / factory / redirecting-factory) → a
|
|
518
|
+
// 'method'. Unnamed (`Box`) → name 'constructor' (TS convention; construction
|
|
519
|
+
// `Box(...)` resolves to the CLASS via bareCallableKinds, this symbol exists for
|
|
520
|
+
// find_symbol + to OWN its body's calls). Named (`Box.named`) → the last name
|
|
521
|
+
// segment, FQN `file:Box.named`, so `Box.named()` member calls resolve via
|
|
522
|
+
// methodsByClass[Box]. The PendingBody is the whole payload so default-arg, the
|
|
523
|
+
// `: a = f()` initializer list, the `: this.y()` redirect, and any factory body
|
|
524
|
+
// all attribute here and self-calls resolve.
|
|
525
|
+
function extractConstructor(ctx, inner, payload, doc, className, qualifier, containerExported) {
|
|
526
|
+
const nameIds = inner.namedChildren.filter((c) => c.type === 'identifier');
|
|
527
|
+
const ctorName = nameIds.length > 1 ? nameIds[nameIds.length - 1].text : 'constructor';
|
|
528
|
+
const exported = containerExported && !isPrivate(ctorName);
|
|
529
|
+
const sym = makeDartSymbol(ctx, payload, dartSig(ctx, payload), 'method', ctorName, memberFqn(ctx, className, ctorName), exported, doc, qualifier);
|
|
530
|
+
ctx.symbols.push(sym);
|
|
531
|
+
ctx.bodies.push({ symbolId: sym.id, body: payload, className });
|
|
532
|
+
}
|
|
533
|
+
// A field / top-level variable declaration → one 'variable' per name. Member
|
|
534
|
+
// fields and top-level vars share three list shapes: `initialized_identifier_list`
|
|
535
|
+
// (typed `int a, b = f()`), `static_final_declaration_list` (const/final), and
|
|
536
|
+
// `identifier_list` (an `external int a, b;` interop var — bare identifiers, no
|
|
537
|
+
// initializer). Each named binding with an initializer owns a PendingBody on its
|
|
538
|
+
// `value:` expression (the Swift per-binding rule), so `final x = compute()`
|
|
539
|
+
// attributes compute() to x; a value-less name (`int a;`) owns nothing.
|
|
540
|
+
function extractVariableDecl(ctx, declNode, doc, className, qualifier, containerExported) {
|
|
541
|
+
const list = childOfType(declNode, 'initialized_identifier_list', 'static_final_declaration_list', 'identifier_list');
|
|
542
|
+
if (!list)
|
|
543
|
+
return;
|
|
544
|
+
const signature = dartSig(ctx, declNode);
|
|
545
|
+
for (const item of list.namedChildren) {
|
|
546
|
+
// `identifier_list` holds bare `identifier`s (no name field/value); the other
|
|
547
|
+
// two hold `initialized_identifier`/`static_final_declaration` with fields.
|
|
548
|
+
const nameNode = item.type === 'identifier' ? item : item.childForFieldName('name');
|
|
549
|
+
if (!nameNode ||
|
|
550
|
+
(item.type !== 'identifier' &&
|
|
551
|
+
item.type !== 'initialized_identifier' &&
|
|
552
|
+
item.type !== 'static_final_declaration')) {
|
|
553
|
+
continue;
|
|
554
|
+
}
|
|
555
|
+
const name = nameNode.text;
|
|
556
|
+
const exported = containerExported && !isPrivate(name);
|
|
557
|
+
const sym = makeDartSymbol(ctx, declNode, signature, 'variable', name, memberFqn(ctx, className, name), exported, doc, qualifier);
|
|
558
|
+
ctx.symbols.push(sym);
|
|
559
|
+
const value = item.childForFieldName('value');
|
|
560
|
+
if (value)
|
|
561
|
+
ctx.bodies.push({ symbolId: sym.id, body: value, className });
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
// Top-level function / getter / setter (and their `external` interop forms) →
|
|
565
|
+
// 'function'. All carry a `signature:` field (function_signature / getter_signature
|
|
566
|
+
// / setter_signature) whose `name:` is the symbol name; a body, when present
|
|
567
|
+
// (external decls have none), becomes a PendingBody so its calls attribute here.
|
|
568
|
+
function extractTopLevelFunction(ctx, decl, doc, qualifier, containerExported) {
|
|
569
|
+
const name = decl.childForFieldName('signature')?.childForFieldName('name')?.text;
|
|
570
|
+
if (!name)
|
|
571
|
+
return;
|
|
572
|
+
const exported = containerExported && !isPrivate(name);
|
|
573
|
+
const sym = makeDartSymbol(ctx, decl, dartSig(ctx, decl), 'function', name, topFqn(ctx, name), exported, doc, qualifier);
|
|
574
|
+
ctx.symbols.push(sym);
|
|
575
|
+
const body = decl.childForFieldName('body');
|
|
576
|
+
if (body)
|
|
577
|
+
ctx.bodies.push({ symbolId: sym.id, body, className: undefined });
|
|
578
|
+
}
|
|
579
|
+
// `extension type Name(T repr) { ... }` (Dart 3.3+ zero-cost wrapper) → 'class'
|
|
580
|
+
// kind: construction `Name(x)` resolves to it like a normal class, and its body
|
|
581
|
+
// members (getters/methods) key on it. The name lives under `extension_type_name`
|
|
582
|
+
// (not a direct `name:` identifier), and the `representation:` declares the wrapped
|
|
583
|
+
// field, extracted as a variable member.
|
|
584
|
+
function extractExtensionType(ctx, decl, doc, parentQualifier, containerExported) {
|
|
585
|
+
const nameNode = decl.childForFieldName('name'); // extension_type_name
|
|
586
|
+
const name = nameNode ? childOfType(nameNode, 'identifier')?.text : undefined;
|
|
587
|
+
if (!name)
|
|
588
|
+
return;
|
|
589
|
+
const exported = containerExported && !isPrivate(name);
|
|
590
|
+
ctx.symbols.push(makeDartSymbol(ctx, decl, dartSig(ctx, decl), 'class', name, topFqn(ctx, name), exported, doc, parentQualifier));
|
|
591
|
+
const qualifier = joinQualifier(parentQualifier, name);
|
|
592
|
+
const repr = decl.childForFieldName('representation');
|
|
593
|
+
const reprName = repr?.childForFieldName('name')?.text;
|
|
594
|
+
if (reprName) {
|
|
595
|
+
ctx.symbols.push(makeDartSymbol(ctx, repr, normalizeSignature(ctx.content.slice(repr.startIndex, repr.endIndex)), 'variable', reprName, memberFqn(ctx, name, reprName), exported && !isPrivate(reprName), null, qualifier));
|
|
596
|
+
}
|
|
597
|
+
const body = decl.childForFieldName('body'); // class_body
|
|
598
|
+
if (body)
|
|
599
|
+
extractMemberBody(ctx, body, name, qualifier, exported);
|
|
600
|
+
}
|
|
601
|
+
// typedef → 'type'. The alias name is the first `type_identifier` child (the
|
|
602
|
+
// aliased type follows, as a `type` node).
|
|
603
|
+
function extractTypeAlias(ctx, decl, doc, qualifier, containerExported) {
|
|
604
|
+
const name = decl.namedChildren.find((c) => c.type === 'type_identifier')?.text;
|
|
605
|
+
if (!name)
|
|
606
|
+
return;
|
|
607
|
+
const exported = containerExported && !isPrivate(name);
|
|
608
|
+
ctx.symbols.push(makeDartSymbol(ctx, decl, dartSig(ctx, decl), 'type', name, topFqn(ctx, name), exported, doc, qualifier));
|
|
609
|
+
}
|
|
610
|
+
// `import 'uri' [as a] [show A, B] [hide C];` → an ImportInfo. `show` → the named
|
|
611
|
+
// list; `hide`/no-combinator → a whole-library namespace import (aliased to the
|
|
612
|
+
// prefix when `as` is present). `export`/`part`/`library` directives are skipped
|
|
613
|
+
// (low cross-file value: Dart URIs don't map to indexed paths — the Rust/Kotlin
|
|
614
|
+
// framing).
|
|
615
|
+
function extractImport(ctx, node) {
|
|
616
|
+
const lib = childOfType(node, 'library_import');
|
|
617
|
+
if (!lib)
|
|
618
|
+
return; // library_export — skipped
|
|
619
|
+
const spec = childOfType(lib, 'import_specification');
|
|
620
|
+
if (!spec)
|
|
621
|
+
return;
|
|
622
|
+
const uriNode = spec.childForFieldName('uri');
|
|
623
|
+
if (!uriNode)
|
|
624
|
+
return;
|
|
625
|
+
const sourceModule = stripQuotes(uriNode.text);
|
|
626
|
+
const line = node.startPosition.row + 1;
|
|
627
|
+
const alias = spec.childForFieldName('alias')?.text;
|
|
628
|
+
const showCombinator = spec.namedChildren.find((c) => c.type === 'combinator' && nodeHasAnonChild(c, 'show'));
|
|
629
|
+
if (showCombinator) {
|
|
630
|
+
// `import 'x' as p show A, B;` binds the shown names under the prefix, so
|
|
631
|
+
// carry the alias onto each (the Kotlin named-import rule).
|
|
632
|
+
const names = showCombinator.namedChildren
|
|
633
|
+
.filter((c) => c.type === 'identifier')
|
|
634
|
+
.map((c) => (alias ? { name: c.text, alias } : { name: c.text }));
|
|
635
|
+
if (names.length > 0) {
|
|
636
|
+
ctx.imports.push({ file: ctx.fileInfo.path, sourceModule, importedNames: names, line });
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
const ns = alias
|
|
641
|
+
? { name: IMPORT_NAMESPACE, kind: 'namespace', alias }
|
|
642
|
+
: { name: IMPORT_NAMESPACE, kind: 'namespace' };
|
|
643
|
+
ctx.imports.push({ file: ctx.fileInfo.path, sourceModule, importedNames: [ns], line });
|
|
644
|
+
}
|
|
645
|
+
// ── helpers ──────────────────────────────────────────────────────────────
|
|
646
|
+
// Extended type's simple name = the LAST direct `type_identifier` of the `class:`
|
|
647
|
+
// (on-)type node. `extension on Map<String,int>` → Map (type_arguments is a
|
|
648
|
+
// separate child); a non-nominal on-type (function/record type) has no
|
|
649
|
+
// type_identifier → null (skip the whole extension).
|
|
650
|
+
function extensionOnTypeName(decl) {
|
|
651
|
+
const classField = decl.childForFieldName('class');
|
|
652
|
+
if (!classField)
|
|
653
|
+
return null;
|
|
654
|
+
let result = null;
|
|
655
|
+
for (const c of classField.namedChildren) {
|
|
656
|
+
if (c.type === 'type_identifier')
|
|
657
|
+
result = c.text;
|
|
658
|
+
}
|
|
659
|
+
return result;
|
|
660
|
+
}
|
|
661
|
+
function topFqn(ctx, name) {
|
|
662
|
+
return `${ctx.fileInfo.path}:${name}`;
|
|
663
|
+
}
|
|
664
|
+
function memberFqn(ctx, className, name) {
|
|
665
|
+
return className ? `${ctx.fileInfo.path}:${className}.${name}` : `${ctx.fileInfo.path}:${name}`;
|
|
666
|
+
}
|
|
667
|
+
// First direct named child of one of the given types (or null).
|
|
668
|
+
function childOfType(node, ...types) {
|
|
669
|
+
return node.namedChildren.find((c) => types.includes(c.type)) ?? null;
|
|
670
|
+
}
|
|
671
|
+
// Declaration signature = source from the declaration start to its body (the
|
|
672
|
+
// `body:` field, present on function/method/getter/setter declarations), cut
|
|
673
|
+
// before a constructor `initializers` list, with a trailing `;` (carried by
|
|
674
|
+
// top-level/typedef nodes) stripped. Feeds symbolId hashing; the stored copy is
|
|
675
|
+
// capped by makeDartSymbol.
|
|
676
|
+
function dartSig(ctx, node) {
|
|
677
|
+
let end = node.endIndex;
|
|
678
|
+
const body = node.childForFieldName('body');
|
|
679
|
+
if (body)
|
|
680
|
+
end = Math.min(end, body.startIndex);
|
|
681
|
+
const initializers = node.namedChildren.find((c) => c.type === 'initializers');
|
|
682
|
+
if (initializers)
|
|
683
|
+
end = Math.min(end, initializers.startIndex);
|
|
684
|
+
let sig = normalizeSignature(ctx.content.slice(node.startIndex, end));
|
|
685
|
+
if (sig.endsWith(';'))
|
|
686
|
+
sig = sig.slice(0, -1).trimEnd();
|
|
687
|
+
return sig;
|
|
688
|
+
}
|
|
689
|
+
// Dart privacy is the leading-underscore convention — there is no `private`
|
|
690
|
+
// keyword. A name starting `_` is library-private; everything else is public.
|
|
691
|
+
// (Operator names `+`/`==`/`[]` and the synthesized 'constructor' never start
|
|
692
|
+
// with `_`.) Members AND-in their container's exportedness via the caller.
|
|
693
|
+
function isPrivate(name) {
|
|
694
|
+
return name.startsWith('_');
|
|
695
|
+
}
|
|
696
|
+
// True if `node` has a direct anonymous child whose token text is `text`.
|
|
697
|
+
function nodeHasAnonChild(node, text) {
|
|
698
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
699
|
+
const c = node.child(i);
|
|
700
|
+
if (c && !c.isNamed && c.type === text)
|
|
701
|
+
return true;
|
|
702
|
+
}
|
|
703
|
+
return false;
|
|
704
|
+
}
|
|
705
|
+
function stripQuotes(s) {
|
|
706
|
+
return s.replace(/^['"]/, '').replace(/['"]$/, '');
|
|
707
|
+
}
|
|
708
|
+
// Module path / enclosing-type chain only disambiguate hashed ids — they never
|
|
709
|
+
// reach FQN parsing — so any unique join works.
|
|
710
|
+
function joinQualifier(a, b) {
|
|
711
|
+
if (!a)
|
|
712
|
+
return b;
|
|
713
|
+
if (!b)
|
|
714
|
+
return a;
|
|
715
|
+
return `${a}.${b}`;
|
|
716
|
+
}
|
|
717
|
+
function makeDartSymbol(ctx, node, signature, kind, name, fqn, exported, doc, qualifier = '') {
|
|
718
|
+
const key = `${name}\0${kind}\0${signature}\0${qualifier}`;
|
|
719
|
+
const n = (ctx.occurrences.get(key) ?? 0) + 1;
|
|
720
|
+
ctx.occurrences.set(key, n);
|
|
721
|
+
const effectiveQualifier = n === 1 ? qualifier : `${qualifier}#${n}`;
|
|
722
|
+
return {
|
|
723
|
+
// The id hashes the FULL signature; only the stored copy is capped.
|
|
724
|
+
id: symbolId(ctx.fileInfo.path, name, kind, signature, effectiveQualifier),
|
|
725
|
+
name,
|
|
726
|
+
fqn,
|
|
727
|
+
kind,
|
|
728
|
+
file: ctx.fileInfo.path,
|
|
729
|
+
startLine: node.startPosition.row + 1,
|
|
730
|
+
endLine: node.endPosition.row + 1,
|
|
731
|
+
signature: signature.slice(0, SIGNATURE_DISPLAY_CAP),
|
|
732
|
+
doc,
|
|
733
|
+
exported,
|
|
734
|
+
language: ctx.fileInfo.language,
|
|
735
|
+
};
|
|
736
|
+
}
|
|
737
|
+
// Doc = the immediately-preceding DocC-style comment: a contiguous `///` line
|
|
738
|
+
// block or a single `/** */`. Dart uses ONE `comment` node type for every form,
|
|
739
|
+
// so the doc is discriminated by text prefix (`///` / `/**`); plain `//` and
|
|
740
|
+
// `/* */` are NOT docs. Annotations live INSIDE the declaration (no Rust-style
|
|
741
|
+
// sibling skip). For members the anchor is the `class_member` wrapper (the
|
|
742
|
+
// comment is its sibling, not the inner declaration's). `///` lines are SEPARATE
|
|
743
|
+
// comment nodes → Go/Swift contiguous-block walk, first line with content.
|
|
744
|
+
function dartDoc(anchor) {
|
|
745
|
+
const nearest = anchor.previousNamedSibling;
|
|
746
|
+
if (!nearest || nearest.type !== 'comment')
|
|
747
|
+
return null;
|
|
748
|
+
if (nearest.endPosition.row !== anchor.startPosition.row - 1)
|
|
749
|
+
return null; // adjacency
|
|
750
|
+
if (isTrailingComment(nearest))
|
|
751
|
+
return null;
|
|
752
|
+
const text = nearest.text;
|
|
753
|
+
if (text.startsWith('/**'))
|
|
754
|
+
return commentDocLine(text); // single block doc
|
|
755
|
+
if (!text.startsWith('///'))
|
|
756
|
+
return null; // plain // or /* */ — not a doc
|
|
757
|
+
const chain = [nearest];
|
|
758
|
+
for (;;) {
|
|
759
|
+
const bottom = chain[chain.length - 1];
|
|
760
|
+
const prev = bottom.previousNamedSibling;
|
|
761
|
+
if (!prev ||
|
|
762
|
+
prev.type !== 'comment' ||
|
|
763
|
+
!prev.text.startsWith('///') ||
|
|
764
|
+
prev.endPosition.row !== bottom.startPosition.row - 1 ||
|
|
765
|
+
isTrailingComment(prev)) {
|
|
766
|
+
break;
|
|
767
|
+
}
|
|
768
|
+
chain.push(prev);
|
|
769
|
+
}
|
|
770
|
+
chain.reverse(); // document order
|
|
771
|
+
for (const comment of chain) {
|
|
772
|
+
const line = commentDocLine(comment.text);
|
|
773
|
+
if (line)
|
|
774
|
+
return line;
|
|
775
|
+
}
|
|
776
|
+
return null;
|
|
777
|
+
}
|