mirror of
https://github.com/safishamsi/graphify.git
synced 2026-07-12 18:37:12 +00:00
improve #1007 fix: use _norm_source_file with resolve() for symlink safety
Replace inlined path normalisation with _norm_source_file (the same function that builds node source_file keys) so prune_set and node attrs are normalised identically. resolve() on root handles symlinked scan roots. Keep both raw and normalised forms in prune_set so nodes with absolute source_file also match. Add edge pruning and Windows backslash path tests per Opus review. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+15
-12
@@ -337,18 +337,21 @@ def build_merge(
|
||||
|
||||
# Prune nodes and edges from deleted source files
|
||||
if prune_sources:
|
||||
prune_set = set(prune_sources)
|
||||
# Manifest stores absolute paths; graph nodes store relative paths.
|
||||
# Add relative variants so both formats match regardless of OS or
|
||||
# whether paths were relativized at build time (#1007).
|
||||
if root is not None:
|
||||
import os as _os
|
||||
_root = Path(root)
|
||||
for p in list(prune_sources):
|
||||
try:
|
||||
prune_set.add(str(Path(p).relative_to(_root)).replace(_os.sep, "/"))
|
||||
except ValueError:
|
||||
pass
|
||||
# Build a set containing both the raw form (matches nodes that kept
|
||||
# absolute source_file) and the normalised relative form (matches nodes
|
||||
# that were relativised by _norm_source_file at build time).
|
||||
# .resolve() handles symlinked roots and redundant ".." / "./" segments
|
||||
# so Path.relative_to() succeeds even when the scan root is a symlink.
|
||||
# (#1007: manifest absolute paths vs graph relative source_file mismatch)
|
||||
_root_str = str(Path(root).resolve()) if root is not None else None
|
||||
prune_set: set[str] = set()
|
||||
for p in prune_sources:
|
||||
if not p:
|
||||
continue
|
||||
prune_set.add(p)
|
||||
norm = _norm_source_file(p, _root_str)
|
||||
if norm:
|
||||
prune_set.add(norm)
|
||||
to_remove = [
|
||||
n for n, d in G.nodes(data=True)
|
||||
if d.get("source_file") in prune_set
|
||||
|
||||
+32
-9
@@ -348,9 +348,8 @@ def test_build_from_json_relative_source_file_unchanged(tmp_path):
|
||||
|
||||
def test_build_merge_prune_absolute_paths_match_relative_nodes(tmp_path):
|
||||
"""#1007: manifest stores absolute paths, graph nodes store relative paths.
|
||||
prune_sources with absolute paths must still remove the right nodes."""
|
||||
import json
|
||||
from graphify.export import to_json
|
||||
prune_sources with absolute paths must still remove the right nodes and edges."""
|
||||
import networkx as nx
|
||||
|
||||
root = tmp_path / "corpus"
|
||||
root.mkdir()
|
||||
@@ -360,20 +359,44 @@ def test_build_merge_prune_absolute_paths_match_relative_nodes(tmp_path):
|
||||
chunk = {"nodes": [
|
||||
{"id": "n1", "label": "login", "file_type": "code", "source_file": "module_a/auth.py"},
|
||||
{"id": "n2", "label": "format_date", "file_type": "code", "source_file": "module_b/utils.py"},
|
||||
], "edges": []}
|
||||
], "edges": [
|
||||
{"source": "n1", "target": "n2", "relation": "calls", "confidence": "EXTRACTED",
|
||||
"source_file": "module_b/utils.py", "weight": 1.0},
|
||||
]}
|
||||
G0 = build([chunk], dedup=False)
|
||||
import networkx as nx
|
||||
graph_path.write_text(
|
||||
json.dumps(nx.node_link_data(G0, edges="edges")), encoding="utf-8"
|
||||
)
|
||||
graph_path.write_text(json.dumps(nx.node_link_data(G0, edges="edges")), encoding="utf-8")
|
||||
|
||||
# prune_sources from manifest — absolute paths
|
||||
# prune_sources from manifest — absolute paths (what detect_incremental emits)
|
||||
deleted_abs = [str(root / "module_b" / "utils.py")]
|
||||
G1 = build_merge([], graph_path, prune_sources=deleted_abs, dedup=False, root=root)
|
||||
|
||||
node_labels = {d["label"] for _, d in G1.nodes(data=True)}
|
||||
assert "format_date" not in node_labels, "stale node from deleted file should be pruned"
|
||||
assert "login" in node_labels, "unrelated node must survive"
|
||||
# Edge from deleted file must also be gone
|
||||
assert G1.number_of_edges() == 0, "edge from deleted source_file should be pruned"
|
||||
|
||||
|
||||
def test_build_merge_prune_windows_backslash_paths(tmp_path):
|
||||
"""#1007: prune_sources with Windows-style backslash absolute paths must still match."""
|
||||
import networkx as nx
|
||||
|
||||
root = tmp_path / "corpus"
|
||||
root.mkdir()
|
||||
graph_path = tmp_path / "graph.json"
|
||||
|
||||
chunk = {"nodes": [
|
||||
{"id": "n1", "label": "parse_date", "file_type": "code", "source_file": "module_b/utils.py"},
|
||||
], "edges": []}
|
||||
G0 = build([chunk], dedup=False)
|
||||
graph_path.write_text(json.dumps(nx.node_link_data(G0, edges="edges")), encoding="utf-8")
|
||||
|
||||
# Simulate Windows manifest path with backslashes
|
||||
win_path = str(root / "module_b" / "utils.py").replace("/", "\\")
|
||||
G1 = build_merge([], graph_path, prune_sources=[win_path], dedup=False, root=root)
|
||||
|
||||
node_labels = {d["label"] for _, d in G1.nodes(data=True)}
|
||||
assert "parse_date" not in node_labels, "node should be pruned even with backslash path"
|
||||
|
||||
|
||||
def test_build_merge_rejects_oversized_existing_graph(monkeypatch, tmp_path):
|
||||
|
||||
Reference in New Issue
Block a user