From 4355d16299ddc85338d7ed2fc4afc4d569b987e6 Mon Sep 17 00:00:00 2001 From: Krishnan Mahadevan Date: Tue, 14 Apr 2026 21:32:20 +0530 Subject: [PATCH] feat(java): resolve Java imports to file paths MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Java imports like `import com.example.auth.User` were stored as raw dot-notation strings because _do_resolve_module() had no Java branch. This caused `importers_of` queries to return 0 — the query looks for file path targets, but the stored edges had raw import strings. Adds a Java branch that converts dot-notation to a relative path (com/example/auth/User.java) and walks up from the caller's directory to find the source root. This resolves same-source-root imports (the common case in Maven modules). Also handles: - Static imports (import static pkg.Class.member) — strips the member name and resolves to the class file - Wildcard imports (import pkg.*) — skipped, can't resolve to one file - JDK/library imports (java.util.*) — remain unresolved (no local file) --- code_review_graph/parser.py | 32 ++++++++++++++++ tests/test_multilang.py | 75 +++++++++++++++++++++++++++++++++++++ 2 files changed, 107 insertions(+) diff --git a/code_review_graph/parser.py b/code_review_graph/parser.py index 31af17f..2e252dd 100644 --- a/code_review_graph/parser.py +++ b/code_review_graph/parser.py @@ -3108,6 +3108,38 @@ def _do_resolve_module( # ``dart:core`` / ``dart:async`` etc. are SDK libraries we do # not track; fall through to return None. + elif language == "java": + # ``import com.example.pkg.ClassName;`` — convert dot-notation + # to a relative path and walk up from the caller's directory to + # find the source root. Wildcards (``import pkg.*``) and static + # member imports (``import static pkg.Class.member``) that don't + # resolve as-is are retried after dropping the last segment + # (the member name). + if module.endswith(".*"): + return None # wildcard import — can't resolve to one file + rel_path = module.replace(".", "/") + ".java" + current = caller_dir + while True: + target = current / rel_path + if target.is_file(): + return str(target.resolve()) + if current == current.parent: + break + current = current.parent + # Static import: ``pkg.Class.member`` — strip member, try again + dot = module.rfind(".") + if dot > 0: + class_module = module[:dot] + rel_path2 = class_module.replace(".", "/") + ".java" + current = caller_dir + while True: + target = current / rel_path2 + if target.is_file(): + return str(target.resolve()) + if current == current.parent: + break + current = current.parent + return None def _find_dart_pubspec_root( diff --git a/tests/test_multilang.py b/tests/test_multilang.py index 1264dc9..ba1b14c 100644 --- a/tests/test_multilang.py +++ b/tests/test_multilang.py @@ -151,6 +151,81 @@ def test_finds_calls(self): assert len(calls) >= 3 +class TestJavaImportResolution: + """Test that Java imports are resolved to absolute file paths.""" + + def test_resolves_project_import(self, tmp_path): + """Import of a project class resolves to its .java file.""" + # Create a mini Java project with two packages + auth = tmp_path / "src/main/java/com/example/auth" + auth.mkdir(parents=True) + (auth / "User.java").write_text( + "package com.example.auth;\npublic class User {}\n" + ) + svc = tmp_path / "src/main/java/com/example/service" + svc.mkdir(parents=True) + (svc / "App.java").write_text( + "package com.example.service;\n" + "import com.example.auth.User;\n" + "public class App {}\n" + ) + + parser = CodeParser() + _, edges = parser.parse_file(svc / "App.java") + imports = [e for e in edges if e.kind == "IMPORTS_FROM"] + assert len(imports) == 1 + assert imports[0].target == str((auth / "User.java").resolve()) + + def test_jdk_import_stays_unresolved(self): + """JDK imports have no local file and remain as raw strings.""" + parser = CodeParser() + _, edges = parser.parse_file(FIXTURES / "SampleJava.java") + imports = [e for e in edges if e.kind == "IMPORTS_FROM"] + # All imports in SampleJava.java are java.util.* (JDK) + for e in imports: + assert not e.target.endswith(".java"), ( + f"JDK import should not resolve to a file: {e.target!r}" + ) + + def test_static_import_resolves_to_class(self, tmp_path): + """Static import of a member resolves to the enclosing class file.""" + pkg = tmp_path / "src/main/java/com/example/util" + pkg.mkdir(parents=True) + (pkg / "Helper.java").write_text( + "package com.example.util;\n" + "public class Helper { public static int MAX = 1; }\n" + ) + app_dir = tmp_path / "src/main/java/com/example/app" + app_dir.mkdir(parents=True) + (app_dir / "App.java").write_text( + "package com.example.app;\n" + "import static com.example.util.Helper.MAX;\n" + "public class App {}\n" + ) + + parser = CodeParser() + _, edges = parser.parse_file(app_dir / "App.java") + imports = [e for e in edges if e.kind == "IMPORTS_FROM"] + assert len(imports) == 1 + assert imports[0].target == str((pkg / "Helper.java").resolve()) + + def test_wildcard_import_stays_unresolved(self, tmp_path): + """Wildcard imports cannot resolve to a single file.""" + app_dir = tmp_path / "src/main/java/com/example" + app_dir.mkdir(parents=True) + (app_dir / "App.java").write_text( + "package com.example;\n" + "import java.util.*;\n" + "public class App {}\n" + ) + + parser = CodeParser() + _, edges = parser.parse_file(app_dir / "App.java") + imports = [e for e in edges if e.kind == "IMPORTS_FROM"] + assert len(imports) == 1 + assert imports[0].target == "java.util.*" + + class TestCParsing: def setup_method(self): self.parser = CodeParser()