use std::collections::BTreeSet;
use syn::Expr;
use syn::ExprCall;
use syn::Item;
use syn::UseTree;
use syn::parse_file;
use syn::spanned::Spanned;
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> {
match tree {
UseTree::Name(name) => vec![name.ident.to_string()],
UseTree::Rename(rename) => vec![rename.rename.to_string()],
UseTree::Path(path) => Self::names(&path.tree),
UseTree::Group(group) => group.items.iter().flat_map(Self::names).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;
}
let segments: Vec<String> = entry
.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("::"),
node.func.span().start().line,
))
}
fn is_type(segment: &str) -> bool {
segment.chars().next().is_some_and(char::is_uppercase)
}
}
impl<'ast> Visit<'ast> for QualifiedCallFinder {
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);
}
}