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