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.
- java_codebase_rag/_deprecation.py +103 -0
- java_codebase_rag/_version.py +2 -2
- java_codebase_rag/ast/ast_java.py +22 -0
- java_codebase_rag/ast/ast_kotlin.py +1794 -0
- java_codebase_rag/ast/chunk_heuristics.py +26 -5
- java_codebase_rag/ast/language.py +117 -0
- java_codebase_rag/cli.py +17 -17
- java_codebase_rag/cli_dispatch.py +251 -0
- java_codebase_rag/config.py +8 -8
- java_codebase_rag/eval/runner.py +3 -3
- java_codebase_rag/graph/build_ast_graph.py +130 -8
- java_codebase_rag/graph/graph_enrich.py +8 -5
- java_codebase_rag/graph/ladybug_queries.py +1 -1
- java_codebase_rag/graph/path_filtering.py +39 -7
- java_codebase_rag/index/java_index_flow_lancedb.py +160 -15
- java_codebase_rag/install_data/agents/explorer-rag-cli.md +6 -4
- java_codebase_rag/install_data/agents/explorer-rag-enhanced.md +4 -4
- java_codebase_rag/install_data/skills/explore-codebase/SKILL.md +4 -4
- java_codebase_rag/install_data/skills/explore-codebase-cli/SKILL.md +5 -5
- java_codebase_rag/installer.py +15 -15
- java_codebase_rag/jrag.py +25 -11
- java_codebase_rag/lance_optimize.py +7 -7
- java_codebase_rag/mcp/mcp_v2.py +2 -2
- java_codebase_rag/mcp/server.py +6 -4
- java_codebase_rag/pipeline.py +4 -4
- java_codebase_rag/progress.py +1 -1
- java_codebase_rag/search/search_lexical.py +1 -1
- java_codebase_rag/search/search_scoring.py +19 -5
- java_codebase_rag/watch/lock.py +1 -1
- java_codebase_rag/watch/watcher.py +45 -21
- {java_codebase_rag-0.11.2.dist-info → java_codebase_rag-0.12.0.dist-info}/METADATA +31 -22
- {java_codebase_rag-0.11.2.dist-info → java_codebase_rag-0.12.0.dist-info}/RECORD +36 -32
- java_codebase_rag-0.12.0.dist-info/entry_points.txt +5 -0
- java_codebase_rag-0.11.2.dist-info/entry_points.txt +0 -4
- {java_codebase_rag-0.11.2.dist-info → java_codebase_rag-0.12.0.dist-info}/WHEEL +0 -0
- {java_codebase_rag-0.11.2.dist-info → java_codebase_rag-0.12.0.dist-info}/licenses/LICENSE +0 -0
- {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
|