mirror of
https://github.com/safishamsi/graphify.git
synced 2026-10-14 07:59:40 +00:00
fix(rust): keep a forward reference unresolved when its type id collides with another item (#3782)
Ids are casefolded, so `fn handle`, `const HANDLE`, `struct HANDLE` or an `impl` for `HANDLE` share the id of `struct Handle`, and walk() keeps whichever item comes first. The pre-scan registered the type id regardless, so a reference placed above the declarations was bound to that shared id and could end on the function or on the other item, where v8's extractor left a sourceless stub. For the same reason a `HANDLE` reference (a `type HANDLE` alias, or an imported name) placed above a local `struct Handle` was bound to the struct. Only pre-register a type when every file-level item behind its id is that same type (`#[cfg]` twins count once), and match it by its exact name. An `impl` block counts as the type it names, since walk() gives it the node of its type text; methods keep impl-qualified ids and are not counted. Otherwise the forward reference keeps its stub. A reference placed after an item that already owns the id still resolves through seen_ids, unchanged from v8. Adds regression cases for a type colliding with a function (also in an `extern` block), a const, a static, another type, an `impl` for another type and a type alias, and controls where the id is not shared: another function name, `#[cfg]` twins, an `impl` for the type itself, a method named like the type, and a generic type only an `impl` names. Assisted-by: assayer harness on Claude Code (https://github.com/Yyunozor/assayer-memory-mcp)
This commit is contained in:
2 files changed
+107
-12
No files matched your search
@@ -169,27 +169,56 @@ def extract_rust(path: Path) -> dict:
|
||||
# items are pre-registered: walk() never makes a node of a `type` alias or a
|
||||
# `union`, so registering one would leave its references pointing at no node.
|
||||
# The scan never descends further than walk() does, so every id registered
|
||||
# here ends up as a node.
|
||||
local_type_ids: set[str] = set()
|
||||
# here ends up as a node. Ids are casefolded, so `fn handle` and
|
||||
# `struct Handle` (or `struct HANDLE` and `struct Handle`) share one and
|
||||
# walk() keeps whichever comes first: a type is only pre-registered when
|
||||
# every file-level item behind its id is that same type (`#[cfg]` twins count
|
||||
# once), and it is matched by its exact name, since a `type HANDLE` alias or
|
||||
# an imported `HANDLE` shares the id of a local `struct Handle`. Otherwise the
|
||||
# forward reference keeps the sourceless stub v8's extractor gives it. An
|
||||
# `impl` block counts as the type it names, because walk() gives it the node
|
||||
# of its type text (`impl Tr for HANDLE` collides with `struct Handle`,
|
||||
# `impl Tr for &Handle<'_>` does not); methods and impl-scoped items have
|
||||
# impl-qualified ids and are not counted.
|
||||
items_by_id: dict[str, set[tuple[bool, str]]] = {}
|
||||
declared_type_ids: set[str] = set()
|
||||
|
||||
def _scan_type_items(node) -> None:
|
||||
t = node.type
|
||||
if t in ("struct_item", "enum_item", "trait_item"):
|
||||
if t in ("struct_item", "enum_item", "trait_item", "function_item",
|
||||
"function_signature_item", "static_item", "const_item"):
|
||||
name_node = node.child_by_field_name("name")
|
||||
if name_node:
|
||||
local_type_ids.add(_make_id(stem, _read_text(name_node, source)))
|
||||
name = _read_text(name_node, source)
|
||||
nid = _make_id(stem, name)
|
||||
is_type = t in ("struct_item", "enum_item", "trait_item")
|
||||
items_by_id.setdefault(nid, set()).add((is_type, name))
|
||||
if is_type:
|
||||
declared_type_ids.add(nid)
|
||||
return
|
||||
if t in ("function_item", "function_signature_item", "static_item",
|
||||
"const_item", "impl_item", "use_declaration"):
|
||||
if t == "impl_item":
|
||||
type_node = node.child_by_field_name("type")
|
||||
if type_node is not None:
|
||||
type_name = _read_text(type_node, source).strip()
|
||||
refs: list[tuple[str, str]] = []
|
||||
_rust_collect_type_refs(type_node, source, False, refs)
|
||||
bare = next((ref for ref, role in refs if role == "type"), type_name)
|
||||
items_by_id.setdefault(_make_id(stem, type_name), set()).add((True, bare))
|
||||
return
|
||||
if t == "use_declaration":
|
||||
return
|
||||
for child in node.children:
|
||||
_scan_type_items(child)
|
||||
|
||||
_scan_type_items(root)
|
||||
local_type_names = {
|
||||
next(iter(items))[1] for nid, items in items_by_id.items()
|
||||
if nid in declared_type_ids and len(items) == 1
|
||||
}
|
||||
|
||||
def ensure_named_node(name: str, line: int) -> str:
|
||||
nid = _make_id(stem, name)
|
||||
if nid in seen_ids or nid in local_type_ids:
|
||||
if nid in seen_ids or name in local_type_names:
|
||||
return nid
|
||||
nid = _make_id(name)
|
||||
if nid not in seen_ids:
|
||||
|
||||
+71
-5
@@ -487,11 +487,12 @@ def test_rust_forward_type_reference_resolves_to_local_declaration(tmp_path):
|
||||
def test_rust_forward_reference_prescan_skips_items_that_are_not_nodes(tmp_path):
|
||||
"""#3782: only struct/enum/trait declarations become nodes, so only they may
|
||||
resolve a forward reference. A `type` alias, a `union`, a struct declared in a
|
||||
function body and a name declared nowhere in the file produce no node here;
|
||||
references to them must keep their sourceless stub instead of being dropped."""
|
||||
function body, a name declared nowhere in the file and a generic type that only
|
||||
an `impl` names produce no node of that name here; references to them must keep
|
||||
their sourceless stub instead of being dropped."""
|
||||
p = tmp_path / "shapes.rs"
|
||||
p.write_text(
|
||||
"fn user(a: &Alias, u: &Bits, l: &Local, e: &Elsewhere) {}\n"
|
||||
"fn user(a: &Alias, u: &Bits, l: &Local, e: &Elsewhere, w: &Wrapper<u8>) {}\n"
|
||||
"\n"
|
||||
"type Alias = u32;\n"
|
||||
"\n"
|
||||
@@ -499,7 +500,11 @@ def test_rust_forward_reference_prescan_skips_items_that_are_not_nodes(tmp_path)
|
||||
"\n"
|
||||
"fn host() {\n"
|
||||
" struct Local;\n"
|
||||
"}\n",
|
||||
"}\n"
|
||||
"\n"
|
||||
"trait Tr {}\n"
|
||||
"\n"
|
||||
"impl<T> Tr for Wrapper<T> {}\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
r = extract_rust(p)
|
||||
@@ -509,7 +514,7 @@ def test_rust_forward_reference_prescan_skips_items_that_are_not_nodes(tmp_path)
|
||||
for e in r["edges"]
|
||||
if e["relation"] == "references" and e["target"] in by_id
|
||||
}
|
||||
for name in ("Alias", "Bits", "Local", "Elsewhere"):
|
||||
for name in ("Alias", "Bits", "Local", "Elsewhere", "Wrapper"):
|
||||
assert name in targets, f"reference to {name} was dropped"
|
||||
assert targets[name]["source_file"] == "", name
|
||||
|
||||
@@ -564,6 +569,67 @@ def test_rust_forward_reference_skips_items_in_const_and_static_initializers(tmp
|
||||
assert targets == {"Hidden": "", "Guarded": ""}
|
||||
|
||||
|
||||
def test_rust_forward_reference_to_a_casefold_colliding_type_keeps_its_stub(tmp_path):
|
||||
"""#3782: ids are casefolded, so `fn handle` (also in an `extern` block),
|
||||
`const HANDLE`, `struct HANDLE` or an `impl` for `HANDLE` share the id of
|
||||
`struct Handle` and walk() keeps whichever comes first, and a `type HANDLE`
|
||||
alias shares it without being a node. A reference above the declarations must
|
||||
keep the sourceless stub v8's extractor gives it, instead of binding to another
|
||||
item."""
|
||||
extern_fn = 'extern "C" {\n fn handle();\n}'
|
||||
impl_for_alias = "type HANDLE = usize;\n\ntrait Tr {}\n\nimpl Tr for HANDLE {}"
|
||||
for name, user, other, label in (
|
||||
("fn_collision.rs", "fn user(h: &Handle) {}", "fn handle() {}", "Handle"),
|
||||
("extern_fn_collision.rs", "fn user(h: &Handle) {}", extern_fn, "Handle"),
|
||||
("const_collision.rs", "fn user(h: &Handle) {}", "const HANDLE: u32 = 0;", "Handle"),
|
||||
("static_collision.rs", "fn user(h: &Handle) {}", "static HANDLE: u32 = 0;", "Handle"),
|
||||
("type_collision.rs", "fn user(h: &Handle) {}", "struct HANDLE;", "Handle"),
|
||||
("impl_collision.rs", "fn user(h: &Handle) {}", impl_for_alias, "Handle"),
|
||||
("alias_collision.rs", "fn user(h: HANDLE) {}", "type HANDLE = usize;", "HANDLE"),
|
||||
):
|
||||
p = tmp_path / name
|
||||
p.write_text(
|
||||
f"{user}\n\n{other}\n\nstruct Handle {{\n n: u32,\n}}\n", encoding="utf-8"
|
||||
)
|
||||
r = extract_rust(p)
|
||||
by_id = {n["id"]: n for n in r["nodes"]}
|
||||
targets = [
|
||||
(by_id[e["target"]]["label"], by_id[e["target"]]["source_file"])
|
||||
for e in r["edges"]
|
||||
if e["relation"] == "references" and e["target"] in by_id
|
||||
]
|
||||
assert targets == [(label, "")], name
|
||||
|
||||
|
||||
def test_rust_forward_reference_resolves_when_no_other_item_shares_the_id(tmp_path):
|
||||
"""#3782: the collision rule only withholds a type whose id another item
|
||||
shares. A function with another name, `#[cfg]` twins of the type itself (one
|
||||
node in walk()), an `impl` for the type itself or a method named like the type
|
||||
(its id is qualified by the impl) still let the reference above resolve to it."""
|
||||
impl_for_type = (
|
||||
"struct Handle<'a> {\n s: &'a str,\n}\n\ntrait Tr {}\n\n"
|
||||
"impl Tr for Handle<'_> {}\n\nimpl Tr for &Handle<'_> {}\n"
|
||||
)
|
||||
for name, source in (
|
||||
("no_collision.rs", "struct Handle {\n n: u32,\n}\n\nfn other() {}\n"),
|
||||
("cfg_twins.rs", "#[cfg(unix)]\nstruct Handle;\n\n#[cfg(not(unix))]\nstruct Handle;\n"),
|
||||
("impl_for_type.rs", impl_for_type),
|
||||
("method_like_type.rs", "struct Handle;\n\nimpl Handle {\n fn handle(&self) {}\n}\n"),
|
||||
):
|
||||
p = tmp_path / name
|
||||
p.write_text("fn user(h: &Handle) {}\n\n" + source, encoding="utf-8")
|
||||
r = extract_rust(p)
|
||||
handle = next(n for n in r["nodes"] if n["label"] == "Handle")
|
||||
user = next(n for n in r["nodes"] if n["label"] == "user()")
|
||||
assert handle["source_file"] == str(p), name
|
||||
assert any(
|
||||
e["relation"] == "references"
|
||||
and e["source"] == user["id"]
|
||||
and e["target"] == handle["id"]
|
||||
for e in r["edges"]
|
||||
), name
|
||||
|
||||
|
||||
def test_rust_forward_reference_to_ambiguous_type_name_stays_local(tmp_path):
|
||||
"""#3782: when two files declare the same struct, the corpus-level rewire
|
||||
declines the ambiguous stub, so the reference above the declaration must
|
||||
|
||||
Reference in new issue
Block a user