From 10b710561afeb172c043dd129450c861979da79e Mon Sep 17 00:00:00 2001 From: safishamsi Date: Tue, 21 Jul 2026 14:22:50 +0100 Subject: [PATCH] fix(extract): harden #2072 src-layout resolution (adversarial-review fixes) Three issues found reviewing #2072: - The import-edge repoint loop matched by target id regardless of the edge's language, so a non-Python dotted import (C# `using Pkg.Mod;`, Java/Go) whose dangling target coincided with a Python alias got repointed onto a Python file, fabricating a cross-language import. Gate the rewrite on the edge being Python-sourced. - The resolver ancestor walk probed package dirs too, resolving an absolute `from helpers import x` to a sibling in the current package (Python-2 implicit- relative semantics) even when `helpers` is external. Only probe sys.path-root candidates (ancestors without their own __init__.py). - Bound the __init__.py package-root chain walk by the path depth so a pathological `/__init__.py` can't loop. Added a cross-language-guard regression test; fixed the tautological (or->and) ambiguity assertion. --- graphify/extract.py | 14 ++++++++-- graphify/extractors/resolution.py | 8 ++++++ tests/test_src_layout_import_resolution.py | 30 +++++++++++++++++++++- 3 files changed, 49 insertions(+), 3 deletions(-) diff --git a/graphify/extract.py b/graphify/extract.py index ccc9c53..67ce012 100644 --- a/graphify/extract.py +++ b/graphify/extract.py @@ -212,7 +212,9 @@ def _repoint_python_package_imports(paths, all_nodes, all_edges, root) -> None: continue # top-level file: scan-root-relative id already matches d = Path(p).resolve().parent levels = 0 - while (d / "__init__.py").is_file(): + # Bounded by the number of dirs between the file and the scan root, so a + # pathological `/__init__.py` chain can't loop forever. + while levels < len(parts) - 1 and (d / "__init__.py").is_file(): levels += 1 d = d.parent if levels == 0: @@ -235,7 +237,15 @@ def _repoint_python_package_imports(paths, all_nodes, all_edges, root) -> None: if not alias_map: return for e in all_edges: - if isinstance(e, dict) and e.get("relation") in ("imports", "imports_from"): + # Only repoint edges emitted from a Python file: a non-Python import edge + # (e.g. C# `using Pkg.Mod;`, Java/Go dotted imports) can have a dangling + # target string that coincides with a Python alias, and repointing it + # would fabricate a cross-language import edge (#2072 review). + if ( + isinstance(e, dict) + and e.get("relation") in ("imports", "imports_from") + and str(e.get("source_file", "")).lower().endswith((".py", ".pyi")) + ): tgt = e.get("target") if tgt in alias_map: e["target"] = alias_map[tgt] diff --git a/graphify/extractors/resolution.py b/graphify/extractors/resolution.py index 7631f1a..d988a72 100644 --- a/graphify/extractors/resolution.py +++ b/graphify/extractors/resolution.py @@ -1645,6 +1645,14 @@ def _resolve_python_module_path(module_name: str, current_path: Path, root: Path break # left the scan root; stop walking up if anc == root: continue # already probed root/rel above + # Only probe sys.path-root candidates — dirs that are NOT themselves part + # of a package. Probing a package dir would resolve an absolute + # `from helpers import x` to a sibling in the current package (Python-2 + # implicit-relative semantics), fabricating edges to what may be an + # external dependency (#2072 review). A src-layout root (src/, no + # __init__.py) is still probed. + if (anc / "__init__.py").is_file(): + continue cand = _probe_python_module_candidate(anc / rel) if cand is not None: return cand diff --git a/tests/test_src_layout_import_resolution.py b/tests/test_src_layout_import_resolution.py index 31d7587..beeb6b9 100644 --- a/tests/test_src_layout_import_resolution.py +++ b/tests/test_src_layout_import_resolution.py @@ -120,6 +120,34 @@ def test_ambiguous_package_alias_is_not_repointed(tmp_path): # repointed onto either file — no fabricated cross-tree import edge. imports = _import_edges(G) targets = {v for _, _, v in imports} - assert "a_src_pkg_mod" not in targets or "b_src_pkg_mod" not in targets, ( + # Neither file may be chosen — an ambiguous alias must stay dangling. + assert "a_src_pkg_mod" not in targets and "b_src_pkg_mod" not in targets, ( f"ambiguous alias was repointed to a specific file: {imports}" ) + + +def test_non_python_import_edge_is_not_repointed(tmp_path): + """#2072 review: the alias map is Python-only, but a non-Python import edge + whose dangling target coincides with a Python alias must NOT be repointed + onto a Python file (that would fabricate a cross-language import).""" + pkg = tmp_path / "src" / "pkg" + pkg.mkdir(parents=True) + (pkg / "__init__.py").write_text("") + (pkg / "mod.py").write_text("def f():\n return 1\n") + # Simulate a non-Python (C#) import edge whose target string collides with the + # Python alias `pkg_mod`, by hand-building the extraction the way extract emits. + result = extract([pkg / "__init__.py", pkg / "mod.py"], cache_root=tmp_path / "c", + root=tmp_path, parallel=False) + result["nodes"].append( + {"id": "app_cs", "label": "app.cs", "file_type": "code", "source_file": "app.cs"} + ) + result["edges"].append( + {"source": "app_cs", "target": "pkg_mod", "relation": "imports", + "confidence": "EXTRACTED", "source_file": "app.cs"} + ) + G = build_from_json(result, root=str(tmp_path)) + # The C# edge's target must remain the (dangling, dropped) `pkg_mod`, never + # repointed to the Python file node src_pkg_mod. + assert not any(v == "src_pkg_mod" and u == "app_cs" for _, u, v in _import_edges(G)), ( + "non-Python import edge was repointed onto a Python file (#2072 review)" + )