unused-pub 0.1.3

A tool to detect unused public items (structs, enums, functions, etc.) in a Rust codebase.
Documentation
use std::collections::HashSet;
use syn::visit::{self, Visit};
use syn::{
    Ident, ItemConst, ItemEnum, ItemFn, ItemImpl, ItemStruct, ItemTrait, ItemType, Path, Type,
    Visibility,
};

#[derive(Debug, Clone)]
pub struct DefInfo {
    pub name: String,
    pub kind: String,
    pub location: String, // file:line
    pub file_path: String,
}

#[derive(Default)]
pub struct DefinitionVisitor {
    pub definitions: Vec<DefInfo>,
    pub current_file: String,
}

impl DefinitionVisitor {
    pub fn new(current_file: String) -> Self {
        Self {
            current_file,
            definitions: Vec::new(),
        }
    }

    fn add_def(&mut self, name: &Ident, kind: &str, vis: &Visibility) {
        if let Visibility::Public(_) = vis {
            self.definitions.push(DefInfo {
                name: name.to_string(),
                kind: kind.to_string(),
                location: format!("{}:{}", self.current_file, name.span().start().line),
                file_path: self.current_file.clone(),
            });
        }
    }
}

impl<'ast> Visit<'ast> for DefinitionVisitor {
    fn visit_item_struct(&mut self, node: &'ast ItemStruct) {
        self.add_def(&node.ident, "struct", &node.vis);
        visit::visit_item_struct(self, node);
    }

    fn visit_item_enum(&mut self, node: &'ast ItemEnum) {
        self.add_def(&node.ident, "enum", &node.vis);
        visit::visit_item_enum(self, node);
    }

    fn visit_item_fn(&mut self, node: &'ast ItemFn) {
        self.add_def(&node.sig.ident, "fn", &node.vis);
        visit::visit_item_fn(self, node);
    }

    fn visit_item_const(&mut self, node: &'ast ItemConst) {
        self.add_def(&node.ident, "const", &node.vis);
        visit::visit_item_const(self, node);
    }

    fn visit_item_trait(&mut self, node: &'ast ItemTrait) {
        self.add_def(&node.ident, "trait", &node.vis);
        visit::visit_item_trait(self, node);
    }

    fn visit_item_type(&mut self, node: &'ast ItemType) {
        self.add_def(&node.ident, "type", &node.vis);
        visit::visit_item_type(self, node);
    }
}

#[derive(Default)]
pub struct UsageVisitor {
    pub usages: HashSet<String>,
}

impl UsageVisitor {
    fn record_ident(&mut self, ident: &Ident) {
        self.usages.insert(ident.to_string());
    }

    fn record_path(&mut self, path: &Path) {
        for segment in &path.segments {
            self.record_ident(&segment.ident);
        }
    }
}

impl<'ast> Visit<'ast> for UsageVisitor {
    // We explicitly do NOT visit the definition identifiers themselves,
    // so that Defining a struct doesn't count as Using it.

    fn visit_item_struct(&mut self, node: &'ast ItemStruct) {
        // Skip node.ident
        for attr in &node.attrs {
            self.visit_attribute(attr);
        }
        for field in &node.fields {
            visit::visit_field(self, field);
        }
        visit::visit_generics(self, &node.generics);
    }

    fn visit_item_enum(&mut self, node: &'ast ItemEnum) {
        // Skip node.ident
        for attr in &node.attrs {
            self.visit_attribute(attr);
        }
        for variant in &node.variants {
            visit::visit_variant(self, variant);
        }
        visit::visit_generics(self, &node.generics);
    }

    fn visit_item_fn(&mut self, node: &'ast ItemFn) {
        // Skip node.sig.ident
        for attr in &node.attrs {
            self.visit_attribute(attr);
        }
        for input in &node.sig.inputs {
            visit::visit_fn_arg(self, input);
        }
        visit::visit_return_type(self, &node.sig.output);
        visit::visit_block(self, &node.block);
        visit::visit_generics(self, &node.sig.generics);
    }

    fn visit_item_const(&mut self, node: &'ast ItemConst) {
        // Skip node.ident
        for attr in &node.attrs {
            self.visit_attribute(attr);
        }
        visit::visit_type(self, &node.ty);
        visit::visit_expr(self, &node.expr);
    }

    fn visit_item_trait(&mut self, node: &'ast ItemTrait) {
        // Skip node.ident
        for attr in &node.attrs {
            self.visit_attribute(attr);
        }
        for item in &node.items {
            visit::visit_trait_item(self, item);
        }
        visit::visit_generics(self, &node.generics);
        for bound in &node.supertraits {
            visit::visit_type_param_bound(self, bound);
        }
    }

    fn visit_item_type(&mut self, node: &'ast ItemType) {
        // Skip node.ident
        for attr in &node.attrs {
            self.visit_attribute(attr);
        }
        visit::visit_type(self, &node.ty);
        visit::visit_generics(self, &node.generics);
    }

