From 83a393b695369ec8a35e1f182e4aea195b6d8946 Mon Sep 17 00:00:00 2001 From: xiongjianxu <6457197+xiongjianxu@users.noreply.github.com> Date: Sun, 20 Sep 2026 10:16:32 +0800 Subject: [PATCH] Park Kotlin object member calls across a repo boundary (#3388) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The cross-file `_resolve_kotlin_member_calls` landed for #1698 binds `Receiver.method()` only when the object/class the receiver names is declared in the same build. When that declaration lives in another repository the pass had the receiver name in hand and dropped the call anyway, so nothing about it reached graph.json — the one artifact merge-graphs and global add read — and no merge-time pass could recover what was never written down. Park the call by name at that bail-out, the same mechanism Java/C++/C#/Swift already use: only when the object is declared nowhere here (an ambiguous 2+ match still fabricates nothing) and never for a builtin receiver. cross_repo_calls then finishes the edge once the declaring repo joins the graph, gated on .kt/.kts so a same-named declaration in another language cannot answer a Kotlin call. Co-Authored-By: Claude Code (cherry picked from commit fa193c96c2d2b4ff4a2e400c723c244236bf306e) --- graphify/cross_repo_calls.py | 1 + graphify/extract.py | 8 ++++++++ tests/test_cross_repo_member_calls.py | 15 ++++++++++++--- 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/graphify/cross_repo_calls.py b/graphify/cross_repo_calls.py index 2e571042d..841ca39f4 100644 --- a/graphify/cross_repo_calls.py +++ b/graphify/cross_repo_calls.py @@ -43,6 +43,7 @@ _LANG_SUFFIXES: dict[str, frozenset[str]] = { "csharp": frozenset({".cs"}), "go": frozenset({".go"}), "java": frozenset({".java"}), + "kotlin": frozenset({".kt", ".kts"}), "swift": frozenset({".swift"}), "php": frozenset({".php", ".phtml", ".php3", ".php4", ".php5", ".php7", ".phps"}), # TS and JS share one key: a TS class legitimately answers a JS call site, so diff --git a/graphify/extract.py b/graphify/extract.py index 07efd41f2..68bdfe388 100644 --- a/graphify/extract.py +++ b/graphify/extract.py @@ -7145,6 +7145,14 @@ def _resolve_kotlin_member_calls( callee = rc["callee"] caller = rc["caller_nid"] type_nids = types_by_name.get(receiver, []) + if not type_nids: + # Object/class declared nowhere here — usually in a repo this build does + # not contain, so park it for the merge (#3152/#3388). A builtin receiver + # (String.format, ...) never crosses a repo boundary. + if receiver not in _LANGUAGE_BUILTIN_GLOBALS: + _park_unresolved_member_call( + node_by_id.get(caller), callee, receiver, "kotlin", rc) + continue if len(type_nids) != 1: continue wanted = f".{callee}" diff --git a/tests/test_cross_repo_member_calls.py b/tests/test_cross_repo_member_calls.py index 4a435509b..0129a3504 100644 --- a/tests/test_cross_repo_member_calls.py +++ b/tests/test_cross_repo_member_calls.py @@ -6,9 +6,9 @@ type already in hand and nothing about it reached `graph.json` — the only arti `merge-graphs` and `global add` read. The two-repo graph was missing precisely the edges that make it a call graph. -The Java, C++, C#, Swift, Go, PHP and TS/JS resolvers now park those calls on the -caller node and this pass finishes them after the merge. The cases below pin what it -must NOT do as much as what it must: the single-definition guard, the cross-repo-only +The Java, C++, C#, Swift, Go, Kotlin, PHP and TS/JS resolvers now park those calls on +the caller node and this pass finishes them after the merge. The cases below pin what +it must NOT do as much as what it must: the single-definition guard, the cross-repo-only scope, and the language guard are what keep it from fabricating an edge from a name collision. """ @@ -47,6 +47,7 @@ needs_csharp = _needs("tree_sitter_c_sharp") needs_swift = _needs("tree_sitter_swift") needs_go = _needs("tree_sitter_go") needs_typescript = _needs("tree_sitter_typescript") +needs_kotlin = _needs("tree_sitter_kotlin") def _caller(repo: str, parked: list[dict], node_id: str = "app_run", @@ -469,6 +470,14 @@ def test_a_java_build_parks_the_call_and_the_merge_finishes_it(tmp_path: Path): "func (g *Greeter) Greet() {}\n"), marks=needs_go, id="go-struct-field", ), + pytest.param( + # The object-qualified shape the Kotlin resolver answers: `Greeter.greet()` + # names the object singleton explicitly, and only the other repo declares it. + "kotlin", "greet", + ("src/App.kt", "fun run() { Greeter.greet() }\n"), + ("src/Greeter.kt", "object Greeter { fun greet() {} }\n"), + marks=needs_kotlin, id="kotlin-object-receiver", + ), ]) def test_each_language_parks_the_call_and_the_merge_finishes_it( tmp_path: Path, lang: str, callee: str, app_file: tuple[str, str],