Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions graphify/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -1660,6 +1660,20 @@ def dispatch_command(cmd: str) -> None:
_dg.add_edges_from(sorted(
(d.get("_src", u), d.get("_tgt", v)) for u, v, d in G.edges(data=True)
))
# A `contains` edge only runs file -> symbol; there is no
# stored edge back out to the containing file, so a route that
# reaches a symbol (via an `imports`/`calls`/`references` hop)
# can never continue on to the file that defines it, and a
# file-to-file dependency routed through a shared symbol finds
# no path at all even though both halves of the route exist
# (#3878). Add the implied reverse hop for traversal only — the
# printed segment still recovers the real stored `contains`
# edge and its true direction from G, same as any other hop.
_dg.add_edges_from(sorted(
(d.get("_tgt", v), d.get("_src", u))
for u, v, d in G.edges(data=True)
if d.get("relation") == "contains"
))
path_nodes = _nx.shortest_path(_dg, src_nid, tgt_nid)
except (_nx.NetworkXNoPath, _nx.NodeNotFound):
if undirected:
Expand Down
32 changes: 32 additions & 0 deletions tests/test_path_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,38 @@ def test_path_canonical_marker_graph_still_forward(monkeypatch, tmp_path, capsys
assert "Beta <--calls [EXTRACTED]-- Alpha" in out


# ── #3878: a `contains` edge has no reverse hop back out to its file ────────

def _file_to_symbol_only_graph(tmp_path):
"""`a.py` imports `helper()`, which `b.py` contains — but no edge runs
file-to-file directly, and `contains` only runs b.py -> helper()."""
data = {
"directed": False, "multigraph": False, "graph": {},
"nodes": [
{"id": "a", "label": "a.py", "source_file": "a.py"},
{"id": "b", "label": "b.py", "source_file": "b.py"},
{"id": "helper", "label": "helper()", "source_file": "b.py"},
],
"links": [
{"source": "a", "target": "helper", "relation": "imports", "confidence": "EXTRACTED"},
{"source": "b", "target": "helper", "relation": "contains", "confidence": "EXTRACTED"},
],
}
p = tmp_path / "graph.json"
p.write_text(json.dumps(data))
return p


def test_path_routes_through_a_contains_edge_to_reach_the_file(monkeypatch, tmp_path, capsys):
"""A file-to-file dependency that only closes through a contained symbol
must still resolve, not report no path despite both halves existing."""
p = _file_to_symbol_only_graph(tmp_path)
out = _run(monkeypatch, p, "a.py", "b.py", capsys)
assert "Shortest path (2 hops):" in out
assert "a.py --imports [EXTRACTED]--> helper() <--contains [EXTRACTED]-- b.py" in out
assert "No directed path found" not in out


def test_explain_direction_recovered_from_src_tgt_markers(monkeypatch, tmp_path, capsys):
"""#2309: explain's in/out classification must honor _src markers — an
edge persisted as hub->spoke but truly spoke->hub is an IN edge of hub."""
Expand Down
Loading