Add and register the Kotlin object member call resolver

Fixes issue 1698.

Mirrors _resolve_kotlin_qualified_calls just above it and the same
type_by_name plus methods_by_type shape _resolve_swift_member_calls
uses for its own type qualified case: index every Kotlin sourced
class or object by its bare name, resolve the receiver against that
index, then look up the callee among that type's methods. Both steps
bail on anything but exactly one candidate, the same god node guard
every cross file resolver in this module already uses.

Verified end to end: the issue's own cross file repro now produces an
EXTRACTED calls edge, the same file control case is unaffected, and
two objects sharing the same name in different packages correctly
produce zero edges instead of an ambiguous guess.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
(cherry picked from commit a6c89df809)
This commit is contained in:
ayushcodes10 authored and safishamsi committed 2026-09-17 11:05:12 +01:00
1 parent 492ea4fda8
commit 095530a335
1 file changed
+94
+94
View File
@@ -4933,6 +4933,92 @@ def _resolve_elixir_import_targets(
e["target"] = target_nid
def _resolve_kotlin_member_calls(
per_file: list[dict],
all_nodes: list[dict],
all_edges: list[dict],
) -> None:
"""Resolve Kotlin object/class-qualified member calls across files (#1698).
``Receiver.method()`` (exactly two navigation segments, a capitalized
receiver — an object singleton or a class/companion member) resolves
in-file through the engine's own bare-name lookup, so a call reaches
``raw_calls`` here only when that lookup already failed: the receiver and
the callee are declared in different files. The shared cross-file pass
skips every ``is_member_call`` unconditionally (a bare method name like
``log`` collides across the corpus and inflates god-nodes, #543/#1219),
and Kotlin deliberately never stamps ``member_receiver`` for this shape
either (see ``extractors/engine.py``), so nothing else can ever answer it.
Guarded by exactly-one-candidate at both steps, the same god-node guard
``_resolve_kotlin_qualified_calls`` uses just above: a receiver name
matching 2+ declared types, or a method name matching 2+ methods of the
matched type, yields no edge. The receiver names the type explicitly in
source, so a unique match is EXTRACTED (mirrors the type-qualified case
in ``_resolve_swift_member_calls``, #1356).
"""
raw = [
rc
for result in per_file
for rc in result.get("raw_calls", [])
if rc.get("lang") == "kotlin" and rc.get("kotlin_object_receiver")
and rc.get("callee") and rc.get("caller_nid")
]
if not raw:
return
node_by_id: dict[str, dict] = {n.get("id"): n for n in all_nodes}
methods_by_type: dict[str, list[str]] = {}
for e in all_edges:
if e.get("relation") == "method":
methods_by_type.setdefault(e.get("source"), []).append(e.get("target"))
# Receiver name -> declaring type node ids, scoped to Kotlin-sourced
# definitions only (a same-named type in another language must not
# answer a Kotlin call). len != 1 is the god-node guard.
types_by_name: dict[str, list[str]] = {}
for n in all_nodes:
if not n.get("_callable_class"):
continue
sf = str(n.get("source_file") or "")
if not sf.endswith((".kt", ".kts")):
continue
name = str(n.get("label", ""))
if name:
types_by_name.setdefault(name, []).append(n["id"])
existing_pairs = {(e.get("source"), e.get("target")) for e in all_edges}
for rc in raw:
receiver = rc["kotlin_object_receiver"]
callee = rc["callee"]
caller = rc["caller_nid"]
type_nids = types_by_name.get(receiver, [])
if len(type_nids) != 1:
continue
wanted = f".{callee}"
candidates = [
m for m in methods_by_type.get(type_nids[0], [])
if str(node_by_id.get(m, {}).get("label", "")).strip("()") == wanted
]
if len(candidates) != 1:
continue
tgt = candidates[0]
if tgt == caller or (caller, tgt) in existing_pairs:
continue
existing_pairs.add((caller, tgt))
all_edges.append({
"source": caller,
"target": tgt,
"relation": "calls",
"context": "call",
"confidence": "EXTRACTED", # the receiver names the type explicitly in source
"confidence_score": 1.0,
"source_file": rc.get("source_file", ""),
"source_location": rc.get("source_location"),
"weight": 1.0,
})
# Kotlin import-target resolution runs EARLY (directly in extract(), before the
# shared call pass builds its import-evidence index) — registering it in the
# tail registry would rewrite the targets after promotion already read them.
@@ -5021,6 +5107,14 @@ register_language_resolver(
"kotlin_qualified_calls", frozenset({".kt", ".kts"}), _resolve_kotlin_qualified_calls
)
)
# Kotlin object/class-qualified member calls resolved across files (#1698):
# `Receiver.method()` where the receiver's object/class is declared in another
# file. Same god-node guard as kotlin_qualified_calls; runs in the tail registry.
register_language_resolver(
LanguageResolver(
"kotlin_member_calls", frozenset({".kt", ".kts"}), _resolve_kotlin_member_calls
)
)
# C# qualified construction (#2997): `new A.B.Cache()` arrives as the bare name,
# so a colliding `Cache` elsewhere makes it ambiguous. Runs in the tail registry
# beside csharp_member_calls and matches the prefix against declared namespaces.