Files
graphify/tests/test_rust_case_collisions.py
T
samie a. 5fcdb2d791 test: pin in-file calls to salted Rust functions and Go ids outside a collision group
Two guards for the review questions on #4468: an in-file call to
`ignore()`, `e()` or `E()` binds by the name as written and reaches the
salted node (on v8 the calls to `ignore()` and `E()` bind nowhere, since
those functions had no node), and the other types in a Go file that holds
a case-only pair keep the ids they have without the pair.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 5c49344807)
2026-10-16 17:12:53 +01:00

174 lines
6.6 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")]
def test_calls_to_salted_functions_keep_their_in_file_edges(tmp_path: Path) -> None:
"""In-file calls bind by the name as written, so they reach the salted ids."""
result = _extract(
tmp_path,
_IGNORE_RS
+ "\nfn e(s: &str) -> u32 {\n 1\n}\n"
+ "\nfn E(s: &str) -> u32 {\n 2\n}\n"
+ "\nfn caller() -> u32 {\n let _ = ignore();\n e(\"a\") + E(\"b\")\n}\n",
)
calls = _edges(result, "calls")
assert {("caller()", "ignore()"), ("caller()", "e()"), ("caller()", "E()")} <= calls, calls