From 85fab922d280a75df5f059eb7d4b226ef7003a6c Mon Sep 17 00:00:00 2001 From: rohit-jsfreaky Date: Sun, 11 Oct 2026 01:25:00 +0530 Subject: [PATCH] fix(java): a superclass method wins over an interface default for a bare call Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01B8gwEpjajiWHVW3c9smuKf --- graphify/extract.py | 22 +++++++++++++++++++--- tests/test_java_member_calls.py | 18 ++++++++++++++++-- 2 files changed, 35 insertions(+), 5 deletions(-) diff --git a/graphify/extract.py b/graphify/extract.py index 1c4be2d65..b8a9f49d1 100644 --- a/graphify/extract.py +++ b/graphify/extract.py @@ -5512,10 +5512,26 @@ def _resolve_java_member_calls( return None def _inherited_method(class_nid: str, callee_key: str) -> str | None: - # Level by level up the supertypes, as Java looks a simple method name - # up in the class's members; the class itself was searched in-file. + # Java's "class wins": a method declared up the superclass chain beats + # an interface default, whatever the depth. Only then the interfaces, + # level by level. The class itself was searched in-file. seen = {class_nid} - level = list(dict.fromkeys(supertypes.get(class_nid, []))) + chain: list[str] = [] + cls = class_nid + while len(parents := [b for b in inherits_bases.get(cls, []) if b not in seen]) == 1: + cls = parents[0] + seen.add(cls) + chain.append(cls) + for nid in chain: + source_file = node_by_id.get(nid, {}).get("source_file") + if source_file and _lang_family(source_file) != "jvm": + return None + declared = method_index.get((nid, callee_key)) + if declared: + return next(iter(declared)) if len(declared) == 1 else None + level = list(dict.fromkeys( + base for nid in [class_nid, *chain] for base in supertypes.get(nid, []) if base not in seen + )) while level: hits: set[str] = set() for nid in level: diff --git a/tests/test_java_member_calls.py b/tests/test_java_member_calls.py index 72344dc4f..9b946d55e 100644 --- a/tests/test_java_member_calls.py +++ b/tests/test_java_member_calls.py @@ -430,13 +430,27 @@ def test_bare_call_takes_the_nearest_supertype_declaring_it(tmp_path: Path): assert (run, _find(result, ".stop()", "root_stop")) not in calls -def test_bare_call_declared_twice_on_one_level_binds_nothing(tmp_path: Path): +def test_superclass_method_wins_over_an_interface_default(tmp_path: Path): + # Java's "class wins": Root.log() beats Loud's default, even from a deeper level. calls, result = _calls(tmp_path, { - "Base.java": "class Base { void log() {} }\n", + "Root.java": "class Root { void log() {} }\n", + "Base.java": "class Base extends Root {}\n", "Loud.java": "interface Loud { default void log() {} }\n", "Child.java": "class Child extends Base implements Loud { void run() { log(); } }\n", }) + run = _find(result, ".run()", "child_run") + assert (run, _find(result, ".log()", "root_log")) in calls + assert (run, _find(result, ".log()", "loud_log")) not in calls + + +def test_bare_call_declared_by_two_interfaces_binds_nothing(tmp_path: Path): + calls, result = _calls(tmp_path, { + "Loud.java": "interface Loud { void log(); }\n", + "Quiet.java": "interface Quiet { void log(); }\n", + "Child.java": "abstract class Child implements Loud, Quiet { void run() { log(); } }\n", + }) + run = _find(result, ".run()", "child_run") assert not any(source == run and "log" in target for source, target in calls)