brick/tests/rust_methods.rs
trvon 04653788fa
fix(rust): keep impls written on a qualified path when leaf names collide
When several files declare the same type leaf name, rust_method keeps
only the impls of the type a call site means, asking each impl's file
which `Foo` it means. An impl in a third file written as
`impl crate:🅰️:Foo` resolved nothing (that file has no `Foo`
declaration or `use` binding), so its methods were dropped and
`x.run()` on an `a::Foo` read "member-not-found". It resolved before #5.

The extractor now records the impl's type path when it is written as
one (`impl_type_path`, generic arguments removed), the method index
keeps it, and the impl filter resolves that path to its declaring file
before falling back to the file's own view of the leaf name. Glob
imports (`use super::*`) are still not expanded; such an impl stays
unresolved rather than guessed.

Test: with `Foo` declared in a.rs and c.rs and `impl crate:🅰️:Foo` in
ext.rs, `x.run()` on `crate:🅰️:Foo` resolves to ext.rs's `run`; it
failed before this change.
2026-09-24 21:07:34 -06:00

240 lines
9.1 KiB
Rust

use brick::model::{Building, EdgeKind, Node, NodeKind};
use brick::scan;
use std::fs;
fn scan_files(files: &[(&str, &str)]) -> Building {
let root = tempfile::tempdir().unwrap();
for (path, content) in files {
let file = root.path().join(path);
fs::create_dir_all(file.parent().unwrap()).unwrap();
fs::write(file, content).unwrap();
}
scan::scan(root.path()).unwrap()
}
fn function<'a>(model: &'a Building, path: &str, name: &str) -> &'a Node {
model
.nodes
.iter()
.find(|n| n.kind == NodeKind::Function && n.path.as_deref() == Some(path) && n.name == name)
.unwrap_or_else(|| panic!("missing {path}::{name}"))
}
/// (target parent name, target name, attribute value) for calls leaving `source`.
fn calls(model: &Building, source: &Node, attribute: &str) -> Vec<(String, String, String)> {
let mut found: Vec<_> = model
.edges
.iter()
.filter(|e| e.kind == EdgeKind::Calls && e.source == source.id)
.map(|e| {
let target = model.nodes.iter().find(|n| n.id == e.target).unwrap();
let parent = target
.parent
.as_ref()
.and_then(|id| model.nodes.iter().find(|n| &n.id == id))
.map(|n| n.name.clone())
.unwrap_or_default();
let value = e
.attributes
.get(attribute)
.and_then(|v| v.as_str())
.unwrap_or("")
.to_owned();
(parent, target.name.clone(), value)
})
.collect();
found.sort();
found
}
#[test]
fn expression_macro_bodies_yield_calls_and_other_macros_stay_opaque() {
let model = scan_files(&[(
"src/lib.rs",
"fn check() -> bool { true }\nfn name() -> u8 { 0 }\nfn hidden() {}\nmacro_rules! custom { ($e:expr) => { $e } }\nfn run() {\n assert!(check(), \"{}\", format!(\"{}\", name()));\n custom!(hidden());\n}\n",
)]);
let run = function(&model, "src/lib.rs", "run");
assert_eq!(
calls(&model, run, "source.extraction"),
[
("lib.rs".into(), "check".into(), "macro-reparse".into()),
("lib.rs".into(), "name".into(), "macro-reparse".into()),
]
);
let hidden = function(&model, "src/lib.rs", "hidden");
assert!(
!model
.edges
.iter()
.any(|e| e.target == hidden.id && e.kind == EdgeKind::Calls)
);
}
#[test]
fn receivers_resolve_through_self_fields_constructors_and_dyn_traits() {
let model = scan_files(&[(
"src/lib.rs",
"use std::sync::Arc;\npub struct Engine;\nimpl Engine { pub fn commit(&self) {} }\npub trait Named { fn label(&self) -> String; }\npub struct Store { inner: Arc<Engine> }\nimpl Store {\n pub fn open() -> Result<Self, ()> { Ok(Self::build()) }\n fn build() -> Self { Store { inner: Arc::new(Engine) } }\n fn flush(&self) {\n self.inner.commit();\n self.log();\n }\n fn log(&self) {}\n}\nfn run(item: &dyn Named) -> Result<(), ()> {\n let store = Store::open()?;\n store.flush();\n item.label();\n Ok(())\n}\n",
)]);
let resolved = |source: &Node| {
calls(&model, source, "source.resolution")
.into_iter()
.filter(|(parent, _, _)| !parent.is_empty())
.collect::<Vec<_>>()
};
assert_eq!(
resolved(function(&model, "src/lib.rs", "flush")),
[
(
"Engine".into(),
"commit".into(),
"declared-receiver-type".into()
),
("Store".into(), "log".into(), "self-receiver".into()),
]
);
assert_eq!(
resolved(function(&model, "src/lib.rs", "open")),
[("Store".into(), "build".into(), "impl-type".into())]
);
assert_eq!(
resolved(function(&model, "src/lib.rs", "run")),
[
(
"Named".into(),
"label".into(),
"declared-receiver-type".into()
),
(
"Store".into(),
"flush".into(),
"declared-receiver-type".into()
),
// Same-file `Type::f` already resolves by qualified lookup, unlabeled.
("Store".into(), "open".into(), "".into()),
]
);
}
#[test]
fn foreign_imports_generics_and_competing_trait_impls_never_guess() {
let model = scan_files(&[
(
"src/lib.rs",
"mod local;\nuse std::fs::File;\npub trait Named { fn label(&self) -> String; }\nstruct A;\nstruct B;\nstruct C;\nimpl Named for A { fn label(&self) -> String { String::new() } }\nimpl Named for C { fn label(&self) -> String { String::new() } }\ntrait Other { fn label(&self) -> String; }\nimpl Other for A { fn label(&self) -> String { String::new() } }\nfn run<T: Named>(generic: &T, a: A) {\n let file = File::open(\"x\").unwrap();\n file.sync_all();\n generic.label();\n a.label();\n}\n",
),
(
"src/local.rs",
"pub struct File;\nimpl File {\n pub fn open(_p: &str) -> Self { File }\n pub fn sync_all(&self) {}\n}\n",
),
]);
let run = function(&model, "src/lib.rs", "run");
let reasons: Vec<_> = calls(&model, run, "source.unresolved-reason")
.into_iter()
.map(|(_, name, reason)| (name, reason))
.collect();
assert_eq!(
reasons,
[
("File::open".into(), "qualifier-unknown".into()),
("label".into(), "ambiguous".into()),
("label".into(), "receiver-type-unknown".into()),
("sync_all".into(), "receiver-type-unknown".into()),
("unwrap".into(), "receiver-type-unknown".into()),
]
);
for name in ["open", "sync_all"] {
let local = function(&model, "src/local.rs", name);
assert!(
!model
.edges
.iter()
.any(|e| e.target == local.id && e.kind == EdgeKind::Calls)
);
}
}
#[test]
fn same_named_types_in_different_modules_resolve_through_use_bindings() {
// Types were keyed by leaf name, so `b::Config` (method from a trait impl) and
// `a::Config` (inherent method) shared one key and the inherent one won: a call on
// `b::Config` was attributed to `a::Config::load`.
let model = scan_files(&[
("src/lib.rs", "pub mod a;\npub mod b;\npub mod user;\n"),
(
"src/a.rs",
"pub struct Config;\nimpl Config {\n pub fn load(&self) -> u32 { 1 }\n}\n",
),
(
"src/b.rs",
"pub struct Config;\npub trait Loader { fn load(&self) -> u32; }\nimpl Loader for Config {\n fn load(&self) -> u32 { 2 }\n}\n",
),
(
"src/user.rs",
"use crate::b::Config;\nuse crate::b::Loader;\npub fn run(c: &Config) -> u32 { c.load() }\n",
),
(
"src/other.rs",
"pub fn guess(c: &Config) -> u32 { c.load() }\n",
),
]);
let targets = |source: &Node| {
model
.edges
.iter()
.filter(|e| e.kind == EdgeKind::Calls && e.source == source.id)
.map(|e| {
let target = model.nodes.iter().find(|n| n.id == e.target).unwrap();
(
target.path.clone().unwrap_or_default(),
e.attributes
.get("source.unresolved-reason")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_owned(),
)
})
.collect::<Vec<_>>()
};
let run = function(&model, "src/user.rs", "run");
assert_eq!(targets(run), [("src/b.rs".to_owned(), String::new())]);
// Without a binding, two same-named types leave the call ambiguous, not guessed.
let guess = function(&model, "src/other.rs", "guess");
assert_eq!(targets(guess), [(String::new(), "ambiguous".to_owned())]);
}
#[test]
fn an_impl_on_a_qualified_path_in_another_file_keeps_its_methods() {
// With several files declaring `Foo`, an impl was kept only if its own file
// resolved `Foo`; `impl crate::a::Foo` in a third file resolved nothing and its
// methods were dropped, so the call read "member-not-found".
let model = scan_files(&[
(
"src/lib.rs",
"pub mod a;\npub mod c;\npub mod ext;\npub mod main_use;\n",
),
("src/a.rs", "pub struct Foo;\n"),
("src/c.rs", "pub struct Foo;\n"),
(
"src/ext.rs",
"impl crate::a::Foo {\n pub fn run(&self) {}\n}\n",
),
(
"src/main_use.rs",
"use crate::a::Foo;\npub fn f(x: Foo) {\n x.run();\n}\n",
),
]);
let caller = function(&model, "src/main_use.rs", "f");
let targets = calls(&model, caller, "source.resolution");
assert!(
targets.iter().any(|(_, name, _)| name == "run"),
"{targets:?}"
);
let run = function(&model, "src/ext.rs", "run");
assert!(
model
.edges
.iter()
.any(|e| e.kind == EdgeKind::Calls && e.source == caller.id && e.target == run.id)
);
}