mirror of
https://github.com/safishamsi/graphify.git
synced 2026-09-03 04:05:48 +00:00
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.
173 lines
6.4 KiB
Python
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"]
|
|
)
|