diff --git a/graphify/extract.py b/graphify/extract.py index a705c655..5b2b2fc2 100644 --- a/graphify/extract.py +++ b/graphify/extract.py @@ -3110,6 +3110,8 @@ def extract_dart(path: Path) -> dict: except OSError: return {"error": f"cannot read {path}"} + # Use stem (not str(path)) for child IDs to keep them machine-independent. + stem = _file_stem(path) file_nid = _make_id(str(path)) nodes = [{"id": file_nid, "label": path.name, "file_type": "code", "source_file": str(path), "source_location": None}] @@ -3118,7 +3120,7 @@ def extract_dart(path: Path) -> dict: # Classes and mixins for m in re.finditer(r"^\s*(?:abstract\s+)?(?:class|mixin)\s+(\w+)", src, re.MULTILINE): - nid = _make_id(str(path), m.group(1)) + nid = _make_id(stem, m.group(1)) if nid not in defined: nodes.append({"id": nid, "label": m.group(1), "file_type": "code", "source_file": str(path), "source_location": None}) @@ -3132,7 +3134,7 @@ def extract_dart(path: Path) -> dict: name = m.group(1) if name in {"if", "for", "while", "switch", "catch", "return"}: continue - nid = _make_id(str(path), name) + nid = _make_id(stem, name) if nid not in defined: nodes.append({"id": nid, "label": name, "file_type": "code", "source_file": str(path), "source_location": None}) diff --git a/tests/test_extract.py b/tests/test_extract.py index fb280699..0d5db2c5 100644 --- a/tests/test_extract.py +++ b/tests/test_extract.py @@ -1022,3 +1022,41 @@ def test_pure_export_no_from_not_treated_as_reexport(): result = extract_js(Path(f.name)) reexports = [e for e in result["edges"] if e["relation"] == "re_exports"] assert reexports == [], f"Pure export should not create re_exports: {reexports}" + + +def test_dart_child_node_ids_are_stem_based(tmp_path): + """Dart child node IDs must be built from _file_stem rather than absolute path.""" + from graphify.extract import extract_dart, _file_stem, _make_id + + src_file = tmp_path / "mydir" / "sample.dart" + src_file.parent.mkdir(parents=True, exist_ok=True) + src_file.write_bytes(b"class MyClass {}\nvoid myFunc() {}\n") + + result = extract_dart(src_file) + + stem = _file_stem(src_file) # -> "mydir.sample" + expected_class_nid = _make_id(stem, "MyClass") # -> "mydir_sample_myclass" + expected_func_nid = _make_id(stem, "myFunc") # -> "mydir_sample_myfunc" + + node_ids = {n["id"] for n in result["nodes"]} + + assert expected_class_nid in node_ids, ( + f"Class node ID '{expected_class_nid}' not found in {node_ids}. " + "extract_dart may still be using str(path) instead of _file_stem(path)." + ) + assert expected_func_nid in node_ids, ( + f"Function node ID '{expected_func_nid}' not found in {node_ids}. " + "extract_dart may still be using str(path) instead of _file_stem(path)." + ) + + # Sanity-check: no child node ID should contain any path separator fragment. + file_nid = next(n["id"] for n in result["nodes"] if n.get("label") == src_file.name) + for node in result["nodes"]: + if node["id"] == file_nid: + continue + assert "_" + stem.replace(".", "_") in node["id"] or node["id"].startswith(stem.replace(".", "_")), ( + f"Child node ID '{node['id']}' does not start with the expected stem prefix '{stem}'. " + "This suggests an absolute path is still leaking into the ID." + ) + +