diff --git a/graphify/extract.py b/graphify/extract.py index 20e697e9e..b1a161fcf 100644 --- a/graphify/extract.py +++ b/graphify/extract.py @@ -82,6 +82,18 @@ def _file_stem(path: Path) -> str: return path.stem +def _file_node_id(path: Path) -> str: + """Canonical file-level node ID matching the semantic subagent format (parent_dir_stem). + + The semantic subagent (skill.md) uses ``{parent_dir}_{filename_without_ext}``, while + the AST extractor was using the full relative path including extension. This mismatch + split one file into two disconnected nodes (#1033). Using ``_file_stem`` as the base + produces ``script_pipeline_step`` instead of ``script_pipeline_step_py``, matching the + subagent format so Step-3C deduplication merges AST and semantic nodes for the same file. + """ + return _make_id(_file_stem(path)) + + _TSCONFIG_ALIAS_CACHE: dict[str, dict[str, str]] = {} _WORKSPACE_PACKAGE_CACHE: dict[str, dict[str, Path]] = {} _JS_CACHE_BYPASS_SUFFIXES = {".js", ".jsx", ".mjs", ".ts", ".tsx", ".vue", ".svelte"} @@ -1086,7 +1098,7 @@ def _import_python(node, source: bytes, file_nid: str, stem: str, edges: list, s for _ in range(dots - 1): base = base.parent rel = (module_name.replace(".", "/") + ".py") if module_name else "__init__.py" - tgt_nid = _make_id(str(base / rel)) + tgt_nid = _file_node_id(base / rel) else: tgt_nid = _make_id(raw) edges.append({ @@ -1112,7 +1124,7 @@ def _resolve_js_import_target(raw: str, str_path: str) -> "tuple[str, Path | Non return None resolved_path = _resolve_js_module_path(raw, Path(str_path).parent) if resolved_path is not None: - return _make_id(str(resolved_path)), resolved_path + return _file_node_id(resolved_path), resolved_path module_name = raw.split("/")[-1] if not module_name: return None @@ -1330,7 +1342,7 @@ def _import_c(node, source: bytes, file_nid: str, stem: str, edges: list, str_pa if child.type != "system_lib_string": resolved = _resolve_c_include_path(raw, str_path) if resolved is not None: - tgt_nid = _make_id(str(resolved)) + tgt_nid = _file_node_id(resolved) edges.append({ "source": file_nid, "target": tgt_nid, @@ -1341,6 +1353,7 @@ def _import_c(node, source: bytes, file_nid: str, stem: str, edges: list, str_pa "source_location": f"L{node.start_point[0] + 1}", "weight": 1.0, }) + break module_name = raw.split("/")[-1].split(".")[0] if module_name: @@ -2056,14 +2069,11 @@ def add_edge(src: str, tgt: str, relation: str, line: int, def ensure_named_node(name: str, line: int) -> str: nid = _make_id(stem, name) - if nid in seen_ids: - return nid - nid = _make_id(name) if nid not in seen_ids: add_node(nid, name, line) return nid - file_nid = _make_id(str(path)) + file_nid = _file_node_id(path) add_node(file_nid, path.name, 1) def walk(node, parent_class_nid: str | None = None) -> None: @@ -2114,16 +2124,14 @@ def walk(node, parent_class_nid: str | None = None) -> None: base = _read_text(arg, source) base_nid = _make_id(stem, base) if base_nid not in seen_ids: - base_nid = _make_id(base) - if base_nid not in seen_ids: - nodes.append({ - "id": base_nid, - "label": base, - "file_type": "code", - "source_file": "", - "source_location": "", - }) - seen_ids.add(base_nid) + nodes.append({ + "id": base_nid, + "label": base, + "file_type": "code", + "source_file": "", + "source_location": "", + }) + seen_ids.add(base_nid) add_edge(class_nid, base_nid, "inherits", line) # Swift-specific: conformance / inheritance @@ -2187,16 +2195,14 @@ def _php_emit_base(base_name: str, rel: str, at_line: int) -> None: return base_nid = _make_id(stem, base_name) if base_nid not in seen_ids: - base_nid = _make_id(base_name) - if base_nid not in seen_ids: - nodes.append({ - "id": base_nid, - "label": base_name, - "file_type": "code", - "source_file": "", - "source_location": "", - }) - seen_ids.add(base_nid) + nodes.append({ + "id": base_nid, + "label": base_name, + "file_type": "code", + "source_file": "", + "source_location": "", + }) + seen_ids.add(base_nid) add_edge(class_nid, base_nid, rel, at_line) for child in node.children: @@ -2253,16 +2259,14 @@ def _php_emit_base(base_name: str, rel: str, at_line: int) -> None: continue base_nid = _make_id(stem, base) if base_nid not in seen_ids: - base_nid = _make_id(base) - if base_nid not in seen_ids: - nodes.append({ - "id": base_nid, - "label": base, - "file_type": "code", - "source_file": "", - "source_location": "", - }) - seen_ids.add(base_nid) + nodes.append({ + "id": base_nid, + "label": base, + "file_type": "code", + "source_file": "", + "source_location": "", + }) + seen_ids.add(base_nid) add_edge(class_nid, base_nid, relation, line) for arg_child in user_type_node.children: if arg_child.type != "type_arguments": @@ -2301,16 +2305,14 @@ def _php_emit_base(base_name: str, rel: str, at_line: int) -> None: continue base_nid = _make_id(stem, base) if base_nid not in seen_ids: - base_nid = _make_id(base) - if base_nid not in seen_ids: - nodes.append({ - "id": base_nid, - "label": base, - "file_type": "code", - "source_file": "", - "source_location": "", - }) - seen_ids.add(base_nid) + nodes.append({ + "id": base_nid, + "label": base, + "file_type": "code", + "source_file": "", + "source_location": "", + }) + seen_ids.add(base_nid) relation = _csharp_classify_base(base, csharp_interface_names) add_edge(class_nid, base_nid, relation, line) if sub.type == "generic_name": @@ -2334,16 +2336,14 @@ def _emit_java_parent(base_name: str, rel: str, at_line: int) -> None: return base_nid = _make_id(stem, base_name) if base_nid not in seen_ids: - base_nid = _make_id(base_name) - if base_nid not in seen_ids: - nodes.append({ - "id": base_nid, - "label": base_name, - "file_type": "code", - "source_file": "", - "source_location": "", - }) - seen_ids.add(base_nid) + nodes.append({ + "id": base_nid, + "label": base_name, + "file_type": "code", + "source_file": "", + "source_location": "", + }) + seen_ids.add(base_nid) add_edge(class_nid, base_nid, rel, at_line) sup = node.child_by_field_name("superclass") @@ -2403,16 +2403,14 @@ def _emit_java_parent(base_name: str, rel: str, at_line: int) -> None: continue base_nid = _make_id(stem, base) if base_nid not in seen_ids: - base_nid = _make_id(base) - if base_nid not in seen_ids: - nodes.append({ - "id": base_nid, - "label": base, - "file_type": "code", - "source_file": "", - "source_location": "", - }) - seen_ids.add(base_nid) + nodes.append({ + "id": base_nid, + "label": base, + "file_type": "code", + "source_file": "", + "source_location": "", + }) + seen_ids.add(base_nid) add_edge(class_nid, base_nid, "inherits", line) # Find body and recurse @@ -3189,7 +3187,7 @@ def _extract_python_rationale(path: Path, result: dict) -> None: nodes = result["nodes"] edges = result["edges"] seen_ids = {n["id"] for n in nodes} - file_nid = _make_id(str(path)) + file_nid = _file_node_id(path) def _get_docstring(body_node) -> tuple[str, int] | None: if not body_node: @@ -3306,10 +3304,7 @@ def extract_svelte(path: Path) -> dict: import re as _re src = path.read_text(encoding="utf-8", errors="replace") existing_ids = {n["id"] for n in result.get("nodes", [])} - # Source file node ID must match the one _extract_generic creates: - # _make_id(str(path)) - single arg, no stem prefix. Otherwise the source - # endpoint is a phantom node and build_from_json drops the edge (#701). - file_node_id = _make_id(str(path)) + file_node_id = _file_node_id(path) aliases = _load_tsconfig_aliases(path.parent) for m in _re.finditer(r"""import\(\s*['"]([^'"]+)['"]\s*\)""", src): raw = m.group(1) @@ -3322,7 +3317,7 @@ def extract_svelte(path: Path) -> dict: # imports of bare paths and .svelte.ts rune files land on real # file nodes instead of phantom ids (#716). resolved = _resolve_js_module_path(resolved) - node_id = _make_id(str(resolved)) + node_id = _file_node_id(resolved) stub_source_file = str(resolved) else: # Check tsconfig.json path aliases (e.g. "$lib/" -> "src/lib/", "@/" -> "src/") @@ -3336,7 +3331,7 @@ def extract_svelte(path: Path) -> dict: break if resolved_alias is not None: resolved_alias = _resolve_js_module_path(resolved_alias) - node_id = _make_id(str(resolved_alias)) + node_id = _file_node_id(resolved_alias) stub_source_file = str(resolved_alias) else: # Bare/scoped import (node_modules) - use last segment; @@ -3388,7 +3383,7 @@ def extract_svelte(path: Path) -> dict: resolved = resolved.with_suffix(".ts") elif resolved.suffix == ".jsx": resolved = resolved.with_suffix(".tsx") - node_id = _make_id(str(resolved)) + node_id = _file_node_id(resolved) stub_source_file = str(resolved) else: resolved_alias = None @@ -3398,7 +3393,7 @@ def extract_svelte(path: Path) -> dict: resolved_alias = Path(os.path.normpath(Path(alias_base) / rest)) break if resolved_alias is not None: - node_id = _make_id(str(resolved_alias)) + node_id = _file_node_id(resolved_alias) stub_source_file = str(resolved_alias) else: module_name = raw.split("/")[-1] @@ -3447,7 +3442,7 @@ def extract_astro(path: Path) -> dict: import re as _re src = path.read_text(encoding="utf-8", errors="replace") existing_ids = {n["id"] for n in result.get("nodes", [])} - file_node_id = _make_id(str(path)) + file_node_id = _file_node_id(path) aliases = _load_tsconfig_aliases(path.parent) # Dynamic imports anywhere in the file: `import('./X.astro')` is legal in # frontmatter setup code and inside expression slots. @@ -3458,7 +3453,7 @@ def extract_astro(path: Path) -> dict: if raw.startswith("."): resolved = Path(os.path.normpath(path.parent / raw)) resolved = _resolve_js_module_path(resolved) - node_id = _make_id(str(resolved)) + node_id = _file_node_id(resolved) stub_source_file = str(resolved) else: resolved_alias = None @@ -3469,7 +3464,7 @@ def extract_astro(path: Path) -> dict: break if resolved_alias is not None: resolved_alias = _resolve_js_module_path(resolved_alias) - node_id = _make_id(str(resolved_alias)) + node_id = _file_node_id(resolved_alias) stub_source_file = str(resolved_alias) else: module_name = raw.split("/")[-1] @@ -3524,7 +3519,7 @@ def extract_astro(path: Path) -> dict: resolved = resolved.with_suffix(".ts") elif resolved.suffix == ".jsx": resolved = resolved.with_suffix(".tsx") - node_id = _make_id(str(resolved)) + node_id = _file_node_id(resolved) stub_source_file = str(resolved) else: resolved_alias = None @@ -3534,7 +3529,7 @@ def extract_astro(path: Path) -> dict: resolved_alias = Path(os.path.normpath(Path(alias_base) / rest)) break if resolved_alias is not None: - node_id = _make_id(str(resolved_alias)) + node_id = _file_node_id(resolved_alias) stub_source_file = str(resolved_alias) else: module_name = raw.split("/")[-1] @@ -3729,7 +3724,7 @@ def extract_blade(path: Path) -> dict: except OSError: return {"error": f"cannot read {path}"} - file_nid = _make_id(str(path)) + file_nid = _file_node_id(path) nodes = [{"id": file_nid, "label": path.name, "file_type": "code", "source_file": str(path), "source_location": None}] edges = [] @@ -3777,7 +3772,7 @@ def extract_dart(path: Path) -> dict: # Use stem (not str(path)) for child IDs to keep them machine-independent. stem = _file_stem(path) - file_nid = _make_id(str(path)) + file_nid = _file_node_id(path) nodes = [{"id": file_nid, "label": path.name, "file_type": "code", "source_file": str(path), "source_location": None}] edges = [] @@ -3859,7 +3854,7 @@ def add_edge(src: str, tgt: str, relation: str, line: int, "confidence": confidence, "confidence_score": score, "source_file": str_path, "source_location": f"L{line}", "weight": 1.0}) - file_nid = _make_id(str(path)) + file_nid = _file_node_id(path) add_node(file_nid, path.name, 1) def walk(node, module_nid: str | None = None) -> None: @@ -4254,7 +4249,7 @@ def add_edge(src: str, tgt: str, relation: str, line: int, edge["context"] = context edges.append(edge) - file_nid = _make_id(str(path)) + file_nid = _file_node_id(path) add_node(file_nid, path.name, 1) def _func_name_from_signature(sig_node) -> str | None: @@ -4506,7 +4501,7 @@ def add_edge(src: str, tgt: str, relation: str, line: int, edge["context"] = context edges.append(edge) - file_nid = _make_id(str(path)) + file_nid = _file_node_id(path) add_node(file_nid, path.name, 1) def _fortran_name(stmt_node) -> str | None: @@ -4678,7 +4673,7 @@ def add_edge(src: str, tgt: str, relation: str, line: int, edge["context"] = context edges.append(edge) - file_nid = _make_id(str(path)) + file_nid = _file_node_id(path) add_node(file_nid, path.name, 1) def ensure_named_node(name: str, line: int) -> str: @@ -5018,7 +5013,7 @@ def add_edge(src: str, tgt: str, relation: str, line: int, edge["context"] = context edges.append(edge) - file_nid = _make_id(str(path)) + file_nid = _file_node_id(path) add_node(file_nid, path.name, 1) def ensure_named_node(name: str, line: int) -> str: @@ -5286,7 +5281,7 @@ def add_edge(src: str, tgt: str, relation: str, line: int, edge["context"] = context edges.append(edge) - file_nid = _make_id(str(path)) + file_nid = _file_node_id(path) add_node(file_nid, path.name, 1) def _extract_import(node) -> None: @@ -5456,7 +5451,7 @@ def add_edge(src: str, tgt: str, relation: str, line: int, edge["context"] = context edges.append(edge) - file_nid = _make_id(str(path)) + file_nid = _file_node_id(path) add_node(file_nid, path.name, 1) _PS_SKIP = frozenset({ @@ -5809,7 +5804,7 @@ def _apply_symbol_resolution_facts( return path_by_resolved = {path.resolve(): path for path in paths} - source_file_id = {path.resolve(): _make_id(str(path)) for path in paths} + source_file_id = {path.resolve(): _file_node_id(path) for path in paths} symbol_nodes: dict[tuple[Path, str], str] = {} for node in nodes: source_path = _js_source_path(str(node.get("source_file", "")), root) @@ -5900,7 +5895,7 @@ def add_edge(source: str, target: str, relation: str, context: str, line: int, s if source_id is not None: add_edge( source_id, - _make_id(str(path_by_resolved.get(target_path, target_path))), + _file_node_id(path_by_resolved.get(target_path, target_path)), "re_exports", "export", star_fact.line, @@ -5924,7 +5919,7 @@ def add_edge(source: str, target: str, relation: str, context: str, line: int, s if source_id is not None: add_edge( source_id, - _make_id(str(path_by_resolved.get(origin[0], origin[0]))), + _file_node_id(path_by_resolved.get(origin[0], origin[0])), "re_exports", "export", export_fact.line, @@ -6908,7 +6903,7 @@ def _resolve_cross_file_java_imports( new_edges: list[dict] = [] seen_pairs: set[tuple[str, str]] = set() for path in paths: - file_nid = _make_id(str(path)) + file_nid = _file_node_id(path) try: source = path.read_bytes() tree = parser.parse(source) @@ -6995,7 +6990,7 @@ def add_edge(src: str, tgt: str, relation: str, line: int, edge["context"] = context edges.append(edge) - file_nid = _make_id(str(path)) + file_nid = _file_node_id(path) add_node(file_nid, path.name, 1) def _read(node) -> str: @@ -7197,7 +7192,7 @@ def add_edge(src: str, tgt: str, relation: str, line: int, edge["context"] = context edges.append(edge) - file_nid = _make_id(str(path)) + file_nid = _file_node_id(path) add_node(file_nid, path.name, 1) _IMPORT_KEYWORDS = frozenset({"alias", "import", "require", "use"}) @@ -7389,7 +7384,7 @@ def add_edge(src: str, tgt: str, relation: str, line: int, "confidence": confidence, "source_file": str_path, "source_location": f"L{line}", "weight": weight}) - file_nid = _make_id(str(path)) + file_nid = _file_node_id(path) add_node(file_nid, path.name, 1) # Track heading stack for nesting: [(level, nid), ...] @@ -7510,7 +7505,7 @@ def _pascal_resolve_unit(from_path: Path, unit_name: str) -> str: unit_map: dict[str, str] = {} for ext in (".pas", ".pp", ".dpr", ".dpk", ".inc"): for f in root.rglob("*" + ext): - unit_map[f.stem.lower()] = _make_id(str(f)) + unit_map[f.stem.lower()] = _file_node_id(f) _pascal_unit_cache[root_key] = unit_map return _pascal_unit_cache[root_key].get(unit_name.lower(), _make_id(unit_name)) @@ -7902,7 +7897,7 @@ def add_edge( edge["context"] = context edges.append(edge) - file_nid = _make_id(str(path)) + file_nid = _file_node_id(path) add_node(file_nid, path.name, 1) module_nid = file_nid @@ -8125,7 +8120,7 @@ def add_edge( edge["context"] = context edges.append(edge) - file_nid = _make_id(str(path)) + file_nid = _file_node_id(path) add_node(file_nid, path.name, 1) obj_re = re.compile(r"^\s*object\s+\w+\s*:\s*(\w+)", re.IGNORECASE) @@ -8225,7 +8220,7 @@ def add_edge( edge["context"] = context edges.append(edge) - file_nid = _make_id(str(path)) + file_nid = _file_node_id(path) add_node(file_nid, path.name, 1) obj_re = re.compile(r"^\s*object\s+\w+\s*:\s*(\w+)", re.IGNORECASE) @@ -8337,7 +8332,7 @@ def add_edge(src: str, tgt: str, relation: str, context: str | None = None) -> N edge["context"] = context edges.append(edge) - file_nid = _make_id(str(path)) + file_nid = _file_node_id(path) add_node(file_nid, path.name) name_elem = xml_root.find(".//Package/Name") @@ -8436,7 +8431,7 @@ def add_edge(src: str, tgt: str, relation: str, line: int, edge["context"] = context edges.append(edge) - file_nid = _make_id(str(path)) + file_nid = _file_node_id(path) # file_nid is fully path-derived and never produced by _make_id(stem, func_name), # so appending "__entry" guarantees a distinct ID from any function node. entry_nid = file_nid + "__entry" @@ -8559,7 +8554,7 @@ def walk(node, parent_nid: str) -> None: # like `source ../../etc/passwd` that traverse outside # the project tree (B-1). if resolved.exists(): - tgt_nid = _make_id(str(resolved)) + tgt_nid = _file_node_id(resolved) add_edge(file_nid, tgt_nid, "imports_from", line, context="import") else: @@ -8622,7 +8617,7 @@ def extract_sln(path: Path) -> dict: except OSError: return {"nodes": [], "edges": [], "error": f"cannot read {path}"} - file_nid = _make_id(str(path)) + file_nid = _file_node_id(path) str_path = str(path) nodes: list[dict] = [{"id": file_nid, "label": path.name, "file_type": "code", "source_file": str_path, "source_location": None}] @@ -8709,7 +8704,7 @@ def extract_csproj(path: Path) -> dict: except ET.ParseError as e: return {"nodes": [], "edges": [], "error": f"XML parse error: {e}"} - file_nid = _make_id(str(path)) + file_nid = _file_node_id(path) str_path = str(path) nodes: list[dict] = [{"id": file_nid, "label": path.name, "file_type": "code", "source_file": str_path, "source_location": None}] @@ -8810,7 +8805,7 @@ def extract_razor(path: Path) -> dict: except OSError: return {"nodes": [], "edges": [], "error": f"cannot read {path}"} - file_nid = _make_id(str(path)) + file_nid = _file_node_id(path) str_path = str(path) nodes: list[dict] = [{"id": file_nid, "label": path.name, "file_type": "code", "source_file": str_path, "source_location": None}] @@ -8969,7 +8964,7 @@ def add_edge(src: str, tgt: str, relation: str, line: int, edge["context"] = context edges.append(edge) - file_nid = _make_id(str(path)) + file_nid = _file_node_id(path) add_node(file_nid, path.name, 1) def _key_text(pair_node) -> str | None: @@ -9105,7 +9100,7 @@ def add_edge(src: str, tgt: str, relation: str, line: int, edge["context"] = context edges.append(edge) - file_nid = _make_id(str(path)) + file_nid = _file_node_id(path) add_node(file_nid, path.name, 1) def _type_path_text(node) -> str: @@ -9146,7 +9141,7 @@ def walk(node, parent_type_path: "str | None" = None, resolved = (path.parent / norm).resolve() edge: dict = { "source": file_nid, - "target": _make_id(str(resolved)) if resolved.exists() else _make_id(norm), + "target": _file_node_id(resolved) if resolved.exists() else _make_id(norm), "relation": "imports_from" if resolved.exists() else "imports", "context": "import", "confidence": "EXTRACTED", @@ -9344,7 +9339,7 @@ def extract_dmi(path: Path) -> dict: str_path = str(path) stem = _file_stem(path) - file_nid = _make_id(str(path)) + file_nid = _file_node_id(path) nodes: list[dict] = [{"id": file_nid, "label": path.name, "file_type": "code", "source_file": str_path, "source_location": "L1"}] edges: list[dict] = [] @@ -9443,7 +9438,7 @@ def extract_dmm(path: Path) -> dict: return {"nodes": [], "edges": [], "error": str(e)} str_path = str(path) - file_nid = _make_id(str(path)) + file_nid = _file_node_id(path) nodes: list[dict] = [{"id": file_nid, "label": path.name, "file_type": "code", "source_file": str_path, "source_location": "L1"}] edges: list[dict] = [] @@ -9516,7 +9511,7 @@ def extract_dmf(path: Path) -> dict: str_path = str(path) stem = _file_stem(path) - file_nid = _make_id(str(path)) + file_nid = _file_node_id(path) nodes: list[dict] = [{"id": file_nid, "label": path.name, "file_type": "code", "source_file": str_path, "source_location": "L1"}] edges: list[dict] = [] @@ -9911,17 +9906,20 @@ def extract( _augment_symbol_resolution_edges(paths, all_nodes, all_edges, root) - # Remap file node IDs from absolute-path-derived to project-relative so - # graph.json edge endpoints are stable across machines (#502) + # Remap legacy absolute-path-derived file node IDs to the canonical + # stem-based format so existing graph.json files from pre-v8.0.17 still + # merge correctly on re-extract (#502, #1033). id_remap: dict[str, str] = {} for path in paths: - old_id = _make_id(str(path)) + canonical = _file_node_id(path) try: - new_id = _make_id(str(path.relative_to(root))) + rel = path.relative_to(root) except ValueError: continue - if old_id != new_id: - id_remap[old_id] = new_id + old_ids = {_make_id(str(path)), _make_id(str(rel))} + for old_id in old_ids: + if old_id != canonical: + id_remap[old_id] = canonical if id_remap: for n in all_nodes: if n.get("id") in id_remap: @@ -10003,7 +10001,7 @@ def extract( sf_rel = sf_path.relative_to(root) if sf_path.is_absolute() else sf_path except ValueError: sf_rel = sf_path - nid_to_file_nid[n["id"]] = _make_id(str(sf_rel)) + nid_to_file_nid[n["id"]] = _file_node_id(sf_rel) existing_pairs = {(e["source"], e["target"]) for e in all_edges} for rc in all_raw_calls: diff --git a/tests/test_astro_extraction.py b/tests/test_astro_extraction.py index c21e66c24..307bed6b6 100644 --- a/tests/test_astro_extraction.py +++ b/tests/test_astro_extraction.py @@ -13,7 +13,7 @@ from graphify.detect import CODE_EXTENSIONS from graphify.extract import ( - _make_id, + _file_node_id, extract_astro, ) @@ -57,8 +57,8 @@ def test_extract_astro_picks_up_frontmatter_static_imports(tmp_path): result = extract_astro(page) targets = _import_targets(result, relation="imports_from") - assert _make_id(str(layout)) in targets - assert _make_id(str(hero)) in targets + assert _file_node_id(layout) in targets + assert _file_node_id(hero) in targets def test_extract_astro_handles_dynamic_import_in_frontmatter(tmp_path): @@ -75,7 +75,7 @@ def test_extract_astro_handles_dynamic_import_in_frontmatter(tmp_path): result = extract_astro(page) targets = _import_targets(result, relation="dynamic_import") - assert _make_id(str(other)) in targets + assert _file_node_id(other) in targets def test_extract_astro_picks_up_client_side_script_imports(tmp_path): @@ -100,8 +100,8 @@ def test_extract_astro_picks_up_client_side_script_imports(tmp_path): result = extract_astro(page) targets = _import_targets(result, relation="imports_from") - assert _make_id(str(layout)) in targets - assert _make_id(str(hydrate)) in targets + assert _file_node_id(layout) in targets + assert _file_node_id(hydrate) in targets def test_extract_astro_no_frontmatter_does_not_crash(tmp_path): @@ -140,4 +140,4 @@ def test_extract_astro_handles_tsconfig_path_alias(tmp_path): result = extract_astro(page) targets = _import_targets(result, relation="imports_from") - assert _make_id(str(hero)) in targets + assert _file_node_id(hero) in targets diff --git a/tests/test_import_extension_resolution.py b/tests/test_import_extension_resolution.py index 0d1222c0a..9a6d6ca34 100644 --- a/tests/test_import_extension_resolution.py +++ b/tests/test_import_extension_resolution.py @@ -11,10 +11,9 @@ from pathlib import Path from graphify.extract import ( - _make_id, + extract_js, extract_python, extract_svelte, + _make_id, _file_node_id, _resolve_js_module_path, - extract_js, - extract_svelte, ) @@ -185,7 +184,7 @@ def test_bare_path_import_resolves_in_ts_file(tmp_path): importer = _write(tmp_path / "page.ts", "import type { GetNestedType } from './type-helpers'\n") result = extract_js(importer) - expected = _make_id(str(target)) + expected = _file_node_id(target) assert expected in _import_targets(result), ( f"Bare-path .ts import must resolve to target node id; " f"expected {expected}; got {_import_targets(result)}" @@ -199,7 +198,7 @@ def test_directory_import_resolves_to_index_ts(tmp_path): importer = _write(tmp_path / "page.ts", "import { enqueue } from './queue'\n") result = extract_js(importer) - expected = _make_id(str(target)) + expected = _file_node_id(target) assert expected in _import_targets(result), ( f"Directory import must resolve to ./queue/index.ts; " f"expected {expected}; got {_import_targets(result)}" @@ -216,7 +215,7 @@ def test_dot_svelte_import_resolves_to_dot_svelte_ts(tmp_path): importer = _write(tmp_path / "page.ts", "import { isMobile } from './is-mobile.svelte'\n") result = extract_js(importer) - expected = _make_id(str(target)) + expected = _file_node_id(target) assert expected in _import_targets(result), ( f".svelte → .svelte.ts resolution failed; " f"expected {expected}; got {_import_targets(result)}" @@ -233,7 +232,7 @@ def test_explicit_ts_import_still_works(tmp_path): importer = _write(tmp_path / "page.ts", "import { x } from './foo.ts'\n") result = extract_js(importer) - expected = _make_id(str(target)) + expected = _file_node_id(target) assert expected in _import_targets(result), ( f"Explicit .ts imports must still resolve; " f"expected {expected}; got {_import_targets(result)}" @@ -247,7 +246,7 @@ def test_explicit_svelte_import_still_works(tmp_path): importer = _write(tmp_path / "page.ts", "import Card from './Card.svelte'\n") result = extract_js(importer) - expected = _make_id(str(target)) + expected = _file_node_id(target) assert expected in _import_targets(result), ( f"Existing .svelte imports must resolve to the .svelte node, " f"not get redirected; expected {expected}; " @@ -285,7 +284,7 @@ def test_alias_import_with_bare_path_resolves(tmp_path): importer = _write(importer_dir / "page.ts", "import type { X } from '$lib/type-helpers'\n") result = extract_js(importer) - expected = _make_id(str(target)) + expected = _file_node_id(target) assert expected in _import_targets(result), ( f"Alias + bare-path resolution failed; " f"expected {expected}; got {_import_targets(result)}" @@ -304,7 +303,7 @@ def test_type_only_import_with_bare_path_resolves(tmp_path): importer = _write(tmp_path / "page.ts", "import type { GetNestedType } from './type-helpers'\n") result = extract_js(importer) - expected = _make_id(str(target)) + expected = _file_node_id(target) assert expected in _import_targets(result), ( f"Type-only import with bare path failed to resolve; " f"expected {expected}; got {_import_targets(result)}" @@ -342,7 +341,7 @@ def test_alias_directory_import_resolves_to_index_ts(tmp_path): importer = _write(src / "routes" / "page.ts", "import { enqueue } from '$lib/queue'\n") result = extract_js(importer) - expected = _make_id(str(target)) + expected = _file_node_id(target) assert expected in _import_targets(result), ( f"Alias + directory resolution failed; " f"expected {expected}; got {_import_targets(result)}" @@ -428,7 +427,7 @@ def test_end_to_end_multi_dot_import_resolves(tmp_path): importer = _write(tmp_path / "page.ts", "import { apply } from './tag-action.shared'\n") result = extract_js(importer) - expected = _make_id(str(target)) + expected = _file_node_id(target) assert expected in _import_targets(result), ( f"Multi-dot import failed end-to-end; " f"expected {expected}; got {_import_targets(result)}" @@ -448,7 +447,7 @@ def test_resolve_chain_alias_and_extension_compose(tmp_path): importer = _write(src / "routes" / "page.ts", "import { isMobile } from '$lib/hooks/is-mobile.svelte'\n") result = extract_js(importer) - expected = _make_id(str(target)) + expected = _file_node_id(target) assert expected in _import_targets(result), ( f"Alias + .svelte→.svelte.ts chain failed to compose; " f"expected {expected}; got {_import_targets(result)}" @@ -473,7 +472,7 @@ def test_ts_dynamic_import_bare_path_resolves(tmp_path): } """) result = extract_js(importer) - expected = _make_id(str(target)) + expected = _file_node_id(target) targets = {str(e.get("target") or "") for e in result["edges"] if e.get("relation") in ("imports", "imports_from")} assert expected in targets, ( @@ -498,7 +497,7 @@ def test_ts_dynamic_import_alias_with_bare_path_resolves(tmp_path): } """) result = extract_js(importer) - expected = _make_id(str(target)) + expected = _file_node_id(target) targets = {str(e.get("target") or "") for e in result["edges"] if e.get("relation") in ("imports", "imports_from")} assert expected in targets, ( @@ -521,7 +520,7 @@ def test_dynamic_import_bare_path_resolves(tmp_path): result = extract_svelte(importer) dyn_targets = {str(e.get("target") or "") for e in result["edges"] if e.get("relation") == "dynamic_import"} - expected = _make_id(str(target)) + expected = _file_node_id(target) assert expected in dyn_targets, ( f"dynamic_import of .svelte that's actually .svelte.ts must " f"resolve through the new resolver; "