cargo-crappy 0.1.0

CRAP metric analysis for Rust — clippy-style diagnostics for change-risk, complexity, coverage, and idiomatic code
use crate::complexity::{AttrExt, format_path, format_type};

pub trait FunctionVisitor {
    fn context_mut(&mut self) -> &mut Vec<String>;
    fn on_function(&mut self, name: &str, sig: &syn::Signature, block: &syn::Block, is_free: bool);

    fn handle_item_fn(&mut self, node: &syn::ItemFn) -> bool {
        if node.attrs.should_skip() {
            return false;
        }
        let name = node.sig.ident.to_string();
        self.on_function(&name, &node.sig, &node.block, true);
        true
    }

    fn handle_impl_item_fn(&mut self, node: &syn::ImplItemFn) -> bool {
        if node.attrs.should_skip() {
            return false;
        }
        let name = node.sig.ident.to_string();
        self.on_function(&name, &node.sig, &node.block, false);
        true
    }

    fn handle_item_impl_enter(&mut self, node: &syn::ItemImpl) {
        let ctx = if let Some((_, path, _)) = &node.trait_ {
            format_path(path)
        } else {
            format_type(&node.self_ty)
        };
        self.context_mut().push(ctx);
    }

    fn handle_item_impl_exit(&mut self) {
        self.context_mut().pop();
    }

    fn handle_item_trait_enter(&mut self, node: &syn::ItemTrait) {
        self.context_mut().push(node.ident.to_string());
    }

    fn handle_item_trait_exit(&mut self) {
        self.context_mut().pop();
    }

    fn handle_trait_item_fn(&mut self, node: &syn::TraitItemFn) {
        if let Some(block) = &node.default
            && !node.attrs.should_skip()
        {
            let name = node.sig.ident.to_string();
            self.on_function(&name, &node.sig, block, false);
        }
    }

    fn should_skip_mod(node: &syn::ItemMod) -> bool {
        node.attrs.has_cfg_test()
    }

    fn qualified_name(&self, name: &str) -> String
    where
        Self: Sized,
    {
        let ctx = self.context();
        if ctx.is_empty() {
            name.to_string()
        } else {
            format!("{}::{name}", ctx.last().unwrap())
        }
    }

    fn context(&self) -> &[String];
}

#[cfg(test)]
mod tests {
    use super::*;

    struct TestVisitor {
        context: Vec<String>,
        calls: Vec<(String, bool)>,
    }

    impl TestVisitor {
        fn new() -> Self {
            Self {
                context: Vec::new(),
                calls: Vec::new(),
            }
        }
    }

    impl FunctionVisitor for TestVisitor {
        fn context_mut(&mut self) -> &mut Vec<String> {
            &mut self.context
        }

        fn context(&self) -> &[String] {
            &self.context
        }

        fn on_function(
            &mut self,
            name: &str,
            _sig: &syn::Signature,
            _block: &syn::Block,
            is_free: bool,
        ) {
            self.calls.push((self.qualified_name(name), is_free));
        }
    }

    impl<'ast> syn::visit::Visit<'ast> for TestVisitor {
        fn visit_item_fn(&mut self, node: &'ast syn::ItemFn) {
            if self.handle_item_fn(node) {
                syn::visit::visit_item_fn(self, node);
            }
        }
        fn visit_item_impl(&mut self, node: &'ast syn::ItemImpl) {
            self.handle_item_impl_enter(node);
            syn::visit::visit_item_impl(self, node);
            self.handle_item_impl_exit();
        }
        fn visit_impl_item_fn(&mut self, node: &'ast syn::ImplItemFn) {
            if self.handle_impl_item_fn(node) {
                syn::visit::visit_impl_item_fn(self, node);
            }
        }
        fn visit_item_trait(&mut self, node: &'ast syn::ItemTrait) {
            self.handle_item_trait_enter(node);
            syn::visit::visit_item_trait(self, node);
            self.handle_item_trait_exit();
        }
        fn visit_trait_item_fn(&mut self, node: &'ast syn::TraitItemFn) {
            self.handle_trait_item_fn(node);
            syn::visit::visit_trait_item_fn(self, node);
        }
        fn visit_item_mod(&mut self, node: &'ast syn::ItemMod) {
            if !Self::should_skip_mod(node) {
                syn::visit::visit_item_mod(self, node);
            }
        }
    }

    fn visit(source: &str) -> Vec<(String, bool)> {
        use syn::visit::Visit;
        let syntax = syn::parse_file(source).expect("parse");
        let mut v = TestVisitor::new();
        v.visit_file(&syntax);
        v.calls
    }

    #[test]
    fn free_function() {
        let calls = visit("fn foo() {}");
        assert_eq!(calls, vec![("foo".into(), true)]);
    }

    #[test]
    fn impl_method_qualified() {
        let calls = visit("struct S; impl S { fn bar(&self) {} }");
        assert_eq!(calls, vec![("S::bar".into(), false)]);
    }

    #[test]
    fn trait_impl_qualified() {
        let calls = visit("struct S; trait T { fn m(&self); } impl T for S { fn m(&self) {} }");
        assert_eq!(calls, vec![("T::m".into(), false)]);
    }

    #[test]
    fn trait_default_method() {
        let calls = visit("trait T { fn default_m(&self) {} }");
        assert_eq!(calls, vec![("T::default_m".into(), false)]);
    }

    #[test]
    fn trait_abstract_method_skipped() {
        let calls = visit("trait T { fn abstract_m(&self); } fn f() {}");
        assert_eq!(calls, vec![("f".into(), true)]);
    }

    #[test]
    fn test_function_skipped() {
        let calls = visit("#[test] fn test_something() {} fn real() {}");
        assert_eq!(calls, vec![("real".into(), true)]);
    }

    #[test]
    fn test_impl_method_skipped() {
        let calls = visit("struct S; impl S { #[test] fn test_m(&self) {} fn real(&self) {} }");
        assert_eq!(calls, vec![("S::real".into(), false)]);
    }

    #[test]
    fn cfg_test_module_skipped() {
        let calls = visit("fn visible() {} #[cfg(test)] mod tests { fn hidden() {} }");
        assert_eq!(calls, vec![("visible".into(), true)]);
    }

    #[test]
    fn crappy_allow_skips_free_fn() {
        let calls = visit("#[allow(crappy)] fn skip() {} fn keep() {}");
        assert_eq!(calls, vec![("keep".into(), true)]);
    }

    #[test]
    fn crappy_allow_skips_impl_method() {
        let calls =
            visit("struct S; impl S { #[allow(crappy)] fn skip(&self) {} fn keep(&self) {} }");
        assert_eq!(calls, vec![("S::keep".into(), false)]);
    }

    #[test]
    fn crappy_allow_skips_trait_default() {
        let calls = visit("trait T { #[allow(crappy)] fn skip(&self) {} fn keep(&self) {} }");
        assert_eq!(calls, vec![("T::keep".into(), false)]);
    }

    #[test]
    fn context_cleaned_after_impl() {
        let calls = visit("struct A; impl A { fn m(&self) {} } fn free() {}");
        assert_eq!(calls.len(), 2);
        assert_eq!(calls[0].0, "A::m");
        assert_eq!(calls[1].0, "free");
    }

    #[test]
    fn context_cleaned_after_trait() {
        let calls = visit("trait T { fn m(&self) {} } fn free() {}");
        assert_eq!(calls.len(), 2);
        assert_eq!(calls[0].0, "T::m");
        assert_eq!(calls[1].0, "free");
    }
}