    // Crucially, handle IMPL blocks
    fn visit_item_impl(&mut self, node: &'ast ItemImpl) {
        // We want to skip the Ident of the type being implemented IF it is a simple path.
        // `impl Foo` -> skip `Foo`.
        // `impl Foo<Bar>` -> skip `Foo`, visit `Bar`.
        // `impl Trait for Foo` -> visit `Trait`. Skip `Foo`.

        // 1. Visit Generics
        visit::visit_generics(self, &node.generics);

        // 2. Visit Trait (if present) -> Use standard visit
        if let Some((_, path, _)) = &node.trait_ {
            visit::visit_path(self, path);
        }

        // 3. Visit Self Type (Carefully)
        match &*node.self_ty {
            Type::Path(type_path) => {
                // Visit the segments arguments, but IGNORE the segment ident itself?
                // Actually this is hard. `Foo<T>` -> `Foo` is the ident. `T` is arg.
                // A simple heuristic: Visit the generics/arguments of the path segments,
                // but do NOT record the main identifier of the path.

                if let Some(qself) = &type_path.qself {
                    visit::visit_qself(self, qself);
                }

                for segment in &type_path.path.segments {
                    // Do NOT record segment.ident (this is the key change!)
                    match &segment.arguments {
                        syn::PathArguments::None => {}
                        syn::PathArguments::AngleBracketed(args) => {
                            visit::visit_angle_bracketed_generic_arguments(self, args)
                        }
                        syn::PathArguments::Parenthesized(args) => {
                            visit::visit_parenthesized_generic_arguments(self, args)
                        }
                    }
                }
            }
            _ => {
                // For other complex types, just visit normally.
                // It's rare to `impl` a complex type that is also a named public struct we defined.
                visit::visit_type(self, &node.self_ty);
            }
        }

        // 4. Visit Items
        for item in &node.items {
            visit::visit_impl_item(self, item);
        }
    }

    // General usage collection
    fn visit_path(&mut self, node: &'ast Path) {
        // This catches `Foo::bar`, `let x: Foo`, `use crate::Foo`.
        self.record_path(node);
        visit::visit_path(self, node);
    }

    // Handle Use declarations to capture renames and imports
    fn visit_use_tree(&mut self, node: &'ast syn::UseTree) {
        match node {
            syn::UseTree::Path(p) => {
                self.record_ident(&p.ident);
                visit::visit_use_tree(self, &p.tree);
            }
            syn::UseTree::Name(n) => {
                self.record_ident(&n.ident);
            }
            syn::UseTree::Rename(r) => {
                self.record_ident(&r.ident);
                // We don't record the *rename* alias itself as a usage of an existing thing,
                // but the thing being renamed (r.ident) IS a usage of the original name.
                // Wait: `use Foo as Bar`. `Foo` is in `r.ident`. `Bar` is `r.rename`.
                // We want to record `Foo`.
            }
            syn::UseTree::Glob(_) => {}
            syn::UseTree::Group(g) => {
                for tree in &g.items {
                    visit::visit_use_tree(self, tree);
                }
            }
        }
    }
    // Handle Macro invocations to finding identifiers inside
    fn visit_macro(&mut self, node: &'ast syn::Macro) {
        visit::visit_macro(self, node); // Visit the macro path (e.g. `println`)
        self.visit_token_stream(node.tokens.clone());
    }

    // Handle attributes (scan doc comments)
    fn visit_attribute(&mut self, node: &'ast syn::Attribute) {
        if node.path().is_ident("doc") {
            if let syn::Meta::NameValue(nv) = &node.meta {
                if let syn::Expr::Lit(syn::ExprLit {
                    lit: syn::Lit::Str(s),
                    ..
                }) = &nv.value
                {
                    let text = s.value();
                    self.scan_string_for_idents(&text);
                }
            }
        }
        visit::visit_attribute(self, node);
    }
}

impl UsageVisitor {
    fn scan_string_for_idents(&mut self, s: &str) {
        let mut chars = s.chars().peekable();
        while let Some(c) = chars.next() {
            if c.is_alphabetic() || c == '_' {
                let mut captured = String::new();
                captured.push(c);
                while let Some(&next_c) = chars.peek() {
                    if next_c.is_alphanumeric() || next_c == '_' {
                        captured.push(next_c);
                        chars.next();
                    } else {
                        break;
                    }
                }

                if !captured.is_empty() {
                    self.usages.insert(captured);
                }
            }
        }
    }
    fn visit_token_stream(&mut self, tokens: proc_macro2::TokenStream) {
        use proc_macro2::TokenTree;
        for token in tokens {
            match token {
                TokenTree::Group(group) => {
                    self.visit_token_stream(group.stream());
                }
                TokenTree::Ident(ident) => {
                    self.record_ident(&ident);
                }
                TokenTree::Punct(_) => {}
                TokenTree::Literal(lit) => {
                    // Check for identifiers captured in format strings e.g. "Values: {FOO} {BAR}"
                    let s = lit.to_string();
                    if s.starts_with('"') || s.starts_with("r#") || s.starts_with("r\"") {
                        self.scan_string_for_idents(&s);
                    }
                }
            }
        }
    }
}