java-codebase-rag 0.11.2__py3-none-any.whl → 0.12.0__py3-none-any.whl

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 (37) hide show
  1. java_codebase_rag/_deprecation.py +103 -0
  2. java_codebase_rag/_version.py +2 -2
  3. java_codebase_rag/ast/ast_java.py +22 -0
  4. java_codebase_rag/ast/ast_kotlin.py +1794 -0
  5. java_codebase_rag/ast/chunk_heuristics.py +26 -5
  6. java_codebase_rag/ast/language.py +117 -0
  7. java_codebase_rag/cli.py +17 -17
  8. java_codebase_rag/cli_dispatch.py +251 -0
  9. java_codebase_rag/config.py +8 -8
  10. java_codebase_rag/eval/runner.py +3 -3
  11. java_codebase_rag/graph/build_ast_graph.py +130 -8
  12. java_codebase_rag/graph/graph_enrich.py +8 -5
  13. java_codebase_rag/graph/ladybug_queries.py +1 -1
  14. java_codebase_rag/graph/path_filtering.py +39 -7
  15. java_codebase_rag/index/java_index_flow_lancedb.py +160 -15
  16. java_codebase_rag/install_data/agents/explorer-rag-cli.md +6 -4
  17. java_codebase_rag/install_data/agents/explorer-rag-enhanced.md +4 -4
  18. java_codebase_rag/install_data/skills/explore-codebase/SKILL.md +4 -4
  19. java_codebase_rag/install_data/skills/explore-codebase-cli/SKILL.md +5 -5
  20. java_codebase_rag/installer.py +15 -15
  21. java_codebase_rag/jrag.py +25 -11
  22. java_codebase_rag/lance_optimize.py +7 -7
  23. java_codebase_rag/mcp/mcp_v2.py +2 -2
  24. java_codebase_rag/mcp/server.py +6 -4
  25. java_codebase_rag/pipeline.py +4 -4
  26. java_codebase_rag/progress.py +1 -1
  27. java_codebase_rag/search/search_lexical.py +1 -1
  28. java_codebase_rag/search/search_scoring.py +19 -5
  29. java_codebase_rag/watch/lock.py +1 -1
  30. java_codebase_rag/watch/watcher.py +45 -21
  31. {java_codebase_rag-0.11.2.dist-info → java_codebase_rag-0.12.0.dist-info}/METADATA +31 -22
  32. {java_codebase_rag-0.11.2.dist-info → java_codebase_rag-0.12.0.dist-info}/RECORD +36 -32
  33. java_codebase_rag-0.12.0.dist-info/entry_points.txt +5 -0
  34. java_codebase_rag-0.11.2.dist-info/entry_points.txt +0 -4
  35. {java_codebase_rag-0.11.2.dist-info → java_codebase_rag-0.12.0.dist-info}/WHEEL +0 -0
  36. {java_codebase_rag-0.11.2.dist-info → java_codebase_rag-0.12.0.dist-info}/licenses/LICENSE +0 -0
  37. {java_codebase_rag-0.11.2.dist-info → java_codebase_rag-0.12.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,1794 @@
1
+ """Kotlin AST extraction on top of tree-sitter (tree-sitter-kotlin PyPI 1.1.0).
2
+
3
+ Task 5 foundation: parse a single ``.kt`` compilation unit's package and
4
+ imports into the existing ``JavaFileAst`` shape (defined in ``ast_java.py``).
5
+ The per-thread ``Parser`` TLS mirrors ``ast_java.py``'s idiom verbatim because
6
+ ``parse_kotlin`` is called from the same concurrent worker threads as
7
+ ``parse_java`` (cocoindex inflight parallelism via ``asyncio.to_thread``).
8
+
9
+ Task 6 walks Kotlin type declarations into ``TypeDecl`` rows using the
10
+ **folded kind map** — Kotlin kinds reuse the five existing Java
11
+ ``_TYPE_KINDS`` strings (``class``/``interface``/``enum``/``record``/
12
+ ``annotation``); no new kind strings are introduced and ``_TYPE_KINDS`` in
13
+ ``ast_java.py`` is NOT extended.
14
+
15
+ Task 7 populates members: ``function_declaration``/``secondary_constructor``
16
+ → ``MethodDecl``; ``property_declaration`` and ``val``/``var``
17
+ ``class_parameter`` (primary-constructor properties) → ``FieldDecl`` **plus**
18
+ synthesized JVM-accessor ``MethodDecl`` s so cross-language CALLS resolve
19
+ (accessors are the only way Java code touches a Kotlin property). Modifier
20
+ vocabulary is emitted into the shared ``modifiers`` list using Java literals
21
+ (``static``/``final``) plus Kotlin-only ride-alongs (``suspend`` etc.) — the
22
+ graph builder reads ``"static" in m.decl.modifiers`` / ``"final" in ...``
23
+ directly, so no separate field. Top-level functions/properties land on a
24
+ synthetic facade ``TypeDecl`` ``<Basename>Kt`` tagged
25
+ ``capabilities=["kotlin_facade"]``.
26
+
27
+ ``file_imports.static_methods`` / ``static_wildcards`` stay empty because
28
+ Kotlin has no ``import static``.
29
+
30
+ Grammar-node facts confirmed by probing the installed 1.1.0 binary (NOT the
31
+ ``fwcd`` grammar; this is the restructured PyPI grammar):
32
+
33
+ * Root: ``source_file``.
34
+ * Package: a top-level ``package_header`` whose child ``qualified_identifier``
35
+ is the dotted path. (There is no ``package_directive``.)
36
+ * Imports: top-level ``import`` nodes (not ``import_declaration``); dotted
37
+ path is child ``qualified_identifier``; a wildcard ends the
38
+ ``qualified_identifier`` text with ``.*`` (the ``.`` and ``*`` are unnamed
39
+ siblings after it); an alias is a trailing ``identifier`` sibling after the
40
+ ``as`` keyword.
41
+ * Names are ``identifier`` everywhere — there is no ``simple_identifier`` or
42
+ ``type_identifier`` in 1.1.0.
43
+ * Type declarations: ``class Foo``, ``interface Bar``, ``enum class E``,
44
+ ``annotation class Ann``, ``data class D`` ALL parse as
45
+ ``class_declaration`` — you DISCRIMINATE the kind via (a) an anonymous
46
+ ``interface`` keyword child → ``interface``; (b) ``modifiers >
47
+ class_modifier`` whose text is ``enum`` / ``annotation`` / ``data`` →
48
+ ``enum`` / ``annotation`` / ``record`` respectively; otherwise ``class``.
49
+ Other ``class_modifier`` values (``sealed``, ``value``, ``inline``) and
50
+ ``inheritance_modifier`` values (``abstract``, ``final``, …) fold to
51
+ ``class``.
52
+ * ``object Singleton`` → ``object_declaration`` → kind ``class``.
53
+ * ``companion object { … }`` → ``companion_object`` (a DISTINCT node, not a
54
+ modifier): name in optional child ``identifier`` (default ``Companion``);
55
+ becomes a NESTED ``TypeDecl`` under its enclosing type.
56
+ * Body is ``class_body`` (or ``enum_class_body`` for enums); nested
57
+ ``class_declaration``/``object_declaration``/``companion_object`` live in
58
+ the body and are attached to the parent's ``nested`` list.
59
+ """
60
+ from __future__ import annotations
61
+
62
+ import os
63
+ import threading
64
+
65
+ import tree_sitter_kotlin as _ts_kotlin
66
+ from tree_sitter import Language, Node, Parser
67
+
68
+ from java_codebase_rag.ast.ast_java import (
69
+ AnnotationRef,
70
+ CallSite,
71
+ FieldDecl,
72
+ FileImports,
73
+ JavaFileAst,
74
+ MethodDecl,
75
+ ParamDecl,
76
+ TypeDecl,
77
+ )
78
+
79
+ __all__ = ["parse_kotlin", "merge_multifile_facades"]
80
+
81
+ # tree-sitter's ``Parser`` mutates internal state during ``parse()`` and is NOT
82
+ # thread-safe, so each OS thread gets its own instance. Mirrors ``ast_java.py``'s
83
+ # ``_parser_tls`` / ``_parser()`` exactly. The ``Language`` is immutable and
84
+ # shared — hoisted to module scope so it is built once (not per thread); each
85
+ # per-thread ``Parser`` is constructed lazily and cheaply off the shared Language.
86
+ _KOTLIN_LANGUAGE = Language(_ts_kotlin.language())
87
+ _parser_tls = threading.local()
88
+
89
+
90
+ def _parser() -> Parser:
91
+ p = getattr(_parser_tls, "parser", None)
92
+ if p is None:
93
+ _parser_tls.parser = p = Parser(_KOTLIN_LANGUAGE)
94
+ return p
95
+
96
+
97
+ def _txt(node: Node, src: bytes) -> str:
98
+ return src[node.start_byte:node.end_byte].decode("utf-8", errors="replace")
99
+
100
+
101
+ # Kotlin declaration nodes that map to a ``TypeDecl``. NOTE: ``_TYPE_KINDS`` in
102
+ # ``ast_java.py`` is intentionally NOT extended — Kotlin kinds fold into the
103
+ # existing five Java kind strings via ``_kotlin_class_kind``.
104
+ _KOTLIN_TYPE_NODES: frozenset[str] = frozenset(
105
+ {"class_declaration", "object_declaration", "companion_object"}
106
+ )
107
+
108
+ # ``class_modifier`` values that override the default ``class`` fold. Everything
109
+ # else (``sealed``, ``value``, ``inline``, inheritance modifiers like
110
+ # ``abstract``/``final``/``open``) folds to ``class`` — DTO/singleton inference
111
+ # is unaffected and modifiers are captured in Task 7.
112
+ _CLASS_MODIFIER_TO_KIND: dict[str, str] = {
113
+ "enum": "enum",
114
+ "annotation": "annotation",
115
+ "data": "record", # the non-obvious fold: Kotlin data class ≈ Java record (DTO inference).
116
+ }
117
+
118
+
119
+ def _kotlin_class_kind(node: Node, src: bytes) -> str:
120
+ """Fold a ``class_declaration`` into one of the five Java kind strings.
121
+
122
+ Discriminator (verified by probing tree-sitter-kotlin 1.1.0):
123
+
124
+ * an anonymous ``interface`` keyword child (literal token, not a named
125
+ node) → ``interface``;
126
+ * otherwise scan ``modifiers > class_modifier`` text for ``enum`` /
127
+ ``annotation`` / ``data`` → ``enum`` / ``annotation`` / ``record``;
128
+ * otherwise → ``class``.
129
+ """
130
+ # `interface Foo` exposes `interface` as an anonymous literal-keyword child.
131
+ for c in node.children:
132
+ if not c.is_named and c.type == "interface":
133
+ return "interface"
134
+ for c in node.named_children:
135
+ if c.type != "modifiers":
136
+ continue
137
+ for mc in c.named_children:
138
+ if mc.type == "class_modifier":
139
+ mod = _txt(mc, src)
140
+ if mod in _CLASS_MODIFIER_TO_KIND:
141
+ return _CLASS_MODIFIER_TO_KIND[mod]
142
+ return "class"
143
+
144
+
145
+ def _kotlin_decl_name(node: Node, src: bytes) -> str:
146
+ """Type name from the ``identifier`` child (companion defaults to 'Companion')."""
147
+ for c in node.named_children:
148
+ if c.type == "identifier":
149
+ return _txt(c, src)
150
+ return "Companion" # unnamed `companion object { … }`.
151
+
152
+
153
+ # ---- Task 7: members ----
154
+
155
+ # Tree-sitter-kotlin 1.1.0 node types that carry a type annotation. ``identifier``
156
+ # is NOT in this set — names are plain ``identifier`` nodes (1.1.0 has no
157
+ # ``simple_identifier``/``type_identifier``).
158
+ _TYPE_NODE_TYPES: frozenset[str] = frozenset(
159
+ {"user_type", "nullable_type", "function_type"}
160
+ )
161
+
162
+ # Kotlin ``function_modifier`` / ``member_modifier`` tokens that ride along in
163
+ # the shared ``modifiers`` list for fidelity (no graph consumer checks them).
164
+ # ``inheritance_modifier`` open/abstract/final map to the Java ``final`` rule
165
+ # below; ``visibility_modifier`` is consumed for accessor decisions only.
166
+ _KOTLIN_INHERITANCE_NON_FINAL: frozenset[str] = frozenset({"open", "abstract"})
167
+
168
+
169
+ def _simple_type_name(node: Node | None, src: bytes) -> str:
170
+ """Simple name from a type-annotation node (``user_type`` / ``nullable_type``).
171
+
172
+ ``String?`` → ``String``; ``com.foo.Bar`` → ``Bar``; ``List<String>`` →
173
+ ``List``. Returns ``""`` for absent / unrecognised type nodes.
174
+ """
175
+ if node is None:
176
+ return ""
177
+ if node.type == "nullable_type":
178
+ node = next(
179
+ (c for c in node.named_children if c.type == "user_type"), None
180
+ )
181
+ if node is None:
182
+ return ""
183
+ if node.type == "user_type":
184
+ ids = [c for c in node.named_children if c.type == "identifier"]
185
+ return _txt(ids[-1], src) if ids else ""
186
+ return ""
187
+
188
+
189
+ def _type_child(node: Node) -> Node | None:
190
+ """The type-annotation child (``user_type``/``nullable_type``/``function_type``)."""
191
+ for c in node.named_children:
192
+ if c.type in _TYPE_NODE_TYPES:
193
+ return c
194
+ return None
195
+
196
+
197
+ def _has_anon_keyword(node: Node, keyword: str) -> bool:
198
+ """True if ``node`` has an anonymous literal child (e.g. ``var``/``val``)."""
199
+ for c in node.children:
200
+ if not c.is_named and c.type == keyword:
201
+ return True
202
+ return False
203
+
204
+
205
+ def _collect_kotlin_modifiers(node: Node, src: bytes) -> dict:
206
+ """Walk the ``modifiers`` child of a member node and return raw modifier info.
207
+
208
+ The 1.1.0 grammar wraps every modifier in ONE ``modifiers`` container whose
209
+ typed sub-containers (``visibility_modifier``/``inheritance_modifier``/
210
+ ``function_modifier``/``member_modifier``/``property_modifier``) each hold a
211
+ single anonymous keyword token — so the sub-container's own text IS the
212
+ keyword (e.g. ``_txt(function_modifier_node) == "suspend"``).
213
+ """
214
+ info: dict = {
215
+ "visibility": None, # "private"/"public"/"protected"/"internal" or None (default public).
216
+ "inheritance": [], # open/abstract/final
217
+ "function": [], # suspend/inline/operator/infix/tailrec/external
218
+ "member": [], # override/lateinit
219
+ "const": False,
220
+ }
221
+ mods_node = next(
222
+ (c for c in node.named_children if c.type == "modifiers"), None
223
+ )
224
+ if mods_node is None:
225
+ return info
226
+ for mc in mods_node.named_children:
227
+ txt = _txt(mc, src)
228
+ if mc.type == "visibility_modifier":
229
+ info["visibility"] = txt
230
+ elif mc.type == "inheritance_modifier":
231
+ info["inheritance"].append(txt)
232
+ elif mc.type == "function_modifier":
233
+ info["function"].append(txt)
234
+ elif mc.type == "member_modifier":
235
+ info["member"].append(txt)
236
+ elif mc.type == "property_modifier":
237
+ if txt == "const":
238
+ info["const"] = True
239
+ else:
240
+ info["member"].append(txt)
241
+ # ``class_modifier`` is not relevant for members; ignored.
242
+ return info
243
+
244
+
245
+ def _build_member_modifiers(
246
+ info: dict, *, is_static: bool, is_final: bool
247
+ ) -> list[str]:
248
+ """Build the shared ``modifiers`` list: Java vocab + Kotlin ride-alongs.
249
+
250
+ Java vocabulary (the only tokens the graph builder reads):
251
+ * ``"static"`` — companion-object member, top-level facade member, or ``const``.
252
+ * ``"final"`` — Kotlin ``fun``/``val`` default; omitted for ``open``/``abstract``.
253
+ Kotlin-only tokens (``suspend``/``inline``/``operator``/``override``/…) ride
254
+ along in source order; no consumer checks them, but fidelity is preserved.
255
+ Visibility keywords are NOT emitted (used only for accessor decisions).
256
+ """
257
+ mods: list[str] = []
258
+ if is_static:
259
+ mods.append("static")
260
+ if is_final:
261
+ mods.append("final")
262
+ for kw in info["function"]:
263
+ if kw not in mods:
264
+ mods.append(kw)
265
+ for kw in info["member"]:
266
+ if kw not in mods:
267
+ mods.append(kw)
268
+ return mods
269
+
270
+
271
+ # ---- Task 8: annotations (with use-site targets) ----
272
+ #
273
+ # Grammar (tree-sitter-kotlin 1.1.0), confirmed by probing:
274
+ # * An ``annotation`` (singular — NO ``annotations`` plural wrapper) sits inside
275
+ # the ``modifiers`` container of the type / property / function / class_parameter.
276
+ # * A function ``parameter``'s annotations live in a SIBLING ``parameter_modifiers``
277
+ # node inside ``function_value_parameters`` (different parent from the property /
278
+ # ctor-param case, which uses ``modifiers`` on the node itself).
279
+ # * No-arg: ``annotation > user_type > identifier``
280
+ # With-args: ``annotation > constructor_invocation > (user_type, value_arguments)``
281
+ # * Use-site target: ``annotation > use_site_target > (field|get|set|param|property,
282
+ # :)`` — the target word is an anonymous token; read the ``use_site_target`` text
283
+ # and strip the trailing ``:``.
284
+ # * Annotation simple name = last ``identifier`` of the ``user_type``; qualified =
285
+ # raw text of the ``user_type``.
286
+
287
+ # Use-site target keywords recognised by the grammar. ``file`` is a file-level
288
+ # target handled in Task 9 (file annotations); recorded here for completeness.
289
+ _KOTLIN_USE_SITE_TARGETS: frozenset[str] = frozenset(
290
+ {"field", "get", "set", "param", "property", "file"}
291
+ )
292
+
293
+
294
+ def _kotlin_use_site_target(ann_node: Node, src: bytes) -> str | None:
295
+ """Read ``annotation > use_site_target`` text (e.g. ``param:`` → ``"param"``)."""
296
+ ust = next(
297
+ (c for c in ann_node.named_children if c.type == "use_site_target"), None
298
+ )
299
+ if ust is None:
300
+ return None
301
+ # ``use_site_target`` text is e.g. ``param:``; strip the trailing colon.
302
+ return _txt(ust, src).rstrip(":").strip() or None
303
+
304
+
305
+ def _kotlin_annotation_value_arguments(
306
+ ann_node: Node, src: bytes
307
+ ) -> tuple[dict[str, str], dict[str, str]]:
308
+ """Extract ``arguments`` / ``argument_kinds`` from a Kotlin annotation.
309
+
310
+ Mirrors the Java arg-extraction shape (``ast_java._parse_annotation_argument_list``)
311
+ but walks Kotlin nodes (``constructor_invocation > value_arguments > value_argument``).
312
+ String literals → kind ``"string"``; enum-like identifiers → ``"enum"``;
313
+ ``collection_literal`` of strings → comma-joined, kind ``"string"``. A bare
314
+ positional argument is keyed under ``"value"``.
315
+ """
316
+ args: dict[str, str] = {}
317
+ kinds: dict[str, str] = {}
318
+ ci = next(
319
+ (c for c in ann_node.named_children if c.type == "constructor_invocation"),
320
+ None,
321
+ )
322
+ if ci is None:
323
+ return args, kinds
324
+ va = next(
325
+ (c for c in ci.named_children if c.type == "value_arguments"), None
326
+ )
327
+ if va is None:
328
+ return args, kinds
329
+ for varg in va.named_children:
330
+ if varg.type != "value_argument":
331
+ continue
332
+ # Named arg: ``key = value`` (an ``identifier`` child + anonymous ``=``).
333
+ key = "value"
334
+ value_node: Node | None = None
335
+ named_key = next(
336
+ (c for c in varg.named_children if c.type == "identifier"), None
337
+ )
338
+ has_eq = any(c.type == "=" for c in varg.children)
339
+ if named_key is not None and has_eq:
340
+ key = _txt(named_key, src)
341
+ # value is the named child after the ``=``
342
+ for c in varg.named_children:
343
+ if c is not named_key:
344
+ value_node = c
345
+ break
346
+ else:
347
+ # positional: the first named child that isn't the key
348
+ value_node = named_key if named_key is not None and not has_eq else None
349
+ if value_node is None:
350
+ for c in varg.named_children:
351
+ value_node = c
352
+ break
353
+ if value_node is None:
354
+ continue
355
+ val, kind = _kotlin_annotation_value(value_node, src)
356
+ if val is None or kind is None:
357
+ continue
358
+ if key not in args: # first-wins, mirroring Java positional behaviour
359
+ args[key] = val
360
+ kinds[key] = kind
361
+ return args, kinds
362
+
363
+
364
+ def _kotlin_annotation_value(node: Node, src: bytes) -> tuple[str | None, str | None]:
365
+ """(value, kind) for a Kotlin annotation argument expression.
366
+
367
+ Returns one of ``("string")`` / ``("enum")`` kinds:
368
+ * ``string_literal`` → ``string_content`` text, ``"string"``;
369
+ * ``collection_literal`` → comma-joined string-literal children, ``"string"``;
370
+ * ``identifier``/``scoped_identifier``/``field_access``/``callable_reference``
371
+ → last segment, ``"enum"``.
372
+ """
373
+ if node.type == "string_literal":
374
+ for ch in node.named_children:
375
+ if ch.type == "string_content":
376
+ return _txt(ch, src), "string"
377
+ return None, None
378
+ if node.type == "collection_literal":
379
+ parts: list[str] = []
380
+ for ch in node.named_children:
381
+ if ch.type == "string_literal":
382
+ for gc in ch.named_children:
383
+ if gc.type == "string_content":
384
+ parts.append(_txt(gc, src))
385
+ if parts:
386
+ return ",".join(parts), "string"
387
+ return None, None
388
+ if node.type in ("identifier", "scoped_identifier", "field_access"):
389
+ raw = _txt(node, src).strip()
390
+ if not raw:
391
+ return None, None
392
+ return raw.rsplit(".", 1)[-1], "enum"
393
+ if node.type == "callable_reference":
394
+ # ``Foo::bar`` → receiver ``Foo``; treat the whole text as enum-like.
395
+ raw = _txt(node, src).strip()
396
+ return (raw.rsplit(".", 1)[-1] if raw else None), "enum"
397
+ return None, None
398
+
399
+
400
+ def _kotlin_annotation_name(
401
+ ann_node: Node, src: bytes
402
+ ) -> tuple[str, str]:
403
+ """(simple, qualified) from ``annotation > [constructor_invocation >] user_type``."""
404
+ user_type = next(
405
+ (c for c in ann_node.named_children if c.type == "user_type"), None
406
+ )
407
+ if user_type is None:
408
+ ci = next(
409
+ (c for c in ann_node.named_children if c.type == "constructor_invocation"),
410
+ None,
411
+ )
412
+ if ci is not None:
413
+ user_type = next(
414
+ (c for c in ci.named_children if c.type == "user_type"), None
415
+ )
416
+ if user_type is None:
417
+ return "", ""
418
+ ids = [c for c in user_type.named_children if c.type == "identifier"]
419
+ simple = _txt(ids[-1], src) if ids else ""
420
+ return simple, _txt(user_type, src)
421
+
422
+
423
+ def _parse_kotlin_annotation(ann_node: Node, src: bytes) -> AnnotationRef:
424
+ """Build an ``AnnotationRef`` from a Kotlin ``annotation`` node."""
425
+ simple, qualified = _kotlin_annotation_name(ann_node, src)
426
+ args, arg_kinds = _kotlin_annotation_value_arguments(ann_node, src)
427
+ return AnnotationRef(
428
+ name=simple,
429
+ qualified=qualified or simple,
430
+ arguments=args,
431
+ argument_kinds=arg_kinds,
432
+ use_site_target=_kotlin_use_site_target(ann_node, src),
433
+ )
434
+
435
+
436
+ def _kotlin_annotations_from_modifiers(node: Node, src: bytes) -> list[AnnotationRef]:
437
+ """All ``annotation`` children of ``node``'s ``modifiers`` container."""
438
+ mods_node = next(
439
+ (c for c in node.named_children if c.type == "modifiers"), None
440
+ )
441
+ if mods_node is None:
442
+ return []
443
+ return [
444
+ _parse_kotlin_annotation(c, src)
445
+ for c in mods_node.named_children
446
+ if c.type == "annotation"
447
+ ]
448
+
449
+
450
+ # The four annotation-routing slots. ``None`` (no explicit target) routes to the
451
+ # caller's chosen ``default_slot``: ``"field"`` for a body property, ``"param"``
452
+ # for a primary-constructor parameter (the dominant Spring-Kotlin DI pattern —
453
+ # ``@Autowired val r: Repo`` defaults to constructor injection).
454
+ _ANN_SLOTS: tuple[str, ...] = ("field", "get", "set", "param")
455
+
456
+
457
+ def _route_kotlin_annotations_by_target(
458
+ anns: list[AnnotationRef], default_slot: str
459
+ ) -> dict[str, list[AnnotationRef]]:
460
+ """Bucket annotations by ``use_site_target``.
461
+
462
+ ``"field"`` / ``"property"`` → ``field``; ``"get"``/``"set"``/``"param"`` →
463
+ themselves; ``None`` / anything else → ``default_slot`` (one of ``_ANN_SLOTS``).
464
+ """
465
+ buckets: dict[str, list[AnnotationRef]] = {s: [] for s in _ANN_SLOTS}
466
+ for a in anns:
467
+ t = a.use_site_target
468
+ if t in ("get", "set", "param"):
469
+ buckets[t].append(a)
470
+ elif t in ("field", "property"):
471
+ buckets["field"].append(a)
472
+ else:
473
+ buckets[default_slot].append(a)
474
+ return buckets
475
+
476
+
477
+ def _attach_targeted_annotations_to_accessors(
478
+ accessors: list[MethodDecl],
479
+ get_anns: list[AnnotationRef],
480
+ set_anns: list[AnnotationRef],
481
+ ) -> None:
482
+ """Attach get-/set-targeted annotations to the synthesized accessor MethodDecls.
483
+
484
+ ``accessors[0]`` is the getter; ``accessors[1]`` (when present) is the setter.
485
+ Privates synthesize no accessors; their get/set annotations are dropped (the
486
+ accessors are not exposed on the JVM surface we model).
487
+ """
488
+ if accessors:
489
+ accessors[0].annotations.extend(get_anns)
490
+ if len(accessors) > 1:
491
+ accessors[1].annotations.extend(set_anns)
492
+
493
+
494
+ def _cap(name: str) -> str:
495
+ """JVM capitalisation: first char uppercased, rest unchanged."""
496
+ return name[:1].upper() + name[1:] if name else name
497
+
498
+
499
+ def _accessor_method_decls(
500
+ prop_name: str,
501
+ type_simple: str,
502
+ is_var: bool,
503
+ info: dict,
504
+ *,
505
+ is_static: bool,
506
+ ) -> list[MethodDecl]:
507
+ """Synthesize JVM-accessor MethodDecl(s) for a non-private Kotlin property.
508
+
509
+ Matches Kotlin's actual JVM codegen (Calling Kotlin from Java → Properties):
510
+ the ``is``-prefix rule is NAME-based and type-agnostic — a property whose
511
+ name starts with ``is`` followed by an uppercase letter keeps the ``is``
512
+ prefix regardless of type. (``isActive`` → ``isActive()``/``setActive()``;
513
+ ``isAwesome: String`` → ``isAwesome()``/``setAwesome()``.)
514
+ * ``is`` + uppercase → getter = property name; setter (var only) drops the
515
+ ``is`` (``set`` + rest-capitalised). The ``[2].isupper()`` word-boundary is
516
+ required: ``issue`` → ``getIssue()`` (NOT ``issue()``), because
517
+ ``is``+lowercase is not the prefix.
518
+ * everything else → ``get`` + Name-capitalised (``name`` → ``getName()``;
519
+ Boolean ``foo`` → ``getFoo()``, NOT ``isFoo()``).
520
+ Setter only for ``var``. Getter is always final; setter is not.
521
+ """
522
+ is_is_prefixed = (
523
+ prop_name.startswith("is")
524
+ and len(prop_name) > 2
525
+ and prop_name[2].isupper()
526
+ )
527
+ if is_is_prefixed:
528
+ getter_name = prop_name
529
+ setter_name = "set" + _cap(prop_name[2:])
530
+ else:
531
+ getter_name = "get" + _cap(prop_name)
532
+ setter_name = "set" + _cap(prop_name)
533
+
534
+ out: list[MethodDecl] = [
535
+ MethodDecl(
536
+ name=getter_name,
537
+ return_type=type_simple,
538
+ is_constructor=False,
539
+ parameters=[],
540
+ signature=f"{getter_name}()",
541
+ modifiers=_build_member_modifiers(info, is_static=is_static, is_final=True),
542
+ )
543
+ ]
544
+ if is_var:
545
+ out.append(
546
+ MethodDecl(
547
+ name=setter_name,
548
+ return_type="",
549
+ is_constructor=False,
550
+ parameters=[
551
+ ParamDecl(
552
+ name="value", type_name=type_simple, type_raw=type_simple
553
+ )
554
+ ],
555
+ signature=f"{setter_name}({type_simple})",
556
+ modifiers=_build_member_modifiers(
557
+ info, is_static=is_static, is_final=False
558
+ ),
559
+ )
560
+ )
561
+ return out
562
+
563
+
564
+ def _function_value_parameters(node: Node | None) -> Node | None:
565
+ return next(
566
+ (c for c in (node.named_children if node is not None else [])
567
+ if c.type == "function_value_parameters"),
568
+ None,
569
+ )
570
+
571
+
572
+ def _params_from_function_value_parameters(
573
+ fv_params: Node | None, src: bytes
574
+ ) -> list[ParamDecl]:
575
+ """``function_value_parameters > parameter > (identifier, type)`` → ParamDecl list.
576
+
577
+ A parameter's annotations live in a preceding SIBLING ``parameter_modifiers``
578
+ node (NOT inside ``parameter``); they attach to the ParamDecl with their
579
+ ``use_site_target`` preserved (function params have no field/getter/setter —
580
+ every target, including ``None``, lands on the ParamDecl).
581
+ """
582
+ params: list[ParamDecl] = []
583
+ if fv_params is None:
584
+ return params
585
+ pending_anns: list[AnnotationRef] = []
586
+ saw_param = False
587
+ for c in fv_params.named_children:
588
+ if c.type == "parameter_modifiers":
589
+ # Accumulate; applies to the next ``parameter`` sibling.
590
+ if saw_param:
591
+ pending_anns = []
592
+ saw_param = False
593
+ pending_anns.extend(
594
+ _parse_kotlin_annotation(mc, src)
595
+ for mc in c.named_children
596
+ if mc.type == "annotation"
597
+ )
598
+ continue
599
+ if c.type != "parameter":
600
+ continue
601
+ name = ""
602
+ for pc in c.named_children:
603
+ if pc.type == "identifier":
604
+ name = _txt(pc, src)
605
+ break
606
+ type_node = _type_child(c)
607
+ type_simple = _simple_type_name(type_node, src)
608
+ params.append(
609
+ ParamDecl(
610
+ name=name,
611
+ type_name=type_simple,
612
+ type_raw=_txt(type_node, src) if type_node is not None else type_simple,
613
+ annotations=list(pending_anns),
614
+ )
615
+ )
616
+ pending_anns = []
617
+ saw_param = True
618
+ return params
619
+
620
+
621
+ # ---- Task 10: call-site extraction + constructor delegation ----
622
+ #
623
+ # Grammar (tree-sitter-kotlin 1.1.0), confirmed by probing:
624
+ # * Receiver call ``r.find(1)``:
625
+ # ``call_expression > (navigation_expression > [identifier 'r', ., identifier 'find'],
626
+ # value_arguments > value_argument)``. The ``navigation_expression`` LEFT SPINE
627
+ # (everything before the final ``.``) is the receiver; the LAST ``identifier``
628
+ # is the callee.
629
+ # * Chained call ``repo.findById(1).orElse(null)`` nests: the outer
630
+ # ``navigation_expression``'s left spine is itself a ``call_expression``; the
631
+ # walker recurses and emits one CallSite per ``call_expression``.
632
+ # * Constructor call ``Other(2)`` (no ``new`` in Kotlin):
633
+ # ``call_expression > (identifier 'Other', value_arguments)`` — callee target
634
+ # is a bare ``identifier`` with NO navigation receiver. Recognised as a
635
+ # constructor ONLY when that identifier names a known type (explicit import or
636
+ # same-CU declaration) — the capitalised-first-letter heuristic is rejected.
637
+ # * Bare receiverless call ``helper()`` has the SAME node shape as a constructor
638
+ # call (``call_expression > (identifier, value_arguments)``); discrimination is
639
+ # by the type-name set: known type → constructor, unknown → bare method call
640
+ # (Task 13 resolves against the file facade).
641
+ # * Method reference ``obj::foo`` parses as a ``navigation_expression`` whose
642
+ # separator is ``::`` (NOT ``.``) — so a standalone ``navigation_expression``
643
+ # with a ``::`` child is a method reference (arg_count = -1).
644
+ # ``a.b()::foo`` is a ``navigation_expression > (call_expression, ::, identifier)``;
645
+ # ``chained_method_reference`` is True when the spine is a ``call_expression``.
646
+ # * Trailing-lambda call ``foo(1) { it }`` wraps as
647
+ # ``call_expression > (call_expression 'foo(1)', annotated_lambda)`` — the
648
+ # wrapper has no clean callee and is skipped; recursing hits the inner
649
+ # ``call_expression`` which emits. ``list.map { it.foo() }`` is
650
+ # ``call_expression > (navigation_expression 'list.map', annotated_lambda)``
651
+ # → emits ``map`` (arg_count 0, no ``value_arguments``) and, inside the lambda,
652
+ # ``foo`` with ``in_lambda=True``.
653
+ # * Constructor delegation:
654
+ # - class header ``class D : Base(7)`` →
655
+ # ``class_declaration > delegation_specifiers > delegation_specifier >
656
+ # constructor_invocation > (user_type, value_arguments)``. When there is no
657
+ # explicit ``primary_constructor`` node, an implicit one is synthesised to
658
+ # carry the super-call site.
659
+ # - secondary ``constructor(...) : this(0)`` →
660
+ # ``secondary_constructor > constructor_delegation_call > (this|super,
661
+ # value_arguments)``.
662
+
663
+
664
+ def _value_argument_count(call_or_invocation: Node) -> int:
665
+ """Number of ``value_argument`` children under the node's ``value_arguments``.
666
+
667
+ Returns 0 when there is no ``value_arguments`` child (e.g. a trailing-lambda
668
+ call with no parenthesised args).
669
+ """
670
+ va = next(
671
+ (c for c in call_or_invocation.named_children if c.type == "value_arguments"),
672
+ None,
673
+ )
674
+ if va is None:
675
+ return 0
676
+ return sum(1 for c in va.named_children if c.type == "value_argument")
677
+
678
+
679
+ def _split_dot_navigation(nav: Node, src: bytes) -> tuple[str, str]:
680
+ """Split a ``.``-``navigation_expression`` into (receiver_text, callee).
681
+
682
+ The receiver is the raw text from the navigation start up to (not including)
683
+ the LAST ``.`` separator; the callee is the last ``identifier`` child. For
684
+ ``r.find`` → (``"r"``, ``"find"``); for ``foo.bar.baz`` → (``"foo.bar"``,
685
+ ``"baz"``); for ``repo.findById(1).orElse`` → (``"repo.findById(1)"``,
686
+ ``"orElse"``). Returns (``""``, ``""``) if no ``identifier`` callee is found.
687
+ """
688
+ ids = [c for c in nav.named_children if c.type == "identifier"]
689
+ if not ids:
690
+ return "", ""
691
+ callee = _txt(ids[-1], src)
692
+ # The last '.' separator (unnamed) marks the receiver/callee boundary.
693
+ dot = next(
694
+ (c for c in nav.children if not c.is_named and c.type == "."), None
695
+ )
696
+ if dot is not None:
697
+ recv = src[nav.start_byte:dot.start_byte].decode("utf-8", errors="replace")
698
+ else:
699
+ recv = "" # defensive: a '.'-navigation always has a '.', but stay safe.
700
+ return recv, callee
701
+
702
+
703
+ def _navigation_is_method_reference(nav: Node) -> bool:
704
+ """A ``navigation_expression`` is a method reference when it uses ``::``."""
705
+ return any(not c.is_named and c.type == "::" for c in nav.children)
706
+
707
+
708
+ def _collect_kotlin_call_sites(
709
+ body: Node | None,
710
+ src: bytes,
711
+ *,
712
+ caller_fqn: str,
713
+ type_names: frozenset[str],
714
+ ) -> list[CallSite]:
715
+ """Walk a function/constructor body and collect raw ``CallSite`` records.
716
+
717
+ Faithful capture only (Task 10): receiver/callee split, constructor vs bare
718
+ discrimination via the type-name set, arg counts (``-1`` for ``::`` refs),
719
+ ``in_lambda``, ``chained_method_reference``. Static-certainty and facade-call
720
+ resolution are deferred to Task 13; ``is_static_call`` is best-effort True
721
+ only when the receiver of a non-constructor call matches a known type name.
722
+ """
723
+ out: list[CallSite] = []
724
+ if body is None:
725
+ return out
726
+
727
+ def emit(site: CallSite) -> None:
728
+ out.append(site)
729
+
730
+ def visit(n: Node, lam: bool) -> None:
731
+ t = n.type
732
+ # Lambda body: descend with the in-lambda flag set on every nested call.
733
+ if t in ("lambda_literal", "lambda_expression"):
734
+ for ch in n.children:
735
+ visit(ch, True)
736
+ return
737
+ if t == "call_expression":
738
+ _emit_call_expression_site(n, src, lam=lam, caller_fqn=caller_fqn,
739
+ type_names=type_names, emit=emit)
740
+ for ch in n.children: # recurse for nested calls in receiver/args/lambda
741
+ visit(ch, lam)
742
+ return
743
+ if t == "navigation_expression":
744
+ # Standalone navigation (not the callee-target of a call_expression).
745
+ if _navigation_is_method_reference(n):
746
+ _emit_method_reference_site(n, src, lam=lam,
747
+ caller_fqn=caller_fqn, emit=emit)
748
+ # Either way, descend (spine may itself contain call_expressions).
749
+ for ch in n.children:
750
+ visit(ch, lam)
751
+ return
752
+ for ch in n.children:
753
+ visit(ch, lam)
754
+
755
+ visit(body, False)
756
+ return out
757
+
758
+
759
+ def _emit_call_expression_site(
760
+ n: Node,
761
+ src: bytes,
762
+ *,
763
+ lam: bool,
764
+ caller_fqn: str,
765
+ type_names: frozenset[str],
766
+ emit,
767
+ ) -> None:
768
+ """Emit one ``CallSite`` for a ``call_expression`` (if it has a clear callee).
769
+
770
+ Shapes:
771
+ * ``call_expression > (navigation_expression, value_arguments [, annotated_lambda])``
772
+ → receiver call.
773
+ * ``call_expression > (identifier, value_arguments)`` → constructor call if
774
+ the identifier is a known type, else a bare receiverless method call.
775
+ * ``call_expression > (call_expression, annotated_lambda)`` → trailing-lambda
776
+ wrapper with no own callee; skip (the inner call_expression emits on recurse).
777
+ """
778
+ named = n.named_children
779
+ if not named:
780
+ return
781
+ first = named[0]
782
+ # Trailing-lambda wrapper: callee lives on the inner call_expression.
783
+ if first.type == "call_expression":
784
+ return
785
+ line = n.start_point[0] + 1
786
+ byte = n.start_byte
787
+ if first.type == "navigation_expression":
788
+ recv, callee = _split_dot_navigation(first, src)
789
+ if not callee:
790
+ return
791
+ # Best-effort static-call flag: receiver text matches a known type name.
792
+ is_static = recv != "" and recv in type_names
793
+ emit(
794
+ CallSite(
795
+ caller_fqn=caller_fqn,
796
+ receiver_expr=recv,
797
+ callee_simple=callee,
798
+ arg_count=_value_argument_count(n),
799
+ is_static_call=is_static,
800
+ is_constructor=False,
801
+ in_lambda=lam,
802
+ line=line,
803
+ byte=byte,
804
+ )
805
+ )
806
+ return
807
+ if first.type == "identifier":
808
+ name = _txt(first, src)
809
+ argc = _value_argument_count(n)
810
+ if name in type_names:
811
+ # Constructor call: ``Other(2)`` where Other is a known type.
812
+ emit(
813
+ CallSite(
814
+ caller_fqn=caller_fqn,
815
+ receiver_expr=name,
816
+ callee_simple="<init>",
817
+ arg_count=argc,
818
+ is_static_call=False,
819
+ is_constructor=True,
820
+ in_lambda=lam,
821
+ line=line,
822
+ byte=byte,
823
+ )
824
+ )
825
+ else:
826
+ # Bare receiverless method call (Task 13 resolves via facade).
827
+ emit(
828
+ CallSite(
829
+ caller_fqn=caller_fqn,
830
+ receiver_expr="",
831
+ callee_simple=name,
832
+ arg_count=argc,
833
+ is_static_call=False,
834
+ is_constructor=False,
835
+ in_lambda=lam,
836
+ line=line,
837
+ byte=byte,
838
+ )
839
+ )
840
+ return
841
+ # Other callee shapes (e.g. ``super_expression``/``this_expression`` direct)
842
+ # have no clean method callee; skip — recursing still visits their children.
843
+
844
+
845
+ def _emit_method_reference_site(
846
+ n: Node, src: bytes, *, lam: bool, caller_fqn: str, emit
847
+ ) -> None:
848
+ """Emit a method-reference ``CallSite`` (arg_count = -1).
849
+
850
+ ``obj::foo`` → callee ``foo``, receiver ``obj``. ``a.b()::foo`` → callee
851
+ ``foo``, receiver ``a.b()``, ``chained_method_reference=True`` because the
852
+ spine is a ``call_expression`` (a call chain).
853
+ """
854
+ ids = [c for c in n.named_children if c.type == "identifier"]
855
+ if not ids:
856
+ return
857
+ callee = _txt(ids[-1], src)
858
+ dc = next((c for c in n.children if not c.is_named and c.type == "::"), None)
859
+ spine_text = (
860
+ src[n.start_byte:dc.start_byte].decode("utf-8", errors="replace")
861
+ if dc is not None
862
+ else ""
863
+ )
864
+ spine_node = n.named_children[0] if n.named_children else None
865
+ chained = spine_node is not None and spine_node.type == "call_expression"
866
+ emit(
867
+ CallSite(
868
+ caller_fqn=caller_fqn,
869
+ receiver_expr=spine_text,
870
+ callee_simple=callee,
871
+ arg_count=-1,
872
+ is_static_call=False,
873
+ is_constructor=False,
874
+ in_lambda=lam,
875
+ line=n.start_point[0] + 1,
876
+ byte=n.start_byte,
877
+ chained_method_reference=chained,
878
+ )
879
+ )
880
+
881
+
882
+ def _primary_ctor_delegation_site(
883
+ class_node: Node, src: bytes, *, caller_fqn: str
884
+ ) -> CallSite | None:
885
+ """``CallSite`` for class-header constructor delegation ``: Base(x)``/``: Super(x)``.
886
+
887
+ Walks ``delegation_specifiers > delegation_specifier > constructor_invocation``;
888
+ the receiver is the ``user_type`` simple name, args counted from
889
+ ``value_arguments``. Returns the first such site, or None when the class
890
+ declares no constructor-style delegation (interface/type supertypes only).
891
+ """
892
+ del_specs = next(
893
+ (c for c in class_node.named_children if c.type == "delegation_specifiers"),
894
+ None,
895
+ )
896
+ if del_specs is None:
897
+ return None
898
+ for spec in del_specs.named_children:
899
+ if spec.type != "delegation_specifier":
900
+ continue
901
+ ci = next(
902
+ (c for c in spec.named_children if c.type == "constructor_invocation"),
903
+ None,
904
+ )
905
+ if ci is None:
906
+ continue
907
+ ut = next((c for c in ci.named_children if c.type == "user_type"), None)
908
+ if ut is None:
909
+ continue
910
+ recv = _simple_type_name(ut, src)
911
+ if not recv:
912
+ continue
913
+ return CallSite(
914
+ caller_fqn=caller_fqn,
915
+ receiver_expr=recv,
916
+ callee_simple="<init>",
917
+ arg_count=_value_argument_count(ci),
918
+ is_static_call=False,
919
+ is_constructor=True,
920
+ in_lambda=False,
921
+ line=class_node.start_point[0] + 1,
922
+ byte=class_node.start_byte,
923
+ )
924
+ return None
925
+
926
+
927
+ def _secondary_ctor_delegation_site(
928
+ ctor_node: Node, src: bytes, *, caller_fqn: str
929
+ ) -> CallSite | None:
930
+ """``CallSite`` for a secondary-constructor delegation ``: this(...)``/``: super(...)``.
931
+
932
+ Source node: ``secondary_constructor > constructor_delegation_call > (this|super,
933
+ value_arguments)``. Receiver text is ``"this"``/``"super"``.
934
+ """
935
+ cdc = next(
936
+ (c for c in ctor_node.named_children if c.type == "constructor_delegation_call"),
937
+ None,
938
+ )
939
+ if cdc is None:
940
+ return None
941
+ is_super = any(c.type == "super" for c in cdc.children)
942
+ is_this = any(c.type == "this" for c in cdc.children)
943
+ recv = "super" if is_super else ("this" if is_this else "")
944
+ return CallSite(
945
+ caller_fqn=caller_fqn,
946
+ receiver_expr=recv,
947
+ callee_simple="<init>",
948
+ arg_count=_value_argument_count(cdc),
949
+ is_static_call=False,
950
+ is_constructor=True,
951
+ in_lambda=False,
952
+ line=ctor_node.start_point[0] + 1,
953
+ byte=ctor_node.start_byte,
954
+ )
955
+
956
+
957
+ def _process_function_declaration(
958
+ node: Node,
959
+ src: bytes,
960
+ *,
961
+ is_static_ctx: bool,
962
+ type_fqn: str,
963
+ type_names: frozenset[str],
964
+ ) -> MethodDecl:
965
+ """``function_declaration`` → MethodDecl (``is_constructor`` always False in 1.1.0).
966
+
967
+ All ``modifiers`` annotations attach to the MethodDecl (functions carry no
968
+ use-site target semantics; their ``use_site_target`` is preserved as-is).
969
+ Task 10 walks the ``function_body`` for ``CallSite`` s attributed to
970
+ ``<type_fqn>#<signature>``.
971
+ """
972
+ name = next(
973
+ (_txt(c, src) for c in node.named_children if c.type == "identifier"), ""
974
+ )
975
+ fv_params = _function_value_parameters(node)
976
+ ret_type_node = next(
977
+ (c for c in node.named_children if c.type in _TYPE_NODE_TYPES), None
978
+ )
979
+ params = _params_from_function_value_parameters(fv_params, src)
980
+ info = _collect_kotlin_modifiers(node, src)
981
+ anns = _kotlin_annotations_from_modifiers(node, src)
982
+ is_final = not (
983
+ _KOTLIN_INHERITANCE_NON_FINAL & set(info["inheritance"])
984
+ )
985
+ mods = _build_member_modifiers(info, is_static=is_static_ctx, is_final=is_final)
986
+ sig = f"{name}({','.join(p.type_name for p in params)})"
987
+ m = MethodDecl(
988
+ name=name,
989
+ return_type=_simple_type_name(ret_type_node, src),
990
+ is_constructor=False,
991
+ parameters=params,
992
+ modifiers=mods,
993
+ annotations=anns,
994
+ signature=sig,
995
+ start_byte=node.start_byte,
996
+ end_byte=node.end_byte,
997
+ start_line=node.start_point[0] + 1,
998
+ end_line=node.end_point[0] + 1,
999
+ )
1000
+ body = next((c for c in node.named_children if c.type == "function_body"), None)
1001
+ m.call_sites = _collect_kotlin_call_sites(
1002
+ body, src, caller_fqn=f"{type_fqn}#{sig}", type_names=type_names
1003
+ )
1004
+ return m
1005
+
1006
+
1007
+ def _process_secondary_constructor(
1008
+ node: Node,
1009
+ src: bytes,
1010
+ class_name: str,
1011
+ *,
1012
+ type_fqn: str,
1013
+ type_names: frozenset[str],
1014
+ ) -> MethodDecl:
1015
+ """``secondary_constructor`` → constructor MethodDecl (name = enclosing class).
1016
+
1017
+ Task 10 attaches the ``constructor_delegation_call`` site (``: this(...)`` /
1018
+ ``: super(...)``) at the constructor's start byte, plus any ``CallSite`` s in
1019
+ the optional ``block`` body.
1020
+ """
1021
+ params = _params_from_function_value_parameters(
1022
+ _function_value_parameters(node), src
1023
+ )
1024
+ anns = _kotlin_annotations_from_modifiers(node, src)
1025
+ sig = f"{class_name}({','.join(p.type_name for p in params)})"
1026
+ m = MethodDecl(
1027
+ name=class_name,
1028
+ return_type="",
1029
+ is_constructor=True,
1030
+ parameters=params,
1031
+ annotations=anns,
1032
+ signature=sig,
1033
+ start_byte=node.start_byte,
1034
+ end_byte=node.end_byte,
1035
+ start_line=node.start_point[0] + 1,
1036
+ end_line=node.end_point[0] + 1,
1037
+ )
1038
+ caller = f"{type_fqn}#{sig}"
1039
+ sites = _collect_kotlin_call_sites(
1040
+ next((c for c in node.named_children if c.type == "block"), None),
1041
+ src,
1042
+ caller_fqn=caller,
1043
+ type_names=type_names,
1044
+ )
1045
+ del_site = _secondary_ctor_delegation_site(node, src, caller_fqn=caller)
1046
+ if del_site is not None:
1047
+ sites.append(del_site)
1048
+ m.call_sites = sites
1049
+ return m
1050
+
1051
+
1052
+ def _process_property_declaration(
1053
+ node: Node, src: bytes, *, is_static_ctx: bool
1054
+ ) -> tuple[FieldDecl, list[MethodDecl]]:
1055
+ """``property_declaration`` → (FieldDecl, synthesized accessor MethodDecl[s]).
1056
+
1057
+ Accessors are synthesized only for non-private properties (private emits the
1058
+ FieldDecl alone). ``const`` forces ``static``.
1059
+
1060
+ Annotation routing (use-site target): ``field``/``property``/``None`` →
1061
+ FieldDecl; ``get`` → getter; ``set`` → setter. A ``param`` target is invalid
1062
+ on a body property and falls back to the FieldDecl.
1063
+ """
1064
+ vdecl = next(
1065
+ (c for c in node.named_children if c.type == "variable_declaration"), None
1066
+ )
1067
+ name = ""
1068
+ type_simple = ""
1069
+ if vdecl is not None:
1070
+ name = next(
1071
+ (_txt(vc, src) for vc in vdecl.named_children if vc.type == "identifier"),
1072
+ "",
1073
+ )
1074
+ type_simple = _simple_type_name(_type_child(vdecl), src)
1075
+
1076
+ is_var = _has_anon_keyword(node, "var")
1077
+ info = _collect_kotlin_modifiers(node, src)
1078
+ anns = _kotlin_annotations_from_modifiers(node, src)
1079
+ buckets = _route_kotlin_annotations_by_target(anns, default_slot="field")
1080
+ is_static = is_static_ctx or info["const"]
1081
+ field = FieldDecl(
1082
+ name=name,
1083
+ type_name=type_simple,
1084
+ type_raw=type_simple,
1085
+ modifiers=_build_member_modifiers(
1086
+ info, is_static=is_static, is_final=(not is_var) or info["const"]
1087
+ ),
1088
+ annotations=buckets["field"],
1089
+ start_byte=node.start_byte,
1090
+ end_byte=node.end_byte,
1091
+ start_line=node.start_point[0] + 1,
1092
+ end_line=node.end_point[0] + 1,
1093
+ )
1094
+ accessors: list[MethodDecl] = []
1095
+ if info["visibility"] != "private":
1096
+ accessors = _accessor_method_decls(
1097
+ name, type_simple, is_var, info, is_static=is_static
1098
+ )
1099
+ _attach_targeted_annotations_to_accessors(
1100
+ accessors, buckets["get"], buckets["set"]
1101
+ )
1102
+ return field, accessors
1103
+
1104
+
1105
+ def _process_class_parameter(
1106
+ node: Node, src: bytes, *, is_static_ctx: bool
1107
+ ) -> tuple[FieldDecl | None, list[MethodDecl], ParamDecl]:
1108
+ """A primary-constructor ``class_parameter`` → (field|None, accessors, ctor ParamDecl).
1109
+
1110
+ ``val``/``var`` parameters are properties (field + accessors); a plain
1111
+ parameter is just a constructor ParamDecl (no field, no accessor).
1112
+
1113
+ Annotation routing (use-site target): ``param``/``None`` → the ctor ParamDecl
1114
+ (None defaults to the ctor-param natural slot — the dominant Spring-Kotlin
1115
+ constructor-injection pattern); ``field``/``property`` → FieldDecl; ``get`` →
1116
+ getter; ``set`` → setter. A plain (non-val/var) param has only the ParamDecl
1117
+ slot, so every annotation lands there regardless of target.
1118
+ """
1119
+ name = next(
1120
+ (_txt(c, src) for c in node.named_children if c.type == "identifier"), ""
1121
+ )
1122
+ type_simple = _simple_type_name(_type_child(node), src)
1123
+ is_var = _has_anon_keyword(node, "var")
1124
+ is_val = _has_anon_keyword(node, "val")
1125
+ info = _collect_kotlin_modifiers(node, src)
1126
+ anns = _kotlin_annotations_from_modifiers(node, src)
1127
+ is_static = is_static_ctx or info["const"]
1128
+
1129
+ if not (is_var or is_val):
1130
+ # Plain constructor parameter: only a ParamDecl slot exists.
1131
+ ctor_param = ParamDecl(
1132
+ name=name, type_name=type_simple, type_raw=type_simple, annotations=anns
1133
+ )
1134
+ return None, [], ctor_param
1135
+
1136
+ buckets = _route_kotlin_annotations_by_target(anns, default_slot="param")
1137
+ ctor_param = ParamDecl(
1138
+ name=name,
1139
+ type_name=type_simple,
1140
+ type_raw=type_simple,
1141
+ annotations=buckets["param"],
1142
+ )
1143
+ field = FieldDecl(
1144
+ name=name,
1145
+ type_name=type_simple,
1146
+ type_raw=type_simple,
1147
+ modifiers=_build_member_modifiers(
1148
+ info, is_static=is_static, is_final=(not is_var) or info["const"]
1149
+ ),
1150
+ annotations=buckets["field"],
1151
+ start_byte=node.start_byte,
1152
+ end_byte=node.end_byte,
1153
+ start_line=node.start_point[0] + 1,
1154
+ end_line=node.end_point[0] + 1,
1155
+ )
1156
+ accessors: list[MethodDecl] = []
1157
+ if info["visibility"] != "private":
1158
+ accessors = _accessor_method_decls(
1159
+ name, type_simple, is_var, info, is_static=is_static
1160
+ )
1161
+ _attach_targeted_annotations_to_accessors(
1162
+ accessors, buckets["get"], buckets["set"]
1163
+ )
1164
+ return field, accessors, ctor_param
1165
+
1166
+
1167
+ def _process_primary_constructor(
1168
+ node: Node, src: bytes, class_name: str, *, is_static_ctx: bool
1169
+ ) -> tuple[MethodDecl, list[FieldDecl], list[MethodDecl]]:
1170
+ """``primary_constructor`` → (ctor MethodDecl, property fields, accessor methods).
1171
+
1172
+ Emits a constructor MethodDecl whose parameters include EVERY class_parameter
1173
+ (properties and plain params), so Java ``new T(...)`` resolves. ``val``/``var``
1174
+ parameters additionally contribute a FieldDecl + accessors.
1175
+ """
1176
+ params_node = next(
1177
+ (c for c in node.named_children if c.type == "class_parameters"), None
1178
+ )
1179
+ ctor_params: list[ParamDecl] = []
1180
+ fields: list[FieldDecl] = []
1181
+ accessors: list[MethodDecl] = []
1182
+ if params_node is not None:
1183
+ for c in params_node.named_children:
1184
+ if c.type != "class_parameter":
1185
+ continue
1186
+ field, accs, param = _process_class_parameter(
1187
+ c, src, is_static_ctx=is_static_ctx
1188
+ )
1189
+ ctor_params.append(param)
1190
+ if field is not None:
1191
+ fields.append(field)
1192
+ accessors.extend(accs)
1193
+ sig = f"{class_name}({','.join(p.type_name for p in ctor_params)})"
1194
+ ctor = MethodDecl(
1195
+ name=class_name,
1196
+ return_type="",
1197
+ is_constructor=True,
1198
+ parameters=ctor_params,
1199
+ modifiers=[],
1200
+ signature=sig,
1201
+ )
1202
+ return ctor, fields, accessors
1203
+
1204
+
1205
+ def _facade_stem(filename: str) -> str:
1206
+ """Filename stem for the top-level facade (``Foo.kt`` → ``Foo``)."""
1207
+ base = os.path.basename(filename) if filename else ""
1208
+ if base.endswith(".kt"):
1209
+ base = base[:-3]
1210
+ return base or "File"
1211
+
1212
+
1213
+ # ---- Task 9: @file:JvmName / @file:JvmMultifileClass facade naming ----
1214
+ #
1215
+ # Grammar (tree-sitter-kotlin 1.1.0), confirmed by probing:
1216
+ # * ``file_annotation`` nodes are siblings of ``package_header`` / ``import``
1217
+ # directly under ``source_file`` (NOT nested in ``modifiers``).
1218
+ # * ``@file:JvmName("X")`` →
1219
+ # ``file_annotation > constructor_invocation > (user_type > identifier "JvmName",
1220
+ # value_arguments > value_argument > string_literal > string_content)``.
1221
+ # * ``@file:JvmMultifileClass()`` →
1222
+ # ``file_annotation > constructor_invocation > user_type > identifier
1223
+ # "JvmMultifileClass"`` (empty ``value_arguments``).
1224
+ # The shared annotation-name / value-argument helpers (Task 8) work unchanged on a
1225
+ # ``file_annotation`` node because they walk the same ``constructor_invocation``
1226
+ # child shape.
1227
+
1228
+
1229
+ def _read_file_annotations(root: Node, src: bytes) -> tuple[str, bool]:
1230
+ """Return ``(jvm_name, is_multifile)`` from top-level ``file_annotation`` nodes.
1231
+
1232
+ ``@file:JvmName("X")`` → ``jvm_name = "X"`` (first wins if repeated);
1233
+ ``@file:JvmMultifileClass()`` → ``is_multifile = True``. Both default to the
1234
+ no-op value (``""`` / ``False``) when the annotation is absent.
1235
+ """
1236
+ jvm_name = ""
1237
+ is_multifile = False
1238
+ for child in root.named_children:
1239
+ if child.type != "file_annotation":
1240
+ continue
1241
+ simple, _ = _kotlin_annotation_name(child, src)
1242
+ if simple == "JvmName" and not jvm_name:
1243
+ args, _ = _kotlin_annotation_value_arguments(child, src)
1244
+ val = args.get("value", "")
1245
+ if val:
1246
+ jvm_name = val
1247
+ elif simple == "JvmMultifileClass":
1248
+ is_multifile = True
1249
+ return jvm_name, is_multifile
1250
+
1251
+
1252
+ # ---- Task 8: extends/implements partition (B7-soft) ----
1253
+ #
1254
+ # Kotlin surfaces every supertype — class to extend, interface to implement, and
1255
+ # ``by``-delegation target — as one comma-separated ``delegation_specifiers``
1256
+ # clause after ``:``. Each ``delegation_specifier`` is one of:
1257
+ # * ``user_type`` (plain interface/type)
1258
+ # * ``constructor_invocation > user_type`` (class with ctor call, e.g. ``Base(c)``)
1259
+ # * ``explicit_delegation > user_type`` (``I by impl()`` — supertype is ``I``)
1260
+ # In all cases the supertype simple name lives in a descendant ``user_type``.
1261
+ #
1262
+ # Partition rule (no cross-file resolution in the extractor): a supertype whose
1263
+ # simple name is declared in THIS compilation unit with folded kind in
1264
+ # {class, record, enum} → extends; declared interface → implements; everything
1265
+ # else (unknown, or annotation kind) → implements (a spurious IMPLEMENTS is less
1266
+ # damaging than a false EXTENDS). An interface declaration emits ONLY implements.
1267
+
1268
+ # Kotlin folded kinds that count as a class-kind for the extends branch.
1269
+ _KOTLIN_CLASS_KINDS: frozenset[str] = frozenset({"class", "record", "enum"})
1270
+
1271
+
1272
+ def _pre_scan_kotlin_type_kinds(root: Node, src: bytes) -> dict[str, str]:
1273
+ """Map simple type name → folded kind for every declaration in this CU.
1274
+
1275
+ Walks all ``class_declaration`` / ``object_declaration`` / ``companion_object``
1276
+ nodes (top-level and nested). Last-wins on name collision (rare; nested names
1277
+ shadow). Used by the supertype partition — same-CU resolution only.
1278
+ """
1279
+ out: dict[str, str] = {}
1280
+
1281
+ def visit(n: Node) -> None:
1282
+ t = n.type
1283
+ if t == "class_declaration":
1284
+ kind = _kotlin_class_kind(n, src)
1285
+ elif t in ("object_declaration", "companion_object"):
1286
+ kind = "class"
1287
+ else:
1288
+ kind = ""
1289
+ if kind:
1290
+ nm = _kotlin_decl_name(n, src)
1291
+ if nm:
1292
+ out[nm] = kind
1293
+ for c in n.children:
1294
+ visit(c)
1295
+
1296
+ visit(root)
1297
+ return out
1298
+
1299
+
1300
+ def _supertype_simple_name(delegation_specifier: Node, src: bytes) -> str:
1301
+ """Head simple name from a ``delegation_specifier`` (generics/nullable stripped).
1302
+
1303
+ Finds the first descendant ``user_type`` (direct, under
1304
+ ``constructor_invocation``, or under ``explicit_delegation``) and returns its
1305
+ head simple name via ``_simple_type_name``. Returns ``""`` if none found.
1306
+ """
1307
+ user_type = next(
1308
+ (c for c in delegation_specifier.named_children if c.type == "user_type"),
1309
+ None,
1310
+ )
1311
+ if user_type is None:
1312
+ for c in delegation_specifier.named_children:
1313
+ if c.type in ("constructor_invocation", "explicit_delegation"):
1314
+ user_type = next(
1315
+ (gc for gc in c.named_children if gc.type == "user_type"),
1316
+ None,
1317
+ )
1318
+ if user_type is not None:
1319
+ break
1320
+ if user_type is None:
1321
+ # Fall back to any nested user_type (defensive — shouldn't happen).
1322
+ for c in delegation_specifier.children:
1323
+ if c.type == "user_type":
1324
+ user_type = c
1325
+ break
1326
+ return _simple_type_name(user_type, src) if user_type is not None else ""
1327
+
1328
+
1329
+ def _kotlin_extends_implements(
1330
+ class_node: Node,
1331
+ src: bytes,
1332
+ self_kind: str,
1333
+ kind_by_simple: dict[str, str],
1334
+ ) -> tuple[list[str], list[str]]:
1335
+ """Partition the ``:`` supertype list of a class/interface into (extends, implements).
1336
+
1337
+ Interfaces (``self_kind == "interface"``) emit only ``implements`` regardless
1338
+ of the supertypes' own kinds. Generics/nullable are stripped to simple names.
1339
+ """
1340
+ extends: list[str] = []
1341
+ implements: list[str] = []
1342
+ del_specs = next(
1343
+ (c for c in class_node.named_children if c.type == "delegation_specifiers"),
1344
+ None,
1345
+ )
1346
+ if del_specs is None:
1347
+ return extends, implements
1348
+ for spec in del_specs.named_children:
1349
+ if spec.type != "delegation_specifier":
1350
+ continue
1351
+ simple = _supertype_simple_name(spec, src)
1352
+ if not simple:
1353
+ continue
1354
+ if self_kind == "interface":
1355
+ implements.append(simple)
1356
+ continue
1357
+ kind = kind_by_simple.get(simple)
1358
+ if kind in _KOTLIN_CLASS_KINDS:
1359
+ extends.append(simple)
1360
+ else:
1361
+ # interface, annotation, or unknown → implements (safe default).
1362
+ implements.append(simple)
1363
+ return extends, implements
1364
+
1365
+
1366
+ def _parse_kotlin_type(
1367
+ node: Node,
1368
+ src: bytes,
1369
+ *,
1370
+ package: str,
1371
+ outer_fqn: str | None,
1372
+ all_types: list[TypeDecl],
1373
+ kind_by_simple: dict[str, str],
1374
+ type_names: frozenset[str],
1375
+ filename: str = "",
1376
+ ) -> TypeDecl | None:
1377
+ """Build a ``TypeDecl`` for a Kotlin type declaration node (Tasks 6–8 + 10).
1378
+
1379
+ Recurses into the declaration's body (``class_body`` / ``enum_class_body``)
1380
+ for nested ``class_declaration`` / ``object_declaration`` /
1381
+ ``companion_object`` nodes and walks member nodes into ``fields`` / ``methods``:
1382
+ ``function_declaration`` / ``secondary_constructor`` → methods;
1383
+ ``property_declaration`` and ``val``/``var`` primary-constructor parameters →
1384
+ field + synthesized accessors. Companion-object direct members carry
1385
+ ``"static"``. Task 8 adds type-level ``annotations`` and the
1386
+ ``extends``/``implements`` partition of the ``:`` supertype list. Task 10
1387
+ populates ``MethodDecl.call_sites`` (function/secondary-ctor bodies + primary
1388
+ + secondary constructor delegation); ``type_names`` drives constructor vs
1389
+ bare-call discrimination and the best-effort ``is_static_call`` flag.
1390
+ """
1391
+ t = node.type
1392
+ if t == "class_declaration":
1393
+ kind = _kotlin_class_kind(node, src)
1394
+ elif t in ("object_declaration", "companion_object"):
1395
+ kind = "class"
1396
+ else:
1397
+ return None
1398
+
1399
+ name = _kotlin_decl_name(node, src)
1400
+ if outer_fqn:
1401
+ fqn = f"{outer_fqn}.{name}"
1402
+ elif package:
1403
+ fqn = f"{package}.{name}"
1404
+ else:
1405
+ fqn = name
1406
+
1407
+ # Direct members of a `companion_object` compile to static JVM members.
1408
+ members_are_static = t == "companion_object"
1409
+
1410
+ # Task 8: type-level annotations + extends/implements partition.
1411
+ type_anns = _kotlin_annotations_from_modifiers(node, src)
1412
+ if t == "class_declaration":
1413
+ extends, implements = _kotlin_extends_implements(
1414
+ node, src, self_kind=kind, kind_by_simple=kind_by_simple
1415
+ )
1416
+ else:
1417
+ extends, implements = [], [] # objects/companion have no supertype clause.
1418
+
1419
+ nested: list[TypeDecl] = []
1420
+ decl = TypeDecl(
1421
+ name=name,
1422
+ kind=kind,
1423
+ fqn=fqn,
1424
+ annotations=type_anns,
1425
+ extends=extends,
1426
+ implements=implements,
1427
+ nested=nested,
1428
+ start_byte=node.start_byte,
1429
+ end_byte=node.end_byte,
1430
+ start_line=node.start_point[0] + 1,
1431
+ end_line=node.end_point[0] + 1,
1432
+ outer_fqn=outer_fqn,
1433
+ )
1434
+ all_types.append(decl)
1435
+
1436
+ fields: list[FieldDecl] = []
1437
+ methods: list[MethodDecl] = []
1438
+
1439
+ # Primary constructor (class header). Only `class_declaration` carries one;
1440
+ # it contributes the constructor MethodDecl + any val/var property members.
1441
+ # Task 10: when the class header declares constructor-style delegation
1442
+ # (``: Base(x)``) but there is no explicit primary_constructor, synthesise an
1443
+ # implicit one to carry the super-call CallSite.
1444
+ primary_ctor: MethodDecl | None = None
1445
+ if t == "class_declaration":
1446
+ pc = next(
1447
+ (c for c in node.named_children if c.type == "primary_constructor"),
1448
+ None,
1449
+ )
1450
+ if pc is not None:
1451
+ primary_ctor, cfields, caccs = _process_primary_constructor(
1452
+ pc, src, name, is_static_ctx=members_are_static
1453
+ )
1454
+ fields.extend(cfields)
1455
+ methods.extend(caccs)
1456
+ del_site = _primary_ctor_delegation_site(
1457
+ node, src, caller_fqn=f"{fqn}#{name}()"
1458
+ )
1459
+ if del_site is not None:
1460
+ if primary_ctor is None:
1461
+ # No explicit primary_constructor: synthesise the implicit ctor
1462
+ # Kotlin generates, and fix its caller_fqn to the real signature.
1463
+ primary_ctor = MethodDecl(
1464
+ name=name,
1465
+ return_type="",
1466
+ is_constructor=True,
1467
+ parameters=[],
1468
+ signature=f"{name}()",
1469
+ )
1470
+ del_site.caller_fqn = f"{fqn}#{primary_ctor.signature}"
1471
+ primary_ctor.call_sites.append(del_site)
1472
+ if primary_ctor is not None:
1473
+ methods.append(primary_ctor)
1474
+
1475
+ body: Node | None = None
1476
+ for c in node.named_children:
1477
+ if c.type in ("class_body", "enum_class_body"):
1478
+ body = c
1479
+ break
1480
+ if body is not None:
1481
+ for ch in body.named_children:
1482
+ ct = ch.type
1483
+ if ct == "function_declaration":
1484
+ methods.append(
1485
+ _process_function_declaration(
1486
+ ch,
1487
+ src,
1488
+ is_static_ctx=members_are_static,
1489
+ type_fqn=fqn,
1490
+ type_names=type_names,
1491
+ )
1492
+ )
1493
+ elif ct == "secondary_constructor":
1494
+ methods.append(
1495
+ _process_secondary_constructor(
1496
+ ch,
1497
+ src,
1498
+ name,
1499
+ type_fqn=fqn,
1500
+ type_names=type_names,
1501
+ )
1502
+ )
1503
+ elif ct == "property_declaration":
1504
+ field, accs = _process_property_declaration(
1505
+ ch, src, is_static_ctx=members_are_static
1506
+ )
1507
+ fields.append(field)
1508
+ methods.extend(accs)
1509
+ elif ct in _KOTLIN_TYPE_NODES:
1510
+ child_decl = _parse_kotlin_type(
1511
+ ch,
1512
+ src,
1513
+ package=package,
1514
+ outer_fqn=fqn,
1515
+ all_types=all_types,
1516
+ kind_by_simple=kind_by_simple,
1517
+ type_names=type_names,
1518
+ filename=filename,
1519
+ )
1520
+ if child_decl is not None:
1521
+ nested.append(child_decl)
1522
+
1523
+ decl.fields = fields
1524
+ decl.methods = methods
1525
+ return decl
1526
+
1527
+
1528
+ def parse_kotlin(source: bytes | str, *, filename: str = "", verbose: bool = False) -> JavaFileAst:
1529
+ """Parse a Kotlin file into a ``JavaFileAst``. Never raises on invalid source.
1530
+
1531
+ Populates ``package``, ``imports``, ``wildcard_imports``,
1532
+ ``explicit_imports``, and ``file_imports``; tags ``language="kotlin"``;
1533
+ sets ``parse_error`` from the tree-sitter error flag. Walks top-level
1534
+ type declarations (``class_declaration`` / ``object_declaration``) into
1535
+ ``top_level_types`` with the folded kind map; ``all_types`` is the flat
1536
+ pre-order list including nested types (``companion_object`` / nested
1537
+ ``class_declaration``). Members (fields/methods) arrive in a later task.
1538
+ """
1539
+ del verbose # accepted for signature parity with JavaBackend.parse; no brownfield events yet.
1540
+
1541
+ if isinstance(source, str):
1542
+ src = source.encode("utf-8", errors="replace")
1543
+ else:
1544
+ src = source
1545
+
1546
+ empty = JavaFileAst(
1547
+ package="",
1548
+ imports=[],
1549
+ wildcard_imports=[],
1550
+ explicit_imports={},
1551
+ top_level_types=[],
1552
+ all_types=[],
1553
+ language="kotlin",
1554
+ parse_error=False,
1555
+ source_bytes=len(src),
1556
+ file_imports=FileImports(),
1557
+ routes_skipped_unresolved=0,
1558
+ )
1559
+
1560
+ if not src:
1561
+ return empty
1562
+
1563
+ try:
1564
+ tree = _parser().parse(src)
1565
+ except Exception:
1566
+ empty.parse_error = True
1567
+ return empty
1568
+
1569
+ root = tree.root_node
1570
+ package = ""
1571
+ imports: list[str] = []
1572
+ wildcard_imports: list[str] = []
1573
+ explicit_imports: dict[str, str] = {}
1574
+
1575
+ for child in root.named_children:
1576
+ t = child.type
1577
+ if t == "package_header":
1578
+ for c in child.named_children:
1579
+ if c.type == "qualified_identifier":
1580
+ package = _txt(c, src)
1581
+ break
1582
+ elif t == "import":
1583
+ qi: Node | None = None
1584
+ alias: Node | None = None
1585
+ has_wild = False
1586
+ for c in child.children:
1587
+ if c.type == "qualified_identifier":
1588
+ qi = c
1589
+ elif c.type == "identifier":
1590
+ # The trailing alias (`import a.B as Q`); the only named
1591
+ # `identifier` sibling is the alias — the path is the
1592
+ # `qualified_identifier` sibling.
1593
+ alias = c
1594
+ elif c.type == "*":
1595
+ has_wild = True
1596
+ if qi is None:
1597
+ continue
1598
+ fqn = _txt(qi, src)
1599
+ if has_wild:
1600
+ imports.append(f"{fqn}.*")
1601
+ wildcard_imports.append(fqn)
1602
+ else:
1603
+ if alias is not None:
1604
+ key = _txt(alias, src)
1605
+ imports.append(f"{fqn} as {key}")
1606
+ else:
1607
+ key = fqn.rsplit(".", 1)[-1]
1608
+ imports.append(fqn)
1609
+ explicit_imports[key] = fqn
1610
+
1611
+ file_imports = FileImports(
1612
+ explicit=explicit_imports,
1613
+ # Kotlin has no `import static`: static_methods / static_wildcards stay empty.
1614
+ )
1615
+
1616
+ # Pre-scan declared type kinds (simple name → folded kind) for the same-CU
1617
+ # supertype partition (Task 8). Built once from the whole tree.
1618
+ kind_by_simple = _pre_scan_kotlin_type_kinds(root, src)
1619
+
1620
+ # Task 10: the set of type names visible in this CU (explicit imports +
1621
+ # same-CU declarations) drives constructor-vs-bare-call discrimination and
1622
+ # the best-effort ``is_static_call`` flag. Built once, threaded everywhere.
1623
+ type_names: frozenset[str] = frozenset(
1624
+ set(explicit_imports.keys()) | set(kind_by_simple.keys())
1625
+ )
1626
+
1627
+ # Walk top-level type declarations (class_declaration / object_declaration /
1628
+ # companion_object) into TypeDecl rows with the folded kind map, now also
1629
+ # populating members (Task 7) and annotations/supertypes (Task 8). Top-level
1630
+ # functions/properties are collected below onto a synthetic facade TypeDecl.
1631
+ top_level_types: list[TypeDecl] = []
1632
+ all_types: list[TypeDecl] = []
1633
+ for child in root.named_children:
1634
+ if child.type in _KOTLIN_TYPE_NODES:
1635
+ decl = _parse_kotlin_type(
1636
+ child,
1637
+ src,
1638
+ package=package,
1639
+ outer_fqn=None,
1640
+ all_types=all_types,
1641
+ kind_by_simple=kind_by_simple,
1642
+ type_names=type_names,
1643
+ filename=filename,
1644
+ )
1645
+ if decl is not None:
1646
+ top_level_types.append(decl)
1647
+
1648
+ # Top-level functions / properties → synthetic facade (Task 9: named via
1649
+ # @file:JvmName else `<CapitalisedStem>Kt`; @file:JvmMultifileClass adds the
1650
+ # ``kotlin_multifile`` capability so ``merge_multifile_facades`` can group).
1651
+ # Facade members are static.
1652
+ top_level_funcs = [
1653
+ c for c in root.named_children if c.type == "function_declaration"
1654
+ ]
1655
+ top_level_props = [
1656
+ c for c in root.named_children if c.type == "property_declaration"
1657
+ ]
1658
+ if top_level_funcs or top_level_props:
1659
+ jvm_name, is_multifile = _read_file_annotations(root, src)
1660
+ # Kotlin's default facade name: capitalise the filename stem's first
1661
+ # letter, append ``Kt`` (foo.kt → FooKt, myFile.kt → MyFileKt).
1662
+ facade_name = jvm_name or (_cap(_facade_stem(filename)) + "Kt")
1663
+ facade_fqn = (
1664
+ f"{package}.{facade_name}" if package else facade_name
1665
+ )
1666
+ capabilities = (
1667
+ ["kotlin_facade", "kotlin_multifile"]
1668
+ if is_multifile
1669
+ else ["kotlin_facade"]
1670
+ )
1671
+ facade = TypeDecl(
1672
+ name=facade_name,
1673
+ kind="class",
1674
+ fqn=facade_fqn,
1675
+ capabilities=capabilities,
1676
+ )
1677
+ all_types.append(facade)
1678
+ top_level_types.append(facade)
1679
+ for fn in top_level_funcs:
1680
+ facade.methods.append(
1681
+ _process_function_declaration(
1682
+ fn,
1683
+ src,
1684
+ is_static_ctx=True,
1685
+ type_fqn=facade_fqn,
1686
+ type_names=type_names,
1687
+ )
1688
+ )
1689
+ for pr in top_level_props:
1690
+ field, accs = _process_property_declaration(
1691
+ pr, src, is_static_ctx=True
1692
+ )
1693
+ facade.fields.append(field)
1694
+ facade.methods.extend(accs)
1695
+
1696
+ return JavaFileAst(
1697
+ package=package,
1698
+ imports=imports,
1699
+ wildcard_imports=wildcard_imports,
1700
+ explicit_imports=explicit_imports,
1701
+ top_level_types=top_level_types,
1702
+ all_types=all_types,
1703
+ language="kotlin",
1704
+ parse_error=root.has_error,
1705
+ source_bytes=len(src),
1706
+ file_imports=file_imports,
1707
+ routes_skipped_unresolved=0,
1708
+ )
1709
+
1710
+
1711
+ # ---- Task 9: cross-file multifile-facade merge ----
1712
+ #
1713
+ # Kotlin compiles every file annotated with the same ``@file:JvmName("X")`` +
1714
+ # ``@file:JvmMultifileClass()`` into ONE JVM class ``pkg.X``. The per-file parse
1715
+ # therefore emits one facade per file, all claiming FQN ``pkg.X``; without a
1716
+ # merge, downstream ``tables.types[fqn] = entry`` overwrites and one file's
1717
+ # top-level functions silently vanish from resolution. This helper merges them
1718
+ # BEFORE the graph builder runs (Task 11 wires it into the index flow).
1719
+ #
1720
+ # Capability strings are the ONLY recognition signal (no new field/column):
1721
+ # ``"kotlin_facade"`` marks a facade; ``"kotlin_multifile"`` marks one that
1722
+ # participates in the cross-file merge. Two files sharing ``@file:JvmName("X")``
1723
+ # WITHOUT ``@JvmMultifileClass`` are the illegal/ambiguous same-FQN collision and
1724
+ # are left alone (both facades survive — never silently dropped).
1725
+
1726
+
1727
+ def _find_facade(ast: JavaFileAst) -> TypeDecl | None:
1728
+ """The single ``kotlin_facade`` TypeDecl of an AST, or None (zero or >1)."""
1729
+ facades = [t for t in ast.top_level_types if "kotlin_facade" in t.capabilities]
1730
+ return facades[0] if len(facades) == 1 else None
1731
+
1732
+
1733
+ def _concat_members_deduped(target: TypeDecl, source: TypeDecl) -> None:
1734
+ """Append ``source`` methods/fields onto ``target``, deduping identical members.
1735
+
1736
+ Methods dedupe on ``(name, signature)``; fields on ``(name, type_name)`` (the
1737
+ closest field analog of a signature — two top-level properties with the same
1738
+ name cannot coexist in one multifile class anyway). First occurrence wins.
1739
+ """
1740
+ seen_methods = {(m.name, m.signature) for m in target.methods}
1741
+ for m in source.methods:
1742
+ key = (m.name, m.signature)
1743
+ if key not in seen_methods:
1744
+ seen_methods.add(key)
1745
+ target.methods.append(m)
1746
+ seen_fields = {(f.name, f.type_name) for f in target.fields}
1747
+ for f in source.fields:
1748
+ key = (f.name, f.type_name)
1749
+ if key not in seen_fields:
1750
+ seen_fields.add(key)
1751
+ target.fields.append(f)
1752
+
1753
+
1754
+ def _strip_facade(ast: JavaFileAst, facade: TypeDecl) -> None:
1755
+ """Remove ``facade`` from the AST's ``top_level_types`` and ``all_types``."""
1756
+ ast.top_level_types = [t for t in ast.top_level_types if t is not facade]
1757
+ ast.all_types = [t for t in ast.all_types if t is not facade]
1758
+
1759
+
1760
+ def merge_multifile_facades(asts: list[JavaFileAst]) -> list[JavaFileAst]:
1761
+ """Merge ``@file:JvmMultifileClass`` facades that share ``(package, name)``.
1762
+
1763
+ For each group of two-or-more ASTs whose facade carries the
1764
+ ``kotlin_multifile`` capability AND shares the same ``(package, facade_name)``,
1765
+ keep ONE facade (on the first AST of the group), concatenate every other
1766
+ member's facade ``methods``/``fields`` onto it (deduped), and strip the
1767
+ duplicate facades from the other ASTs' type lists. Non-facade types are
1768
+ untouched in every AST. Non-multifile facades are left as-is — including the
1769
+ collision case of two same-``@file:JvmName`` files without
1770
+ ``@JvmMultifileClass`` (both survive).
1771
+
1772
+ Returns the reshaped list (same length; merges applied in place on the
1773
+ retained ASTs).
1774
+ """
1775
+ # Group AST indices by (package, facade_name) for multifile facades only.
1776
+ groups: dict[tuple[str, str], list[int]] = {}
1777
+ for i, ast in enumerate(asts):
1778
+ facade = _find_facade(ast)
1779
+ if facade is None or "kotlin_multifile" not in facade.capabilities:
1780
+ continue
1781
+ groups.setdefault((ast.package, facade.name), []).append(i)
1782
+
1783
+ for indices in groups.values():
1784
+ if len(indices) <= 1:
1785
+ continue # single multifile file — nothing to merge.
1786
+ retained = _find_facade(asts[indices[0]])
1787
+ assert retained is not None # grouped facades exist by construction.
1788
+ for other_idx in indices[1:]:
1789
+ other = _find_facade(asts[other_idx])
1790
+ assert other is not None
1791
+ _concat_members_deduped(retained, other)
1792
+ _strip_facade(asts[other_idx], other)
1793
+
1794
+ return asts