fix/audit-findings #5

Merged
trevon merged 14 commits from fix/audit-findings into main 2026-09-25 02:39:30 +00:00
2 changed files with 131 additions and 12 deletions
Showing only changes of commit b281a52255 - Show all commits

fix(rust): resolve methods on the type the caller means, not its leaf name

The Rust impl index keyed types by leaf name, so `a::Config` and
`b::Config` shared one `(Config, load)` entry and the inherent method
won: `use crate:🅱️:Config; c.load()` produced a `declared-receiver-type`
edge to `a::Config::load`.

The index now records which files declare each type name and the file
of every impl. When several files declare a name, a call resolves only
among impls of the type the calling file means (its own declaration,
then a `use` binding, then the only declaration); with no such binding
the call is left external as `ambiguous` instead of guessed. Unique
names behave as before.

Test: rust_methods::same_named_types_in_different_modules_resolve_through_use_bindings
fails before this change (edge to src/a.rs).
trvon 2026-09-24 19:51:11 -06:00
Signed by: trevon
GPG key ID: 38686EA797D7A8A7

View file

@ -140,16 +140,21 @@ struct RustCall {
receiver: Option<crate::source::Receiver>,
}
/// A method node id and the trait it implements, if any.
type RustMethod = (String, Option<String>);
/// A method node id, the trait it implements (if any), and the file of its `impl`.
type RustMethod = (String, Option<String>, String);
/// Crate-wide Rust impl/trait facts keyed by leaf type names.
/// Crate-wide Rust impl/trait facts keyed by leaf type names. Leaf names are not
/// identities: when several files declare the same name, `rust_type_file` settles
/// which one a use site means (declaring file, then `use` binding) or reports it
/// as ambiguous.
#[derive(Default)]
struct RustIndex {
/// (type or trait, method) -> candidate methods.
methods: HashMap<(String, String), Vec<RustMethod>>,
/// Struct, enum, and trait names declared in the repository.
types: HashSet<String>,
/// Leaf type/trait name -> files declaring it.
type_files: HashMap<String, Vec<String>>,
/// (struct, field) -> declared type.
fields: HashMap<(String, String), crate::source::CppType>,
/// Method node id -> declared return type.
@ -657,6 +662,14 @@ impl ScanState {
let Some(id) = id else { continue };
if declaration.kind == NodeKind::Class {
self.rust.types.insert(declaration.name.clone());
let files = self
.rust
.type_files
.entry(declaration.name.clone())
.or_default();
if !files.iter().any(|file| file == path) {
files.push(path.to_owned());
}
continue;
}
let Some(owner) = declaration
@ -676,7 +689,7 @@ impl ScanState {
.methods
.entry((owner, declaration.name.clone()))
.or_default()
.push((id.clone(), implemented));
.push((id.clone(), implemented, path.to_owned()));
if let Some(returns) = &declaration.return_type {
self.rust.returns.insert(id.clone(), returns.clone());
}
@ -1690,7 +1703,7 @@ impl ScanState {
(field.accessed(true), "declared-receiver-type")
}
Some(Receiver::Returned { ty, func, fallible }) => (
self.rust_constructed_type(ty, func, *fallible, call)
self.rust_constructed_type(ty, func, *fallible, call, path)
.ok_or("receiver-type-unknown")?,
"declared-receiver-type",
),
@ -1699,7 +1712,7 @@ impl ScanState {
if self.rust_type_is_foreign(&ty, path) {
return Err("receiver-type-external");
}
self.rust_method(&ty, method).map(|id| (id, label))
self.rust_method(&ty, method, path).map(|id| (id, label))
}
/// A type name the caller's file imports from outside the crate.
@ -1746,30 +1759,86 @@ impl ScanState {
if !self.rust.types.contains(&ty) {
return Err("qualifier-unknown");
}
self.rust_method(&ty, method)
self.rust_method(&ty, method, path)
}
fn rust_method(&self, ty: &str, method: &str) -> Result<String, &'static str> {
fn rust_method(&self, ty: &str, method: &str, path: &str) -> Result<String, &'static str> {
let candidates = self
.rust
.methods
.get(&(ty.to_owned(), method.to_owned()))
.map(Vec::as_slice)
.unwrap_or_default();
let inherent: Vec<_> = candidates.iter().filter(|(_, t)| t.is_none()).collect();
let chosen: Vec<_> = if inherent.is_empty() {
// Several repository types share this leaf name: keep only the impls of the
// one this use site means, or refuse to guess.
let candidates: Vec<&RustMethod> = if self
.rust
.type_files
.get(ty)
.is_some_and(|files| files.len() > 1)
{
let Some(meant) = self.rust_type_file(ty, path) else {
return Err("ambiguous");
};
candidates
.iter()
.filter(|(_, _, impl_path)| {
self.rust_type_file(ty, impl_path).as_deref() == Some(meant.as_str())
})
.collect()
} else {
candidates.iter().collect()
};
let inherent: Vec<_> = candidates
.iter()
.copied()
.filter(|(_, t, _)| t.is_none())
.collect();
let chosen: Vec<_> = if inherent.is_empty() {
candidates
} else {
inherent
};
match chosen.as_slice() {
[(id, _)] => Ok(id.clone()),
[(id, _, _)] => Ok(id.clone()),
[] if self.rust.types.contains(ty) => Err("member-not-found"),
[] => Err("receiver-type-external"),
_ => Err("ambiguous"),
}
}
/// The file declaring the repository type `leaf` as seen from `path`: the file
/// itself, then a `use` binding that names it, then the only declaration. `None`
/// when several files declare `leaf` and nothing in `path` says which.
fn rust_type_file(&self, leaf: &str, path: &str) -> Option<String> {
let files = self.rust.type_files.get(leaf)?;
if files.iter().any(|file| file == path) {
return Some(path.to_owned());
}
for binding in self
.source_bindings
.get(path)
.into_iter()
.flatten()
.filter(|binding| binding.local == leaf)
{
let Some(file) = self
.resolve_import(&binding.module, "rust", path)
.and_then(|id| self.node(&id))
.and_then(|node| node.path.clone())
else {
continue;
};
if files.contains(&file) {
return Some(file);
}
}
match files.as_slice() {
[only] => Some(only.clone()),
_ => None,
}
}
/// `Type::func(..)` yields `Type` when `func` is declared to return `Self`
/// or `Type` (behind `?`, also `Result<..>`/`Option<..>` of either).
fn rust_constructed_type(
@ -1778,13 +1847,14 @@ impl ScanState {
func: &str,
fallible: bool,
call: &RustCall,
path: &str,
) -> Option<String> {
let ty = if ty == "Self" {
call.self_type.clone()?
} else {
ty.to_owned()
};
let id = self.rust_method(&ty, func).ok()?;
let id = self.rust_method(&ty, func, path).ok()?;
let returns = self.rust.returns.get(&id)?;
let returns_type = |value: &str| value == "Self" || value == ty;
let direct = returns_type(returns);

View file

@ -153,3 +153,52 @@ fn foreign_imports_generics_and_competing_trait_impls_never_guess() {
);
}
}
#[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())]);
}