fix: drop collapsed import self-loops

This commit is contained in:
Endogen
2026-07-20 11:29:22 +01:00
committed by safishamsi
parent edec9eabec
commit c105633b56
2 changed files with 117 additions and 0 deletions
+10
View File
@@ -767,6 +767,16 @@ def build_from_json(extraction: dict, *, directed: bool = False, root: str | Pat
# (unknown ext, e.g. a manifest) is never mistaken for a phantom.
if src_fam is not None and tgt_fam is not None and src_fam != tgt_fam:
continue
# A file-level import or re-export cannot carry useful connectivity when
# both endpoints resolve to the same node. This most often happens when
# the target is an unresolved bare module name (``builtins``, ``poseidon``)
# that the legacy-ID alias index above mistakes for the importing file's
# own old stem. It also covers a nested module importing its parent file:
# at file-node granularity that relationship necessarily collapses. Keep
# other self-edges, notably recursive ``calls``, because those are real
# program structure rather than import-resolution artifacts.
if src == tgt and _edge_rel in ("imports", "imports_from", "re_exports"):
continue
# Preserve original edge direction - undirected graphs lose it otherwise,
# causing display functions to show edges backwards.
attrs["_src"] = src
+107
View File
@@ -0,0 +1,107 @@
from pathlib import Path
import pytest
from graphify.build import build_from_json
from graphify.extract import extract
def _write(path: Path, source: str) -> Path:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(source, encoding="utf-8")
return path
def _import_self_loops(result: dict) -> list[dict]:
return [
edge
for edge in result["edges"]
if edge.get("relation") in {"imports", "imports_from", "re_exports"}
and edge.get("source") == edge.get("target")
]
def _built_import_self_loops(result: dict) -> list[tuple[str, str, dict]]:
graph = build_from_json(result, directed=True)
return [
(source, target, data)
for source, target, data in graph.edges(data=True)
if source == target
and data.get("relation") in {"imports", "imports_from", "re_exports"}
]
@pytest.mark.parametrize(
("relative_path", "source"),
[
("src/contracting/stdlib/builtins.py", "import builtins\n"),
("playground/services/contracting.py", "from contracting import constants\n"),
],
)
def test_python_external_import_matching_current_basename_has_no_self_loop(
tmp_path: Path,
relative_path: str,
source: str,
) -> None:
module = _write(tmp_path / relative_path, source)
result = extract([module], cache_root=tmp_path, parallel=False)
assert _import_self_loops(result) == []
assert _built_import_self_loops(result) == []
@pytest.mark.parametrize(
("relative_path", "source"),
[
(
"packages/compiler/src/fixture.rs",
"mod tests { use crate::fixture::Fixture; }\n",
),
(
"packages/zk/src/poseidon.rs",
"use ark_crypto_primitives::sponge::poseidon::{PoseidonConfig, PoseidonSponge};\n",
),
],
)
def test_rust_import_matching_current_basename_has_no_self_loop(
tmp_path: Path,
relative_path: str,
source: str,
) -> None:
module = _write(tmp_path / relative_path, source)
result = extract([module], cache_root=tmp_path, parallel=False)
assert _import_self_loops(result) == []
assert _built_import_self_loops(result) == []
def test_recursive_call_self_loop_is_preserved() -> None:
result = {
"nodes": [
{
"id": "module_recurse",
"label": "recurse()",
"file_type": "code",
"source_file": "module.py",
"source_location": "L1",
"_origin": "ast",
}
],
"edges": [
{
"source": "module_recurse",
"target": "module_recurse",
"relation": "calls",
"confidence": "EXTRACTED",
"source_file": "module.py",
"source_location": "L2",
}
],
}
graph = build_from_json(result, directed=True)
assert graph.has_edge("module_recurse", "module_recurse")
assert graph["module_recurse"]["module_recurse"]["relation"] == "calls"