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,709 @@
|
|
|
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 { cFamilyBooleanOperatorKind, computeComplexity, isCFamilyBooleanOperator, } from '../complexity.js';
|
|
4
|
+
// Nested `fun` 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: calls inside `items.forEach { f() }`
|
|
8
|
+
// attribute to the enclosing body (the Go func_literal / Java lambda rule, not
|
|
9
|
+
// the TS arrow rule).
|
|
10
|
+
const KOTLIN_FUNCTION_BODY_SKIP_TYPES = new Set(['function_declaration']);
|
|
11
|
+
// walkCalls skip set: nested funcs (above) PLUS `modifiers` (the Swift
|
|
12
|
+
// property-wrapper rule). Kotlin annotation arguments must be compile-time
|
|
13
|
+
// constants — never function calls — in valid code, so this is mostly
|
|
14
|
+
// defensive: when an annotation attaches in a declaration's `modifiers` (the
|
|
15
|
+
// normal form), skipping it keeps any nested constructor-invocation arg out of
|
|
16
|
+
// the call graph. (A real call inside an annotation arg only parses on invalid
|
|
17
|
+
// Kotlin, where tree-sitter detaches the annotation into a sibling expression
|
|
18
|
+
// the `modifiers` skip can't reach — an uncompilable, ignorable edge case.)
|
|
19
|
+
const KOTLIN_SKIP_TYPES = new Set(['function_declaration', 'modifiers']);
|
|
20
|
+
// A bare `identifier` callee binds to free functions AND classes. 'class' is
|
|
21
|
+
// here — the inverse of Go/Rust — because Kotlin construction is
|
|
22
|
+
// shape-identical to a call (`Foo()` has no `new` and no distinct construction
|
|
23
|
+
// node, exactly like Swift), so `bareCallableKinds` is the ONLY lever to
|
|
24
|
+
// resolve `Foo()` to its type. Accepted error class: a bare call colliding with
|
|
25
|
+
// a same-named type resolves to the type, which for Kotlin `Type(...)` is
|
|
26
|
+
// construction by convention. The enclosing-class fallback runs FIRST
|
|
27
|
+
// (bareCallsBindToEnclosingClass), so an implicit-this method call beats a
|
|
28
|
+
// same-named type — which keeps 'class' safe. (Unlike Swift, the bare callee is
|
|
29
|
+
// the engine-default `identifier`, so no plainCalleeType override is needed.)
|
|
30
|
+
const KOTLIN_BARE_CALLABLE_KINDS = new Set(['function', 'class']);
|
|
31
|
+
// Kinds sharing the simple-name FQN namespace — duplicates among these are
|
|
32
|
+
// excluded from extract-time resolution. (class/object/companion→class,
|
|
33
|
+
// interface→interface, enum→enum, typealias→type.)
|
|
34
|
+
const KOTLIN_TYPE_KINDS = new Set(['class', 'interface', 'enum', 'type']);
|
|
35
|
+
// Kotlin scope functions and stdlib globals that parse as bare calls and would
|
|
36
|
+
// otherwise flood the name-keyed reference store. Suppressed ONLY when
|
|
37
|
+
// unresolved (a file-local function shadowing the name keeps its refs). Start
|
|
38
|
+
// small; extend after dogfood measurement.
|
|
39
|
+
const KOTLIN_IGNORED_BARE_CALLEES = new Set([
|
|
40
|
+
// scope functions — appear in nearly every file, never resolve
|
|
41
|
+
'let', 'run', 'apply', 'also', 'with', 'takeIf', 'takeUnless', 'use',
|
|
42
|
+
// assertions / control
|
|
43
|
+
'require', 'requireNotNull', 'check', 'checkNotNull', 'error', 'TODO',
|
|
44
|
+
'assert',
|
|
45
|
+
// printing
|
|
46
|
+
'print', 'println',
|
|
47
|
+
// collection / delegate builders — the dominant unresolvable bare-call flood
|
|
48
|
+
// measured on okio/moshi (every `by lazy {}` + collection literal). All
|
|
49
|
+
// gated on unresolved, so a file-local definition of the same name keeps refs.
|
|
50
|
+
'lazy', 'lazyOf', 'listOf', 'listOfNotNull', 'mutableListOf', 'arrayListOf',
|
|
51
|
+
'setOf', 'mutableSetOf', 'hashSetOf', 'linkedSetOf', 'sortedSetOf',
|
|
52
|
+
'mapOf', 'mutableMapOf', 'hashMapOf', 'linkedMapOf', 'sortedMapOf',
|
|
53
|
+
'arrayOf', 'emptyList', 'emptyMap', 'emptySet', 'emptyArray',
|
|
54
|
+
'sequenceOf', 'buildList', 'buildMap', 'buildSet', 'buildString',
|
|
55
|
+
]);
|
|
56
|
+
// Type nodes that can carry an extension receiver (`fun String.f()`,
|
|
57
|
+
// `val String?.x`). Non-nominal receivers (function_type, tuple, etc.) are
|
|
58
|
+
// skipped — they have no single type name to key the member on.
|
|
59
|
+
const KOTLIN_RECEIVER_TYPES = new Set(['user_type', 'nullable_type']);
|
|
60
|
+
// Callee of a call_expression = its first named child (identifier for
|
|
61
|
+
// bare/construction calls, navigation_expression for member/static calls).
|
|
62
|
+
function kotlinCallCallee(node) {
|
|
63
|
+
return node.firstNamedChild;
|
|
64
|
+
}
|
|
65
|
+
const KOTLIN_SELECTORS = [
|
|
66
|
+
{ nodeType: 'call_expression', getCallee: kotlinCallCallee },
|
|
67
|
+
];
|
|
68
|
+
// Peels transparent receiver wrappers off a navigation receiver so a wrapped
|
|
69
|
+
// receiver resolves like the bare form — non-null assertion `a!!`, parens `(a)`,
|
|
70
|
+
// and an `as`/`as?` cast — so `a!!.x()` / `(a).x()` resolve like `a.x()`. `a!!`,
|
|
71
|
+
// `a++`/`a--`, and a parenthesized prefix `-a`/`!a` are all `unary_expression`, so
|
|
72
|
+
// peel ONLY when the trailing child is the anon `!!` token (prefix forms have the
|
|
73
|
+
// named operand last → skipped). firstNamedChild is the operand. A peeled
|
|
74
|
+
// `(super)`/`(this)` lands on the super_expression/this_expression and re-hits
|
|
75
|
+
// kotlinMemberCallInfo's super-drop / self handling below. NOTE: an `as`-cast is
|
|
76
|
+
// peeled to its VALUE (the type is discarded) — `(this as Foo).m()` becomes a
|
|
77
|
+
// self-call on the enclosing class (correct for virtual members via dynamic
|
|
78
|
+
// dispatch; a rare wrong-target only for shadowing extension funcs — see
|
|
79
|
+
// MCR-java-cast-receiver, accepted).
|
|
80
|
+
function unwrapKotlinReceiver(node) {
|
|
81
|
+
let n = node;
|
|
82
|
+
for (;;) {
|
|
83
|
+
if (n.type === 'parenthesized_expression') {
|
|
84
|
+
let inner = n.firstNamedChild;
|
|
85
|
+
// tree-sitter-kotlin names comments `line_comment`/`block_comment`, never `comment`.
|
|
86
|
+
while (inner && (inner.type === 'line_comment' || inner.type === 'block_comment'))
|
|
87
|
+
inner = inner.nextNamedSibling;
|
|
88
|
+
if (!inner)
|
|
89
|
+
break;
|
|
90
|
+
n = inner;
|
|
91
|
+
}
|
|
92
|
+
else if (n.type === 'unary_expression') {
|
|
93
|
+
const last = n.child(n.childCount - 1);
|
|
94
|
+
if (!last || last.isNamed || last.text !== '!!')
|
|
95
|
+
break; // non-null assertion only
|
|
96
|
+
const inner = n.firstNamedChild;
|
|
97
|
+
if (!inner)
|
|
98
|
+
break;
|
|
99
|
+
n = inner;
|
|
100
|
+
}
|
|
101
|
+
else if (n.type === 'as_expression') {
|
|
102
|
+
const inner = n.firstNamedChild;
|
|
103
|
+
if (!inner)
|
|
104
|
+
break;
|
|
105
|
+
n = inner;
|
|
106
|
+
}
|
|
107
|
+
else
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
return n;
|
|
111
|
+
}
|
|
112
|
+
// Reduces a `navigation_expression` callee (`obj.m()`, `this.m()`, `C.make()`)
|
|
113
|
+
// to {receiver, property}, after unwrapKotlinReceiver peels any wrapper off the
|
|
114
|
+
// receiver. A chained `a.b.c()` receiver → RECEIVER_OPAQUE (findable by name,
|
|
115
|
+
// never resolved); `this`/labeled `this@Label` → self / label class (decided
|
|
116
|
+
// here like Swift/Python — no PendingBody.selfReceiverName); `super` → null
|
|
117
|
+
// (parent dispatch, not tracked); `::` callable refs (`Foo::bar`) and computed
|
|
118
|
+
// receivers (no `identifier` property) emit nothing.
|
|
119
|
+
function kotlinMemberCallInfo(callee) {
|
|
120
|
+
if (callee.type !== 'navigation_expression')
|
|
121
|
+
return null;
|
|
122
|
+
// children: <receiver expr> <op '.'|'?.'|'::'> <identifier property>
|
|
123
|
+
const rawReceiver = callee.namedChild(0);
|
|
124
|
+
const property = callee.namedChild(1);
|
|
125
|
+
if (!rawReceiver || property?.type !== 'identifier')
|
|
126
|
+
return null;
|
|
127
|
+
// `::` is a member/callable reference, not a member call — skip it.
|
|
128
|
+
if (nodeHasAnonChild(callee, '::'))
|
|
129
|
+
return null;
|
|
130
|
+
const receiver = unwrapKotlinReceiver(rawReceiver);
|
|
131
|
+
if (receiver.type === 'this_expression') {
|
|
132
|
+
// `this@Label.m()` (a labeled this) names an OUTER receiver — resolve
|
|
133
|
+
// against the labeled class, not the enclosing one (binding it as self
|
|
134
|
+
// could resolve to a same-named method on the wrong, inner class). A plain
|
|
135
|
+
// `this` has no identifier child and stays a self-call.
|
|
136
|
+
const label = childOfType(receiver, 'identifier');
|
|
137
|
+
if (label)
|
|
138
|
+
return { receiver: label.text, property: property.text, isSelf: false };
|
|
139
|
+
return { receiver: 'this', property: property.text, isSelf: true };
|
|
140
|
+
}
|
|
141
|
+
if (receiver.type === 'identifier') {
|
|
142
|
+
return { receiver: receiver.text, property: property.text, isSelf: false };
|
|
143
|
+
}
|
|
144
|
+
// `super.m()` is a parent-class dispatch we deliberately don't track (the
|
|
145
|
+
// TS/Java/Swift/C#/Dart rule) — `super` is its own `super_expression` node.
|
|
146
|
+
if (receiver.type === 'super_expression')
|
|
147
|
+
return null;
|
|
148
|
+
return { receiver: RECEIVER_OPAQUE, property: property.text, isSelf: false }; // chained receiver
|
|
149
|
+
}
|
|
150
|
+
// Dominant Kotlin stdlib/collection/string/scope method names (>=4 chars)
|
|
151
|
+
// suppressed when a member call to them is unresolved — capturing chained
|
|
152
|
+
// `.map { }.filter { }` calls otherwise floods the name-keyed store. Domain
|
|
153
|
+
// method names are deliberately absent. <=3-char names (`.map`) are gated
|
|
154
|
+
// downstream by SHORT_NAME_THRESHOLD.
|
|
155
|
+
const KOTLIN_IGNORED_MEMBER_CALLEES = new Set([
|
|
156
|
+
'filter', 'filterNot', 'forEach', 'flatMap', 'reduce', 'fold', 'sortedBy',
|
|
157
|
+
'sortedByDescending', 'groupBy', 'associateBy', 'distinct', 'first',
|
|
158
|
+
'firstOrNull', 'last', 'lastOrNull', 'single', 'count', 'sumOf', 'maxOf',
|
|
159
|
+
'minOf', 'maxByOrNull', 'minByOrNull', 'contains', 'containsKey', 'isEmpty',
|
|
160
|
+
'isNotEmpty', 'toList', 'toSet', 'toMap', 'toMutableList', 'joinToString',
|
|
161
|
+
'take', 'drop', 'plus', 'minus', 'indexOf', 'remove', 'clear',
|
|
162
|
+
'startsWith', 'endsWith', 'substring', 'replace', 'split', 'trim',
|
|
163
|
+
'lowercase', 'uppercase', 'getOrNull', 'getOrDefault', 'getOrElse',
|
|
164
|
+
// Scope functions in MEMBER position (`x.apply{}`, `foo().also{}`) are THE
|
|
165
|
+
// dominant chained Kotlin member call and pure-stdlib — measured on okio/moshi:
|
|
166
|
+
// apply 73 call-sites/~1.4% in-repo, also 49/0% → flood, ~0 recall stake (the
|
|
167
|
+
// bare `with(x){}` form is covered by KOTLIN_IGNORED_BARE_CALLEES, but the
|
|
168
|
+
// member forms route through here). let/run/use (<=3 chars) are SHORT_NAME_THRESHOLD-gated.
|
|
169
|
+
'apply', 'also', 'takeIf', 'takeUnless',
|
|
170
|
+
]);
|
|
171
|
+
// ── complexity (cyclomatic + cognitive, BOTH pinned to sonar-kotlin) ─────────
|
|
172
|
+
// CYCLOMATIC (sonar-kotlin CyclomaticComplexityVisitor): `1 + decision points`, +1 per
|
|
173
|
+
// `if` (incl. an if-used-as-EXPRESSION — every Kotlin `if` is one `if_expression`), per
|
|
174
|
+
// EACH `when_entry` INCLUDING the `else` entry (sonar-kotlin visits every whenEntry — a
|
|
175
|
+
// deliberate divergence from the `default`/`else`-EXCLUDED rule in TS/Go/Java/Swift), per
|
|
176
|
+
// loop, and per `&&`/`||`. NOT counted: Elvis `?:` (a `binary_expression` whose operator
|
|
177
|
+
// token `?:` isCFamilyBooleanOperator rejects), break/continue, catch, scope functions.
|
|
178
|
+
// Lambdas are DESCENDED (lambda_literal ∉ KOTLIN_SKIP_TYPES) so their branches count
|
|
179
|
+
// toward the enclosing function.
|
|
180
|
+
const KOTLIN_DECISION_NODE_TYPES = new Set([
|
|
181
|
+
'if_expression', 'for_statement', 'while_statement', 'do_while_statement', 'when_entry',
|
|
182
|
+
]);
|
|
183
|
+
// A labeled break/continue is a `labeled_expression` whose `label` child text is the jump
|
|
184
|
+
// keyword + `@` (`break@`/`continue@`); the target label name follows as an identifier.
|
|
185
|
+
// Labeled LOOPS attach `label` directly to the loop (not via labeled_expression), and a
|
|
186
|
+
// labeled non-jump (`tag@ run {}`) carries a different label text — so this gate fires
|
|
187
|
+
// only for labeled jumps (+1 flat cognitive, the whitepaper rule). Plain break/continue
|
|
188
|
+
// parse as bare `identifier`s (no labeled_expression) → +0.
|
|
189
|
+
function kotlinHasJumpLabel(node) {
|
|
190
|
+
const label = childOfType(node, 'label')?.text;
|
|
191
|
+
return label === 'break@' || label === 'continue@';
|
|
192
|
+
}
|
|
193
|
+
// COGNITIVE — pinned EXACTLY to sonar-kotlin's CognitiveComplexity (verbatim source read,
|
|
194
|
+
// NOT plain whitepaper: it diverges in three sonar-kotlin-specific ways, all replicated for
|
|
195
|
+
// SonarQube-parity). (1) Kotlin's `if` is POSITIONAL with an anonymous `else` and possibly
|
|
196
|
+
// brace-less branches → ifConsequenceFromNamedChildren (see complexity.ts), and the else +1 is
|
|
197
|
+
// charged ONLY when the else BODY is a `block` or an else-if (elseChargeBlockType) — a
|
|
198
|
+
// brace-less `else expr` is the ternary form, NO +1 (sonar-kotlin handleIfExpression's
|
|
199
|
+
// `it is KtBlockExpression || it is KtIfExpression` gate). (2) `when` is the switch analog
|
|
200
|
+
// (whole +1, entries nest). (3) Booleans are C-family with NO paren-unwrap (sonar-kotlin's
|
|
201
|
+
// flattenOperators recurses only into KtBinaryExpression operands, so `(a&&b)&&c` = 2 runs —
|
|
202
|
+
// unlike sonar-java). do-while NESTS its body but adds NO increment (sonar-kotlin's cognitive
|
|
203
|
+
// visit handles KtFor/KtWhile but NOT KtDoWhileExpression — a sibling, not a subclass — while
|
|
204
|
+
// KtLoopExpression still raises nesting); so do_while_statement is nestOnly, not a loopType.
|
|
205
|
+
// Cyclomatic still counts do-while (its visitLoopExpression covers all loops). NO recursion /
|
|
206
|
+
// Elvis (sonar-kotlin omits both).
|
|
207
|
+
const KOTLIN_COGNITIVE_OPTIONS = {
|
|
208
|
+
ifType: 'if_expression',
|
|
209
|
+
conditionField: 'condition',
|
|
210
|
+
// Positional path (no consequence/alternative field): unused placeholders.
|
|
211
|
+
consequenceField: '__kotlin_unused__',
|
|
212
|
+
alternativeField: '__kotlin_unused__',
|
|
213
|
+
ifConsequenceFromNamedChildren: true,
|
|
214
|
+
elseKeywordType: 'else', // anon `else` token splits consequence/else (handles `;` empty branches)
|
|
215
|
+
elseChargeBlockType: 'block', // else +1 ONLY for a block or else-if body (sonar-kotlin ternary gate)
|
|
216
|
+
loopTypes: new Set(['for_statement', 'while_statement']), // NOT do_while (sonar-kotlin omits its increment)
|
|
217
|
+
switchTypes: new Set(['when_expression']), // whole when +1, entries nest
|
|
218
|
+
ternaryType: '__kotlin_no_ternary__', // the if-expression IS the ternary (handled by ifType)
|
|
219
|
+
catchType: 'catch_block', // each catch surcharges; try/finally pass through
|
|
220
|
+
// closures nest +0; do_while nests its body but adds NO increment (sonar-kotlin omits it).
|
|
221
|
+
nestOnlyTypes: new Set(['lambda_literal', 'do_while_statement']),
|
|
222
|
+
labeledJumpTypes: new Set(['labeled_expression']),
|
|
223
|
+
hasLabel: kotlinHasJumpLabel,
|
|
224
|
+
booleanOperatorKind: cFamilyBooleanOperatorKind, // &&/|| (Elvis ?: → null); default left/right
|
|
225
|
+
parenthesizedType: '__kotlin_no_paren__', // NO unwrap: (a&&b)&&c = 2 runs (sonar-kotlin)
|
|
226
|
+
};
|
|
227
|
+
export function extractKotlin(tree, content, fileInfo) {
|
|
228
|
+
const ctx = {
|
|
229
|
+
content,
|
|
230
|
+
fileInfo,
|
|
231
|
+
occurrences: new Map(),
|
|
232
|
+
symbols: [],
|
|
233
|
+
imports: [],
|
|
234
|
+
bodies: [],
|
|
235
|
+
};
|
|
236
|
+
extractTopLevel(ctx, tree.rootNode);
|
|
237
|
+
// Same-name types in one file are invalid Kotlin, so this only fires on
|
|
238
|
+
// broken parses — where refusing resolution beats binding through a
|
|
239
|
+
// half-parsed type.
|
|
240
|
+
const ambiguousTypeNames = collectAmbiguousTypeNames(ctx.symbols, KOTLIN_TYPE_KINDS);
|
|
241
|
+
const references = resolveCalls(ctx.bodies, tree.rootNode, ctx.symbols, fileInfo, KOTLIN_SELECTORS, KOTLIN_SKIP_TYPES, KOTLIN_FUNCTION_BODY_SKIP_TYPES, kotlinMemberCallInfo, {
|
|
242
|
+
// Bare/construction callee is the engine-default `identifier` — no
|
|
243
|
+
// plainCalleeType override.
|
|
244
|
+
// Kotlin allows implicit-this bare method calls (`fun a(){ b() }` calls
|
|
245
|
+
// this.b()), so a bare call resolves against the enclosing class first.
|
|
246
|
+
bareCallsBindToEnclosingClass: true,
|
|
247
|
+
bareCallableKinds: KOTLIN_BARE_CALLABLE_KINDS,
|
|
248
|
+
// No constructorKinds: construction has no distinct node; it resolves as
|
|
249
|
+
// a bare call to a 'class'-kind symbol via bareCallableKinds.
|
|
250
|
+
ambiguousClassNames: ambiguousTypeNames,
|
|
251
|
+
ignoredBareCallees: KOTLIN_IGNORED_BARE_CALLEES,
|
|
252
|
+
ignoredMemberCallees: KOTLIN_IGNORED_MEMBER_CALLEES,
|
|
253
|
+
});
|
|
254
|
+
// Per-symbol cyclomatic + cognitive complexity, computed while the tree is alive
|
|
255
|
+
// (same boundary as resolveCalls: nested funcs skipped, lambdas descended).
|
|
256
|
+
computeComplexity(ctx.bodies, ctx.symbols, {
|
|
257
|
+
decisionNodeTypes: KOTLIN_DECISION_NODE_TYPES,
|
|
258
|
+
extraDecisionPredicate: isCFamilyBooleanOperator, // &&/|| (+1 each); Elvis excluded
|
|
259
|
+
skipTypes: KOTLIN_SKIP_TYPES,
|
|
260
|
+
cognitive: KOTLIN_COGNITIVE_OPTIONS,
|
|
261
|
+
});
|
|
262
|
+
return { symbols: ctx.symbols, references, imports: ctx.imports };
|
|
263
|
+
}
|
|
264
|
+
// Top-level source_file items. containerExported is true (the file is the
|
|
265
|
+
// module surface); qualifier is empty.
|
|
266
|
+
function extractTopLevel(ctx, root) {
|
|
267
|
+
for (const child of root.namedChildren) {
|
|
268
|
+
switch (child.type) {
|
|
269
|
+
case 'import':
|
|
270
|
+
extractImport(ctx, child);
|
|
271
|
+
break;
|
|
272
|
+
case 'class_declaration':
|
|
273
|
+
extractClass(ctx, child, '', true);
|
|
274
|
+
break;
|
|
275
|
+
case 'object_declaration':
|
|
276
|
+
extractObject(ctx, child, '', true);
|
|
277
|
+
break;
|
|
278
|
+
case 'function_declaration':
|
|
279
|
+
extractFunction(ctx, child, undefined, '', true);
|
|
280
|
+
break;
|
|
281
|
+
case 'property_declaration':
|
|
282
|
+
extractProperty(ctx, child, undefined, '', true);
|
|
283
|
+
break;
|
|
284
|
+
case 'type_alias':
|
|
285
|
+
extractTypeAlias(ctx, child, undefined, '', true);
|
|
286
|
+
break;
|
|
287
|
+
// package_header, comments, top-level statements, ERROR nodes — no symbols.
|
|
288
|
+
default:
|
|
289
|
+
break;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
// class / interface / data / sealed / enum — one `class_declaration` node,
|
|
294
|
+
// discriminated by the `interface` keyword token and the `enum_class_body`.
|
|
295
|
+
function extractClass(ctx, decl, parentQualifier, containerExported) {
|
|
296
|
+
const name = decl.childForFieldName('name')?.text;
|
|
297
|
+
if (!name)
|
|
298
|
+
return;
|
|
299
|
+
const kind = classKind(decl);
|
|
300
|
+
const exported = containerExported && !isHidden(decl);
|
|
301
|
+
const sym = makeKotlinSymbol(ctx, decl, declSignature(decl, ctx.content), kind, name, memberFqn(ctx, undefined, name), exported, kotlinDoc(decl), parentQualifier);
|
|
302
|
+
ctx.symbols.push(sym);
|
|
303
|
+
const qualifier = joinQualifier(parentQualifier, name);
|
|
304
|
+
// Primary-constructor `val`/`var` parameters are class properties; the
|
|
305
|
+
// constructor surface (init blocks + default args) becomes a synthesized
|
|
306
|
+
// 'constructor' method.
|
|
307
|
+
const primary = childOfType(decl, 'primary_constructor');
|
|
308
|
+
if (primary)
|
|
309
|
+
extractPrimaryCtorProperties(ctx, primary, name, qualifier, exported);
|
|
310
|
+
// class/enum are constructable (synthesize a 'constructor'); interface has no
|
|
311
|
+
// construction surface but never triggers synthesis anyway.
|
|
312
|
+
const body = childOfType(decl, 'class_body', 'enum_class_body');
|
|
313
|
+
if (body)
|
|
314
|
+
extractClassBody(ctx, body, name, qualifier, exported, primary, sym.id, true);
|
|
315
|
+
else if (primary)
|
|
316
|
+
maybeSynthesizeConstructor(ctx, primary, [], [], name, qualifier, exported);
|
|
317
|
+
}
|
|
318
|
+
// `object Foo { ... }` (named singleton) → 'class' kind, members keyed on Foo.
|
|
319
|
+
function extractObject(ctx, decl, parentQualifier, containerExported) {
|
|
320
|
+
const name = decl.childForFieldName('name')?.text;
|
|
321
|
+
if (!name)
|
|
322
|
+
return;
|
|
323
|
+
const exported = containerExported && !isHidden(decl);
|
|
324
|
+
const sym = makeKotlinSymbol(ctx, decl, declSignature(decl, ctx.content), 'class', name, memberFqn(ctx, undefined, name), exported, kotlinDoc(decl), parentQualifier);
|
|
325
|
+
ctx.symbols.push(sym);
|
|
326
|
+
const body = childOfType(decl, 'class_body');
|
|
327
|
+
// An object is a singleton — NOT constructable. Its init-block calls attribute
|
|
328
|
+
// to the object symbol itself, not a phantom 'constructor'.
|
|
329
|
+
if (body)
|
|
330
|
+
extractClassBody(ctx, body, name, joinQualifier(parentQualifier, name), exported, null, sym.id, false);
|
|
331
|
+
}
|
|
332
|
+
// A class/object/enum body. Members key on `className`. A companion object's
|
|
333
|
+
// members merge into the SAME className (so `Outer.foo()` resolves) — its name
|
|
334
|
+
// is intentionally ignored. enum_entry cases are NOT extracted (the
|
|
335
|
+
// TS/Java/Go/Rust/Swift enum-member rule); member declarations after the `;`
|
|
336
|
+
// ARE. `primary` (the enclosing class's primary constructor, if any) is folded
|
|
337
|
+
// into the synthesized constructor alongside any init blocks.
|
|
338
|
+
function extractClassBody(ctx, body, className, qualifier, containerExported, primary, containerId, constructable) {
|
|
339
|
+
const initBlocks = [];
|
|
340
|
+
const enumEntries = [];
|
|
341
|
+
for (const member of body.namedChildren) {
|
|
342
|
+
switch (member.type) {
|
|
343
|
+
case 'function_declaration':
|
|
344
|
+
extractFunction(ctx, member, className, qualifier, containerExported);
|
|
345
|
+
break;
|
|
346
|
+
case 'property_declaration':
|
|
347
|
+
extractProperty(ctx, member, className, qualifier, containerExported);
|
|
348
|
+
break;
|
|
349
|
+
case 'secondary_constructor':
|
|
350
|
+
extractSecondaryConstructor(ctx, member, className, qualifier, containerExported);
|
|
351
|
+
break;
|
|
352
|
+
case 'anonymous_initializer':
|
|
353
|
+
initBlocks.push(member);
|
|
354
|
+
break;
|
|
355
|
+
// Entries aren't symbols, but their constructor-argument calls
|
|
356
|
+
// (`RED(make())`) run at enum init — owned by the synthesized constructor.
|
|
357
|
+
case 'enum_entry':
|
|
358
|
+
enumEntries.push(member);
|
|
359
|
+
break;
|
|
360
|
+
case 'companion_object': {
|
|
361
|
+
// Members key on the ENCLOSING class (companions are accessed via the
|
|
362
|
+
// class name). The companion's own visibility gates them. A companion is
|
|
363
|
+
// NOT constructable: its init-block calls attribute to the enclosing
|
|
364
|
+
// class symbol, never a phantom/duplicate `<Class>.constructor`.
|
|
365
|
+
const compExported = containerExported && !isHidden(member);
|
|
366
|
+
const compBody = childOfType(member, 'class_body');
|
|
367
|
+
if (compBody)
|
|
368
|
+
extractClassBody(ctx, compBody, className, qualifier, compExported, null, containerId, false);
|
|
369
|
+
break;
|
|
370
|
+
}
|
|
371
|
+
// Nested types: simple-name FQN, the enclosing chain folds into the
|
|
372
|
+
// hashed qualifier only (Java/Rust/Swift nested-type rule).
|
|
373
|
+
case 'class_declaration':
|
|
374
|
+
extractClass(ctx, member, qualifier, containerExported);
|
|
375
|
+
break;
|
|
376
|
+
case 'object_declaration':
|
|
377
|
+
extractObject(ctx, member, qualifier, containerExported);
|
|
378
|
+
break;
|
|
379
|
+
case 'type_alias':
|
|
380
|
+
extractTypeAlias(ctx, member, className, qualifier, containerExported);
|
|
381
|
+
break;
|
|
382
|
+
default:
|
|
383
|
+
break;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
if (constructable) {
|
|
387
|
+
// An entry's `value_arguments` (`RED(make())`) AND its anonymous class_body
|
|
388
|
+
// (`RED { val x = compute() }`) are both construction-time code to own.
|
|
389
|
+
const enumHasCtorCode = enumEntries.some((e) => childOfType(e, 'value_arguments') != null || childOfType(e, 'class_body') != null);
|
|
390
|
+
if (primary || initBlocks.length > 0 || enumHasCtorCode) {
|
|
391
|
+
maybeSynthesizeConstructor(ctx, primary, initBlocks, enumEntries, className, qualifier, containerExported);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
else {
|
|
395
|
+
// object / companion: no constructor — own any init-block calls on the
|
|
396
|
+
// container symbol directly (objects/companions have no primary ctor or
|
|
397
|
+
// enum entries, so init blocks are the only construction-time code here).
|
|
398
|
+
for (const init of initBlocks) {
|
|
399
|
+
ctx.bodies.push({ symbolId: containerId, body: childOfType(init, 'block') ?? init, className });
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
// function_declaration as a top-level 'function' (className undefined) or a
|
|
404
|
+
// 'method' (className set). An extension function (`fun Type.name()`) is
|
|
405
|
+
// methods-apart: keyed on the EXTENDED type, merged into methodsByClass[Type]
|
|
406
|
+
// (the Go-receiver / Rust-impl / Swift-extension pattern), exported per its OWN
|
|
407
|
+
// visibility. The body becomes a PendingBody so its calls attribute here and
|
|
408
|
+
// self-calls resolve against the class.
|
|
409
|
+
function extractFunction(ctx, decl, className, qualifier, containerExported) {
|
|
410
|
+
const nameNode = decl.childForFieldName('name');
|
|
411
|
+
const name = nameNode?.text;
|
|
412
|
+
if (!name)
|
|
413
|
+
return;
|
|
414
|
+
// An extension (`fun Type.name()`) keys on the receiver type (methods-apart);
|
|
415
|
+
// a plain member keys on its enclosing class. Either way exportedness is
|
|
416
|
+
// container-gated AND-ed with own visibility — for a TOP-LEVEL extension
|
|
417
|
+
// containerExported is true, so this reduces to own visibility; for a MEMBER
|
|
418
|
+
// extension (declared inside a class) it correctly inherits the container.
|
|
419
|
+
const effClass = extensionReceiverName(decl, nameNode) ?? className;
|
|
420
|
+
const exported = containerExported && !isHidden(decl);
|
|
421
|
+
const kind = effClass ? 'method' : 'function';
|
|
422
|
+
const sym = makeKotlinSymbol(ctx, decl, declSignature(decl, ctx.content), kind, name, memberFqn(ctx, effClass, name), exported, kotlinDoc(decl), qualifier);
|
|
423
|
+
ctx.symbols.push(sym);
|
|
424
|
+
const fnBody = childOfType(decl, 'function_body');
|
|
425
|
+
if (fnBody)
|
|
426
|
+
ctx.bodies.push({ symbolId: sym.id, body: fnBody, className: effClass });
|
|
427
|
+
}
|
|
428
|
+
// secondary_constructor → a 'method' named 'constructor' (Java convention). The
|
|
429
|
+
// `block` becomes a PendingBody so its calls attribute here and self-calls
|
|
430
|
+
// resolve.
|
|
431
|
+
function extractSecondaryConstructor(ctx, decl, className, qualifier, containerExported) {
|
|
432
|
+
const sym = makeKotlinSymbol(ctx, decl, declSignature(decl, ctx.content), 'method', 'constructor', memberFqn(ctx, className, 'constructor'), containerExported && !isHidden(decl), kotlinDoc(decl), qualifier);
|
|
433
|
+
ctx.symbols.push(sym);
|
|
434
|
+
// Push the WHOLE declaration so calls in the `: this(...)`/`: super(...)`
|
|
435
|
+
// delegation args (a `constructor_delegation_call` sibling of `block`) and any
|
|
436
|
+
// param default-args attribute to the constructor, not module scope. The
|
|
437
|
+
// `block` body is walked too; `modifiers` is skipped, and `this`/`super` are
|
|
438
|
+
// anon tokens (no spurious ref).
|
|
439
|
+
ctx.bodies.push({ symbolId: sym.id, body: decl, className });
|
|
440
|
+
}
|
|
441
|
+
// The primary constructor surface: a single synthesized 'constructor' method
|
|
442
|
+
// owning init-block bodies, primary-ctor default-argument expressions, and
|
|
443
|
+
// enum-entry constructor arguments (all run at construction / enum init). Only
|
|
444
|
+
// emitted when there's primary-ctor params or init code — a plain `class Empty`
|
|
445
|
+
// gets no phantom constructor.
|
|
446
|
+
function maybeSynthesizeConstructor(ctx, primary, initBlocks, enumEntries, className, qualifier, containerExported) {
|
|
447
|
+
const params = primary ? childOfType(primary, 'class_parameters') : null;
|
|
448
|
+
const hasParams = params != null && childOfType(params, 'class_parameter') != null;
|
|
449
|
+
// An enum entry's ctor args (`RED(make())`) and its anonymous class_body
|
|
450
|
+
// (`RED { val x = compute() }`) are both construction-time code.
|
|
451
|
+
const enumBodies = enumEntries.flatMap((e) => [childOfType(e, 'value_arguments'), childOfType(e, 'class_body')].filter((n) => n != null));
|
|
452
|
+
if (!hasParams && initBlocks.length === 0 && enumBodies.length === 0)
|
|
453
|
+
return;
|
|
454
|
+
const signature = primary
|
|
455
|
+
? normalizeSignature(`constructor${params ? ctx.content.slice(params.startIndex, params.endIndex) : '()'}`)
|
|
456
|
+
: 'constructor';
|
|
457
|
+
// Primary constructors are public unless explicitly restricted on the
|
|
458
|
+
// `constructor` keyword (rare); follow the class's exportedness.
|
|
459
|
+
const sym = makeKotlinSymbol(ctx, primary ?? initBlocks[0] ?? enumEntries[0], signature, 'method', 'constructor', memberFqn(ctx, className, 'constructor'), containerExported, null, qualifier);
|
|
460
|
+
ctx.symbols.push(sym);
|
|
461
|
+
// Default-argument expressions live inside the primary constructor's params.
|
|
462
|
+
if (primary)
|
|
463
|
+
ctx.bodies.push({ symbolId: sym.id, body: primary, className });
|
|
464
|
+
for (const init of initBlocks) {
|
|
465
|
+
ctx.bodies.push({ symbolId: sym.id, body: childOfType(init, 'block') ?? init, className });
|
|
466
|
+
}
|
|
467
|
+
// Enum-entry ctor args (`RED(make())`) and anonymous-class-body property
|
|
468
|
+
// initializers (`RED { val x = compute() }`) evaluate at enum init — attribute
|
|
469
|
+
// their calls here, not to module scope. (Per-entry override-method bodies stay
|
|
470
|
+
// pruned: function_declaration is in the skip set, so walking the class_body
|
|
471
|
+
// descends property initializers but not the override fun bodies.)
|
|
472
|
+
for (const body of enumBodies) {
|
|
473
|
+
ctx.bodies.push({ symbolId: sym.id, body, className });
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
// Primary-constructor parameters declared `val`/`var` are class properties.
|
|
477
|
+
// Plain parameters (no val/var) are not. Default-value calls are owned by the
|
|
478
|
+
// synthesized constructor, not here.
|
|
479
|
+
function extractPrimaryCtorProperties(ctx, primary, className, qualifier, containerExported) {
|
|
480
|
+
const params = childOfType(primary, 'class_parameters');
|
|
481
|
+
if (!params)
|
|
482
|
+
return;
|
|
483
|
+
for (const param of params.namedChildren) {
|
|
484
|
+
if (param.type !== 'class_parameter')
|
|
485
|
+
continue;
|
|
486
|
+
const isProp = nodeHasAnonChild(param, 'val') || nodeHasAnonChild(param, 'var');
|
|
487
|
+
if (!isProp)
|
|
488
|
+
continue;
|
|
489
|
+
const id = childOfType(param, 'identifier');
|
|
490
|
+
if (!id)
|
|
491
|
+
continue;
|
|
492
|
+
ctx.symbols.push(makeKotlinSymbol(ctx, param, normalizeSignature(ctx.content.slice(param.startIndex, param.endIndex)), 'variable', id.text, memberFqn(ctx, className, id.text), containerExported && !isHidden(param), null, qualifier));
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
// property_declaration → 'variable' member(s). A single `val`/`var x = init`
|
|
496
|
+
// gets one symbol plus a PendingBody (the whole declaration, so initializer and
|
|
497
|
+
// getter/setter calls attribute here and self-calls resolve; `modifiers` is
|
|
498
|
+
// skipped by the walk so annotation args don't leak in). A destructuring
|
|
499
|
+
// `val (a, b) = f()` extracts each name but owns no body — the initializer has
|
|
500
|
+
// no single owner, so its calls stay module-level (the Swift tuple-binding
|
|
501
|
+
// rule). Extension properties (`val Type.x`) key on the extended type.
|
|
502
|
+
function extractProperty(ctx, decl, className, qualifier, containerExported) {
|
|
503
|
+
const signature = propertySignature(decl, ctx.content);
|
|
504
|
+
const doc = kotlinDoc(decl);
|
|
505
|
+
const varDecl = childOfType(decl, 'variable_declaration');
|
|
506
|
+
const multiDecl = childOfType(decl, 'multi_variable_declaration');
|
|
507
|
+
// Extension property keys on the receiver type; container-gated AND own
|
|
508
|
+
// visibility (top-level → own visibility since containerExported is true).
|
|
509
|
+
const effClass = (varDecl ? extensionReceiverName(decl, varDecl) : null) ?? className;
|
|
510
|
+
const exported = containerExported && !isHidden(decl);
|
|
511
|
+
if (multiDecl) {
|
|
512
|
+
for (const sub of multiDecl.namedChildren) {
|
|
513
|
+
if (sub.type !== 'variable_declaration')
|
|
514
|
+
continue;
|
|
515
|
+
const id = childOfType(sub, 'identifier');
|
|
516
|
+
if (!id)
|
|
517
|
+
continue;
|
|
518
|
+
ctx.symbols.push(makeKotlinSymbol(ctx, decl, signature, 'variable', id.text, memberFqn(ctx, effClass, id.text), exported, doc, qualifier));
|
|
519
|
+
}
|
|
520
|
+
return; // destructuring initializer has no single owner — no PendingBody
|
|
521
|
+
}
|
|
522
|
+
const id = varDecl ? childOfType(varDecl, 'identifier') : null;
|
|
523
|
+
if (!id)
|
|
524
|
+
return;
|
|
525
|
+
const sym = makeKotlinSymbol(ctx, decl, signature, 'variable', id.text, memberFqn(ctx, effClass, id.text), exported, doc, qualifier);
|
|
526
|
+
ctx.symbols.push(sym);
|
|
527
|
+
ctx.bodies.push({ symbolId: sym.id, body: decl, className: effClass });
|
|
528
|
+
}
|
|
529
|
+
// typealias → 'type'. The alias name is the `type` FIELD (not the aliased type,
|
|
530
|
+
// which is the trailing `type` child).
|
|
531
|
+
function extractTypeAlias(ctx, decl, className, qualifier, containerExported) {
|
|
532
|
+
const name = decl.childForFieldName('type')?.text;
|
|
533
|
+
if (!name)
|
|
534
|
+
return;
|
|
535
|
+
ctx.symbols.push(makeKotlinSymbol(ctx, decl, normalizeSignature(ctx.content.slice(decl.startIndex, decl.endIndex)), 'type', name, memberFqn(ctx, className, name), containerExported && !isHidden(decl), kotlinDoc(decl), qualifier));
|
|
536
|
+
}
|
|
537
|
+
// `import a.b.C` → single-symbol import (name = last segment, module = the
|
|
538
|
+
// rest). `import a.b.*` → whole-package namespace import. `import a.b.C as D` →
|
|
539
|
+
// the binding D. Lower cross-file value than Go (Kotlin paths don't map to
|
|
540
|
+
// files, no directory carve-out) — same framing as Rust.
|
|
541
|
+
function extractImport(ctx, decl) {
|
|
542
|
+
const qi = childOfType(decl, 'qualified_identifier');
|
|
543
|
+
if (!qi)
|
|
544
|
+
return;
|
|
545
|
+
const segments = qi.namedChildren.filter((c) => c.type === 'identifier').map((c) => c.text);
|
|
546
|
+
if (segments.length === 0)
|
|
547
|
+
return;
|
|
548
|
+
const line = decl.startPosition.row + 1;
|
|
549
|
+
const wildcard = nodeHasAnonChild(decl, '*');
|
|
550
|
+
// An alias identifier sits AFTER the qualified_identifier (`... as Alias`).
|
|
551
|
+
const alias = decl.namedChildren.find((c) => c.type === 'identifier' && c.startIndex > qi.endIndex);
|
|
552
|
+
if (wildcard) {
|
|
553
|
+
ctx.imports.push({
|
|
554
|
+
file: ctx.fileInfo.path,
|
|
555
|
+
sourceModule: segments.join('.'),
|
|
556
|
+
importedNames: [{ name: IMPORT_NAMESPACE, kind: 'namespace' }],
|
|
557
|
+
line,
|
|
558
|
+
});
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
const name = segments[segments.length - 1];
|
|
562
|
+
const sourceModule = segments.slice(0, -1).join('.');
|
|
563
|
+
const importedName = alias ? { name, alias: alias.text } : { name };
|
|
564
|
+
ctx.imports.push({ file: ctx.fileInfo.path, sourceModule, importedNames: [importedName], line });
|
|
565
|
+
}
|
|
566
|
+
// ── helpers ──────────────────────────────────────────────────────────────
|
|
567
|
+
// class_declaration covers class / interface / enum. interface = the literal
|
|
568
|
+
// `interface` keyword token; enum = an `enum_class_body` (or an `enum`
|
|
569
|
+
// class_modifier for a bodiless enum); everything else (incl. data/sealed/value
|
|
570
|
+
// /annotation) → class.
|
|
571
|
+
function classKind(decl) {
|
|
572
|
+
if (nodeHasAnonChild(decl, 'interface'))
|
|
573
|
+
return 'interface';
|
|
574
|
+
if (childOfType(decl, 'enum_class_body'))
|
|
575
|
+
return 'enum';
|
|
576
|
+
const mods = childOfType(decl, 'modifiers');
|
|
577
|
+
if (mods?.namedChildren.some((m) => m.type === 'class_modifier' && m.text === 'enum'))
|
|
578
|
+
return 'enum';
|
|
579
|
+
return 'class';
|
|
580
|
+
}
|
|
581
|
+
// An extension receiver = a user_type/nullable_type child appearing BEFORE the
|
|
582
|
+
// declaration's name (the hidden `_receiver_type`). Returns the receiver's
|
|
583
|
+
// simple type name (scoped `a.b.C`→C, generic `List<Int>`→List, nullable
|
|
584
|
+
// `String?`→String); null for a plain (non-extension) declaration or a
|
|
585
|
+
// non-nominal receiver.
|
|
586
|
+
function extensionReceiverName(decl, nameNode) {
|
|
587
|
+
for (const c of decl.namedChildren) {
|
|
588
|
+
if (c.startIndex >= nameNode.startIndex)
|
|
589
|
+
break;
|
|
590
|
+
if (KOTLIN_RECEIVER_TYPES.has(c.type))
|
|
591
|
+
return receiverTypeName(c);
|
|
592
|
+
}
|
|
593
|
+
return null;
|
|
594
|
+
}
|
|
595
|
+
function receiverTypeName(typeNode) {
|
|
596
|
+
let t = typeNode;
|
|
597
|
+
if (t.type === 'nullable_type') {
|
|
598
|
+
const inner = childOfType(t, 'user_type');
|
|
599
|
+
if (!inner)
|
|
600
|
+
return null;
|
|
601
|
+
t = inner;
|
|
602
|
+
}
|
|
603
|
+
if (t.type !== 'user_type')
|
|
604
|
+
return null;
|
|
605
|
+
// The last direct `identifier` child is the simple type name (type_arguments
|
|
606
|
+
// is a separate node; a scoped `a.b.C` keeps its last segment).
|
|
607
|
+
let result = null;
|
|
608
|
+
for (const c of t.namedChildren) {
|
|
609
|
+
if (c.type === 'identifier')
|
|
610
|
+
result = c.text;
|
|
611
|
+
}
|
|
612
|
+
return result;
|
|
613
|
+
}
|
|
614
|
+
function memberFqn(ctx, className, name) {
|
|
615
|
+
return className
|
|
616
|
+
? `${ctx.fileInfo.path}:${className}.${name}`
|
|
617
|
+
: `${ctx.fileInfo.path}:${name}`;
|
|
618
|
+
}
|
|
619
|
+
// First direct named child of one of the given types (or null). Collapses the
|
|
620
|
+
// many `namedChildren.find(c => c.type === 'X')` body/node lookups.
|
|
621
|
+
function childOfType(node, ...types) {
|
|
622
|
+
return node.namedChildren.find((c) => types.includes(c.type)) ?? null;
|
|
623
|
+
}
|
|
624
|
+
// Declaration signature = source from the declaration start to its body. The
|
|
625
|
+
// body is NOT a named field in tree-sitter-kotlin, so it's found by type.
|
|
626
|
+
function declSignature(decl, content) {
|
|
627
|
+
const body = childOfType(decl, 'function_body', 'block', 'class_body', 'enum_class_body');
|
|
628
|
+
const sigEnd = body ? body.startIndex : decl.endIndex;
|
|
629
|
+
return normalizeSignature(content.slice(decl.startIndex, sigEnd));
|
|
630
|
+
}
|
|
631
|
+
// Property signature stops before the getter/setter/delegate so a long computed
|
|
632
|
+
// body doesn't blow the 120-char cap (the `= initializer` is kept — informative
|
|
633
|
+
// for constants).
|
|
634
|
+
function propertySignature(decl, content) {
|
|
635
|
+
let cut = decl.endIndex;
|
|
636
|
+
for (const child of decl.namedChildren) {
|
|
637
|
+
if (child.type === 'getter' || child.type === 'setter' || child.type === 'property_delegate') {
|
|
638
|
+
cut = child.startIndex;
|
|
639
|
+
break;
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
return normalizeSignature(content.slice(decl.startIndex, cut));
|
|
643
|
+
}
|
|
644
|
+
// exported = NO `private` visibility modifier (so absent = public, internal,
|
|
645
|
+
// protected, and public/open all export — Kotlin's default is public, there is
|
|
646
|
+
// no directory→package carve-out, and treating internal-and-up as exported
|
|
647
|
+
// preserves cross-file member-call recall, the Swift rule). Members AND-in
|
|
648
|
+
// their container's exportedness via the caller.
|
|
649
|
+
function isHidden(decl) {
|
|
650
|
+
const mods = childOfType(decl, 'modifiers');
|
|
651
|
+
if (!mods)
|
|
652
|
+
return false;
|
|
653
|
+
for (const m of mods.namedChildren) {
|
|
654
|
+
if (m.type === 'visibility_modifier' && m.text === 'private')
|
|
655
|
+
return true;
|
|
656
|
+
}
|
|
657
|
+
return false;
|
|
658
|
+
}
|
|
659
|
+
// True if `node` has a direct anonymous child whose token text is `text`.
|
|
660
|
+
function nodeHasAnonChild(node, text) {
|
|
661
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
662
|
+
const c = node.child(i);
|
|
663
|
+
if (c && !c.isNamed && c.type === text)
|
|
664
|
+
return true;
|
|
665
|
+
}
|
|
666
|
+
return false;
|
|
667
|
+
}
|
|
668
|
+
// Module path / enclosing-type chain only disambiguate hashed ids — they never
|
|
669
|
+
// reach FQN parsing — so any unique join works.
|
|
670
|
+
function joinQualifier(a, b) {
|
|
671
|
+
if (!a)
|
|
672
|
+
return b;
|
|
673
|
+
if (!b)
|
|
674
|
+
return a;
|
|
675
|
+
return `${a}.${b}`;
|
|
676
|
+
}
|
|
677
|
+
function makeKotlinSymbol(ctx, node, signature, kind, name, fqn, exported, doc, qualifier = '') {
|
|
678
|
+
const key = `${name}\0${kind}\0${signature}\0${qualifier}`;
|
|
679
|
+
const n = (ctx.occurrences.get(key) ?? 0) + 1;
|
|
680
|
+
ctx.occurrences.set(key, n);
|
|
681
|
+
const effectiveQualifier = n === 1 ? qualifier : `${qualifier}#${n}`;
|
|
682
|
+
return {
|
|
683
|
+
// The id hashes the FULL signature; only the stored copy is capped.
|
|
684
|
+
id: symbolId(ctx.fileInfo.path, name, kind, signature, effectiveQualifier),
|
|
685
|
+
name,
|
|
686
|
+
fqn,
|
|
687
|
+
kind,
|
|
688
|
+
file: ctx.fileInfo.path,
|
|
689
|
+
startLine: node.startPosition.row + 1,
|
|
690
|
+
endLine: node.endPosition.row + 1,
|
|
691
|
+
signature: signature.slice(0, SIGNATURE_DISPLAY_CAP),
|
|
692
|
+
doc,
|
|
693
|
+
exported,
|
|
694
|
+
language: ctx.fileInfo.language,
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
// Doc = the immediately-preceding KDoc `/** */` block_comment. Plain `//` /
|
|
698
|
+
// `/* */` are NOT doc comments (KDoc convention). Annotations live INSIDE the
|
|
699
|
+
// declaration (in `modifiers`), so no Rust-style sibling skipping is needed.
|
|
700
|
+
function kotlinDoc(decl) {
|
|
701
|
+
const prev = decl.previousNamedSibling;
|
|
702
|
+
if (!prev || prev.type !== 'block_comment' || !prev.text.startsWith('/**'))
|
|
703
|
+
return null;
|
|
704
|
+
if (prev.endPosition.row !== decl.startPosition.row - 1)
|
|
705
|
+
return null; // adjacency
|
|
706
|
+
if (isTrailingComment(prev))
|
|
707
|
+
return null;
|
|
708
|
+
return commentDocLine(prev.text);
|
|
709
|
+
}
|