mirror of
https://github.com/safishamsi/graphify.git
synced 2026-10-16 17:09:17 +00:00
fix(json_config): stop emitting self-referential dep imports and array-as-extends edges
This commit is contained in:
1 parent
33362d9692
commit
33f02c936b
2 files changed
+87
-4
No files matched your search
@@ -161,8 +161,14 @@ def extract_json(path: Path) -> dict:
|
||||
if val.type == "object":
|
||||
walk_object(val, key_nid, key, depth + 1, pair_count)
|
||||
|
||||
elif val.type == "array":
|
||||
# For "extends" arrays (tsconfig, eslint): each string element.
|
||||
elif val.type == "array" and key == "extends":
|
||||
# Only a genuine `extends` array (tsconfig 5.0+, eslint) is
|
||||
# inheritance. This branch used to fire for *every* array under
|
||||
# a tracked key, so `compilerOptions.lib: ["dom","esnext"]` and
|
||||
# `exclude: ["node_modules",".next"]` each emitted an `extends`
|
||||
# edge — array membership relabelled as inheritance. The string
|
||||
# branch below already gates on `key == "extends"`; this matches
|
||||
# it. Non-`extends` arrays keep their `contains` edge only.
|
||||
# Prefix with "ref_" so external refs don't collide with real
|
||||
# code/file node IDs that share the same collapsed _make_id (J-4).
|
||||
for item in val.children:
|
||||
@@ -193,10 +199,28 @@ def extract_json(path: Path) -> dict:
|
||||
add_edge(parent_nid, ref_nid, "references", line)
|
||||
|
||||
elif parent_key in _DEP_KEYS and val_text:
|
||||
dep_nid = _make_id(key)
|
||||
# Two defects fixed here, both from the #1764 shape:
|
||||
#
|
||||
# 1. The edge ran `key_nid -> dep_nid`, and both nodes carry
|
||||
# the *same label* (the dependency name), so every
|
||||
# dependency produced a visually self-referential
|
||||
# `react --imports--> react` (22 of them on a mid-size
|
||||
# Next.js repo). The self-loop guard in
|
||||
# test_extract_json_import_and_extends_targets_are_real_nodes
|
||||
# compares node ids, and these two ids differ, so the
|
||||
# duplicate slipped past it. Sourcing the edge at the
|
||||
# manifest yields `package.json --imports--> react`.
|
||||
#
|
||||
# 2. `_make_id(key)` minted a *bare* id, skipping the "ref"
|
||||
# namespacing every other external reference in this file
|
||||
# applies (J-4). A dependency named `utils`, `colors` or
|
||||
# `types` could then be collapsed onto a same-named local
|
||||
# module by build.py's alias index -- the #1638 failure
|
||||
# mode reached through a different door.
|
||||
dep_nid = _make_id("ref", key)
|
||||
if dep_nid:
|
||||
add_node(dep_nid, key, line, file_type="concept")
|
||||
add_edge(key_nid, dep_nid, "imports", line, context="import")
|
||||
add_edge(file_nid, dep_nid, "imports", line, context="import")
|
||||
|
||||
# Entry: find root document → object
|
||||
doc = root
|
||||
|
||||
@@ -3289,6 +3289,65 @@ def test_extract_json_extends_resolved():
|
||||
assert extends_edges[0].get("context") == "import"
|
||||
|
||||
|
||||
def test_extract_json_dependency_edges_are_not_label_self_loops(tmp_path):
|
||||
"""A dependency must not point at another node carrying its own label.
|
||||
|
||||
test_extract_json_import_and_extends_targets_are_real_nodes already forbids
|
||||
self-loops, but it compares node *ids*. The dependency branch minted a
|
||||
second node for the same name under a different id, so every dependency
|
||||
rendered as ``react --imports--> react`` -- a self-loop by label that an
|
||||
id-based guard cannot see.
|
||||
"""
|
||||
package_json = tmp_path / "package.json"
|
||||
package_json.write_text(json.dumps({
|
||||
"name": "demo",
|
||||
"dependencies": {"react": "^19.0.0", "next": "^15.0.0"},
|
||||
"devDependencies": {"wrangler": "^4.71.0"},
|
||||
}))
|
||||
|
||||
result = extract_json(package_json)
|
||||
labels = {n["id"]: n["label"] for n in result["nodes"]}
|
||||
offenders = [
|
||||
(labels.get(e["source"]), e["relation"], labels.get(e["target"]))
|
||||
for e in result["edges"]
|
||||
if e["relation"] == "imports"
|
||||
and labels.get(e["source"]) == labels.get(e["target"])
|
||||
]
|
||||
assert offenders == [], f"label self-loop: {offenders}"
|
||||
|
||||
# The dependency structure #1764 restored is still present.
|
||||
imports = [e for e in result["edges"] if e["relation"] == "imports"]
|
||||
assert {labels.get(e["target"]) for e in imports} == {"react", "next", "wrangler"}
|
||||
|
||||
# External dependency ids stay "ref"-namespaced (J-4), so build.py's alias
|
||||
# index cannot collapse a dep named `utils`/`colors` onto a local module.
|
||||
assert all(e["target"].startswith("ref") for e in imports), [e["target"] for e in imports]
|
||||
|
||||
|
||||
def test_extract_json_non_extends_arrays_are_not_inheritance(tmp_path):
|
||||
"""Only an ``extends`` array is inheritance.
|
||||
|
||||
``compilerOptions.lib`` and ``exclude`` are ordinary string lists; emitting
|
||||
``extends`` for them turns array membership into a supertype relation and
|
||||
manufactures a hub out of a build-output glob.
|
||||
"""
|
||||
tsconfig = tmp_path / "tsconfig.json"
|
||||
tsconfig.write_text(json.dumps({
|
||||
"extends": ["./base.json", "./strict.json"],
|
||||
"compilerOptions": {"lib": ["dom", "esnext"]},
|
||||
"exclude": ["node_modules", ".next"],
|
||||
}))
|
||||
|
||||
result = extract_json(tsconfig)
|
||||
labels = {n["id"]: n["label"] for n in result["nodes"]}
|
||||
extends_targets = {
|
||||
labels.get(e["target"]) for e in result["edges"] if e["relation"] == "extends"
|
||||
}
|
||||
assert extends_targets == {"./base.json", "./strict.json"}
|
||||
for not_inheritance in ("dom", "esnext", ".next", "node_modules"):
|
||||
assert not_inheritance not in extends_targets
|
||||
|
||||
|
||||
def test_extract_json_import_and_extends_targets_are_real_nodes(tmp_path):
|
||||
package_json = tmp_path / "package.json"
|
||||
package_json.write_text(json.dumps({
|
||||
|
||||
Reference in new issue
Block a user