fix(extract): resolve Java cross-module type references by import (#1744)
Two classes with the same simple name in different Maven modules (FinancialEntryValidator in payment/ and core/) already survive as distinct path-scoped nodes on v8 -- the "node silently disappears" report from 0.9.9 is fixed. But a cross-module field/type `references` edge was still left dangling on a sourceless phantom stub: _resolve_java_type_references (#1318) re-pointed implements/inherits/extends/imports edges to the real definition using the importing file's `import` statement, but its REPOINT_RELATIONS omitted `references`, so bare-name resolution's shadow stub survived for field types. A query about the referenced class could then miss it. Add `references` to the Java resolver's REPOINT_RELATIONS. The C# sibling already covers references; this brings Java to parity. The reference now resolves to the imported package's class (falling back to same-package), and the orphaned phantom is dropped. Regression test covers the ambiguous two-module case: both reals present, no phantom, reference lands on the imported class. Reported with a precise root-cause and repro by @aviciot. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
d2d1f68ff9
commit
ee1ff3d691
@@ -2,6 +2,10 @@
|
||||
|
||||
Full release notes with details on each version: [GitHub Releases](https://github.com/safishamsi/graphify/releases)
|
||||
|
||||
## 0.9.12 (unreleased)
|
||||
|
||||
- Fix: a Java field/parameter/return-type reference to a class whose simple name is shared by two modules no longer dangles on a sourceless phantom node (#1744, thanks @aviciot). Both same-named classes already survive as distinct path-scoped nodes, but the cross-module `references` edge was left pointing at a bare no-source stub because `_resolve_java_type_references` re-pointed `implements`/`inherits`/`imports` but not `references` — so a query about the referenced class could miss it. The Java resolver now disambiguates `references` by the importing file's `import` statement (falling back to same-package), mirroring the C# resolver, and drops the orphaned phantom.
|
||||
|
||||
## 0.9.11 (2026-07-08)
|
||||
|
||||
- Fix: file enumeration no longer silently drops a directory subtree. `detect()`'s `os.walk` had no `onerror` handler, so an `os.scandir` failure (a permission error, or a directory created/deleted mid-walk by concurrent writes) was swallowed and that whole subtree vanished from the scan with no log, yielding a silently partial `graph.json`. The walk now records every skipped directory (surfaced in the result's `walk_errors`) and warns to stderr, while still enumerating the rest. Relatedly, `to_json`'s anti-shrink guard (#479) now fails safe: a non-empty but unreadable existing `graph.json` refuses the overwrite (pass `force=True` to override) instead of silently clobbering a good graph; an empty file still proceeds.
|
||||
|
||||
@@ -2173,8 +2173,13 @@ def _resolve_java_type_references(
|
||||
# `imports` is included so the file-level import edge that also lands on the
|
||||
# shadow stub gets re-pointed too, leaving the stub unreferenced (and dropped).
|
||||
# External/stdlib imports never resolve (no internal def / same-package match),
|
||||
# so their edges correctly stay on their stub.
|
||||
REPOINT_RELATIONS = {"implements", "inherits", "extends", "imports"}
|
||||
# so their edges correctly stay on their stub. `references` (field/parameter/
|
||||
# return-type uses) is included so a cross-module reference to a same-named
|
||||
# class doesn't dangle on a sourceless phantom node when two packages define
|
||||
# the same simple name — the node itself survives with a path-scoped id, but
|
||||
# the reference must point at the RIGHT one (#1744). Mirrors the C# resolver,
|
||||
# whose REPOINT set already covers `references`.
|
||||
REPOINT_RELATIONS = {"implements", "inherits", "extends", "imports", "references"}
|
||||
repointed_from: set[str] = set()
|
||||
for edge in all_edges:
|
||||
if edge.get("relation") not in REPOINT_RELATIONS:
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "graphifyy"
|
||||
version = "0.9.11"
|
||||
version = "0.9.12"
|
||||
description = "AI coding assistant skill (Claude Code, CodeBuddy, Codex, OpenCode, Kilo Code, Cursor, Gemini CLI, Aider, OpenClaw, Factory Droid, Trae, Hermes, Kiro, Pi, Devin CLI, Google Antigravity) - turn any folder of code, docs, papers, images, or videos into a queryable knowledge graph"
|
||||
readme = "README.md"
|
||||
license = { file = "LICENSE" }
|
||||
|
||||
@@ -77,6 +77,58 @@ def test_java_ambiguous_implements_disambiguated_by_import(tmp_path: Path):
|
||||
assert "com/b/handler" not in tgt["source_file"]
|
||||
|
||||
|
||||
def test_java_ambiguous_reference_disambiguated_by_import(tmp_path: Path):
|
||||
# #1744: two classes with the SAME simple name in different modules/packages.
|
||||
# Both survive as distinct path-scoped nodes, but a cross-module field/type
|
||||
# `references` edge used to dangle on a sourceless phantom stub (the
|
||||
# implements/inherits case was handled, references was not). The importing
|
||||
# file's `import` must re-point the reference to the right class and leave no
|
||||
# orphan phantom.
|
||||
payment = _write(
|
||||
tmp_path / "payment/src/com/example/payment/FinancialEntryValidator.java",
|
||||
"package com.example.payment;\n"
|
||||
"public class FinancialEntryValidator {\n"
|
||||
" public boolean validateCurrency(String c) { return c.length() == 3; }\n"
|
||||
"}\n",
|
||||
)
|
||||
core = _write(
|
||||
tmp_path / "core/src/com/example/core/FinancialEntryValidator.java",
|
||||
"package com.example.core;\n"
|
||||
"public class FinancialEntryValidator {\n"
|
||||
" public void auditEntry(String id) {}\n"
|
||||
"}\n",
|
||||
)
|
||||
consumer = _write(
|
||||
tmp_path / "app/src/com/example/app/PaymentService.java",
|
||||
"package com.example.app;\n"
|
||||
"import com.example.payment.FinancialEntryValidator;\n"
|
||||
"public class PaymentService {\n"
|
||||
" private FinancialEntryValidator validator = new FinancialEntryValidator();\n"
|
||||
"}\n",
|
||||
)
|
||||
result = extract([payment, core, consumer], cache_root=tmp_path)
|
||||
|
||||
# Both real classes survive (path-scoped ids); no sourceless phantom remains.
|
||||
fev = [n for n in result["nodes"] if n.get("label") == "FinancialEntryValidator"]
|
||||
reals = [n for n in fev if n.get("source_file")]
|
||||
phantoms = [n for n in fev if not n.get("source_file")]
|
||||
assert len(reals) == 2, f"expected both real classes, got {[n.get('source_file') for n in fev]}"
|
||||
assert not phantoms, f"orphan phantom node(s) remain: {[n['id'] for n in phantoms]}"
|
||||
|
||||
# The reference must resolve to the IMPORTED (payment) class, not core.
|
||||
refs = [
|
||||
e for e in result["edges"]
|
||||
if e["relation"] == "references"
|
||||
and (_node_by_id(result, e["target"]) or {}).get("label") == "FinancialEntryValidator"
|
||||
]
|
||||
assert refs, "expected a references edge to FinancialEntryValidator"
|
||||
for e in refs:
|
||||
tgt = _node_by_id(result, e["target"])
|
||||
assert tgt is not None and tgt.get("source_file")
|
||||
assert "payment/" in tgt["source_file"]
|
||||
assert "core/" not in tgt["source_file"]
|
||||
|
||||
|
||||
def test_java_implements_edge_survives_build(tmp_path: Path):
|
||||
# #1318: the re-pointed edge must connect real nodes after graph assembly,
|
||||
# so the interface is not classified as an isolated community.
|
||||
|
||||
Reference in New Issue
Block a user