mirror of
https://github.com/safishamsi/graphify.git
synced 2026-10-22 03:41:30 +00:00
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)
174 lines
6.6 KiB
Python
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
|