use syn::spanned::Spanned;
use syn::visit::Visit;
use syn::ItemFn;
pub struct ComplexityScorer {
conditionals: u32,
loops: u32,
assertions: u32,
nesting_depth: u32,
max_nesting: u32,
line_count: usize,
}
#[derive(Debug, Clone)]
pub struct TestComplexityScore {
pub total_score: f32,
pub factors: ComplexityFactors,
pub maintainability_index: f32,
}
#[derive(Debug, Clone)]
pub struct ComplexityFactors {
pub conditionals: u32,
pub loops: u32,
pub assertions: u32,
pub nesting_depth: u32,
pub line_count: usize,
}
impl ComplexityScorer {
pub const DEFAULT_THRESHOLD: f32 = 10.0;
pub fn new() -> Self {
Self {
conditionals: 0,
loops: 0,
assertions: 0,
nesting_depth: 0,
max_nesting: 0,
line_count: 0,
}
}
pub fn calculate_complexity(
&mut self,
func: &ItemFn,
assertion_count: usize,
) -> TestComplexityScore {
self.reset();
self.assertions = assertion_count as u32;
self.line_count = self.count_lines(func);
self.visit_block(&func.block);
let total_score = self.compute_total_score();
let maintainability_index = self.compute_maintainability_index(total_score);
TestComplexityScore {
total_score,
factors: ComplexityFactors {
conditionals: self.conditionals,
loops: self.loops,
assertions: self.assertions,
nesting_depth: self.max_nesting,
line_count: self.line_count,
},
maintainability_index,
}
}
fn reset(&mut self) {
self.conditionals = 0;
self.loops = 0;
self.assertions = 0;
self.nesting_depth = 0;
self.max_nesting = 0;
self.line_count = 0;
}
fn compute_total_score(&self) -> f32 {
let mut score = 0.0;
score += self.conditionals as f32 * 2.0;
score += self.loops as f32 * 3.0;
if self.assertions > 5 {
score += (self.assertions - 5) as f32;
}
if self.max_nesting > 2 {
score += (self.max_nesting - 2) as f32 * 2.0;
}
if self.line_count > 30 {
score += ((self.line_count - 30) as f32) / 10.0;
}
score
}
fn compute_maintainability_index(&self, total_score: f32) -> f32 {
100.0 - (total_score * 2.0).min(100.0)
}
fn count_lines(&self, func: &ItemFn) -> usize {
let span = func.span();
let start_line = span.start().line;
let end_line = span.end().line;
if end_line >= start_line {
end_line - start_line + 1
} else {
1
}
}
fn enter_nested(&mut self) {
self.nesting_depth += 1;
if self.nesting_depth > self.max_nesting {
self.max_nesting = self.nesting_depth;
}
}
fn exit_nested(&mut self) {
if self.nesting_depth > 0 {
self.nesting_depth -= 1;
}
}
}
impl Default for ComplexityScorer {
fn default() -> Self {
Self::new()
}
}
impl<'ast> Visit<'ast> for ComplexityScorer {
fn visit_expr_if(&mut self, expr: &'ast syn::ExprIf) {
self.conditionals += 1;
self.enter_nested();
syn::visit::visit_expr_if(self, expr);
self.exit_nested();
}
fn visit_expr_match(&mut self, expr: &'ast syn::ExprMatch) {
self.conditionals += 1;
self.enter_nested();
syn::visit::visit_expr_match(self, expr);
self.exit_nested();
}
fn visit_expr_while(&mut self, expr: &'ast syn::ExprWhile) {
self.loops += 1;
self.enter_nested();
syn::visit::visit_expr_while(self, expr);
self.exit_nested();
}
fn visit_expr_for_loop(&mut self, expr: &'ast syn::ExprForLoop) {
self.loops += 1;
self.enter_nested();
syn::visit::visit_expr_for_loop(self, expr);
self.exit_nested();
}
fn visit_expr_loop(&mut self, expr: &'ast syn::ExprLoop) {
self.loops += 1;
self.enter_nested();
syn::visit::visit_expr_loop(self, expr);
self.exit_nested();
}
}
#[cfg(test)]
mod tests {
use super::*;
use syn::parse_quote;
#[test]
fn test_simple_test_low_complexity() {
let func: ItemFn = parse_quote! {
#[test]
fn test_simple() {
let x = 42;
assert_eq!(x, 42);
}
};
let mut scorer = ComplexityScorer::new();
let score = scorer.calculate_complexity(&func, 1);
assert!(score.total_score < 5.0);
}
#[test]
fn test_conditional_increases_complexity() {
let func: ItemFn = parse_quote! {
#[test]
fn test_conditional() {
if true {
assert!(true);
}
}
};
let mut scorer = ComplexityScorer::new();
let score = scorer.calculate_complexity(&func, 1);
assert_eq!(score.factors.conditionals, 1);
assert!(score.total_score >= 2.0);
}
#[test]
fn test_loop_increases_complexity() {
let func: ItemFn = parse_quote! {
#[test]
fn test_loop() {
for i in 0..10 {
assert!(i < 10);
}
}
};
let mut scorer = ComplexityScorer::new();
let score = scorer.calculate_complexity(&func, 1);
assert_eq!(score.factors.loops, 1);
assert!(score.total_score >= 3.0);
}
#[test]
fn test_excessive_assertions() {
let func: ItemFn = parse_quote! {
#[test]
fn test_many_assertions() {
assert!(true);
assert!(true);
assert!(true);
assert!(true);
assert!(true);
assert!(true);
assert!(true);
}
};
let mut scorer = ComplexityScorer::new();
let score = scorer.calculate_complexity(&func, 7);
assert_eq!(score.factors.assertions, 7);
assert!(score.total_score >= 2.0);
}
#[test]
fn test_nested_complexity() {
let func: ItemFn = parse_quote! {
#[test]
fn test_nested() {
if true {
for i in 0..10 {
if i % 2 == 0 {
assert!(true);
}
}
}
}
};
let mut scorer = ComplexityScorer::new();
let score = scorer.calculate_complexity(&func, 1);
assert!(score.factors.nesting_depth >= 3);
assert!(score.total_score > 5.0);
}
#[test]
fn test_maintainability_index() {
let func: ItemFn = parse_quote! {
#[test]
fn test_simple() {
assert!(true);
}
};
let mut scorer = ComplexityScorer::new();
let score = scorer.calculate_complexity(&func, 1);
assert!(score.maintainability_index > 90.0);
}
#[test]
fn test_complex_test_low_maintainability() {
let func: ItemFn = parse_quote! {
#[test]
fn test_complex() {
for i in 0..10 {
if i % 2 == 0 {
for j in 0..5 {
if j > i {
assert!(true);
}
}
}
}
}
};
let mut scorer = ComplexityScorer::new();
let score = scorer.calculate_complexity(&func, 1);
assert!(score.maintainability_index < 80.0);
}
}