mirror of
https://github.com/safishamsi/graphify.git
synced 2026-10-14 07:59:40 +00:00
extract_rust bound every in-file call through one bare-name index (last one wins),
so `Matcher::new()` reached `Printer::new()` when the file defined both, `Arc::new(x)`
reached the file's own `fn new`, `self.go()` another type's `go`, `drop(x)` the
`impl Drop` method and `x.total()` a free `fn total`, all EXTRACTED.
`Type::f()` / `Self::f()` / `self.f()` now bind only to that type's method in the file
(type = last path segment without generics, on the call and on the impl; `i32` and
other primitives count as types). With none, `Enum::Variant(x)` still gets its
constructor reference and `self.f()` falls through to the cross-file self pass. A bare
`f()` binds only to a free function or a type, `x.f()` only to a method.
Against rust-analyzer: wrong edges ripgrep 1232 -> 532, bat 138 -> 36, fd 29 -> 9;
correct edges 1852 -> 2236, 602 -> 655, 148 -> 154.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B8gwEpjajiWHVW3c9smuKf
(cherry picked from commit de9b728ac6)
131 lines
5.1 KiB
Python
131 lines
5.1 KiB
Python
"""Rust in-file call binding follows the call's shape.
|
|
|
|
A call was bound to whichever node in the same file carried the callee's bare
|
|
name, so `B::new()` reached `A::new()` when the file defined both, `Arc::new(x)`
|
|
reached the file's own `fn new`, and `drop(x)` reached `impl Drop`'s method.
|
|
`Type::f()`, `Self::f()` and `self.f()` now bind only to a method of that type,
|
|
a bare `f()` only to a free function or a constructor, and `x.f()` only to a
|
|
method.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
from graphify.extract import extract
|
|
from graphify.extractors.rust import extract_rust
|
|
|
|
|
|
def _edges(tmp_path: Path, source: str, relation: str = "calls") -> set[tuple[str, str, str]]:
|
|
path = tmp_path / "lib.rs"
|
|
path.write_text(source, encoding="utf-8")
|
|
result = extract_rust(path)
|
|
nodes = {n["id"]: n for n in result["nodes"]}
|
|
return {
|
|
(nodes[e["source"]]["label"], nodes[e["target"]]["label"], nodes[e["target"]]["source_location"])
|
|
for e in result["edges"]
|
|
if e["relation"] == relation and e["source"] in nodes and e["target"] in nodes
|
|
}
|
|
|
|
|
|
def test_type_qualified_call_binds_to_that_types_method(tmp_path: Path):
|
|
calls = _edges(tmp_path, (
|
|
"struct A;\n"
|
|
"struct B;\n"
|
|
"impl A { fn new() -> A { A } }\n" # L3
|
|
"impl B { fn new() -> B { B } }\n" # L4
|
|
"fn make_a() -> A { A::new() }\n"
|
|
"fn make_b() -> B { crate::B::new() }\n"
|
|
"impl A { fn again() -> A { Self::new() } }\n"
|
|
))
|
|
assert ("make_a()", ".new()", "L3") in calls
|
|
assert ("make_b()", ".new()", "L4") in calls
|
|
assert (".again()", ".new()", "L3") in calls
|
|
assert ("make_a()", ".new()", "L4") not in calls
|
|
assert ("make_b()", ".new()", "L3") not in calls
|
|
|
|
|
|
def test_call_on_a_type_the_file_does_not_implement_gets_no_edge(tmp_path: Path):
|
|
calls = _edges(tmp_path, (
|
|
"use std::sync::Arc;\n"
|
|
"struct Builder;\n"
|
|
"impl Builder { fn new() -> Builder { Builder } fn default() -> Builder { Builder } }\n"
|
|
"fn shared() -> Arc<u8> { Arc::new(1) }\n"
|
|
"fn options() -> Vec<u8> { Vec::default() }\n"
|
|
))
|
|
assert not {c for c in calls if c[0] in ("shared()", "options()")}, calls
|
|
|
|
|
|
def test_self_call_binds_to_the_callers_own_type(tmp_path: Path):
|
|
calls = _edges(tmp_path, (
|
|
"struct A;\n"
|
|
"struct B;\n"
|
|
"impl A {\n"
|
|
" fn run(&self) { self.go(); self.stop(); }\n"
|
|
" fn go(&self) {}\n" # L5
|
|
"}\n"
|
|
"impl B {\n"
|
|
" fn go(&self) {}\n" # L8
|
|
" fn stop(&self) {}\n" # L9
|
|
"}\n"
|
|
))
|
|
assert (".run()", ".go()", "L5") in calls
|
|
assert (".run()", ".go()", "L8") not in calls
|
|
# A has no `stop`: B's is never the target (the cross-file `self` pass decides).
|
|
assert (".run()", ".stop()", "L9") not in calls
|
|
|
|
|
|
def test_bare_call_never_binds_to_a_method_and_member_call_never_to_a_free_fn(tmp_path: Path):
|
|
calls = _edges(tmp_path, (
|
|
"struct Child;\n"
|
|
"impl Drop for Child { fn drop(&mut self) {} }\n"
|
|
"fn total() -> usize { 0 }\n"
|
|
"fn close(c: Child) { drop(c); }\n"
|
|
"fn count(v: &Stats) -> usize { v.total() }\n"
|
|
))
|
|
assert not {c for c in calls if c[0] in ("close()", "count()")}, calls
|
|
|
|
|
|
def test_primitive_and_path_qualified_impl_types_bind(tmp_path: Path):
|
|
calls = _edges(tmp_path, (
|
|
"use std::io;\n"
|
|
"struct Code;\n"
|
|
"struct Failure;\n"
|
|
"impl From<Code> for i32 { fn from(c: Code) -> i32 { 0 } }\n" # L4
|
|
"impl From<Failure> for io::Error { fn from(f: Failure) -> io::Error { todo!() } }\n" # L5
|
|
"fn status(c: Code) -> i32 { i32::from(c) }\n"
|
|
"fn fail(f: Failure) -> io::Error { io::Error::from(f) }\n"
|
|
))
|
|
assert ("status()", ".from()", "L4") in calls
|
|
assert ("fail()", ".from()", "L5") in calls
|
|
|
|
|
|
def test_enum_variant_constructor_is_still_a_reference(tmp_path: Path):
|
|
refs = _edges(tmp_path, (
|
|
"enum Shape { Circle(u32), Square(u32) }\n"
|
|
"fn mk() -> Shape { Shape::Circle(1) }\n"
|
|
), relation="references")
|
|
assert ("mk()", "Circle", "L1") in refs
|
|
|
|
|
|
def test_self_call_to_another_files_impl_still_resolves(tmp_path: Path):
|
|
# The caller's file defines a same-named method on another type; the call
|
|
# must reach its own type's method in the other file, not the local one.
|
|
for name, body in {
|
|
"a.rs": "pub struct A;\nimpl A { pub fn launch(&self) {} }\n",
|
|
"b.rs": (
|
|
"use crate::a::A;\n"
|
|
"pub struct B;\n"
|
|
"impl B { pub fn launch(&self) {} }\n"
|
|
"impl A { pub fn run(&self) { self.launch(); } }\n"
|
|
),
|
|
}.items():
|
|
(tmp_path / name).write_text(body, encoding="utf-8")
|
|
result = extract([tmp_path / "a.rs", tmp_path / "b.rs"], cache_root=tmp_path / "graphify-out")
|
|
nodes = {n["id"]: n for n in result["nodes"]}
|
|
targets = {
|
|
Path(str(nodes[e["target"]]["source_file"])).name
|
|
for e in result["edges"]
|
|
if e["relation"] == "calls" and nodes.get(e["source"], {}).get("label") == ".run()"
|
|
}
|
|
assert targets == {"a.rs"}, targets
|