mirror of
https://github.com/safishamsi/graphify.git
synced 2026-10-22 03:41:30 +00:00
fix(rust): resolve a forward reference to a type that keeps its id beside a case-only sibling
#3782 left a reference placed above `struct Handle` on a sourceless stub whenever another file-level item folded to the same id (`fn handle`, `const HANDLE`, an `impl` for `HANDLE`), because walk() gave that id to whichever item came first. Those items now get ids of their own, so the struct owns its id and the reference can resolve to it, as a reference below the declaration already did. Without this, the stub only folds back when the type's name is unique across the corpus. A declared type is now pre-registered unless a non-type item has its exact name (`fn Foo()` beside `struct Foo {}` still share one node), and a `type HANDLE` alias still keeps its stub. The #3782 test that pinned the stub for the case-only shapes now asserts the resolution; the shapes that still share a node keep the stub assertion. local_type_names becomes a frozenset, which is what _rust_collect_type_refs is annotated to take. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
bef78ffdbd
commit
f710c8a16e
3 files changed
+72
-29
No files matched your search
+16
-17
@@ -322,19 +322,17 @@ 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. 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.
|
||||
# here ends up as a node. A type is matched by its exact name, since a `type
|
||||
# HANDLE` alias or an imported `HANDLE` folds to the id of a local `struct
|
||||
# Handle`. Distinct names that fold to one id get distinct ids (see
|
||||
# file_item_nid below), so a declared type is pre-registered unless a
|
||||
# non-type item has its exact name (`struct Foo {}` beside `fn Foo()`, which
|
||||
# still share one node); then 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`
|
||||
# folds to `struct Handle`'s id, `impl Tr for &Handle<'_>` too); 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()
|
||||
declared_type_names: dict[str, set[str]] = {}
|
||||
|
||||
def _scan_type_items(node) -> None:
|
||||
@@ -350,7 +348,6 @@ def extract_rust(path: Path) -> dict:
|
||||
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)
|
||||
declared_type_names.setdefault(nid, set()).add(name)
|
||||
return
|
||||
if t == "impl_item":
|
||||
@@ -370,10 +367,12 @@ def extract_rust(path: Path) -> dict:
|
||||
|
||||
impl_bare_names: dict[int, str] = {}
|
||||
_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
|
||||
}
|
||||
local_type_names = frozenset(
|
||||
name
|
||||
for nid, names in declared_type_names.items()
|
||||
for name in names
|
||||
if all(is_type for is_type, item_name in items_by_id[nid] if item_name == name)
|
||||
)
|
||||
|
||||
# The same scan is the collision census for file-level items (#3302's rule in
|
||||
# Rust's shape). Distinct names that fold to one id (`fn ignore` / `struct
|
||||
|
||||
+35
-12
@@ -587,23 +587,46 @@ 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):
|
||||
def test_rust_forward_reference_to_a_casefold_colliding_type_resolves_to_it(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."""
|
||||
`const HANDLE`, `struct HANDLE` or an `impl` for `HANDLE` fold to the id of
|
||||
`struct Handle`. Those items now get ids of their own (#3302's rule), so the
|
||||
struct owns its id and a reference above the declarations resolves to it, as
|
||||
a reference below already did, instead of to a sourceless stub."""
|
||||
extern_fn = 'extern "C" {\n fn handle();\n}'
|
||||
impl_for_alias = "type HANDLE = usize;\n\ntrait Tr {}\n\nimpl Tr for HANDLE {}"
|
||||
for name, other in (
|
||||
("fn_collision.rs", "fn handle() {}"),
|
||||
("extern_fn_collision.rs", extern_fn),
|
||||
("const_collision.rs", "const HANDLE: u32 = 0;"),
|
||||
("static_collision.rs", "static HANDLE: u32 = 0;"),
|
||||
("type_collision.rs", "struct HANDLE;"),
|
||||
("impl_collision.rs", impl_for_alias),
|
||||
):
|
||||
p = tmp_path / name
|
||||
p.write_text(
|
||||
f"fn user(h: &Handle) {{}}\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"]}
|
||||
handle = next(n for n in r["nodes"] if n["label"] == "Handle")
|
||||
targets = [
|
||||
e["target"] for e in r["edges"]
|
||||
if e["relation"] == "references" and e["target"] in by_id
|
||||
]
|
||||
assert targets == [handle["id"]], name
|
||||
assert handle["source_file"] == str(p), name
|
||||
|
||||
|
||||
def test_rust_forward_reference_keeps_its_stub_when_a_non_type_shares_the_name(tmp_path):
|
||||
"""#3782: a `type HANDLE` alias is not a node, and `fn Handle()` beside
|
||||
`struct Handle {}` has the type's exact name, so the two still share one node
|
||||
that walk() gives to whichever comes first. A reference above the
|
||||
declarations keeps the sourceless stub v8's extractor gives it."""
|
||||
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"),
|
||||
("same_name_fn.rs", "fn user(h: &Handle) {}", "fn Handle() {}", "Handle"),
|
||||
):
|
||||
p = tmp_path / name
|
||||
p.write_text(
|
||||
|
||||
@@ -138,3 +138,24 @@ def test_cfg_twins_still_share_one_node(tmp_path: Path) -> None:
|
||||
assert len(_sourced(result, "device_num()")) == 1
|
||||
assert len(_sourced(result, "Device_Num")) == 1
|
||||
assert _sourced(result, "Device_Num")[0]["id"] == twin_nodes[0]["id"]
|
||||
|
||||
|
||||
def test_forward_reference_to_the_retained_type_lands_on_it(tmp_path: Path) -> None:
|
||||
"""A reference above ``struct Ignore`` in a file that also has ``fn ignore``
|
||||
resolves to the struct even when another file declares an ``Ignore`` too,
|
||||
so the corpus-level rewire cannot fold a sourceless stub back onto it."""
|
||||
(tmp_path / "dir.rs").write_text(
|
||||
"fn before() -> Ignore {\n ignore()\n}\n\n" + _IGNORE_RS, encoding="utf-8"
|
||||
)
|
||||
(tmp_path / "other.rs").write_text("pub struct Ignore;\n", encoding="utf-8")
|
||||
result = extract(sorted(tmp_path.glob("*.rs")), cache_root=tmp_path, root=tmp_path,
|
||||
parallel=False)
|
||||
by_id = {n["id"]: n for n in result["nodes"]}
|
||||
before = next(n["id"] for n in result["nodes"] if n.get("label") == "before()")
|
||||
targets = [
|
||||
by_id[e["target"]] for e in result["edges"]
|
||||
if e.get("relation") == "references" and e.get("source") == before
|
||||
]
|
||||
assert [(n["label"], Path(n["source_file"]).name if n.get("source_file") else "")
|
||||
for n in targets] == [("Ignore", "dir.rs")]
|
||||
|
||||
Reference in new issue
Block a user