diff --git a/graphify/extract.py b/graphify/extract.py index 40b9c8b..ed05504 100644 --- a/graphify/extract.py +++ b/graphify/extract.py @@ -3450,10 +3450,16 @@ def _ruby_extra_walk(node, source: bytes, file_nid: str, stem: str, str_path: st if base_nid not in seen_ids: base_nid = _make_id(base) if base_nid not in seen_ids: + # origin_file lets _disambiguate_colliding_node_ids + # tell this file's unresolved reference apart from + # another file's same-named one, instead of every + # file's stub collapsing onto one shared bare id + # (see ensure_named_node(), which sets the same + # field for this exact reason). nodes.append({ "id": base_nid, "label": base, "file_type": "code", "source_file": "", - "source_location": "", + "source_location": "", "origin_file": str_path, }) seen_ids.add(base_nid) add_edge(class_nid, base_nid, "inherits", line) @@ -3707,18 +3713,7 @@ def _extract_generic( for arg in args.children: if arg.type == "identifier": 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) + base_nid = ensure_named_node(base, line) add_edge(class_nid, base_nid, "inherits", line) # Swift-specific: conformance / inheritance @@ -3857,18 +3852,7 @@ def _extract_generic( base = _kotlin_user_type_name(user_type_node, source) if not base: 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) + base_nid = ensure_named_node(base, line) add_edge(class_nid, base_nid, relation, line) for arg_child in user_type_node.children: if arg_child.type != "type_arguments": @@ -3902,18 +3886,7 @@ def _extract_generic( base = _read_text(consts[-1], source) break if base: - 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) + base_nid = ensure_named_node(base, line) add_edge(class_nid, base_nid, "inherits", line) # `include`/`extend`/`prepend ` in the class/module body -> @@ -4179,18 +4152,7 @@ def _extract_generic( continue if not base: 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) + base_nid = ensure_named_node(base, line) add_edge(class_nid, base_nid, "inherits", line) # Emit a generic_arg reference for each type argument on the # base (Base -> Car references Dep). _cpp_collect_type_refs diff --git a/tests/test_extract.py b/tests/test_extract.py index 04baf39..9254a25 100644 --- a/tests/test_extract.py +++ b/tests/test_extract.py @@ -100,6 +100,37 @@ def test_extract_disambiguates_duplicate_symbol_ids_by_source_path(tmp_path): assert edge["target"] in node_ids, f"Dangling structural target: {edge}" +def test_cpp_unresolved_base_class_stubs_stay_disambiguated_by_file(tmp_path): + """Two different files' same-named, otherwise-undefined base class must not + collapse onto one shared stub node. + + The C++ base_class_clause handler used to build its stub inline instead of + calling ensure_named_node(), so it never tagged the stub with origin_file. + Without that tag, _disambiguate_colliding_node_ids couldn't tell file A's + reference to unresolved `Base` apart from file B's, and every file's + unresolved base class merged onto one bare id -- which could then collide + with an unrelated same-named real definition anywhere else in the corpus. + """ + first = tmp_path / "a" / "Foo.cpp" + second = tmp_path / "b" / "Bar.cpp" + first.parent.mkdir(parents=True) + second.parent.mkdir(parents=True) + first.write_text("class Foo : public Base {};\n", encoding="utf-8") + second.write_text("class Bar : public Base {};\n", encoding="utf-8") + + result = extract([first, second], cache_root=tmp_path) + base_stubs = [ + node for node in result["nodes"] + if node["label"] == "Base" and not node.get("source_file") + ] + assert len(base_stubs) == 2 + assert len({node["id"] for node in base_stubs}) == 2 + + inherits_edges = [e for e in result["edges"] if e["relation"] == "inherits"] + assert len(inherits_edges) == 2 + assert len({e["target"] for e in inherits_edges}) == 2 + + def test_cross_file_type_annotation_refs_resolve_to_single_node(tmp_path): """#1402: a class defined once but referenced via type annotations in N other files must NOT create 1+N phantom duplicate nodes (with the referencing file's