cargo-mend 0.21.2

Opinionated visibility auditing for Rust crates and workspaces
use proc_macro2::Span;
use syn::Field;
use syn::Fields;
use syn::ImplItem;
use syn::Item;
use syn::ItemImpl;
use syn::Path;
use syn::TraitItem;
use syn::Visibility;
use syn::visit;
use syn::visit::Visit;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum SurfaceReferenceMatch {
    Missing,
    Found,
}

struct ItemSurfaceReferenceVisitor<'a> {
    item_name: &'a str,
    found:     SurfaceReferenceMatch,
}

pub(super) enum ItemSignatureCarrier<'syntax> {
    Declaration,
    StructOrUnionField {
        field:       &'syntax Field,
        field_index: usize,
    },
}

#[derive(Clone, Copy)]
pub(super) enum OutwardDeclarationKind {
    Const,
    Enum,
    Function,
    Static,
    Struct,
    Trait,
    TypeAlias,
    Union,
}

pub(super) struct OutwardDeclaration<'syntax> {
    pub(super) item:            &'syntax Item,
    pub(super) name:            String,
    pub(super) identifier_span: Span,
    pub(super) kind:            OutwardDeclarationKind,
}

pub(super) enum OutwardDeclarationClassification<'syntax> {
    Outward(OutwardDeclaration<'syntax>),
    NotOutward,
}

impl<'a> ItemSurfaceReferenceVisitor<'a> {
    const fn new(item_name: &'a str) -> Self {
        Self {
            item_name,
            found: SurfaceReferenceMatch::Missing,
        }
    }
}

impl<'ast> Visit<'ast> for ItemSurfaceReferenceVisitor<'_> {
    fn visit_path(&mut self, path: &'ast Path) {
        if self.found == SurfaceReferenceMatch::Found {
            return;
        }
        if path
            .segments
            .last()
            .is_some_and(|segment| segment.ident == self.item_name)
        {
            self.found = SurfaceReferenceMatch::Found;
            return;
        }
        visit::visit_path(self, path);
    }
}

pub(super) fn classify_outward_declaration(item: &Item) -> OutwardDeclarationClassification<'_> {
    let (identifier, kind) = match item {
        Item::Const(item) if has_explicit_visibility(&item.vis) => {
            (&item.ident, OutwardDeclarationKind::Const)
        },
        Item::Enum(item) if has_explicit_visibility(&item.vis) => {
            (&item.ident, OutwardDeclarationKind::Enum)
        },
        Item::Fn(item) if has_explicit_visibility(&item.vis) => {
            (&item.sig.ident, OutwardDeclarationKind::Function)
        },
        Item::Static(item) if has_explicit_visibility(&item.vis) => {
            (&item.ident, OutwardDeclarationKind::Static)
        },
        Item::Struct(item) if has_explicit_visibility(&item.vis) => {
            (&item.ident, OutwardDeclarationKind::Struct)
        },
        Item::Trait(item) if has_explicit_visibility(&item.vis) => {
            (&item.ident, OutwardDeclarationKind::Trait)
        },
        Item::Type(item) if has_explicit_visibility(&item.vis) => {
            (&item.ident, OutwardDeclarationKind::TypeAlias)
        },
        Item::Union(item) if has_explicit_visibility(&item.vis) => {
            (&item.ident, OutwardDeclarationKind::Union)
        },
        _ => return OutwardDeclarationClassification::NotOutward,
    };
    let source_name = identifier.to_string();
    OutwardDeclarationClassification::Outward(OutwardDeclaration {
        item,
        name: source_name
            .strip_prefix("r#")
            .unwrap_or(&source_name)
            .to_string(),
        identifier_span: identifier.span(),
        kind,
    })
}

