cargo-crap4rust 0.6.2

Cargo subcommand for computing CRAP scores across Rust crates
Documentation
// Copyright 2025 Umberto Gotti <umberto.gotti@umbertogotti.dev>
// Licensed under the MIT License or Apache License, Version 2.0
// SPDX-License-Identifier: MIT OR Apache-2.0

use proc_macro2::Span;
use syn::spanned::Spanned;
use syn::{Attribute, ImplItem, ItemImpl, Path as SynPath, Type};

use crate::complexity::cognitive_complexity;
use crate::model::{PackageContext, SourceFunction};

pub(crate) struct ImplCollector<'a> {
    package: &'a PackageContext,
    path_key: &'a str,
    relative_file: &'a str,
    module_prefix: &'a [String],
    inline_modules: &'a [String],
}

impl<'a> ImplCollector<'a> {
    pub(crate) fn new(
        package: &'a PackageContext,
        path_key: &'a str,
        relative_file: &'a str,
        module_prefix: &'a [String],
        inline_modules: &'a [String],
    ) -> Self {
        Self {
            package,
            path_key,
            relative_file,
            module_prefix,
            inline_modules,
        }
    }

    pub(crate) fn collect(&self, item_impl: &ItemImpl) -> Vec<SourceFunction> {
        let receiver = impl_type_name(&item_impl.self_ty);
        let mut functions = Vec::new();
        for item in &item_impl.items {
            if let Some(function) = self.process_item(item, &receiver) {
                functions.push(function);
            }
        }
        functions
    }

    pub(crate) fn process_item(&self, item: &ImplItem, receiver: &str) -> Option<SourceFunction> {
        if let ImplItem::Fn(method) = item {
            if is_test_attrs(&method.attrs) {
                return None;
            }
            let name = qualified_name(
                self.module_prefix,
                self.inline_modules,
                Some(receiver),
                &method.sig.ident.to_string(),
            );
            Some(SourceFunction {
                package_name: self.package.name.clone(),
                name,
                path_key: self.path_key.to_string(),
                relative_file: self.relative_file.to_string(),
                line: start_line(method.sig.ident.span()),
                end_line: end_line(method.span()),
                complexity: cognitive_complexity(&method.block),
            })
        } else {
            None
        }
    }
}

pub(crate) fn visit_impl(
    package: &PackageContext,
    item_impl: &ItemImpl,
    path_key: &str,
    relative_file: &str,
    module_prefix: &[String],
    inline_modules: &[String],
    functions: &mut Vec<SourceFunction>,
) {
    let collector = ImplCollector::new(
        package,
        path_key,
        relative_file,
        module_prefix,
        inline_modules,
    );
    let result = collector.collect(item_impl);
    functions.extend(result);
}

fn impl_type_name(ty: &Type) -> String {
    match ty {
        Type::Path(path) => path
            .path
            .segments
            .last()
            .map(|segment| segment.ident.to_string())
            .unwrap_or_else(|| "impl".to_string()),
        Type::Reference(reference) => impl_type_name(&reference.elem),
        _ => "impl".to_string(),
    }
}

pub(crate) fn qualified_name(
    module_prefix: &[String],
    inline_modules: &[String],
    receiver: Option<&str>,
    function_name: &str,
) -> String {
    let mut parts = Vec::new();
    parts.extend(module_prefix.iter().cloned());
    parts.extend(inline_modules.iter().cloned());
    if let Some(receiver) = receiver {
        parts.push(receiver.to_string());
    }
    parts.push(function_name.to_string());
    parts.join("::")
}

pub fn is_test_attrs(attrs: &[syn::Attribute]) -> bool {
    attrs.iter().any(is_test_attr)
}

fn is_test_attr(attr: &Attribute) -> bool {
    if is_test_path(attr.path()) {
        return true;
    }

    let mut found = false;
    let _ = attr.parse_nested_meta(|meta| {
        if is_test_path(&meta.path) {
            found = true;
        }

        let _ = meta.parse_nested_meta(|nested| {
            if is_test_path(&nested.path) {
                found = true;
            }
            Ok(())
        });

        Ok(())
    });

    found
}

fn is_test_path(path: &SynPath) -> bool {
    path.is_ident("test")
        || path
            .segments
            .last()
            .is_some_and(|segment| segment.ident == "test")
}

pub(crate) fn start_line(span: Span) -> usize {
    span.start().line
}

pub(crate) fn end_line(span: Span) -> usize {
    span.end().line
}