pub(crate) mod classify;
pub mod types;
pub(crate) mod visitor;
pub use classify::classify_function;
use syn::{File, ImplItem, Item, ItemFn, TraitItem};
pub use types::*;
use crate::config::Config;
use crate::scope::ProjectScope;
use classify::extract_type_name;
fn count_non_self_params(sig: &syn::Signature) -> usize {
sig.inputs
.iter()
.filter(|arg| matches!(arg, syn::FnArg::Typed(_)))
.count()
}
fn build_function_analysis(
name: String,
file_path: &str,
line: usize,
classification: Classification,
parent_type: Option<String>,
complexity: Option<ComplexityMetrics>,
own_calls: Vec<String>,
) -> FunctionAnalysis {
let qualified_name = parent_type
.as_ref()
.map(|parent| format!("{parent}::{name}"))
.unwrap_or_else(|| name.clone());
let severity_of = |c: &Classification| compute_severity(c);
let severity = severity_of(&classification);
let effort_score = if let Classification::Violation {
logic_locations,
call_locations,
..
} = &classification
{
let nesting = complexity.as_ref().map_or(0, |c| c.max_nesting);
Some(
logic_locations.len() as f64 * EFFORT_LOGIC_WEIGHT
+ call_locations.len() as f64 * EFFORT_CALL_WEIGHT
+ nesting as f64 * EFFORT_NESTING_WEIGHT,
)
} else {
None
};
FunctionAnalysis {
name,
file: file_path.to_string(),
line,
classification,
parent_type,
suppressed: false,
complexity,
qualified_name,
severity,
cognitive_warning: false,
cyclomatic_warning: false,
nesting_depth_warning: false,
function_length_warning: false,
unsafe_warning: false,
error_handling_warning: false,
complexity_suppressed: false,
own_calls,
parameter_count: 0,
is_trait_impl: false,
is_test: false,
effort_score,
}
}
pub struct Analyzer<'a> {
config: &'a Config,
scope: &'a ProjectScope,
}
impl<'a> Analyzer<'a> {
pub fn new(config: &'a Config, scope: &'a ProjectScope) -> Self {
Self { config, scope }
}
pub fn analyze_file(&self, file: &File, file_path: &str) -> Vec<FunctionAnalysis> {
file.items
.iter()
.flat_map(|item| match item {
Item::Fn(f) => self
.analyze_item_fn(f, file_path, None, false)
.into_iter()
.collect::<Vec<_>>(),
Item::Impl(i) => {
let test = crate::dry::has_cfg_test(&i.attrs);
self.analyze_impl(i, file_path, test)
}
Item::Trait(t) => self.analyze_trait(t, file_path, false),
Item::Mod(m) => self.analyze_mod(m, file_path, false),
_ => vec![],
})
.collect()
}
fn classify_and_build(
&self,
name: String,
file_path: &str,
line: usize,
body: &syn::Block,
parent_type: Option<String>,
) -> FunctionAnalysis {
let (classification, complexity, own_calls) =
classify_function(body, self.config, self.scope, &name);
build_function_analysis(
name,
file_path,
line,
classification,
parent_type,
complexity,
own_calls,
)
}
fn analyze_item_fn(
&self,
item_fn: &ItemFn,
file_path: &str,
parent_type: Option<String>,
in_test: bool,
) -> Option<FunctionAnalysis> {
let name = item_fn.sig.ident.to_string();
(!self.config.is_ignored_function(&name)).then(|| {
let mut fa = self.classify_and_build(
name,
file_path,
item_fn.sig.ident.span().start().line,
&item_fn.block,
parent_type,
);
fa.parameter_count = count_non_self_params(&item_fn.sig);
fa.is_test = in_test || crate::dry::has_test_attr(&item_fn.attrs);
fa
})
}
fn analyze_impl(
&self,
item_impl: &syn::ItemImpl,
file_path: &str,
in_test: bool,
) -> Vec<FunctionAnalysis> {
let type_name = extract_type_name(item_impl);
let trait_impl = item_impl.trait_.is_some();
item_impl
.items
.iter()
.filter_map(|impl_item| {
if let ImplItem::Fn(method) = impl_item {
let name = method.sig.ident.to_string();
if self.config.is_ignored_function(&name) {
return None;
}
let mut fa = self.classify_and_build(
name,
file_path,
method.sig.ident.span().start().line,
&method.block,
type_name.clone(),
);
fa.parameter_count = count_non_self_params(&method.sig);
fa.is_trait_impl = trait_impl;
fa.is_test = in_test;
Some(fa)
} else {
None
}
})
.collect()
}
fn analyze_trait(
&self,
item_trait: &syn::ItemTrait,
file_path: &str,
in_test: bool,
) -> Vec<FunctionAnalysis> {
let trait_name = item_trait.ident.to_string();
item_trait
.items
.iter()
.filter_map(|trait_item| {
if let TraitItem::Fn(method) = trait_item {
let block = method.default.as_ref()?;
let name = method.sig.ident.to_string();
if self.config.is_ignored_function(&name) {
return None;
}
let mut fa = self.classify_and_build(
name,
file_path,
method.sig.ident.span().start().line,
block,
Some(trait_name.clone()),
);
fa.parameter_count = count_non_self_params(&method.sig);
fa.is_trait_impl = true;
fa.is_test = in_test;
Some(fa)
} else {
None
}
})
.collect()
}
fn analyze_mod(
&self,
item_mod: &syn::ItemMod,
file_path: &str,
in_test: bool,
) -> Vec<FunctionAnalysis> {
let mod_is_test = in_test || crate::dry::has_cfg_test(&item_mod.attrs);
item_mod
.content
.as_ref()
.map(|(_, items)| {
items
.iter()
.flat_map(|item| match item {
Item::Fn(f) => self
.analyze_item_fn(f, file_path, None, mod_is_test)
.into_iter()
.collect::<Vec<_>>(),
Item::Impl(i) => {
let test = mod_is_test || crate::dry::has_cfg_test(&i.attrs);
self.analyze_impl(i, file_path, test)
}
Item::Trait(t) => self.analyze_trait(t, file_path, mod_is_test),
Item::Mod(m) => self.analyze_mod(m, file_path, mod_is_test),
_ => vec![],
})
.collect()
})
.unwrap_or_default()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::Config;
use crate::scope::ProjectScope;
fn parse_and_analyze(code: &str) -> Vec<FunctionAnalysis> {
let syntax = syn::parse_file(code).expect("Failed to parse test code");
let scope_files = vec![("test.rs", &syntax)];
let scope = ProjectScope::from_files(&scope_files);
let config = Config::default();
let analyzer = Analyzer::new(&config, &scope);
analyzer.analyze_file(&syntax, "test.rs")
}
fn parse_and_analyze_with_config(code: &str, config: &Config) -> Vec<FunctionAnalysis> {
let syntax = syn::parse_file(code).expect("Failed to parse test code");
let scope_files = vec![("test.rs", &syntax)];
let scope = ProjectScope::from_files(&scope_files);
let analyzer = Analyzer::new(config, &scope);
analyzer.analyze_file(&syntax, "test.rs")
}
#[test]
fn test_pure_integration() {
let code = r#"
fn helper_a() {}
fn helper_b() {}
fn integrator() {
helper_a();
helper_b();
}
"#;
let results = parse_and_analyze(code);
let integrator = results.iter().find(|r| r.name == "integrator").unwrap();
assert_eq!(integrator.classification, Classification::Integration);
}
#[test]
fn test_pure_operation() {
let code = r#"
fn operation(x: i32) -> &'static str {
let _y = x;
if _y > 0 {
"positive"
} else {
"non-positive"
}
}
"#;
let results = parse_and_analyze(code);
let op = results.iter().find(|r| r.name == "operation").unwrap();
assert_eq!(op.classification, Classification::Operation);
}
#[test]
fn test_violation_mixed() {
let code = r#"
fn helper() {}
fn violator(x: i32) {
let _y = x;
if _y > 0 {
helper();
}
}
"#;
let results = parse_and_analyze(code);
let v = results.iter().find(|r| r.name == "violator").unwrap();
assert!(
matches!(v.classification, Classification::Violation { .. }),
"Expected Violation, got {:?}",
v.classification
);
}
#[test]
fn test_violation_locations() {
let code = r#"fn helper() {}
fn violator(x: i32) {
let _y = x;
if _y > 0 {
helper();
}
}
"#;
let results = parse_and_analyze(code);
let v = results.iter().find(|r| r.name == "violator").unwrap();
if let Classification::Violation {
logic_locations,
call_locations,
..
} = &v.classification
{
assert!(
logic_locations
.iter()
.any(|l| l.kind == "if" && l.line == 4),
"Expected 'if' on line 4, got: {:?}",
logic_locations
);
assert!(
call_locations
.iter()
.any(|c| c.name == "helper" && c.line == 5),
"Expected 'helper' call on line 5, got: {:?}",
call_locations
);
} else {
panic!("Expected Violation, got {:?}", v.classification);
}
}
#[test]
fn test_trivial_empty_body() {
let code = r#"
fn f() {}
"#;
let results = parse_and_analyze(code);
let f = results.iter().find(|r| r.name == "f").unwrap();
assert_eq!(f.classification, Classification::Trivial);
}
#[test]
fn test_trivial_single_return() {
let code = r#"
fn f() -> i32 { 42 }
"#;
let results = parse_and_analyze(code);
let f = results.iter().find(|r| r.name == "f").unwrap();
assert_eq!(f.classification, Classification::Trivial);
}
#[test]
fn test_trivial_getter() {
let code = r#"
struct Foo { x: i32 }
impl Foo {
fn get_x(&self) -> i32 { self.x }
}
"#;
let results = parse_and_analyze(code);
let getter = results.iter().find(|r| r.name == "get_x").unwrap();
assert_eq!(getter.classification, Classification::Trivial);
}
#[test]
fn test_single_stmt_with_own_call() {
let code = r#"
fn helper() {}
fn f() { helper() }
"#;
let results = parse_and_analyze(code);
let f = results.iter().find(|r| r.name == "f").unwrap();
assert_eq!(
f.classification,
Classification::Integration,
"Single-statement body with own call should be Integration, got {:?}",
f.classification
);
}
#[test]
fn test_single_stmt_with_logic() {
let code = r#"
fn f(x: i32) -> i32 { if x > 0 { 1 } else { 0 } }
"#;
let results = parse_and_analyze(code);
let f = results.iter().find(|r| r.name == "f").unwrap();
assert_eq!(
f.classification,
Classification::Operation,
"Single-statement body with logic should be Operation, got {:?}",
f.classification
);
}
#[test]
fn test_closure_lenient_ignores_logic() {
let code = r#"
fn f() {
let v = vec![1, 2, 3];
let _: Vec<_> = v.into_iter().collect();
let _ = (|| { if true { 1 } else { 2 } })();
}
"#;
let results = parse_and_analyze(code);
let f = results.iter().find(|r| r.name == "f").unwrap();
assert!(
!matches!(f.classification, Classification::Violation { .. }),
"Logic inside a closure should not cause a violation in lenient mode, got {:?}",
f.classification
);
}
#[test]
fn test_closure_strict_counts_logic() {
let mut config = Config::default();
config.strict_closures = true;
let code = r#"
fn f() {
let v = vec![1, 2, 3];
let _ = (|| { if true { 1 } else { 2 } })();
let _ = v.len();
}
"#;
let results = parse_and_analyze_with_config(code, &config);
let f = results.iter().find(|r| r.name == "f").unwrap();
assert!(
matches!(
f.classification,
Classification::Operation | Classification::Violation { .. }
),
"Expected logic to be counted in strict closure mode, got {:?}",
f.classification
);
}
#[test]
fn test_closure_lenient_ignores_calls() {
let code = r#"
fn helper() {}
fn f() {
let c = || { helper(); };
c();
let _ = 1;
}
"#;
let results = parse_and_analyze(code);
let f = results.iter().find(|r| r.name == "f").unwrap();
assert!(
!matches!(f.classification, Classification::Violation { .. }),
"Own call inside closure should be ignored in lenient mode, got {:?}",
f.classification
);
}
#[test]
fn test_closure_strict_counts_calls() {
let mut config = Config::default();
config.strict_closures = true;
let code = r#"
fn helper() {}
fn f() {
let c = || { helper(); };
c();
let _ = 1;
}
"#;
let results = parse_and_analyze_with_config(code, &config);
let f = results.iter().find(|r| r.name == "f").unwrap();
assert!(
matches!(
f.classification,
Classification::Integration | Classification::Violation { .. }
),
"Own call inside closure should be counted in strict mode, got {:?}",
f.classification
);
}
#[test]
fn test_iterator_lenient_not_own_call() {
let code = r#"
fn f() -> Vec<i32> {
let v = vec![1, 2, 3];
v.iter().map(|x| x + 1).collect()
}
"#;
let results = parse_and_analyze(code);
let f = results.iter().find(|r| r.name == "f").unwrap();
assert!(
!matches!(f.classification, Classification::Violation { .. }),
"Iterator methods should not be own calls in lenient mode, got {:?}",
f.classification
);
}
#[test]
fn test_iterator_strict_counts_as_logic() {
let mut config = Config::default();
config.strict_iterator_chains = true;
let code = r#"
struct Foo;
impl Foo {
fn map(&self) {}
}
fn f() -> Vec<i32> {
let v = vec![1, 2, 3];
let x = v.iter().map(|x| x + 1).collect::<Vec<_>>();
x
}
"#;
let results = parse_and_analyze_with_config(code, &config);
let f = results.iter().find(|r| r.name == "f").unwrap();
assert!(
matches!(
f.classification,
Classification::Integration | Classification::Violation { .. }
),
"Iterator methods should be counted in strict mode when in scope, got {:?}",
f.classification
);
}
#[test]
fn test_method_call_own_type() {
let code = r#"
struct MyStruct;
impl MyStruct {
fn do_work(&self) {}
fn orchestrate(&self) {
self.do_work();
self.do_work();
}
}
"#;
let results = parse_and_analyze(code);
let orch = results.iter().find(|r| r.name == "orchestrate").unwrap();
assert_eq!(orch.classification, Classification::Integration);
}
#[test]
fn test_method_call_external() {
let code = r#"
fn operation_fn() {
let mut v = Vec::new();
if v.is_empty() {
v.push(1);
}
}
"#;
let results = parse_and_analyze(code);
let f = results.iter().find(|r| r.name == "operation_fn").unwrap();
assert_eq!(f.classification, Classification::Operation);
}
#[test]
fn test_function_call_own() {
let code = r#"
fn step_a() {}
fn step_b() {}
fn orchestrate() {
step_a();
step_b();
}
"#;
let results = parse_and_analyze(code);
let orch = results.iter().find(|r| r.name == "orchestrate").unwrap();
assert_eq!(orch.classification, Classification::Integration);
}
#[test]
fn test_path_call_own_type() {
let code = r#"
struct MyType;
impl MyType {
fn create() -> Self { MyType }
}
fn f() {
let _a = MyType::create();
let _b = MyType::create();
}
"#;
let results = parse_and_analyze(code);
let f = results.iter().find(|r| r.name == "f").unwrap();
assert_eq!(f.classification, Classification::Integration);
}
#[test]
fn test_path_call_external_type() {
let code = r#"
fn f() {
let _a = String::new();
let _b = String::new();
}
"#;
let results = parse_and_analyze(code);
let f = results.iter().find(|r| r.name == "f").unwrap();
assert!(
!matches!(f.classification, Classification::Integration),
"String::new() should not be counted as own call, got {:?}",
f.classification
);
}
#[test]
fn test_impl_block_parent_type() {
let code = r#"
struct Foo;
impl Foo {
fn bar(&self) {}
}
"#;
let results = parse_and_analyze(code);
let bar = results.iter().find(|r| r.name == "bar").unwrap();
assert_eq!(bar.parent_type, Some("Foo".to_string()));
}
#[test]
fn test_trait_default_impl() {
let code = r#"
fn step() {}
trait MyTrait {
fn default_method(&self) {
step();
step();
}
}
"#;
let results = parse_and_analyze(code);
let dm = results.iter().find(|r| r.name == "default_method").unwrap();
assert_eq!(dm.classification, Classification::Integration);
assert_eq!(dm.parent_type, Some("MyTrait".to_string()));
}
#[test]
fn test_ignored_function_skipped() {
let mut config = Config::default();
config.ignore_functions.push("test_*".to_string());
let code = r#"
fn test_something() {
if true { }
}
fn real_function() -> i32 { 42 }
"#;
let results = parse_and_analyze_with_config(code, &config);
assert!(
results.iter().all(|r| r.name != "test_something"),
"Ignored function should not appear in results"
);
assert!(
results.iter().any(|r| r.name == "real_function"),
"Non-ignored function should appear in results"
);
}
#[test]
fn test_nested_module() {
let code = r#"
mod inner {
fn nested_fn() -> i32 { 42 }
}
"#;
let results = parse_and_analyze(code);
let nested = results.iter().find(|r| r.name == "nested_fn").unwrap();
assert_eq!(nested.classification, Classification::Trivial);
}
#[test]
fn test_recursion_default_is_violation() {
let code = r#"
fn fib(n: u32) -> u32 {
let _x = n;
if n <= 1 { n } else { fib(n - 1) + fib(n - 2) }
}
"#;
let results = parse_and_analyze(code);
let fib = results.iter().find(|r| r.name == "fib").unwrap();
assert!(
matches!(fib.classification, Classification::Violation { .. }),
"Recursive function should be Violation by default, got {:?}",
fib.classification
);
}
#[test]
fn test_recursion_allowed_becomes_operation() {
let mut config = Config::default();
config.allow_recursion = true;
let code = r#"
fn fib(n: u32) -> u32 {
let _x = n;
if n <= 1 { n } else { fib(n - 1) + fib(n - 2) }
}
"#;
let results = parse_and_analyze_with_config(code, &config);
let fib = results.iter().find(|r| r.name == "fib").unwrap();
assert_eq!(
fib.classification,
Classification::Operation,
"Recursive function with allow_recursion should be Operation, got {:?}",
fib.classification
);
}
#[test]
fn test_question_mark_default_not_logic() {
let code = r#"
fn f() -> Result<(), String> {
let _x = 1;
let _y: Result<i32, String> = Ok(1);
Ok(())
}
"#;
let results = parse_and_analyze(code);
let f = results.iter().find(|r| r.name == "f").unwrap();
assert!(
!matches!(f.classification, Classification::Violation { .. }),
"? operator should not count as logic by default"
);
}
#[test]
fn test_question_mark_strict_counts_as_logic() {
let mut config = Config::default();
config.strict_error_propagation = true;
let code = r#"
fn helper() -> Result<i32, String> { Ok(42) }
fn f() -> Result<(), String> {
let _x = helper()?;
let _ = 1;
Ok(())
}
"#;
let results = parse_and_analyze_with_config(code, &config);
let f = results.iter().find(|r| r.name == "f").unwrap();
assert!(
matches!(f.classification, Classification::Violation { .. }),
"? operator should count as logic with strict_error_propagation, got {:?}",
f.classification
);
}
#[test]
fn test_async_block_lenient_ignores_logic() {
let code = r#"
fn f() {
let _ = async { if true { 1 } else { 2 } };
let _ = 1;
}
"#;
let results = parse_and_analyze(code);
let f = results.iter().find(|r| r.name == "f").unwrap();
assert!(
!matches!(f.classification, Classification::Violation { .. }),
"Logic inside async block should be ignored in lenient mode, got {:?}",
f.classification
);
}
#[test]
fn test_complexity_metrics_present() {
let code = r#"
fn f(x: i32) {
let _y = x;
if x > 0 {
if x > 10 {
let _ = x + 1;
}
}
}
"#;
let results = parse_and_analyze(code);
let f = results.iter().find(|r| r.name == "f").unwrap();
let metrics = f
.complexity
.as_ref()
.expect("Should have complexity metrics");
assert!(metrics.logic_count > 0, "Should have logic count");
assert!(metrics.max_nesting > 0, "Should have nesting depth");
}
#[test]
fn test_complexity_nesting_depth() {
let code = r#"
fn f(x: i32) {
let _y = x;
if x > 0 {
if x > 10 {
while x > 100 {
break;
}
}
}
}
"#;
let results = parse_and_analyze(code);
let f = results.iter().find(|r| r.name == "f").unwrap();
let metrics = f.complexity.as_ref().unwrap();
assert_eq!(
metrics.max_nesting, 3,
"Expected nesting depth 3 (if > if > while)"
);
}
#[test]
fn test_severity_low() {
let code = r#"
fn helper() {}
fn f(x: bool) {
let _y = x;
if x { helper(); }
}
"#;
let results = parse_and_analyze(code);
let f = results.iter().find(|r| r.name == "f").unwrap();
assert_eq!(f.severity, Some(Severity::Low));
}
#[test]
fn test_severity_none_for_non_violation() {
let code = r#"
fn f(x: i32) {
let _y = x;
if x > 0 { }
}
"#;
let results = parse_and_analyze(code);
let f = results.iter().find(|r| r.name == "f").unwrap();
assert_eq!(f.severity, None);
}
#[test]
fn test_suppressed_flag_default_false() {
let code = r#"
fn f() {}
"#;
let results = parse_and_analyze(code);
let f = results.iter().find(|r| r.name == "f").unwrap();
assert!(!f.suppressed);
}
#[test]
fn test_qualified_name_free_fn() {
let code = r#"
fn my_function() {}
"#;
let results = parse_and_analyze(code);
let f = results.iter().find(|r| r.name == "my_function").unwrap();
assert_eq!(f.qualified_name, "my_function");
}
#[test]
fn test_qualified_name_impl_method() {
let code = r#"
struct Foo;
impl Foo {
fn bar(&self) {}
}
"#;
let results = parse_and_analyze(code);
let bar = results.iter().find(|r| r.name == "bar").unwrap();
assert_eq!(bar.qualified_name, "Foo::bar");
}
#[test]
fn test_trivial_self_getter_not_violation() {
let code = r#"
struct Counter { count: usize }
impl Counter {
fn symbol_count(&self) -> usize { self.count }
fn next_symbol(&self) -> usize {
if self.symbol_count() > 0 {
self.symbol_count() + 1
} else {
0
}
}
}
"#;
let results = parse_and_analyze(code);
let next = results.iter().find(|r| r.name == "next_symbol").unwrap();
assert_eq!(
next.classification,
Classification::Operation,
"Trivial getter should not make next_symbol a Violation, got {:?}",
next.classification
);
}
#[test]
fn test_type_new_not_own_call() {
let code = r#"
struct Adx { period: usize }
impl Adx {
fn new(period: usize) -> Self { Adx { period } }
}
fn compute(data: &[f64]) -> f64 {
let indicator = Adx::new(14);
if data.is_empty() { 0.0 } else { data[0] }
}
"#;
let results = parse_and_analyze(code);
let f = results.iter().find(|r| r.name == "compute").unwrap();
assert_eq!(
f.classification,
Classification::Operation,
"Adx::new() should not be own call, got {:?}",
f.classification
);
}
#[test]
fn test_trivial_getter_get_not_violation() {
let code = r#"
struct Browser { results: Vec<String>, selected: usize }
impl Browser {
fn current(&self) -> Option<&String> { self.results.get(self.selected) }
fn process(&self) -> String {
if let Some(item) = self.current() {
item.clone()
} else {
String::new()
}
}
}
"#;
let results = parse_and_analyze(code);
let f = results.iter().find(|r| r.name == "process").unwrap();
assert_eq!(
f.classification,
Classification::Operation,
"Trivial .get() getter should not make process a Violation, got {:?}",
f.classification
);
}
#[test]
fn test_for_loop_delegation_not_violation() {
let code = r#"
fn process(_x: i32) {}
fn f(items: Vec<i32>) {
for x in items {
process(x);
}
}
"#;
let results = parse_and_analyze(code);
let f = results.iter().find(|r| r.name == "f").unwrap();
assert_eq!(
f.classification,
Classification::Integration,
"For-loop delegation should be Integration, got {:?}",
f.classification
);
}
#[test]
fn test_match_dispatch_is_integration() {
let code = r#"
fn call_a() {}
fn call_b() {}
fn dispatch(x: i32) {
match x {
0 => call_a(),
_ => call_b(),
}
}
"#;
let results = parse_and_analyze(code);
let f = results.iter().find(|r| r.name == "dispatch").unwrap();
assert_eq!(
f.classification,
Classification::Integration,
"Match dispatch should be Integration, got {:?}",
f.classification
);
}
#[test]
fn test_match_dispatch_method_is_integration() {
let code = r#"
struct S;
impl S {
fn run_a(&self) {}
fn run_b(&self) {}
fn dispatch(&self, x: i32) {
match x {
0 => self.run_a(),
_ => self.run_b(),
}
}
}
"#;
let results = parse_and_analyze(code);
let f = results.iter().find(|r| r.name == "dispatch").unwrap();
assert_eq!(
f.classification,
Classification::Integration,
"Match method dispatch should be Integration, got {:?}",
f.classification
);
}
#[test]
fn test_match_with_logic_in_arm_is_violation() {
let code = r#"
fn call_a(_x: i32) {}
fn call_b() {}
fn dispatch(x: i32) {
match x {
0 => call_a(x + 1),
_ => call_b(),
}
}
"#;
let results = parse_and_analyze(code);
let f = results.iter().find(|r| r.name == "dispatch").unwrap();
assert!(
matches!(f.classification, Classification::Violation { .. }),
"Match with logic in arm should be Violation, got {:?}",
f.classification
);
}
#[test]
fn test_match_with_guard_is_violation() {
let code = r#"
fn call_a() {}
fn call_b() {}
fn dispatch(x: i32) {
match x {
n if n > 0 => call_a(),
_ => call_b(),
}
}
"#;
let results = parse_and_analyze(code);
let f = results.iter().find(|r| r.name == "dispatch").unwrap();
assert!(
matches!(f.classification, Classification::Violation { .. }),
"Match with guard should be Violation, got {:?}",
f.classification
);
}
#[test]
fn test_match_dispatch_complexity_still_tracked() {
let code = r#"
fn call_a() {}
fn call_b() {}
fn dispatch(x: i32) {
match x {
0 => call_a(),
_ => call_b(),
}
}
"#;
let results = parse_and_analyze(code);
let f = results.iter().find(|r| r.name == "dispatch").unwrap();
assert_eq!(
f.classification,
Classification::Integration,
"Match dispatch should be Integration, got {:?}",
f.classification
);
assert!(
f.complexity.as_ref().unwrap().cognitive_complexity >= 1,
"Complexity should still be tracked for dispatch match"
);
}
#[test]
fn test_fn_with_test_attr_is_test() {
let code = r#"
fn helper() {}
#[test]
fn my_test() {
helper();
if true {}
}
"#;
let results = parse_and_analyze(code);
let test_fn = results.iter().find(|f| f.name == "my_test").unwrap();
assert!(
test_fn.is_test,
"Function with #[test] should have is_test=true"
);
}
#[test]
fn test_fn_inside_cfg_test_mod_is_test() {
let code = r#"
fn production_fn() {}
#[cfg(test)]
mod tests {
fn test_helper() {}
}
"#;
let results = parse_and_analyze(code);
let prod = results.iter().find(|f| f.name == "production_fn").unwrap();
assert!(
!prod.is_test,
"Production function should have is_test=false"
);
let helper = results.iter().find(|f| f.name == "test_helper").unwrap();
assert!(
helper.is_test,
"Function inside #[cfg(test)] mod should have is_test=true"
);
}
#[test]
fn test_regular_fn_not_test() {
let code = r#"
fn regular() { if true {} }
"#;
let results = parse_and_analyze(code);
let f = results.iter().find(|f| f.name == "regular").unwrap();
assert!(!f.is_test, "Regular function should have is_test=false");
}
#[test]
fn test_cfg_test_impl_methods_are_test() {
let code = r#"
pub struct Config { pub name: String }
impl Config {
pub fn new(name: String) -> Self { Self { name } }
}
#[cfg(test)]
impl Config {
fn test_helper(&self) -> bool { true }
pub fn another_helper() -> i32 { if true { 1 } else { 2 } }
}
"#;
let results = parse_and_analyze(code);
let helper = results.iter().find(|f| f.name == "test_helper").unwrap();
assert!(
helper.is_test,
"Method inside #[cfg(test)] impl should have is_test=true"
);
let another = results.iter().find(|f| f.name == "another_helper").unwrap();
assert!(
another.is_test,
"Pub method inside #[cfg(test)] impl should have is_test=true"
);
let new_fn = results.iter().find(|f| f.name == "new").unwrap();
assert!(
!new_fn.is_test,
"Method in regular impl should have is_test=false"
);
}
}