pub(super) fn potentially_outward_item_surface_carriers_mentioning_name<'syntax>(
    item: &'syntax Item,
    item_name: &str,
) -> Vec<ItemSignatureCarrier<'syntax>> {
    let mut carriers = Vec::new();
    let mut visitor = ItemSurfaceReferenceVisitor::new(item_name);
    match item {
        Item::Const(item) if has_explicit_visibility(&item.vis) => {
            visitor.visit_type(&item.ty);
        },
        Item::Enum(item) if has_explicit_visibility(&item.vis) => {
            for variant in &item.variants {
                visit_field_types(&mut visitor, &variant.fields);
            }
        },
        Item::Fn(item) if has_explicit_visibility(&item.vis) => {
            visitor.visit_signature(&item.sig);
        },
        Item::Static(item) if has_explicit_visibility(&item.vis) => {
            visitor.visit_type(&item.ty);
        },
        Item::Struct(item) if has_explicit_visibility(&item.vis) => {
            match &item.fields {
                Fields::Named(fields) => carriers.extend(visible_field_carriers(
                    fields.named.iter().enumerate(),
                    item_name,
                )),
                Fields::Unnamed(fields) => carriers.extend(visible_field_carriers(
                    fields.unnamed.iter().enumerate(),
                    item_name,
                )),
                Fields::Unit => {},
            }
            return carriers;
        },
        Item::Trait(item) if has_explicit_visibility(&item.vis) => {
            for trait_item in &item.items {
                match trait_item {
                    TraitItem::Fn(item) => visitor.visit_signature(&item.sig),
                    TraitItem::Type(item) => {
                        if let Some((_, ty)) = &item.default {
                            visitor.visit_type(ty);
                        }
                    },
                    TraitItem::Const(item) => visitor.visit_type(&item.ty),
                    _ => {},
                }
            }
        },
        Item::Type(item) if has_explicit_visibility(&item.vis) => {
            visitor.visit_type(&item.ty);
        },
        Item::Union(item) if has_explicit_visibility(&item.vis) => {
            carriers.extend(visible_field_carriers(
                item.fields.named.iter().enumerate(),
                item_name,
            ));
            return carriers;
        },
        _ => {},
    }
    if visitor.found == SurfaceReferenceMatch::Found {
        carriers.push(ItemSignatureCarrier::Declaration);
    }
    carriers
}

fn visible_field_carriers<'syntax>(
    fields: impl Iterator<Item = (usize, &'syntax Field)>,
    item_name: &str,
) -> Vec<ItemSignatureCarrier<'syntax>> {
    fields
        .filter(|(_, field)| has_explicit_visibility(&field.vis))
        .filter(|(_, field)| field_type_mentions_name(field, item_name))
        .map(|(field_index, field)| ItemSignatureCarrier::StructOrUnionField { field, field_index })
        .collect()
}

fn visit_field_types(visitor: &mut ItemSurfaceReferenceVisitor<'_>, fields: &Fields) {
    match fields {
        Fields::Named(fields) => {
            for field in &fields.named {
                visitor.visit_type(&field.ty);
            }
        },
        Fields::Unnamed(fields) => {
            for field in &fields.unnamed {
                visitor.visit_type(&field.ty);
            }
        },
        Fields::Unit => {},
    }
}

pub(super) fn potentially_outward_impl_surface_items_mentioning_name<'syntax>(
    item_impl: &'syntax ItemImpl,
    item_name: &str,
) -> Vec<&'syntax ImplItem> {
    let outward = item_impl.trait_.is_some();

    item_impl
        .items
        .iter()
        .filter(|impl_item| outward || impl_item_has_explicit_visibility(impl_item))
        .filter(|impl_item| impl_item_surface_mentions_name(impl_item, item_name))
        .collect()
}

const fn has_explicit_visibility(visibility: &Visibility) -> bool {
    !matches!(visibility, Visibility::Inherited)
}

const fn impl_item_has_explicit_visibility(item: &ImplItem) -> bool {
    match item {
        ImplItem::Const(item) => has_explicit_visibility(&item.vis),
        ImplItem::Fn(item) => has_explicit_visibility(&item.vis),
        ImplItem::Type(item) => has_explicit_visibility(&item.vis),
        _ => false,
    }
}

fn impl_item_surface_mentions_name(item: &ImplItem, item_name: &str) -> bool {
    let mut visitor = ItemSurfaceReferenceVisitor::new(item_name);
    match item {
        ImplItem::Fn(item) => {
            visitor.visit_signature(&item.sig);
        },
        ImplItem::Const(item) => {
            visitor.visit_type(&item.ty);
        },
        ImplItem::Type(item) => {
            visitor.visit_type(&item.ty);
        },
        _ => {},
    }
    visitor.found == SurfaceReferenceMatch::Found
}

fn field_type_mentions_name(field: &Field, item_name: &str) -> bool {
    let mut visitor = ItemSurfaceReferenceVisitor::new(item_name);
    visitor.visit_type(&field.ty);
    visitor.found == SurfaceReferenceMatch::Found
}