Files
graphify/tests/test_go_qualified_resolution.py
T
BackendDev e4d132b7da fix(go): resolve qualified symbols by import path
Prevent package-qualified calls and types from collapsing onto unrelated bare-name symbols. Preserve Go import evidence, resolve internal packages exactly, canonicalize external type stubs, and support incremental resolution context.

Investigation, implementation, and regression fixtures prepared with OpenAI Codex.
2026-08-11 14:40:01 +01:00

173 lines
6.4 KiB
Python

"""Regression coverage for package-qualified Go calls and type references."""
from pathlib import Path
from graphify.extract import extract
def _extract(root: Path) -> dict:
return extract(
sorted(root.rglob("*.go")),
cache_root=root,
root=root,
parallel=False,
)
def _ids(result: dict, *, label: str, suffix: str) -> set[str]:
return {
node["id"]
for node in result["nodes"]
if str(node.get("source_file", "")).endswith(suffix)
and str(node.get("label", "")).strip(".()") == label
}
def test_external_package_new_does_not_bind_to_local_new(tmp_path: Path) -> None:
"""``errors.New`` must not become a call to an unrelated local ``New``."""
(tmp_path / "go.mod").write_text("module example.com/repro\n\ngo 1.22\n")
(tmp_path / "factory.go").write_text("package repro\n\nfunc New() int { return 1 }\n")
(tmp_path / "worker.go").write_text(
'package repro\n\nimport "errors"\n\nfunc Build() error { return errors.New("boom") }\n'
)
result = _extract(tmp_path)
build_ids = _ids(result, label="Build", suffix="worker.go")
local_new_ids = _ids(result, label="New", suffix="factory.go")
phantom = [
edge
for edge in result["edges"]
if edge.get("relation") == "calls"
and edge.get("source") in build_ids
and edge.get("target") in local_new_ids
]
assert phantom == [], f"errors.New bound to the local New: {phantom}"
def test_internal_aliased_package_new_resolves_exact_import(tmp_path: Path) -> None:
"""An imported internal package selector resolves despite same-name decoys."""
(tmp_path / "go.mod").write_text("module example.com/repro\n\ngo 1.22\n")
factory = tmp_path / "factory"
factory.mkdir()
(factory / "factory.go").write_text("package factory\n\nfunc New() int { return 1 }\n")
app = tmp_path / "app"
app.mkdir()
(app / "decoy.go").write_text("package app\n\nfunc New() int { return 2 }\n")
(app / "worker.go").write_text(
'package app\n\nimport maker "example.com/repro/factory"\n\n'
"func Build() int { return maker.New() }\n"
)
result = _extract(tmp_path)
build_ids = _ids(result, label="Build", suffix="app/worker.go")
factory_new_ids = _ids(result, label="New", suffix="factory/factory.go")
decoy_new_ids = _ids(result, label="New", suffix="app/decoy.go")
calls = [
edge
for edge in result["edges"]
if edge.get("relation") == "calls" and edge.get("source") in build_ids
]
assert len(calls) == 1, calls
assert calls[0]["target"] in factory_new_ids
assert calls[0]["target"] not in decoy_new_ids
assert calls[0]["confidence"] == "EXTRACTED"
def test_external_qualified_type_does_not_bind_to_local_function(tmp_path: Path) -> None:
"""``*testing.T`` must not reference an unrelated local function ``T``."""
(tmp_path / "go.mod").write_text("module example.com/repro\n\ngo 1.22\n")
(tmp_path / "translate.go").write_text(
"package repro\n\nfunc T(message string) string { return message }\n"
)
(tmp_path / "worker_test.go").write_text(
'package repro\n\nimport "testing"\n\nfunc TestBuild(t *testing.T) {}\n'
)
result = _extract(tmp_path)
test_ids = _ids(result, label="TestBuild", suffix="worker_test.go")
local_t_ids = _ids(result, label="T", suffix="translate.go")
refs = [
edge
for edge in result["edges"]
if edge.get("relation") == "references" and edge.get("source") in test_ids
]
assert refs
assert all(edge.get("target") not in local_t_ids for edge in refs), refs
nodes = {node["id"]: node for node in result["nodes"]}
assert any(nodes[edge["target"]]["label"] == "testing.T" for edge in refs)
def test_internal_qualified_type_resolves_exact_import(tmp_path: Path) -> None:
"""An aliased internal qualified type points to its package definition."""
(tmp_path / "go.mod").write_text("module example.com/repro\n\ngo 1.22\n")
model = tmp_path / "model"
model.mkdir()
(model / "user.go").write_text("package model\n\ntype User struct{}\n")
app = tmp_path / "app"
app.mkdir()
(app / "decoy.go").write_text("package app\n\ntype User struct{}\n")
(app / "handler.go").write_text(
'package app\n\nimport domain "example.com/repro/model"\n\n'
"func Handle(user *domain.User) {}\n"
)
result = _extract(tmp_path)
handle_ids = _ids(result, label="Handle", suffix="app/handler.go")
model_user_ids = _ids(result, label="User", suffix="model/user.go")
decoy_user_ids = _ids(result, label="User", suffix="app/decoy.go")
refs = [
edge
for edge in result["edges"]
if edge.get("relation") == "references" and edge.get("source") in handle_ids
]
assert len(refs) == 1, refs
assert refs[0]["target"] in model_user_ids
assert refs[0]["target"] not in decoy_user_ids
def test_incremental_qualified_resolution_uses_unchanged_context(tmp_path: Path) -> None:
"""Changed callers still resolve calls and types defined in unchanged files."""
(tmp_path / "go.mod").write_text("module example.com/repro\n\ngo 1.22\n")
factory = tmp_path / "factory"
factory.mkdir()
target = factory / "factory.go"
target.write_text(
"package factory\n\ntype User struct{}\n\n"
"func New() *User { return &User{} }\n"
)
app = tmp_path / "app"
app.mkdir()
caller = app / "worker.go"
caller.write_text(
'package app\n\nimport maker "example.com/repro/factory"\n\n'
"func Build() *maker.User { return maker.New() }\n"
)
full = _extract(tmp_path)
caller.write_text(caller.read_text() + "\n")
changed = extract(
[caller],
cache_root=tmp_path,
root=tmp_path,
parallel=False,
resolution_context_nodes=full["nodes"],
resolution_context_edges=full["edges"],
)
build_ids = _ids(changed, label="Build", suffix="app/worker.go")
full_new_ids = _ids(full, label="New", suffix="factory/factory.go")
full_user_ids = _ids(full, label="User", suffix="factory/factory.go")
assert any(
edge.get("relation") == "calls"
and edge.get("source") in build_ids
and edge.get("target") in full_new_ids
for edge in changed["edges"]
)
assert any(
edge.get("relation") == "references"
and edge.get("source") in build_ids
and edge.get("target") in full_user_ids
for edge in changed["edges"]
)