Skip to content

Commit b523898

Browse files
safishamsiclaude
andcommitted
harden: graceful merge-graphs --previous errors; JS/TS super fail-close
Two small follow-ups on the 0.9.75 batch: - merge-graphs --previous now reports a clear error and exits 1 on a malformed/unreadable JSON file instead of crashing with an uncaught traceback (follow-up to #4006). - JS/TS super.m() fails closed (no edge) instead of falling back to the file-wide name map, which bound it to the overriding method as a wrong self-loop. The extends chain is unknown at this pass, so super can never be resolved here; inherited this.m() still keeps the plain lookup (follow-up to #4012). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent e3ac168 commit b523898

5 files changed

Lines changed: 51 additions & 5 deletions

File tree

‎graphify/cli.py‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2712,7 +2712,11 @@ def _load_graph(p: str):
27122712
if not previous_path.exists():
27132713
print(f"error: --previous file not found: {previous_path}", file=sys.stderr)
27142714
sys.exit(1)
2715-
_prev_raw = json.loads(previous_path.read_text(encoding="utf-8"))
2715+
try:
2716+
_prev_raw = json.loads(previous_path.read_text(encoding="utf-8"))
2717+
except (json.JSONDecodeError, OSError, UnicodeDecodeError) as e:
2718+
print(f"error: --previous file is not readable JSON: {previous_path} ({e})", file=sys.stderr)
2719+
sys.exit(1)
27162720
for _n in _prev_raw.get("nodes", []):
27172721
if isinstance(_n, dict) and isinstance(_n.get("community"), int) and _n.get("id"):
27182722
previous_community[_n["id"]] = _n["community"]

‎graphify/extractors/engine.py‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1406,7 +1406,11 @@ class to whichever class declared save() last. A file-wide hit that is not
14061406
if hits:
14071407
return hits.pop() if len(hits) == 1 else None
14081408
if not walk_bases:
1409-
return fallback
1409+
# JS/TS: the `extends` chain is unknown at this pass. An inherited
1410+
# `this.m()` must keep the plain lookup, but `super.m()` can never
1411+
# mean the caller's own method, so a file-wide fallback here is just
1412+
# a wrong self-loop onto the overriding method — fail closed instead.
1413+
return None if skip_own else fallback
14101414
skip_own = False
14111415
level = list(dict.fromkeys(
14121416
base for c in level for base in class_bases.get(c, ()) if base not in seen

‎tests/test_cli_export.py‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1002,11 +1002,12 @@ def test_cluster_only_preserves_parallel_edges_across_an_id_remap(tmp_path):
10021002

10031003
data = json.loads((out / "graph.json").read_text())
10041004
node_ids = {n["id"] for n in data["nodes"]}
1005+
# both endpoints remap: claude_alpha -> claude_claude_alpha, beta -> pkg_beta
10051006
assert "claude_claude_alpha" in node_ids, node_ids # remap fired
10061007
assert "claude_alpha" not in node_ids
10071008
pairs = sorted((l["source"], l["target"], l["relation"]) for l in data["links"])
1008-
# both parallel edges survive AND are keyed on the remapped id
1009+
# both parallel edges survive AND track the remapped endpoint ids
10091010
assert pairs == [
1010-
("claude_claude_alpha", "beta", "mentions"),
1011-
("claude_claude_alpha", "beta", "references"),
1011+
("claude_claude_alpha", "pkg_beta", "mentions"),
1012+
("claude_claude_alpha", "pkg_beta", "references"),
10121013
], pairs

‎tests/test_extract.py‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2278,6 +2278,23 @@ def test_js_this_call_to_inherited_method_keeps_its_edge(tmp_path, ext):
22782278
assert ("svc_server_run", "svc_base_ping") in calls
22792279

22802280

2281+
@pytest.mark.parametrize("ext", ["ts", "js"])
2282+
def test_js_super_call_does_not_self_loop_onto_the_overriding_method(tmp_path, ext):
2283+
"""`super.greet()` can never mean the caller's own method. The `extends`
2284+
chain is unknown at this pass, so it fails closed (no edge) rather than
2285+
falling back to the file-wide name map and binding to the overriding
2286+
`greet` as a wrong self-loop."""
2287+
calls = _single_file_call_pairs(tmp_path, (
2288+
"class Base {\n"
2289+
" greet() { return 0; }\n"
2290+
"}\n"
2291+
"class Server extends Base {\n"
2292+
" greet() { return super.greet(); }\n"
2293+
"}\n"
2294+
), ext)
2295+
assert ("svc_server_greet", "svc_server_greet") not in calls
2296+
2297+
22812298
def test_swift_self_calls_bind_within_own_class_chain(tmp_path):
22822299
"""`self.save()`, a bare `save()` (implicit self) and `super.ping()` must stay
22832300
on Server's chain, not jump to the class the file declares last."""

‎tests/test_merge_graphs_cli.py‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -365,3 +365,23 @@ def test_merge_graphs_previous_drops_community_for_a_new_node(tmp_path):
365365
by_id = {n["id"]: n for n in data["nodes"]}
366366
assert by_id["alpha::a0"]["community"] == 7
367367
assert "community" not in by_id["beta::b0"]
368+
369+
370+
def test_merge_graphs_previous_malformed_json_errors_gracefully(tmp_path):
371+
"""A malformed --previous file must fail with a clear message and exit 1,
372+
not crash with an uncaught JSONDecodeError traceback."""
373+
a = tmp_path / "r1" / "graphify-out" / "graph.json"
374+
b = tmp_path / "r2" / "graphify-out" / "graph.json"
375+
_write(a, directed=False, multigraph=False, node_id="x")
376+
_write(b, directed=False, multigraph=False, node_id="y")
377+
out = tmp_path / "merged.json"
378+
previous = tmp_path / "previous.json"
379+
previous.write_text("{ not valid json ,,, ")
380+
381+
r = _run(
382+
["merge-graphs", str(a), str(b), "--out", str(out), "--previous", str(previous)],
383+
tmp_path,
384+
)
385+
assert r.returncode == 1, r.stdout + r.stderr
386+
assert "--previous file is not readable JSON" in r.stderr
387+
assert "Traceback" not in r.stderr

0 commit comments

Comments
 (0)