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,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
|
+
}
|