mirror of
https://github.com/safishamsi/graphify.git
synced 2026-10-22 11:51:11 +00:00
#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>
162 lines
6.0 KiB
Python
162 lines
6.0 KiB
Python
"""Rust file-level items whose names fold to one node id (#3302's rule, Rust shape).
|
|
|
|
``make_id`` casefolds, so ``fn ignore`` / ``struct Ignore`` (ripgrep's
|
|
``crates/ignore/src/dir.rs``) or ``fn e`` / ``fn E`` (``crates/regex/src/literal.rs``)
|
|
minted one id and walk() dropped whichever came second.
|
|
"""
|
|
|
|
from pathlib import Path
|
|
|
|
from graphify.extract import extract
|
|
|
|
_IGNORE_RS = (
|
|
"pub struct Ignore {\n"
|
|
" parent: u32,\n"
|
|
"}\n"
|
|
"\n"
|
|
"impl Ignore {\n"
|
|
" pub fn matched(&self) -> bool {\n"
|
|
" true\n"
|
|
" }\n"
|
|
"}\n"
|
|
"\n"
|
|
"pub fn ignore() -> Ignore {\n"
|
|
" build()\n"
|
|
"}\n"
|
|
"\n"
|
|
"fn build() -> Ignore {\n"
|
|
" Ignore { parent: 0 }\n"
|
|
"}\n"
|
|
)
|
|
|
|
|
|
def _extract(tmp_path: Path, source: str, name: str = "dir.rs") -> dict:
|
|
path = tmp_path / name
|
|
path.write_text(source, encoding="utf-8")
|
|
return extract([path], cache_root=tmp_path, root=tmp_path, parallel=False)
|
|
|
|
|
|
def _sourced(result: dict, label: str) -> list[dict]:
|
|
return [n for n in result["nodes"] if n.get("label") == label and n.get("source_file")]
|
|
|
|
|
|
def _edges(result: dict, relation: str) -> set[tuple[str, str]]:
|
|
by_id = {n["id"]: n["label"] for n in result["nodes"]}
|
|
return {
|
|
(by_id.get(e["source"], ""), by_id.get(e["target"], ""))
|
|
for e in result["edges"]
|
|
if e.get("relation") == relation
|
|
}
|
|
|
|
|
|
def test_function_and_struct_differing_by_case_are_both_extracted(tmp_path: Path) -> None:
|
|
result = _extract(tmp_path, _IGNORE_RS)
|
|
struct = _sourced(result, "Ignore")
|
|
func = _sourced(result, "ignore()")
|
|
assert [n["source_location"] for n in struct] == ["L1"], struct
|
|
assert [n["source_location"] for n in func] == ["L11"], func
|
|
assert struct[0]["id"] != func[0]["id"]
|
|
|
|
|
|
def test_struct_keeps_its_id_and_methods_when_a_function_shares_it(tmp_path: Path) -> None:
|
|
"""The declared type keeps the plain id, so it and its methods do not move."""
|
|
solo = _extract(tmp_path, _IGNORE_RS.replace("pub fn ignore() -> Ignore {\n build()\n}\n", ""))
|
|
solo_ids = {n["id"] for n in solo["nodes"] if n.get("label") in ("Ignore", ".matched()")}
|
|
assert len(solo_ids) == 2
|
|
|
|
result = _extract(tmp_path, _IGNORE_RS)
|
|
ids = {n["id"] for n in result["nodes"] if n.get("label") in ("Ignore", ".matched()")}
|
|
assert ids == solo_ids
|
|
assert ("Ignore", ".matched()") in _edges(result, "method")
|
|
|
|
|
|
def test_salted_function_owns_its_own_body(tmp_path: Path) -> None:
|
|
"""The call inside ``ignore()`` is credited to ``ignore()``, not to the struct."""
|
|
result = _extract(tmp_path, _IGNORE_RS)
|
|
calls = _edges(result, "calls")
|
|
assert ("ignore()", "build()") in calls, calls
|
|
assert not any(src == "Ignore" for src, _ in calls), calls
|
|
assert ("ignore()", "Ignore") in _edges(result, "references")
|
|
|
|
|
|
def test_type_keeps_the_plain_id_whatever_the_declaration_order(tmp_path: Path) -> None:
|
|
forward = {(n["label"], n["id"]) for n in _extract(tmp_path, _IGNORE_RS)["nodes"]
|
|
if n.get("source_file")}
|
|
reordered = (
|
|
"pub fn ignore() -> Ignore {\n"
|
|
" build()\n"
|
|
"}\n"
|
|
"\n"
|
|
"fn build() -> Ignore {\n"
|
|
" Ignore { parent: 0 }\n"
|
|
"}\n"
|
|
"\n"
|
|
"impl Ignore {\n"
|
|
" pub fn matched(&self) -> bool {\n"
|
|
" true\n"
|
|
" }\n"
|
|
"}\n"
|
|
"\n"
|
|
"pub struct Ignore {\n"
|
|
" parent: u32,\n"
|
|
"}\n"
|
|
)
|
|
backward = {(n["label"], n["id"]) for n in _extract(tmp_path, reordered)["nodes"]
|
|
if n.get("source_file")}
|
|
assert forward == backward
|
|
|
|
|
|
def test_case_only_functions_are_both_salted(tmp_path: Path) -> None:
|
|
"""With no declared type in the group, neither member keeps the plain id."""
|
|
lower_src = "fn e(s: &str) -> u32 {\n 1\n}\n"
|
|
plain = _sourced(_extract(tmp_path, lower_src, name="literal.rs"), "e()")[0]["id"]
|
|
|
|
result = _extract(
|
|
tmp_path, lower_src + "\nfn E(s: &str) -> u32 {\n 2\n}\n", name="literal.rs"
|
|
)
|
|
lower, upper = _sourced(result, "e()"), _sourced(result, "E()")
|
|
assert len(lower) == 1 and len(upper) == 1
|
|
assert lower[0]["id"] != upper[0]["id"]
|
|
assert plain not in {lower[0]["id"], upper[0]["id"]}
|
|
|
|
|
|
def test_cfg_twins_still_share_one_node(tmp_path: Path) -> None:
|
|
"""``#[cfg]`` variants of one function are one symbol and keep the plain id."""
|
|
twins = (
|
|
"#[cfg(unix)]\n"
|
|
"fn device_num(p: &str) -> u64 {\n 1\n}\n"
|
|
"\n"
|
|
"#[cfg(windows)]\n"
|
|
"fn device_num(p: &str) -> u64 {\n 2\n}\n"
|
|
)
|
|
solo = _extract(tmp_path, twins, name="walk.rs")
|
|
twin_nodes = _sourced(solo, "device_num()")
|
|
assert [n["source_location"] for n in twin_nodes] == ["L2"]
|
|
|
|
# A case-only sibling splits the group, but the twins stay one node.
|
|
result = _extract(tmp_path, twins + "\nstruct Device_Num;\n", name="walk.rs")
|
|
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")]
|
|
|