use std::collections::HashMap;
use std::path::Path;
use quote::ToTokens;
use syn::visit::Visit;
use syn::{Attribute, Item, ItemFn, Visibility};
use crate::contribution_schedule::ContributionSchedule;
use crate::function_info::FunctionInfo;
use crate::hidden_dep_finder::HiddenDepFinder;
use crate::io_call_finder::IoCallFinder;
use crate::item_counts::ItemCounts;
use crate::unsafe_finder::UnsafeFinder;
fn self_ty_name(ty: &syn::Type) -> String {
if let syn::Type::Path(type_path) = ty {
type_path
.path
.segments
.first()
.map(|s| s.ident.to_string())
.unwrap_or_default()
} else {
String::new()
}
}
fn is_trait_object_type(ty: &syn::Type) -> bool {
match ty {
syn::Type::TraitObject(_) => true,
syn::Type::Reference(r) => is_trait_object_type(&r.elem),
syn::Type::Path(p) => p.path.segments.iter().any(|seg| match &seg.arguments {
syn::PathArguments::AngleBracketed(args) => args.args.iter().any(|arg| match arg {
syn::GenericArgument::Type(t) => is_trait_object_type(t),
_ => false,
}),
_ => false,
}),
_ => false,
}
}
const KNOWN_FOREIGN_TRAITS: &[&str] = &[
"Display",
"Debug",
"Clone",
"Default",
"PartialEq",
"Eq",
"PartialOrd",
"Ord",
"Hash",
"Into",
"From",
"TryFrom",
"Drop",
"Deref",
"DerefMut",
"Index",
"IndexMut",
"Add",
"Sub",
"Mul",
"Div",
"Rem",
"Neg",
"Not",
"Fn",
"FnMut",
"FnOnce",
"Send",
"Sync",
"Sized",
"ToString",
"AsRef",
"AsMut",
"Borrow",
"BorrowMut",
"Error",
"Read",
"Write",
"Seek",
"BufRead",
"Iterator",
"IntoIterator",
"Future",
"IntoFuture",
"Serialize",
"Deserialize",
];
#[derive(Debug)]
pub struct Collector {
counts: ItemCounts,
functions: Vec<FunctionInfo>,
current_file: String,
struct_concrete_fields: HashMap<String, Vec<String>>,
contribution_schedule: ContributionSchedule,
}
impl Collector {
fn new(file: String) -> Self {
Self {
counts: ItemCounts::default(),
functions: Vec::new(),
current_file: file,
struct_concrete_fields: HashMap::new(),
contribution_schedule: ContributionSchedule::new(),
}
}
pub fn collect(source: &str, path: &Path) -> (ItemCounts, Vec<FunctionInfo>) {
let file = path.to_string_lossy().replace('\\', "/");
let syntax = match syn::parse_file(source) {
Ok(s) => s,
Err(_) => return (ItemCounts::default(), Vec::new()),
};
let mut collector = Self::new(file);
for item in &syntax.items {
collector.visit_item(item);
}
(collector.counts, collector.functions)
}
}
impl<'ast> Visit<'ast> for Collector {
fn visit_item(&mut self, item: &'ast Item) {
match item {
Item::Fn(item_fn) if !self.has_test_attr(&item_fn.attrs) => self.visit_fn(item_fn),
Item::Struct(item_struct) if !self.has_test_attr(&item_struct.attrs) => {
self.visit_struct(item_struct);
}
Item::Trait(item_trait) if !self.has_test_attr(&item_trait.attrs) => {
self.visit_trait(item_trait);
}
Item::Enum(item_enum) if !self.has_test_attr(&item_enum.attrs) => {
self.visit_enum(item_enum);
}
Item::Mod(item_mod) if !self.has_test_attr(&item_mod.attrs) => self.visit_mod(item_mod),
Item::Impl(item_impl) if !self.has_test_attr(&item_impl.attrs) => {
self.visit_impl(item_impl);
}
_ => {}
}
}
}
impl Collector {
fn visit_fn(&mut self, item_fn: &ItemFn) {
let name = item_fn.sig.ident.to_string();
let is_pub = matches!(
self.classify_visibility(&item_fn.vis),
VisibilityLevel::Pub | VisibilityLevel::PubCrate
);
let is_pure = self.is_probably_pure(item_fn);
let finder = self.count_hidden_deps_in_block(&item_fn.block);
let has_trait_seam = false;
let contr = self
.contribution_schedule
.contribution(is_pure, has_trait_seam, finder.weight);
self.counts.total_functions += 1;
self.counts.total_items += 1;
self.counts.total_contribution += contr;
if contr == 1.0 {
self.counts.clean_functions += 1;
}
if is_pub {
self.counts.public_items += 1;
}
if is_pure {
self.counts.pure_functions += 1;
}
self.functions.push(FunctionInfo {
name,
file: self.current_file.clone(),
is_pure,
is_public: is_pub,
hidden_deps: finder.count,
has_trait_seam,
dep_weight: finder.weight,
hidden_dep_labels: finder.labels,
grip_absolute: contr,
grip_normalized: (contr * 100.0).round() as u32,
});
}
fn visit_struct(&mut self, item_struct: &syn::ItemStruct) {
self.counts.total_items += 1;
if matches!(
self.classify_visibility(&item_struct.vis),
VisibilityLevel::Pub | VisibilityLevel::PubCrate
) {
self.counts.public_items += 1;
}
let name = item_struct.ident.to_string();
let concrete: Vec<String> = item_struct
.fields
.iter()
.filter(|f| !is_trait_object_type(&f.ty))
.map(|f| f.ident.as_ref().map(|i| i.to_string()).unwrap_or_default())
.filter(|n| !n.is_empty())
.collect();
self.struct_concrete_fields.insert(name, concrete);
}
fn visit_trait(&mut self, item_trait: &syn::ItemTrait) {
self.counts.total_items += 1;
if matches!(
self.classify_visibility(&item_trait.vis),
VisibilityLevel::Pub | VisibilityLevel::PubCrate
) {
self.counts.public_items += 1;
}
}
fn visit_enum(&mut self, item_enum: &syn::ItemEnum) {
self.counts.total_items += 1;
if matches!(
self.classify_visibility(&item_enum.vis),
VisibilityLevel::Pub | VisibilityLevel::PubCrate
) {
self.counts.public_items += 1;
}
}
fn visit_mod(&mut self, item_mod: &syn::ItemMod) {
if let Some((_, items)) = &item_mod.content {
for inner in items {
self.visit_item(inner);
}
}
}
fn visit_impl(&mut self, item_impl: &syn::ItemImpl) {
if item_impl
.trait_
.as_ref()
.is_some_and(|(_, p, _)| self.is_foreign_trait(p))
{
return;
}
let is_trait_impl = item_impl.trait_.is_some();
for item in &item_impl.items {
if let syn::ImplItem::Fn(method) = item {
if self.has_test_attr(&method.attrs) {
continue;
}
let is_pure = !self.is_impl_method_impure(method);
let self_ty_name = self::self_ty_name(&item_impl.self_ty);
let concrete_fields = self
.struct_concrete_fields
.get(&self_ty_name)
.cloned()
.unwrap_or_default();
let finder = self.count_hidden_deps_in_impl_method(method, concrete_fields);
let has_trait_seam = is_trait_impl;
let contr =
self.contribution_schedule
.contribution(is_pure, has_trait_seam, finder.weight);
self.counts.total_functions += 1;
self.counts.total_contribution += contr;
if contr == 1.0 {
self.counts.clean_functions += 1;
}
if is_pure {
self.counts.pure_functions += 1;
}
let is_pub = matches!(
self.classify_visibility(&method.vis),
VisibilityLevel::Pub | VisibilityLevel::PubCrate
);
if is_trait_impl {
self.counts.local_trait_methods += 1;
if !is_pure {
self.counts.local_trait_impure += 1;
}
} else {
self.counts.inherent_methods += 1;
if !is_pure {
self.counts.inherent_impure += 1;
}
}
self.functions.push(FunctionInfo {
name: method.sig.ident.to_string(),
file: self.current_file.clone(),
is_pure,
is_public: is_pub,
hidden_deps: finder.count,
has_trait_seam,
dep_weight: finder.weight,
hidden_dep_labels: finder.labels,
grip_absolute: contr,
grip_normalized: (contr * 100.0).round() as u32,
});
}
}
}
fn is_impl_method_impure(&self, method: &syn::ImplItemFn) -> bool {
if self.has_mut_param(&method.sig) {
return true;
}
if self.is_unit_return(&method.sig) {
return true;
}
if method.sig.unsafety.is_some() {
return true;
}
self.has_unsafe_block(&method.block) || self.has_io_call(&method.block)
}
fn is_foreign_trait(&self, path: &syn::Path) -> bool {
if let Some(last) = path.segments.last() {
let name = last.ident.to_string();
if KNOWN_FOREIGN_TRAITS.contains(&name.as_str()) {
return true;
}
}
if path.segments.len() > 1 {
if let Some(first) = path.segments.first() {
let name = first.ident.to_string();
return name == "std" || name == "core" || name == "alloc";
}
}
false
}
fn classify_visibility(&self, vis: &Visibility) -> VisibilityLevel {
match vis {
Visibility::Public(_) => VisibilityLevel::Pub,
Visibility::Restricted(_) => VisibilityLevel::PubCrate,
_ => VisibilityLevel::Private,
}
}
fn has_test_attr(&self, attrs: &[Attribute]) -> bool {
attrs.iter().any(|attr| {
let tokens = attr.to_token_stream().to_string();
let path = attr.path().get_ident().map(|i| i.to_string());
matches!(path.as_deref(), Some("cfg")) && tokens.contains("test")
|| matches!(path.as_deref(), Some("test"))
|| matches!(path.as_deref(), Some("cfg_attr")) && tokens.contains("test")
})
}
fn is_probably_pure(&self, item_fn: &ItemFn) -> bool {
if self.has_mut_param(&item_fn.sig) {
return false;
}
if self.is_unit_return(&item_fn.sig) {
return false;
}
if item_fn.sig.unsafety.is_some() {
return false;
}
!self.has_unsafe_block(&item_fn.block)
}
fn has_mut_param(&self, sig: &syn::Signature) -> bool {
sig.inputs.iter().any(|arg| match arg {
syn::FnArg::Receiver(recv) => recv.mutability.is_some(),
syn::FnArg::Typed(pat_type) => self.has_mut_in_type(&pat_type.ty),
})
}
#[allow(clippy::only_used_in_recursion)]
fn has_mut_in_type(&self, ty: &syn::Type) -> bool {
use syn::Type;
match ty {
Type::Reference(reference) => reference.mutability.is_some(),
Type::Paren(inner) => self.has_mut_in_type(&inner.elem),
_ => false,
}
}
fn is_unit_return(&self, sig: &syn::Signature) -> bool {
match &sig.output {
syn::ReturnType::Default => true,
syn::ReturnType::Type(_, ty) => {
if let syn::Type::Tuple(tuple) = ty.as_ref() {
tuple.elems.is_empty()
} else {
false
}
}
}
}
fn has_unsafe_block(&self, block: &syn::Block) -> bool {
let mut finder = UnsafeFinder::new();
finder.visit_block(block);
finder.found
}
fn has_io_call(&self, block: &syn::Block) -> bool {
let mut finder = IoCallFinder::new();
finder.visit_block(block);
finder.found
}
fn count_hidden_deps_in_block(&self, block: &syn::Block) -> HiddenDepFinder {
let mut finder = HiddenDepFinder::new();
finder.visit_block(block);
finder
}
fn count_hidden_deps_in_impl_method(
&self,
method: &syn::ImplItemFn,
concrete_fields: Vec<String>,
) -> HiddenDepFinder {
let mut finder = HiddenDepFinder::new();
finder.set_concrete_fields(concrete_fields);
finder.visit_block(&method.block);
finder
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum VisibilityLevel {
Private,
PubCrate,
Pub,
}