use std::collections::BTreeSet;
use syn::Expr;
use syn::ExprCall;
use syn::Item;
use syn::ItemImpl;
use syn::Path;
use syn::TypePath;
use syn::UseTree;
use syn::parse_file;
use syn::spanned::Spanned;
use syn::visit;
use syn::visit::Visit;
use syn::visit::visit_expr_call;
use crate::finding::model::qualified_call::QualifiedCall;
use crate::source_file::SourceFile;
pub struct QualifiedCallFinder {
imported: BTreeSet<String>,
found: Vec<QualifiedCall>,
}
impl QualifiedCallFinder {
pub fn find(file: &SourceFile) -> Option<Vec<QualifiedCall>> {
let syntax = parse_file(&file.contents()).ok()?;
let mut finder = Self {
imported: Self::imports(&syntax.items).into_iter().collect(),
found: Vec::new(),
};
finder.visit_file(&syntax);
Some(finder.found)
}
fn imports(items: &[Item]) -> Vec<String> {
items
.iter()
.flat_map(|item| match item {
Item::Use(entry) => Self::names(&entry.tree),
Item::Mod(module) => module
.content
.as_ref()
.map(|(_, inner)| Self::imports(inner))
.unwrap_or_default(),
_ => Vec::new(),
})
.collect()
}
fn names(tree: &UseTree) -> Vec<String> {
Self::names_under(tree, None)
}
fn names_under(tree: &UseTree, parent: Option<&str>) -> Vec<String> {
match tree {
UseTree::Name(name) => {
let ident = name.ident.to_string();
if ident == "self" {
parent.map(ToString::to_string).into_iter().collect()
} else {
vec![ident]
}
}
UseTree::Rename(rename) => vec![rename.rename.to_string()],
UseTree::Path(path) => Self::names_under(&path.tree, Some(&path.ident.to_string())),
UseTree::Group(group) => group
.items
.iter()
.flat_map(|item| Self::names_under(item, parent))
.collect(),
UseTree::Glob(_) => Vec::new(),
}
}
fn offending(&self, node: &ExprCall) -> Option<QualifiedCall> {
let Expr::Path(entry) = node.func.as_ref() else {
return None;
};
if entry.qself.is_some() {
return None;
}
self.offending_path(&entry.path, node.func.span().start().line)
}
fn offending_path(&self, path: &Path, line: usize) -> Option<QualifiedCall> {
let segments: Vec<String> = path
.segments
.iter()
.map(|segment| segment.ident.to_string())
.collect();
let first = segments.first()?;
if segments.len() < 2 || Self::is_type(first) {
return None;
}
if segments.len() == 2 && self.imported.contains(first) {
return None;
}
Some(QualifiedCall::new(&segments.join("::"), line))
}
fn is_type(segment: &str) -> bool {
segment.chars().next().is_some_and(char::is_uppercase) || Self::is_primitive(segment)
}
fn is_primitive(segment: &str) -> bool {
matches!(
segment,
"u8" | "u16"
| "u32"
| "u64"
| "u128"
| "usize"
| "i8"
| "i16"
| "i32"
| "i64"
| "i128"
| "isize"
| "f32"
| "f64"
| "bool"
| "char"
| "str"
)
}
}
impl<'ast> Visit<'ast> for QualifiedCallFinder {
fn visit_type_path(&mut self, node: &'ast TypePath) {
if node.qself.is_none()
&& let Some(found) = self.offending_path(&node.path, node.path.span().start().line)
{
self.found.push(found);
}
visit::visit_type_path(self, node);
}
fn visit_item_impl(&mut self, node: &'ast ItemImpl) {
if let Some((_, path, _)) = node.trait_.as_ref()
&& let Some(found) = self.offending_path(path, path.span().start().line)
{
self.found.push(found);
}
visit::visit_item_impl(self, node);
}
fn visit_expr_call(&mut self, node: &'ast ExprCall) {
if let Some(call) = self.offending(node) {
self.found.push(call);
}
visit_expr_call(self, node);
}
}