fix(affected): resolve a seed given as ./relative or absolute path form (#2584 follow-up)

This commit is contained in:
phudayyy
2026-08-13 13:30:24 +01:00
committed by safishamsi
parent cc0ee60c40
commit a05b4084d9
2 changed files with 58 additions and 2 deletions
+32 -2
View File
@@ -67,6 +67,31 @@ def _normalize_label(label: str) -> str:
return unicodedata.normalize("NFC", label).casefold()
def _as_repo_relative(query: str) -> str:
"""Repo-relative form of a path query, for matching a stored `source_file`.
The graph stores repo-relative paths, so `./src/x.py` and
`/abs/repo/src/x.py` name the same file as `src/x.py` and yet matched
nothing. `affected` then printed an empty list and exited 0 — a blast-radius
tool answering "nothing depends on this" about a file with sixteen
dependents, and indistinguishable from a genuine zero or a typo.
Non-path queries pass through unchanged: `Path("myFunc()").as_posix()` is
`"myFunc()"`, so label resolution is untouched. An absolute path rooted
outside the repo is left alone — no basename guessing.
"""
path = Path(query)
if path.is_absolute():
try:
return path.relative_to(Path.cwd()).as_posix()
except ValueError:
# Rooted outside the repo: nothing here can make it repo-relative,
# so leave it alone rather than guess at a basename that would match
# some unrelated file with the same name.
return query
return path.as_posix()
def _prefer_file_node(
graph: nx.Graph,
node_ids: list[str],
@@ -128,15 +153,20 @@ def resolve_seed(graph: nx.Graph, query: str) -> str | None:
]
if len(bare_name_matches) == 1:
return bare_name_matches[0]
# Compare paths in repo-relative form. Only this branch is path-shaped; the
# label branches above keep the query verbatim.
query_path = _normalize_label(_as_repo_relative(query))
exact_source_matches = [
str(node_id)
for node_id, data in graph.nodes(data=True)
if _normalize_label(str(data.get("source_file", ""))) == query_lower
if _normalize_label(str(data.get("source_file", ""))) in (query_lower, query_path)
]
if len(exact_source_matches) == 1:
return exact_source_matches[0]
if exact_source_matches:
preferred_file_node = _prefer_file_node(graph, exact_source_matches, query)
preferred_file_node = _prefer_file_node(
graph, exact_source_matches, _as_repo_relative(query)
)
if preferred_file_node is not None:
return preferred_file_node
contains_matches = [
+26
View File
@@ -311,3 +311,29 @@ def test_affected_falls_back_to_def_line_when_edge_has_no_location(monkeypatch,
monkeypatch.setattr(mainmod.sys, "argv", ["graphify", "affected", "target", "--graph", str(gp)])
mainmod.main()
assert "a.py:L90" in capsys.readouterr().out
def test_affected_resolves_equivalent_path_forms(tmp_path, monkeypatch):
"""`./x.py`, an absolute path and `x.py` name one file and must resolve alike.
The graph stores repo-relative `source_file`, and `resolve_seed` compared the
query to it as a plain string. `./pkg/foo.py` and `/abs/repo/pkg/foo.py`
therefore matched nothing, `affected` printed an empty list and exited 0 — a
blast-radius tool reporting "nothing depends on this" about a file with three
dependents, and indistinguishable both from a genuine zero and from a typo.
"""
from graphify.affected import resolve_seed
graph = nx.DiGraph()
graph.add_node("target", label="Foo", source_file="pkg/foo.py", source_location="L1")
graph.add_node("caller", label="X()", source_file="app.py", source_location="L4")
graph.add_edge("caller", "target", relation="calls")
monkeypatch.chdir(tmp_path)
for query in (
"pkg/foo.py",
"./pkg/foo.py",
str(tmp_path / "pkg" / "foo.py"),
):
assert resolve_seed(graph, query) == "target", query