Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 11 additions & 49 deletions graphify/extract.py
Original file line number Diff line number Diff line change
Expand Up @@ -3424,10 +3424,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)
Expand Down Expand Up @@ -3681,18 +3687,7 @@ def walk(node, parent_class_nid: str | None = None) -> None:
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
Expand Down Expand Up @@ -3831,18 +3826,7 @@ def _php_emit_base(base_name: str, rel: str, at_line: int) -> None:
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":
Expand Down Expand Up @@ -3876,18 +3860,7 @@ def _php_emit_base(base_name: str, rel: str, at_line: int) -> None:
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 <Const>` in the class/module body ->
Expand Down Expand Up @@ -4153,18 +4126,7 @@ def _emit_java_parent_type(type_node, rel: str, at_line: int) -> None:
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<Dep> -> Car references Dep). _cpp_collect_type_refs
Expand Down
31 changes: 31 additions & 0 deletions tests/test_extract.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down