codedeep-mcp 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +177 -0
  3. package/dist/config.js +223 -0
  4. package/dist/git/analyzer.js +177 -0
  5. package/dist/git/git-service.js +568 -0
  6. package/dist/git/head-watcher.js +113 -0
  7. package/dist/git/runner.js +204 -0
  8. package/dist/index.js +138 -0
  9. package/dist/indexer/code-index.js +1801 -0
  10. package/dist/indexer/complexity.js +633 -0
  11. package/dist/indexer/extractor.js +354 -0
  12. package/dist/indexer/languages/cpp.js +934 -0
  13. package/dist/indexer/languages/csharp.js +854 -0
  14. package/dist/indexer/languages/dart.js +777 -0
  15. package/dist/indexer/languages/go.js +665 -0
  16. package/dist/indexer/languages/java.js +507 -0
  17. package/dist/indexer/languages/kotlin.js +709 -0
  18. package/dist/indexer/languages/objc.js +397 -0
  19. package/dist/indexer/languages/php.js +771 -0
  20. package/dist/indexer/languages/python.js +455 -0
  21. package/dist/indexer/languages/ruby.js +697 -0
  22. package/dist/indexer/languages/rust.js +754 -0
  23. package/dist/indexer/languages/swift.js +691 -0
  24. package/dist/indexer/languages/typescript.js +485 -0
  25. package/dist/indexer/parser.js +175 -0
  26. package/dist/indexer/pipeline.js +342 -0
  27. package/dist/indexer/scanner.js +279 -0
  28. package/dist/indexer/watcher.js +353 -0
  29. package/dist/logger.js +16 -0
  30. package/dist/server.js +170 -0
  31. package/dist/tools/common.js +207 -0
  32. package/dist/tools/find-references.js +224 -0
  33. package/dist/tools/find-symbol.js +94 -0
  34. package/dist/tools/get-context.js +370 -0
  35. package/dist/tools/impact.js +218 -0
  36. package/dist/tools/overview.js +482 -0
  37. package/dist/tools/search-structure.js +303 -0
  38. package/dist/types.js +61 -0
  39. package/grammars/tree-sitter-c.wasm +0 -0
  40. package/grammars/tree-sitter-c_sharp.wasm +0 -0
  41. package/grammars/tree-sitter-cpp.wasm +0 -0
  42. package/grammars/tree-sitter-dart.wasm +0 -0
  43. package/grammars/tree-sitter-go.wasm +0 -0
  44. package/grammars/tree-sitter-java.wasm +0 -0
  45. package/grammars/tree-sitter-javascript.wasm +0 -0
  46. package/grammars/tree-sitter-kotlin.wasm +0 -0
  47. package/grammars/tree-sitter-objc.wasm +0 -0
  48. package/grammars/tree-sitter-php.wasm +0 -0
  49. package/grammars/tree-sitter-python.wasm +0 -0
  50. package/grammars/tree-sitter-ruby.wasm +0 -0
  51. package/grammars/tree-sitter-rust.wasm +0 -0
  52. package/grammars/tree-sitter-swift.wasm +0 -0
  53. package/grammars/tree-sitter-tsx.wasm +0 -0
  54. package/grammars/tree-sitter-typescript.wasm +0 -0
  55. package/package.json +67 -0
@@ -0,0 +1,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
+ }