use super::match_patterns::detect_match_expression;
use syn::{Block, Expr, Stmt};
enum WorkItem<'ast> {
Expr(&'ast Expr, bool),
Stmt(&'ast Stmt),
}
struct TraversalState<'ast> {
complexity: u32,
work: Vec<WorkItem<'ast>>,
}
impl<'ast> TraversalState<'ast> {
fn from_block(block: &'ast Block) -> Self {
let mut traversal = Self {
complexity: 1,
work: Vec::new(),
};
traversal.push_block(block);
traversal
}
fn run(mut self) -> u32 {
while let Some(item) = self.work.pop() {
self.process_item(item);
}
self.complexity
}
fn process_item(&mut self, item: WorkItem<'ast>) {
match item {
WorkItem::Expr(expr, in_condition) => self.process_expr(expr, in_condition),
WorkItem::Stmt(stmt) => self.process_stmt(stmt),
}
}
fn process_stmt(&mut self, stmt: &'ast Stmt) {
if let Some(expr) = stmt_expr(stmt) {
self.push_expr(expr, false);
}
}
fn process_expr(&mut self, expr: &'ast Expr, in_condition: bool) {
self.complexity += calculate_expr_complexity(expr, in_condition);
push_expr_children(self, expr, in_condition);
}
fn push_expr(&mut self, expr: &'ast Expr, in_condition: bool) {
self.work.push(WorkItem::Expr(expr, in_condition));
}
fn push_stmt(&mut self, stmt: &'ast Stmt) {
self.work.push(WorkItem::Stmt(stmt));
}
fn push_block(&mut self, block: &'ast Block) {
for stmt in block.stmts.iter().rev() {
self.push_stmt(stmt);
}
}
}
pub fn calculate_cyclomatic(block: &Block) -> u32 {
TraversalState::from_block(block).run()
}
fn stmt_expr(stmt: &Stmt) -> Option<&Expr> {
match stmt {
Stmt::Local(local) => local.init.as_ref().map(|init| init.expr.as_ref()),
Stmt::Expr(expr, _) => Some(expr),
Stmt::Item(_) | Stmt::Macro(_) => None,
}
}
fn push_expr_children<'ast>(
traversal: &mut TraversalState<'ast>,
expr: &'ast Expr,
in_condition: bool,
) {
if push_control_flow_children(traversal, expr) {
return;
}
if push_call_and_access_children(traversal, expr) {
return;
}
if push_operator_children(traversal, expr, in_condition) {
return;
}
let _ = push_literal_children(traversal, expr);
}
fn push_control_flow_children<'ast>(
traversal: &mut TraversalState<'ast>,
expr: &'ast Expr,
) -> bool {
match expr {
Expr::If(expr_if) => {
push_optional_expr(
traversal,
expr_if.else_branch.as_ref().map(|(_, expr)| expr.as_ref()),
);
traversal.push_block(&expr_if.then_branch);
traversal.push_expr(&expr_if.cond, true);
true
}
Expr::While(expr_while) => {
traversal.push_block(&expr_while.body);
traversal.push_expr(&expr_while.cond, true);
true
}
Expr::ForLoop(expr_for) => {
traversal.push_block(&expr_for.body);
traversal.push_expr(&expr_for.expr, false);
true
}
Expr::Loop(expr_loop) => {
traversal.push_block(&expr_loop.body);
true
}
Expr::Match(expr_match) => {
for arm in expr_match.arms.iter().rev() {
traversal.push_expr(&arm.body, false);
push_optional_expr(
traversal,
arm.guard.as_ref().map(|(_, guard)| guard.as_ref()),
);
}
traversal.push_expr(&expr_match.expr, false);
true
}
Expr::Block(expr_block) => {
traversal.push_block(&expr_block.block);
true
}
Expr::Closure(closure) => {
traversal.push_expr(&closure.body, false);
true
}
Expr::Async(async_block) => {
traversal.push_block(&async_block.block);
true
}
Expr::Try(expr_try) => {
traversal.push_expr(&expr_try.expr, false);
true
}
_ => false,
}
}
fn push_call_and_access_children<'ast>(
traversal: &mut TraversalState<'ast>,
expr: &'ast Expr,
) -> bool {
match expr {
Expr::Call(call) => {
for arg in call.args.iter().rev() {
traversal.push_expr(arg, false);
}
traversal.push_expr(&call.func, false);
true
}
Expr::MethodCall(method_call) => {
for arg in method_call.args.iter().rev() {
traversal.push_expr(arg, false);
}
traversal.push_expr(&method_call.receiver, false);
true
}
Expr::Field(field) => {
traversal.push_expr(&field.base, false);
true
}
Expr::Index(index) => {
traversal.push_expr(&index.index, false);
traversal.push_expr(&index.expr, false);
true
}
Expr::Await(await_expr) => {
traversal.push_expr(&await_expr.base, false);
true
}
_ => false,
}
}
fn push_operator_children<'ast>(
traversal: &mut TraversalState<'ast>,
expr: &'ast Expr,
in_condition: bool,
) -> bool {
match expr {
Expr::Binary(binary) => {
traversal.push_expr(&binary.right, in_condition);
traversal.push_expr(&binary.left, in_condition);
true
}
Expr::Unary(unary) => {
traversal.push_expr(&unary.expr, in_condition);
true
}
Expr::Paren(paren) => {
traversal.push_expr(&paren.expr, in_condition);
true
}
Expr::Reference(reference) => {
traversal.push_expr(&reference.expr, false);
true
}
Expr::Cast(cast) => {
traversal.push_expr(&cast.expr, false);
true
}
Expr::Assign(assign) => {
traversal.push_expr(&assign.right, false);
traversal.push_expr(&assign.left, false);
true
}
Expr::Let(let_expr) => {
traversal.push_expr(&let_expr.expr, false);
true
}
_ => false,
}
}
fn push_literal_children<'ast>(traversal: &mut TraversalState<'ast>, expr: &'ast Expr) -> bool {
match expr {
Expr::Return(ret) => {
push_optional_expr(traversal, ret.expr.as_deref());
true
}
Expr::Break(brk) => {
push_optional_expr(traversal, brk.expr.as_deref());
true
}
Expr::Tuple(tuple) => {
for elem in tuple.elems.iter().rev() {
traversal.push_expr(elem, false);
}
true
}
Expr::Array(array) => {
for elem in array.elems.iter().rev() {
traversal.push_expr(elem, false);
}
true
}
Expr::Struct(struct_expr) => {
push_optional_expr(traversal, struct_expr.rest.as_deref());
for field in struct_expr.fields.iter().rev() {
traversal.push_expr(&field.expr, false);
}
true
}
Expr::Repeat(repeat) => {
traversal.push_expr(&repeat.len, false);
traversal.push_expr(&repeat.expr, false);
true
}
Expr::Range(range) => {
push_optional_expr(traversal, range.end.as_deref());
push_optional_expr(traversal, range.start.as_deref());
true
}
Expr::Yield(yield_expr) => {
push_optional_expr(traversal, yield_expr.expr.as_deref());
true
}
_ => false,
}
}
fn push_optional_expr<'ast>(traversal: &mut TraversalState<'ast>, expr: Option<&'ast Expr>) {
if let Some(expr) = expr {
traversal.push_expr(expr, false);
}
}
pub fn calculate_cyclomatic_adjusted(block: &Block) -> u32 {
let base = calculate_cyclomatic(block);
for stmt in &block.stmts {
if let Stmt::Expr(expr, _) = stmt {
if let Some(info) = detect_match_expression(expr) {
let original_match_contribution = info.condition_count.saturating_sub(1) as u32;
let adjusted_match = (info.condition_count as f32).log2().ceil() as u32;
let default_penalty = if !info.has_default { 1 } else { 0 };
return base - original_match_contribution + adjusted_match + default_penalty;
}
}
}
use super::pattern_adjustments::{PatternMatchRecognizer, PatternRecognizer};
let recognizer = PatternMatchRecognizer::new();
if let Some(info) = recognizer.detect(block) {
return recognizer.adjust_complexity(&info, base);
}
base
}
fn calculate_expr_complexity(expr: &Expr, _in_condition: bool) -> u32 {
match expr {
Expr::If(_) => 1,
Expr::While(_) | Expr::ForLoop(_) | Expr::Loop(_) => 1,
Expr::Try(_) => 1,
Expr::Match(expr_match) => expr_match.arms.len().saturating_sub(1) as u32,
Expr::Binary(binary) if is_logical_operator(&binary.op) => 1,
_ => 0,
}
}
fn is_logical_operator(op: &syn::BinOp) -> bool {
matches!(op, syn::BinOp::And(_) | syn::BinOp::Or(_))
}
pub fn calculate_cyclomatic_for_function(complexity: u32, params: usize) -> u32 {
complexity + params.saturating_sub(1) as u32
}
pub fn combine_cyclomatic(branches: Vec<u32>) -> u32 {
branches.iter().sum::<u32>() + 1
}
#[cfg(test)]
mod tests {
use super::*;
use syn::parse_quote;
#[test]
fn test_if_else_counts_as_one_decision_point() {
let block_if_only: Block = parse_quote! {{
if x > 0 {
do_something();
}
}};
assert_eq!(
calculate_cyclomatic(&block_if_only),
2,
"if without else should add 1 to base complexity"
);
let block_if_else: Block = parse_quote! {{
if x > 0 {
do_something();
} else {
do_other();
}
}};
assert_eq!(
calculate_cyclomatic(&block_if_else),
2,
"if-else should add 1 to base complexity, not 2 (else is not a decision point)"
);
}
#[test]
fn test_multiple_if_else_chains() {
let block: Block = parse_quote! {{
if a { x(); } else { y(); }
if b { x(); } else { y(); }
if c { x(); } else { y(); }
}};
assert_eq!(
calculate_cyclomatic(&block),
4,
"3 if-else statements should add 3 to base complexity"
);
}
#[test]
fn test_nested_if_else() {
let block: Block = parse_quote! {{
if a {
if b {
x();
} else {
y();
}
} else {
z();
}
}};
assert_eq!(
calculate_cyclomatic(&block),
3,
"nested if-else should count 2 decisions (outer if + inner if)"
);
}
#[test]
fn test_match_complexity() {
let block: Block = parse_quote! {{
match x {
A => 1,
B => 2,
_ => 3,
}
}};
assert_eq!(
calculate_cyclomatic(&block),
3,
"match with 3 arms should add 2 (arms - 1) to base complexity"
);
}
#[test]
fn test_loop_complexity() {
let block: Block = parse_quote! {{
while condition {
do_work();
}
for i in items {
process(i);
}
loop {
if done { break; }
}
}};
assert_eq!(calculate_cyclomatic(&block), 5);
}
#[test]
fn test_logical_operators_in_conditions_add_complexity() {
let single_condition: Block = parse_quote! {{
if a {
do_something();
}
}};
assert_eq!(
calculate_cyclomatic(&single_condition),
2,
"Single condition should have complexity 2"
);
let three_conditions: Block = parse_quote! {{
if a && b && c {
do_something();
}
}};
assert!(
calculate_cyclomatic(&three_conditions) > 2,
"if a && b && c should have higher complexity than if a (got {})",
calculate_cyclomatic(&three_conditions)
);
let mixed_operators: Block = parse_quote! {{
if a && b || c {
do_something();
}
}};
assert!(
calculate_cyclomatic(&mixed_operators) > 2,
"if a && b || c should have higher complexity than if a (got {})",
calculate_cyclomatic(&mixed_operators)
);
}
#[test]
fn test_traverses_closure_and_async_children() {
let block: Block = parse_quote! {{
let closure = || if ready { work() } else { wait() };
async {
while pending {
poll().await;
}
};
}};
assert_eq!(
calculate_cyclomatic(&block),
3,
"closure if and async while should both contribute complexity"
);
}
#[test]
fn test_traverses_nested_literal_children() {
let block: Block = parse_quote! {{
let value = Config {
enabled: if flag { true } else { false },
..default_config()
};
let repeated = [if value.enabled { 1 } else { 0 }; if count > 0 { count } else { 1 }];
let range = (if start_ok { start } else { 0 })..(if end_ok { end } else { 1 });
}};
assert_eq!(
calculate_cyclomatic(&block),
6,
"struct fields, repeat expressions, and range bounds should be traversed"
);
}
#[test]
fn test_adjusted_preserves_other_complexity() {
let block: Block = parse_quote! {{
if condition {
do_something();
}
for i in items {
process(i);
}
match x {
A => 1,
B => 2,
C => 3,
D => 4,
E => 5,
F => 6,
G => 7,
_ => 8,
}
}};
let base = calculate_cyclomatic(&block);
let adjusted = calculate_cyclomatic_adjusted(&block);
assert_eq!(base, 10, "Base complexity should include all control flow");
assert!(
adjusted > 3,
"Adjusted complexity ({}) should preserve non-match control flow, not just return match adjustment",
adjusted
);
}
}