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,691 @@
1
+ import { IMPORT_NAMESPACE, RECEIVER_OPAQUE } from '../../types.js';
2
+ import { SIGNATURE_DISPLAY_CAP, collectAmbiguousTypeNames, commentDocLine, declSignature, isTrailingComment, normalizeSignature, resolveCalls, symbolId, } from '../extractor.js';
3
+ import { computeComplexity } from '../complexity.js';
4
+ // Nested `func` declarations create their own scope — their calls must NOT
5
+ // attribute to an enclosing body, so they're pruned from the body walk (and
6
+ // aren't extracted, the top-level + member-only rule). `lambda_literal`
7
+ // (closures) is deliberately ABSENT: a closure can't be a symbol, so calls
8
+ // inside `items.forEach { f() }` attribute to the enclosing body (the Go
9
+ // func_literal / Java lambda rule, not the TS arrow rule).
10
+ const SWIFT_FUNCTION_BODY_SKIP_TYPES = new Set(['function_declaration']);
11
+ // walkCalls skip set: nested funcs (own scope, above) PLUS `modifiers` —
12
+ // attribute / property-wrapper arguments (`@Wrapper(call())`,
13
+ // `@Option(help: f())`) parse a REAL call_expression inside the declaration's
14
+ // `modifiers`, which the body and module-root walks would otherwise emit as
15
+ // spurious `calls` refs (a false edge from the decl, or a null-sourced
16
+ // module-level ref). Skipping `modifiers` drops attribute-arg calls everywhere.
17
+ const SWIFT_SKIP_TYPES = new Set(['function_declaration', 'modifiers']);
18
+ // Swift's bare/constructor call callee is a `simple_identifier` (NOT the
19
+ // engine default `identifier`). `Point(x:1)` construction parses IDENTICALLY
20
+ // to a function call — there is no separate construction node (unlike Rust's
21
+ // struct_expression / Go's composite_literal) — so `simple_identifier` is the
22
+ // single plain callee type, declared as `plainCalleeType` so it routes through
23
+ // nameToId + the enclosing-class fallback rather than the constructor branch.
24
+ const SWIFT_BARE_CALLEE_TYPES = new Set(['simple_identifier']);
25
+ // A bare `simple_identifier` callee binds to free functions AND classes.
26
+ // 'class' is here — the inverse of Go/Rust — because Swift construction is
27
+ // shape-identical to a call and `bareCallableKinds` is the ONLY lever to
28
+ // resolve `Point(x:1)` to its type. Accepted error class: a bare call
29
+ // colliding with a same-named type resolves to the type, which for Swift
30
+ // `Type(...)` is construction by convention. The enclosing-class fallback runs
31
+ // FIRST (bareCallsBindToEnclosingClass), so an implicit-self method call beats
32
+ // a same-named type — which is what keeps 'class' safe.
33
+ const SWIFT_BARE_CALLABLE_KINDS = new Set(['function', 'class']);
34
+ // Kinds sharing the simple-name FQN namespace — duplicates among these are
35
+ // excluded from extract-time resolution. (class/struct/actor→class,
36
+ // protocol→interface, enum→enum, typealias→type.) Extensions are NOT symbols,
37
+ // so two extensions of one type never make that type look ambiguous.
38
+ const SWIFT_TYPE_KINDS = new Set(['class', 'interface', 'enum', 'type']);
39
+ // Stdlib globals and scalar conversions that parse as bare calls and would
40
+ // otherwise flood the name-keyed reference store. Suppressed ONLY when
41
+ // unresolved (a file-local function/type shadowing the name keeps its refs).
42
+ // Scalar type names (`String(x)`, `Int(s)`) parse identically to calls — the
43
+ // Go conversion-callee analog. Start small; extend after dogfood measurement.
44
+ const SWIFT_IGNORED_BARE_CALLEES = new Set([
45
+ // global functions
46
+ 'print', 'debugPrint', 'dump', 'assert', 'assertionFailure', 'precondition',
47
+ 'preconditionFailure', 'fatalError', 'min', 'max', 'abs', 'swap', 'zip',
48
+ 'stride', 'type',
49
+ // scalar conversion / init callees
50
+ 'String', 'Int', 'UInt', 'Int8', 'Int16', 'Int32', 'Int64',
51
+ 'UInt8', 'UInt16', 'UInt32', 'UInt64', 'Double', 'Float', 'Float32',
52
+ 'Float64', 'Bool', 'Character',
53
+ ]);
54
+ // `import struct Mod.Sym` form: a kind keyword sits as a direct anon child
55
+ // between `import` and the dotted path, marking a single-symbol import.
56
+ const SWIFT_IMPORT_KINDS = new Set([
57
+ 'typealias', 'struct', 'class', 'enum', 'protocol', 'let', 'var', 'func',
58
+ ]);
59
+ // Callee of a call_expression = its first named child (simple_identifier for
60
+ // bare/constructor calls, navigation_expression for member/static calls).
61
+ // Returns null for SUBSCRIPT access (`arr[i]`, `dict[k]`, `self.items[i]`),
62
+ // which tree-sitter-swift ALSO models as a call_expression — but with a
63
+ // bracketed `value_arguments` ([...]) instead of (...). Without this guard
64
+ // every subscript read emits a spurious `calls` ref (and resolves to a
65
+ // same-named function/method if one exists). Trailing-closure calls
66
+ // (`items.forEach { }`) carry a lambda_literal and no value_arguments — kept.
67
+ function swiftCallCallee(node) {
68
+ const callee = node.firstNamedChild;
69
+ if (!callee)
70
+ return null;
71
+ const suffix = node.namedChildren.find((c) => c.type === 'call_suffix');
72
+ const args = suffix?.namedChildren.find((c) => c.type === 'value_arguments');
73
+ if (args && args.child(0)?.text === '[')
74
+ return null;
75
+ return callee;
76
+ }
77
+ const SWIFT_SELECTORS = [
78
+ { nodeType: 'call_expression', getCallee: swiftCallCallee },
79
+ ];
80
+ // Peels transparent receiver wrappers off a navigation target so a wrapped
81
+ // receiver resolves like the bare form — force-unwrap `a!` (postfix_expression
82
+ // with a `bang` child), bare parens `(a)` (a single-element tuple_expression —
83
+ // Swift has no 1-tuples; comments are NAMED children, so it counts non-comment
84
+ // elements), and an `as` cast (`a as T`, the value is firstNamedChild) — so
85
+ // `a!.x()` / `(a).x()` / `(a as T).x()` resolve like `a.x()`. A peeled `(super)`
86
+ // lands on the super_expression and is dropped by swiftMemberCallInfo's guard.
87
+ function unwrapSwiftReceiver(node) {
88
+ let n = node;
89
+ for (;;) {
90
+ if (n.type === 'postfix_expression') {
91
+ if (n.child(n.childCount - 1)?.type !== 'bang')
92
+ break; // only force-unwrap `!`
93
+ const inner = n.firstNamedChild;
94
+ if (!inner)
95
+ break;
96
+ n = inner;
97
+ }
98
+ else if (n.type === 'tuple_expression') {
99
+ // A parenthesized single value `(a)` parses as a 1-element tuple. Comments
100
+ // (`//`→comment, `/* */`→multiline_comment) are NAMED children, so count by
101
+ // non-comment elements: exactly one means a transparent wrapper (a real
102
+ // 2-tuple `(a, b)` is not), and that element is the operand.
103
+ const elems = n.namedChildren.filter((c) => c.type !== 'comment' && c.type !== 'multiline_comment');
104
+ if (elems.length !== 1)
105
+ break;
106
+ n = elems[0];
107
+ }
108
+ else if (n.type === 'as_expression') {
109
+ const inner = n.firstNamedChild;
110
+ if (!inner)
111
+ break;
112
+ n = inner;
113
+ }
114
+ else
115
+ break;
116
+ }
117
+ return n;
118
+ }
119
+ // Reduces a `navigation_expression` callee (`obj.m()`, `self.m()`, `C.make()`,
120
+ // `Self.make()`) to {receiver, property}, after unwrapSwiftReceiver peels any
121
+ // wrapper off the target. A chained `a.b.c()` target → RECEIVER_OPAQUE (findable
122
+ // by name, never resolved); `self`/`Self` → isSelf (decided here like
123
+ // Python/Rust — no PendingBody.selfReceiverName); `super` → null (parent
124
+ // dispatch, not tracked); a computed/optional suffix (no `simple_identifier`
125
+ // property) emits nothing.
126
+ function swiftMemberCallInfo(callee) {
127
+ if (callee.type !== 'navigation_expression')
128
+ return null;
129
+ const rawTarget = callee.childForFieldName('target');
130
+ const suffix = callee.childForFieldName('suffix');
131
+ if (!rawTarget || suffix?.type !== 'navigation_suffix')
132
+ return null;
133
+ const property = suffix.childForFieldName('suffix');
134
+ if (property?.type !== 'simple_identifier')
135
+ return null; // computed/optional suffix → skip
136
+ const target = unwrapSwiftReceiver(rawTarget);
137
+ if (target.type === 'self_expression') {
138
+ return { receiver: 'self', property: property.text, isSelf: true };
139
+ }
140
+ if (target.type === 'simple_identifier') {
141
+ if (target.text === 'Self')
142
+ return { receiver: 'Self', property: property.text, isSelf: true };
143
+ return { receiver: target.text, property: property.text, isSelf: false };
144
+ }
145
+ // `super.m()` is a parent-class dispatch we deliberately don't track (the
146
+ // TS/Java/Kotlin/C#/Dart rule) — `super` is its own `super_expression` node.
147
+ if (target.type === 'super_expression')
148
+ return null;
149
+ return { receiver: RECEIVER_OPAQUE, property: property.text, isSelf: false }; // chained receiver
150
+ }
151
+ // Dominant Swift stdlib/collection/string/Optional method names (>=4 chars)
152
+ // suppressed when a member call to them is unresolved — capturing chained
153
+ // `.map().filter()` calls otherwise floods the name-keyed store. Domain method
154
+ // names are deliberately absent. <=3-char names (`.map`) are gated downstream
155
+ // by SHORT_NAME_THRESHOLD.
156
+ const SWIFT_IGNORED_MEMBER_CALLEES = new Set([
157
+ 'append', 'insert', 'remove', 'removeAll', 'removeFirst', 'removeLast',
158
+ 'contains', 'filter', 'reduce', 'forEach', 'flatMap', 'compactMap',
159
+ 'sorted', 'reversed', 'first', 'last', 'count', 'isEmpty', 'joined',
160
+ 'prefix', 'suffix', 'dropFirst', 'dropLast', 'enumerated',
161
+ 'replacingOccurrences', 'components', 'split', 'hasPrefix', 'hasSuffix',
162
+ 'lowercased', 'uppercased', 'trimmingCharacters', 'description',
163
+ 'allSatisfy', 'firstIndex', 'lastIndex', 'updateValue',
164
+ 'removeValue', 'sink', 'store', 'receive', 'assign',
165
+ ]);
166
+ // === Complexity (cyclomatic + cognitive) ===
167
+ // CYCLOMATIC pins SwiftLint's `cyclomatic_complexity` (the exact runnable oracle;
168
+ // counts guard/catch — Swift's dominant constructs — the gocyclo/rust-code-analysis
169
+ // precedent of pinning the community tool rather than a closed analyzer). Each
170
+ // node here adds +1: `if`/`else if` (if_statement), the three loops, `guard`, each
171
+ // `catch_block`, and EVERY switch case INCL. `default` (switch_entry covers both).
172
+ // SwiftLint does NOT count `&&`/`||`, ternary, or `??` — so Swift is the only codedeep-mcp
173
+ // language without cyclomatic booleans (cognitive still counts them). A `fallthrough`
174
+ // subtracts 1 (cancelling the case it falls through from) via `cyclomaticDecrement`
175
+ // at the call site. Nested funcs are skipped (SWIFT_SKIP_TYPES); closures
176
+ // (lambda_literal) are descended, so their branches count toward the enclosing
177
+ // function (the gocyclo / Go func_literal closure model).
178
+ const SWIFT_DECISION_NODE_TYPES = new Set([
179
+ 'if_statement', 'for_statement', 'while_statement', 'repeat_while_statement',
180
+ 'guard_statement', 'catch_block', 'switch_entry',
181
+ ]);
182
+ // `&&`/`||` are DISTINCT node types in tree-sitter-swift (not a C-family
183
+ // binary_expression with an operator field), so the shared cFamilyBooleanOperatorKind
184
+ // can't read them — Swift needs its own reader. `??` (nil_coalescing_expression) is
185
+ // NOT a logical operator → null (uncounted, like the whitepaper's &&/|| scope).
186
+ function swiftBooleanOperatorKind(node) {
187
+ if (node.type === 'conjunction_expression')
188
+ return '&&';
189
+ if (node.type === 'disjunction_expression')
190
+ return '||';
191
+ return null;
192
+ }
193
+ // COGNITIVE pins the SonarSource whitepaper (no published cognitive spec for Swift
194
+ // exists, so there is no tool oracle; validated
195
+ // against hand-computed whitepaper fixtures, NOT a tool diff).
196
+ const SWIFT_COGNITIVE_OPTIONS = {
197
+ ifType: 'if_statement',
198
+ conditionField: 'condition',
199
+ // Swift's if_statement is POSITIONAL (no consequence/alternative field) — the
200
+ // engine routes to its positional handler when ifPositionalBlockType is set, and
201
+ // consequenceField/alternativeField are unused there (placeholders).
202
+ consequenceField: '__swift_unused__',
203
+ alternativeField: '__swift_unused__',
204
+ ifPositionalBlockType: 'statements',
205
+ // The `else` KEYWORD (a named token) — splits the consequence from the else branch so
206
+ // an EMPTY `{}` consequence/else body doesn't drop the else's +1 (an empty block emits
207
+ // no `statements` node, so the else must be detected by the keyword, not a 2nd block).
208
+ elseKeywordType: 'else',
209
+ loopTypes: new Set(['for_statement', 'while_statement', 'repeat_while_statement']),
210
+ // loopBodyField UNSET → bump-all (Swift loop headers hold only expressions, so the
211
+ // accepted loop-header overbump never bites — booleans there are flat anyway).
212
+ switchTypes: new Set(['switch_statement']), // whole switch +1, cases nest
213
+ ternaryType: 'ternary_expression',
214
+ catchType: 'catch_block', // each catch surcharges; do_statement is pass-through
215
+ // Closures raise nesting +0 and are descended (Go func_literal rule; matches the
216
+ // extractor, which also descends lambda_literal for call resolution).
217
+ nestOnlyTypes: new Set(['lambda_literal']),
218
+ // break/continue/return/fallthrough are ALL `control_transfer_statement`; only a
219
+ // labeled break/continue (the keyword + a simple_identifier `result`) is +1 flat.
220
+ // `return x` also has a `result`, so the keyword gate is required.
221
+ labeledJumpTypes: new Set(['control_transfer_statement']),
222
+ hasLabel: (n) => {
223
+ const kw = n.child(0)?.text;
224
+ if (kw !== 'break' && kw !== 'continue')
225
+ return false;
226
+ return n.childForFieldName('result')?.type === 'simple_identifier';
227
+ },
228
+ booleanOperatorKind: swiftBooleanOperatorKind,
229
+ // conjunction/disjunction nodes use lhs/rhs operand fields (not left/right).
230
+ booleanLeftField: 'lhs',
231
+ booleanRightField: 'rhs',
232
+ // No-unwrap SENTINEL: a parenthesized boolean is its own run (`(a&&b)&&c`=2, the
233
+ // gocognit/sonar-python convention). tree-sitter-swift wraps parens in a
234
+ // `tuple_expression`, which the engine's skipParens would mis-unwrap (it takes
235
+ // namedChild(0) with no single-element guard, so a real 2-tuple `(a,b)` or a
236
+ // leading comment would be misread) — and there is NO oracle to pin unwrap-vs-not,
237
+ // so the safe choice is no unwrap.
238
+ parenthesizedType: '__swift_no_paren__',
239
+ // `guard` = +1 FLAT (descend at same nesting): the irrefutable-binding analog of
240
+ // Rust's let-else, and Swift's nesting-REDUCING idiom (guard exists to AVOID the
241
+ // nesting an `if let` would add), so no surcharge. Its condition's `&&`/`||` are
242
+ // still counted (flatIncrement descends children). A whitepaper-PRINCIPLE pin — no
243
+ // Swift cognitive oracle exists, so this is documented and fixture-pinned.
244
+ flatIncrement: (n) => n.type === 'guard_statement',
245
+ };
246
+ // A `fallthrough` STATEMENT triggers SwiftLint's `complexity -= 1` (it cancels the +1
247
+ // of the switch case it falls through from — the two cases are one path).
248
+ // tree-sitter-swift parses `fallthrough` in TWO shapes depending on context, and the
249
+ // cyclomatic DFS only walks NAMED children, so both must be detected at a VISITED node:
250
+ // (A) ALONE in a block — a case whose ONLY statement is `fallthrough`, or a
251
+ // `fallthrough` nested inside an if/loop/do (even alongside siblings THERE): a
252
+ // NAMED `simple_identifier` text `fallthrough` whose parent is `statements`.
253
+ // The parent gate is REQUIRED: `fallthrough` is reserved ONLY as a statement, so
254
+ // a member/property/enum-case name or labeled arg (`o.fallthrough()`,
255
+ // `E.fallthrough`, `f(fallthrough:)`) is ALSO a `simple_identifier` text
256
+ // `fallthrough` but sits under `navigation_suffix`/`value_argument_label`, not
257
+ // `statements` — without the gate those spuriously decrement (oracle-confirmed).
258
+ // (B) a case body's TOP-LEVEL statement alongside siblings (`case 1: work();
259
+ // fallthrough`): an ANONYMOUS `fallthrough` node, a DIRECT child of
260
+ // `switch_entry`, which is NOT a `simple_identifier` AND not a NAMED child (so the
261
+ // DFS never visits it). Detected on the `switch_entry` itself (a visited decision
262
+ // node, +1): a switch_entry with a direct `fallthrough` child nets to 0.
263
+ // Both shapes decrement exactly once per fallthrough (a case falls through at most once;
264
+ // shape A is under `statements`, shape B is a direct switch_entry child — never both).
265
+ function swiftFallthroughDecrement(n) {
266
+ if (n.type === 'simple_identifier') {
267
+ return n.text === 'fallthrough' && n.parent?.type === 'statements';
268
+ }
269
+ if (n.type === 'switch_entry') {
270
+ return n.children.some((c) => c?.type === 'fallthrough');
271
+ }
272
+ return false;
273
+ }
274
+ export function extractSwift(tree, content, fileInfo) {
275
+ const ctx = {
276
+ content,
277
+ fileInfo,
278
+ occurrences: new Map(),
279
+ symbols: [],
280
+ imports: [],
281
+ bodies: [],
282
+ };
283
+ extractTopLevel(ctx, tree.rootNode);
284
+ // Same-name types in one file are invalid Swift, so this only fires on
285
+ // broken parses — where refusing resolution beats binding through a
286
+ // half-parsed type.
287
+ const ambiguousTypeNames = collectAmbiguousTypeNames(ctx.symbols, SWIFT_TYPE_KINDS);
288
+ const references = resolveCalls(ctx.bodies, tree.rootNode, ctx.symbols, fileInfo, SWIFT_SELECTORS, SWIFT_SKIP_TYPES, SWIFT_FUNCTION_BODY_SKIP_TYPES, swiftMemberCallInfo, {
289
+ bareCalleeTypes: SWIFT_BARE_CALLEE_TYPES,
290
+ plainCalleeType: 'simple_identifier',
291
+ // Swift allows implicit-self bare method calls (`func a(){ b() }` calls
292
+ // self.b()), so a bare call resolves against the enclosing class first.
293
+ bareCallsBindToEnclosingClass: true,
294
+ bareCallableKinds: SWIFT_BARE_CALLABLE_KINDS,
295
+ // No constructorKinds: construction has no distinct node; it resolves as
296
+ // a bare call to a 'class'-kind symbol via bareCallableKinds.
297
+ ambiguousClassNames: ambiguousTypeNames,
298
+ ignoredBareCallees: SWIFT_IGNORED_BARE_CALLEES,
299
+ ignoredMemberCallees: SWIFT_IGNORED_MEMBER_CALLEES,
300
+ });
301
+ // Per-symbol cyclomatic + cognitive complexity, computed while the tree is alive
302
+ // (the same boundary as resolveCalls: nested funcs skipped, closures descended).
303
+ computeComplexity(ctx.bodies, ctx.symbols, {
304
+ decisionNodeTypes: SWIFT_DECISION_NODE_TYPES,
305
+ skipTypes: SWIFT_SKIP_TYPES,
306
+ // SwiftLint's `fallthrough` −1 — see swiftFallthroughDecrement (two parse shapes).
307
+ cyclomaticDecrement: swiftFallthroughDecrement,
308
+ cognitive: SWIFT_COGNITIVE_OPTIONS,
309
+ });
310
+ return { symbols: ctx.symbols, references, imports: ctx.imports };
311
+ }
312
+ // Top-level source_file items. containerExported is true (the file is the
313
+ // module surface); qualifier is empty.
314
+ function extractTopLevel(ctx, root) {
315
+ for (const child of root.namedChildren) {
316
+ switch (child.type) {
317
+ case 'import_declaration':
318
+ extractImport(ctx, child);
319
+ break;
320
+ case 'class_declaration':
321
+ extractType(ctx, child, '', true);
322
+ break;
323
+ case 'protocol_declaration':
324
+ extractProtocol(ctx, child, '', true);
325
+ break;
326
+ case 'function_declaration':
327
+ extractFunctionLike(ctx, child, 'function', undefined, '', true);
328
+ break;
329
+ case 'property_declaration':
330
+ extractProperty(ctx, child, undefined, '', true);
331
+ break;
332
+ case 'typealias_declaration':
333
+ extractTypealias(ctx, child, undefined, '', true);
334
+ break;
335
+ // comments, operator/precedencegroup declarations, top-level statements,
336
+ // ERROR nodes from #if directive lines — no symbols.
337
+ default:
338
+ break;
339
+ }
340
+ }
341
+ }
342
+ // class / struct / actor / enum (one `class_declaration` node, discriminated by
343
+ // the `declaration_kind` field token) — or an extension, which is methods-apart
344
+ // and not a symbol.
345
+ function extractType(ctx, decl, parentQualifier, containerExported) {
346
+ const declKind = decl.childForFieldName('declaration_kind')?.text;
347
+ if (declKind === 'extension') {
348
+ extractExtension(ctx, decl, parentQualifier, containerExported);
349
+ return;
350
+ }
351
+ const name = decl.childForFieldName('name')?.text;
352
+ if (!name)
353
+ return;
354
+ const kind = declKind === 'enum' ? 'enum' : 'class';
355
+ const exported = containerExported && !isHidden(decl);
356
+ ctx.symbols.push(makeSwiftSymbol(ctx, decl, declSignature(decl, ctx.content), kind, name, `${ctx.fileInfo.path}:${name}`, exported, swiftDoc(decl), parentQualifier));
357
+ const body = decl.childForFieldName('body'); // class_body | enum_class_body
358
+ if (body)
359
+ extractTypeBody(ctx, body, name, joinQualifier(parentQualifier, name), exported);
360
+ }
361
+ // `extension Foo { ... }` — not a symbol. Its members key on the EXTENDED type
362
+ // (`file:Foo.member`), merging into the same methodsByClass[Foo] as Foo's own
363
+ // methods (the Rust impl-merge / Go-receiver pattern), so `self.m()` here and
364
+ // `foo.m()` elsewhere both resolve. The extension's own visibility is the
365
+ // container default for its members (`public extension` exports them).
366
+ function extractExtension(ctx, decl, parentQualifier, containerExported) {
367
+ const name = extensionTypeName(decl);
368
+ if (!name)
369
+ return;
370
+ const exported = containerExported && !isHidden(decl);
371
+ const body = decl.childForFieldName('body');
372
+ if (body)
373
+ extractTypeBody(ctx, body, name, joinQualifier(parentQualifier, name), exported);
374
+ }
375
+ // protocol → 'interface'; its body members are declaration-only methods (no
376
+ // body → populate methodsByClass so conformance and `obj.m()` resolve),
377
+ // property requirements, and associated types.
378
+ function extractProtocol(ctx, decl, parentQualifier, containerExported) {
379
+ const name = decl.childForFieldName('name')?.text;
380
+ if (!name)
381
+ return;
382
+ const exported = containerExported && !isHidden(decl);
383
+ ctx.symbols.push(makeSwiftSymbol(ctx, decl, declSignature(decl, ctx.content), 'interface', name, `${ctx.fileInfo.path}:${name}`, exported, swiftDoc(decl), parentQualifier));
384
+ const body = decl.childForFieldName('body'); // protocol_body
385
+ if (!body)
386
+ return;
387
+ const qualifier = joinQualifier(parentQualifier, name);
388
+ for (const member of body.namedChildren) {
389
+ switch (member.type) {
390
+ case 'protocol_function_declaration': {
391
+ const mname = member.childForFieldName('name')?.text;
392
+ if (!mname)
393
+ break;
394
+ // Declaration-only (no body) — never a PendingBody.
395
+ ctx.symbols.push(makeSwiftSymbol(ctx, member, declSignature(member, ctx.content), 'method', mname, `${ctx.fileInfo.path}:${name}.${mname}`, exported && !isHidden(member), swiftDoc(member), qualifier));
396
+ break;
397
+ }
398
+ case 'protocol_property_declaration':
399
+ extractProperty(ctx, member, name, qualifier, exported);
400
+ break;
401
+ case 'associatedtype_declaration': {
402
+ const aname = member.childForFieldName('name')?.text;
403
+ if (!aname)
404
+ break;
405
+ ctx.symbols.push(makeSwiftSymbol(ctx, member, declSignature(member, ctx.content), 'type', aname, `${ctx.fileInfo.path}:${name}.${aname}`, exported && !isHidden(member), swiftDoc(member), qualifier));
406
+ break;
407
+ }
408
+ default:
409
+ break;
410
+ }
411
+ }
412
+ }
413
+ // A type/extension body (class_body | enum_class_body). enum_entry cases are
414
+ // NOT extracted (the TS/Java/Go/Rust enum-member rule).
415
+ function extractTypeBody(ctx, body, className, qualifier, containerExported) {
416
+ for (const member of body.namedChildren) {
417
+ switch (member.type) {
418
+ case 'function_declaration':
419
+ extractFunctionLike(ctx, member, 'method', className, qualifier, containerExported);
420
+ break;
421
+ case 'init_declaration':
422
+ extractNamedMethod(ctx, member, 'init', className, qualifier, containerExported);
423
+ break;
424
+ case 'deinit_declaration':
425
+ extractNamedMethod(ctx, member, 'deinit', className, qualifier, containerExported);
426
+ break;
427
+ case 'subscript_declaration':
428
+ extractSubscript(ctx, member, className, qualifier, containerExported);
429
+ break;
430
+ case 'property_declaration':
431
+ extractProperty(ctx, member, className, qualifier, containerExported);
432
+ break;
433
+ // Nested types: simple-name FQN, the enclosing chain folds into the
434
+ // hashed qualifier only (Java/Rust nested-type rule).
435
+ case 'class_declaration':
436
+ extractType(ctx, member, qualifier, containerExported);
437
+ break;
438
+ case 'protocol_declaration':
439
+ extractProtocol(ctx, member, qualifier, containerExported);
440
+ break;
441
+ case 'typealias_declaration':
442
+ extractTypealias(ctx, member, className, qualifier, containerExported);
443
+ break;
444
+ default:
445
+ break;
446
+ }
447
+ }
448
+ }
449
+ // function_declaration as a top-level 'function' (className undefined) or a
450
+ // 'method' (className set). Operator funcs name to the operator token text
451
+ // (`+`, `==`). The body becomes a PendingBody so its calls attribute here and
452
+ // self-calls resolve against className.
453
+ function extractFunctionLike(ctx, decl, kind, className, qualifier, containerExported) {
454
+ const name = decl.childForFieldName('name')?.text;
455
+ if (!name)
456
+ return;
457
+ const fqn = className
458
+ ? `${ctx.fileInfo.path}:${className}.${name}`
459
+ : `${ctx.fileInfo.path}:${name}`;
460
+ const sym = makeSwiftSymbol(ctx, decl, declSignature(decl, ctx.content), kind, name, fqn, containerExported && !isHidden(decl), swiftDoc(decl), qualifier);
461
+ ctx.symbols.push(sym);
462
+ const body = decl.childForFieldName('body');
463
+ if (body)
464
+ ctx.bodies.push({ symbolId: sym.id, body, className });
465
+ }
466
+ // init / deinit → a 'method' with a fixed name (TS/Java convention adapted —
467
+ // `init` matches the Swift keyword so find_symbol works and `self.init(...)`
468
+ // delegating calls resolve via methodsByClass[Type]['init']).
469
+ function extractNamedMethod(ctx, decl, name, className, qualifier, containerExported) {
470
+ const sym = makeSwiftSymbol(ctx, decl, declSignature(decl, ctx.content), 'method', name, `${ctx.fileInfo.path}:${className}.${name}`, containerExported && !isHidden(decl), swiftDoc(decl), qualifier);
471
+ ctx.symbols.push(sym);
472
+ const body = decl.childForFieldName('body');
473
+ if (body)
474
+ ctx.bodies.push({ symbolId: sym.id, body, className });
475
+ }
476
+ // subscript → a 'method' named 'subscript' (its `name` field is the RETURN
477
+ // TYPE, so the name is fixed manually). Signature stops at the accessor block.
478
+ function extractSubscript(ctx, decl, className, qualifier, containerExported) {
479
+ const computed = decl.namedChildren.find((c) => c.type === 'computed_property');
480
+ const sigEnd = computed ? computed.startIndex : decl.endIndex;
481
+ const sym = makeSwiftSymbol(ctx, decl, normalizeSignature(ctx.content.slice(decl.startIndex, sigEnd)), 'method', 'subscript', `${ctx.fileInfo.path}:${className}.subscript`, containerExported && !isHidden(decl), swiftDoc(decl), qualifier);
482
+ ctx.symbols.push(sym);
483
+ if (computed)
484
+ ctx.bodies.push({ symbolId: sym.id, body: computed, className });
485
+ }
486
+ // property_declaration / protocol_property_declaration → one 'variable' per
487
+ // bound name. Walk children in order: each `name` pattern opens a binding, and
488
+ // the `value` initializer, `computed_property` (getter/setter), and
489
+ // `willset_didset_block` observers that FOLLOW it belong to THAT binding —
490
+ // so `let a = foo(), b = bar()` attributes bar() to b, not a. Each becomes a
491
+ // PendingBody (className set in a type) so its calls attribute to the property
492
+ // and self-calls resolve — the densest recall surface in idiomatic Swift. The
493
+ // PendingBody body is the accessor/initializer subtree, never the whole
494
+ // declaration, so property-wrapper attribute arguments aren't attributed here
495
+ // (the module-root walk skips them too — `modifiers` ∈ SWIFT_SKIP_TYPES).
496
+ function extractProperty(ctx, decl, className, qualifier, containerExported) {
497
+ const signature = propertySignature(decl, ctx.content);
498
+ const exported = containerExported && !isHidden(decl);
499
+ const doc = swiftDoc(decl);
500
+ let currentId = null;
501
+ for (let i = 0; i < decl.childCount; i++) {
502
+ const child = decl.child(i);
503
+ if (!child)
504
+ continue;
505
+ const field = decl.fieldNameForChild(i);
506
+ if (field === 'name' && child.type === 'pattern') {
507
+ const id = child.childForFieldName('bound_identifier');
508
+ if (id?.type === 'simple_identifier') {
509
+ const fqn = className
510
+ ? `${ctx.fileInfo.path}:${className}.${id.text}`
511
+ : `${ctx.fileInfo.path}:${id.text}`;
512
+ const sym = makeSwiftSymbol(ctx, decl, signature, 'variable', id.text, fqn, exported, doc, qualifier);
513
+ ctx.symbols.push(sym);
514
+ currentId = sym.id;
515
+ }
516
+ else {
517
+ currentId = null; // tuple/wildcard binding — no symbol to attribute to
518
+ }
519
+ }
520
+ else if (currentId !== null &&
521
+ (field === 'value' || child.type === 'computed_property' || child.type === 'willset_didset_block')) {
522
+ ctx.bodies.push({ symbolId: currentId, body: child, className });
523
+ }
524
+ }
525
+ }
526
+ // typealias / associatedtype → 'type'. The `name` field is the alias name
527
+ // (childForFieldName returns the first such field, not the aliased type).
528
+ function extractTypealias(ctx, decl, className, qualifier, containerExported) {
529
+ const name = decl.childForFieldName('name')?.text;
530
+ if (!name)
531
+ return;
532
+ const fqn = className
533
+ ? `${ctx.fileInfo.path}:${className}.${name}`
534
+ : `${ctx.fileInfo.path}:${name}`;
535
+ ctx.symbols.push(makeSwiftSymbol(ctx, decl, declSignature(decl, ctx.content), 'type', name, fqn, containerExported && !isHidden(decl), swiftDoc(decl), qualifier));
536
+ }
537
+ // `import Foundation` → namespace import of the whole module; `import struct
538
+ // Foundation.Data` → a single-symbol import (last segment = name, the rest =
539
+ // module). Dotted module paths (`import A.B`) stay whole-module namespaces.
540
+ function extractImport(ctx, decl) {
541
+ const idNode = decl.namedChildren.find((c) => c.type === 'identifier');
542
+ if (!idNode)
543
+ return;
544
+ const segments = idNode.namedChildren
545
+ .filter((c) => c.type === 'simple_identifier')
546
+ .map((c) => c.text);
547
+ if (segments.length === 0)
548
+ return;
549
+ const line = decl.startPosition.row + 1;
550
+ const hasKind = decl.children.some((c) => c != null && !c.isNamed && SWIFT_IMPORT_KINDS.has(c.text));
551
+ if (hasKind && segments.length >= 2) {
552
+ const name = segments[segments.length - 1];
553
+ ctx.imports.push({
554
+ file: ctx.fileInfo.path,
555
+ sourceModule: segments.slice(0, -1).join('.'),
556
+ importedNames: [{ name }],
557
+ line,
558
+ });
559
+ }
560
+ else {
561
+ ctx.imports.push({
562
+ file: ctx.fileInfo.path,
563
+ sourceModule: segments.join('.'),
564
+ importedNames: [{ name: IMPORT_NAMESPACE, kind: 'namespace' }],
565
+ line,
566
+ });
567
+ }
568
+ }
569
+ // Extended type's simple name = the LAST direct `type_identifier` child of the
570
+ // `user_type` name node. `extension Swift.String` → String (scoped, last
571
+ // segment); `extension Array<Int>` → Array (the type_arguments node is a
572
+ // separate child, not a type_identifier); `extension Dictionary` → Dictionary.
573
+ function extensionTypeName(decl) {
574
+ const nameNode = decl.childForFieldName('name');
575
+ if (!nameNode)
576
+ return null;
577
+ let result = null;
578
+ for (const child of nameNode.namedChildren) {
579
+ if (child.type === 'type_identifier')
580
+ result = child.text;
581
+ }
582
+ return result;
583
+ }
584
+ // Property signature stops before the accessor/observer/requirement block so a
585
+ // long computed body doesn't blow the 120-char display cap (the initializer
586
+ // value is kept — it's informative for constants).
587
+ function propertySignature(decl, content) {
588
+ let cut = decl.endIndex;
589
+ for (const child of decl.namedChildren) {
590
+ if (child.type === 'computed_property' ||
591
+ child.type === 'willset_didset_block' ||
592
+ child.type === 'protocol_property_requirements') {
593
+ cut = child.startIndex;
594
+ break;
595
+ }
596
+ }
597
+ return normalizeSignature(content.slice(decl.startIndex, cut));
598
+ }
599
+ // exported = NO `private`/`fileprivate` visibility modifier (so absent =
600
+ // internal, public, and open all export — internal is the module's default and
601
+ // counts as exported). `private(set)` keeps a getter at the declared level, so
602
+ // its visibility_modifier text is `private(set)` (≠ `private`) and stays
603
+ // visible. Members AND-in their container's exportedness via the caller.
604
+ function isHidden(decl) {
605
+ const mods = decl.namedChildren.find((c) => c.type === 'modifiers');
606
+ if (!mods)
607
+ return false;
608
+ for (const m of mods.namedChildren) {
609
+ if (m.type === 'visibility_modifier' && (m.text === 'private' || m.text === 'fileprivate')) {
610
+ return true;
611
+ }
612
+ }
613
+ return false;
614
+ }
615
+ // Module path / enclosing-type chain only disambiguate hashed ids — they never
616
+ // reach FQN parsing — so any unique join works.
617
+ function joinQualifier(a, b) {
618
+ if (!a)
619
+ return b;
620
+ if (!b)
621
+ return a;
622
+ return `${a}.${b}`;
623
+ }
624
+ function makeSwiftSymbol(ctx, node, signature, kind, name, fqn, exported, doc, qualifier = '') {
625
+ // Repeated identical (name, kind, signature, qualifier) tuples — e.g. a
626
+ // same-signature method across an extension and the type — get an ordinal so
627
+ // ids stay unique per file.
628
+ const key = `${name}\0${kind}\0${signature}\0${qualifier}`;
629
+ const n = (ctx.occurrences.get(key) ?? 0) + 1;
630
+ ctx.occurrences.set(key, n);
631
+ const effectiveQualifier = n === 1 ? qualifier : `${qualifier}#${n}`;
632
+ return {
633
+ // The id hashes the FULL signature; only the stored copy is capped.
634
+ id: symbolId(ctx.fileInfo.path, name, kind, signature, effectiveQualifier),
635
+ name,
636
+ fqn,
637
+ kind,
638
+ file: ctx.fileInfo.path,
639
+ startLine: node.startPosition.row + 1,
640
+ endLine: node.endPosition.row + 1,
641
+ signature: signature.slice(0, SIGNATURE_DISPLAY_CAP),
642
+ doc,
643
+ exported,
644
+ language: ctx.fileInfo.language,
645
+ };
646
+ }
647
+ // Doc = the immediately-preceding `///` line block or a `/** */` block. Swift
648
+ // attributes live INSIDE the declaration (no Rust-style sibling skipping) and
649
+ // comments carry no trailing newline, so this is the Go contiguous-block walk:
650
+ // nearest adjacent non-trailing doc comment, first line with content. Plain
651
+ // `//` / `/* */` are NOT doc comments (DocC convention).
652
+ function swiftDoc(decl) {
653
+ const nearest = decl.previousNamedSibling;
654
+ if (!nearest || !isDocComment(nearest))
655
+ return null;
656
+ if (nearest.endPosition.row !== decl.startPosition.row - 1)
657
+ return null;
658
+ if (isTrailingComment(nearest))
659
+ return null;
660
+ // A `/** */` block is a single node.
661
+ if (nearest.type === 'multiline_comment')
662
+ return commentDocLine(nearest.text);
663
+ // Walk up the contiguous `///` line block.
664
+ const chain = [nearest];
665
+ for (;;) {
666
+ const bottom = chain[chain.length - 1];
667
+ const prev = bottom.previousNamedSibling;
668
+ if (!prev ||
669
+ prev.type !== 'comment' ||
670
+ !prev.text.startsWith('///') ||
671
+ prev.endPosition.row !== bottom.startPosition.row - 1 ||
672
+ isTrailingComment(prev)) {
673
+ break;
674
+ }
675
+ chain.push(prev);
676
+ }
677
+ chain.reverse(); // document order
678
+ for (const comment of chain) {
679
+ const line = commentDocLine(comment.text);
680
+ if (line)
681
+ return line;
682
+ }
683
+ return null;
684
+ }
685
+ function isDocComment(node) {
686
+ if (node.type === 'comment')
687
+ return node.text.startsWith('///');
688
+ if (node.type === 'multiline_comment')
689
+ return node.text.startsWith('/**');
690
+ return false;
691
+ }