Skip to main content

crap4rust/
complexity.rs

1// Copyright 2025 Umberto Gotti <umberto.gotti@umbertogotti.dev>
2// Licensed under the MIT License or Apache License, Version 2.0
3// SPDX-License-Identifier: MIT OR Apache-2.0
4
5use syn::{
6    Arm, Block, Expr, ExprBinary, ExprBlock, ExprForLoop, ExprIf, ExprLoop, ExprMatch, ExprWhile,
7    ImplItem, Item, LocalInit, Pat, Stmt,
8};
9
10#[must_use]
11pub fn cognitive_complexity(block: &Block) -> u32 {
12    score_block(block, 0)
13}
14
15fn score_block(block: &Block, nesting: u32) -> u32 {
16    block
17        .stmts
18        .iter()
19        .map(|stmt| score_stmt(stmt, nesting))
20        .sum()
21}
22
23fn score_stmt(stmt: &Stmt, nesting: u32) -> u32 {
24    match stmt {
25        Stmt::Local(local) => score_local_init(&local.init, nesting),
26        Stmt::Item(item) => match item {
27            Item::Fn(item_fn) => score_block(&item_fn.block, nesting),
28            Item::Mod(item_mod) => item_mod.content.as_ref().map_or(0, |(_, items)| {
29                items
30                    .iter()
31                    .map(|item| score_nested_item(item, nesting))
32                    .sum()
33            }),
34            _ => 0,
35        },
36        Stmt::Expr(expr, _) => score_expr(expr, nesting),
37        Stmt::Macro(_) => 0,
38    }
39}
40
41fn score_nested_item(item: &Item, nesting: u32) -> u32 {
42    match item {
43        Item::Fn(item_fn) => score_block(&item_fn.block, nesting),
44        Item::Impl(item_impl) => item_impl
45            .items
46            .iter()
47            .map(|item| match item {
48                ImplItem::Fn(method) => score_block(&method.block, nesting),
49                _ => 0,
50            })
51            .sum(),
52        Item::Mod(item_mod) => item_mod.content.as_ref().map_or(0, |(_, items)| {
53            items
54                .iter()
55                .map(|item| score_nested_item(item, nesting))
56                .sum()
57        }),
58        _ => 0,
59    }
60}
61
62fn score_local_init(init: &Option<LocalInit>, nesting: u32) -> u32 {
63    init.as_ref().map_or(0, |init| {
64        score_expr(&init.expr, nesting)
65            + init
66                .diverge
67                .as_ref()
68                .map_or(0, |(_, expr)| score_expr(expr, nesting))
69    })
70}
71
72fn score_expr(expr: &Expr, nesting: u32) -> u32 {
73    match expr {
74        Expr::If(expr_if) => score_if(expr_if, nesting),
75        Expr::Match(expr_match) => score_match(expr_match, nesting),
76        Expr::ForLoop(expr_for) => score_for(expr_for, nesting),
77        Expr::While(expr_while) => score_while(expr_while, nesting),
78        Expr::Loop(expr_loop) => score_loop(expr_loop, nesting),
79        Expr::Block(ExprBlock { block, .. }) => score_block(block, nesting),
80        Expr::Binary(expr_binary) => {
81            logical_ops(expr_binary)
82                + score_expr(&expr_binary.left, nesting)
83                + score_expr(&expr_binary.right, nesting)
84        }
85        Expr::Call(expr_call) => {
86            score_expr(&expr_call.func, nesting)
87                + expr_call
88                    .args
89                    .iter()
90                    .map(|argument| score_expr(argument, nesting))
91                    .sum::<u32>()
92        }
93        Expr::MethodCall(expr_call) => {
94            score_expr(&expr_call.receiver, nesting)
95                + expr_call
96                    .args
97                    .iter()
98                    .map(|argument| score_expr(argument, nesting))
99                    .sum::<u32>()
100        }
101        Expr::Closure(expr_closure) => score_expr(&expr_closure.body, nesting),
102        Expr::Async(expr_async) => score_block(&expr_async.block, nesting),
103        Expr::Await(expr_await) => score_expr(&expr_await.base, nesting),
104        Expr::Try(expr_try) => score_expr(&expr_try.expr, nesting),
105        Expr::TryBlock(expr_try_block) => {
106            1 + nesting + score_block(&expr_try_block.block, nesting + 1)
107        }
108        Expr::Unary(expr_unary) => score_expr(&expr_unary.expr, nesting),
109        Expr::Reference(expr_reference) => score_expr(&expr_reference.expr, nesting),
110        Expr::Return(expr_return) => expr_return
111            .expr
112            .as_ref()
113            .map_or(0, |expr| score_expr(expr, nesting)),
114        Expr::Break(expr_break) => expr_break
115            .expr
116            .as_ref()
117            .map_or(0, |expr| score_expr(expr, nesting)),
118        Expr::Paren(expr_paren) => score_expr(&expr_paren.expr, nesting),
119        Expr::Array(expr_array) => expr_array
120            .elems
121            .iter()
122            .map(|expr| score_expr(expr, nesting))
123            .sum(),
124        Expr::Assign(expr_assign) => {
125            score_expr(&expr_assign.left, nesting) + score_expr(&expr_assign.right, nesting)
126        }
127        Expr::Field(expr_field) => score_expr(&expr_field.base, nesting),
128        Expr::Index(expr_index) => {
129            score_expr(&expr_index.expr, nesting) + score_expr(&expr_index.index, nesting)
130        }
131        Expr::Let(expr_let) => score_expr(&expr_let.expr, nesting),
132        Expr::Macro(_) => 0,
133        Expr::Range(expr_range) => {
134            expr_range
135                .start
136                .as_ref()
137                .map_or(0, |expr| score_expr(expr, nesting))
138                + expr_range
139                    .end
140                    .as_ref()
141                    .map_or(0, |expr| score_expr(expr, nesting))
142        }
143        Expr::Repeat(expr_repeat) => {
144            score_expr(&expr_repeat.expr, nesting) + score_expr(&expr_repeat.len, nesting)
145        }
146        Expr::Struct(expr_struct) => {
147            expr_struct
148                .fields
149                .iter()
150                .map(|field| score_expr(&field.expr, nesting))
151                .sum::<u32>()
152                + expr_struct
153                    .rest
154                    .as_ref()
155                    .map_or(0, |expr| score_expr(expr, nesting))
156        }
157        Expr::Tuple(expr_tuple) => expr_tuple
158            .elems
159            .iter()
160            .map(|expr| score_expr(expr, nesting))
161            .sum(),
162        Expr::Unsafe(expr_unsafe) => score_block(&expr_unsafe.block, nesting),
163        Expr::Yield(expr_yield) => expr_yield
164            .expr
165            .as_ref()
166            .map_or(0, |expr| score_expr(expr, nesting)),
167        _ => 0,
168    }
169}
170
171fn score_if(expr_if: &ExprIf, nesting: u32) -> u32 {
172    let mut score = 1
173        + nesting
174        + logical_expr_score(&expr_if.cond)
175        + score_block(&expr_if.then_branch, nesting + 1);
176    if let Some((_, else_branch)) = &expr_if.else_branch {
177        score += match else_branch.as_ref() {
178            Expr::If(else_if) => score_if(else_if, nesting),
179            other => score_expr(other, nesting + 1),
180        };
181    }
182    score
183}
184
185fn score_match(expr_match: &ExprMatch, nesting: u32) -> u32 {
186    1 + nesting
187        + score_expr(&expr_match.expr, nesting)
188        + expr_match
189            .arms
190            .iter()
191            .map(|arm| score_arm(arm, nesting + 1))
192            .sum::<u32>()
193}
194
195fn score_arm(arm: &Arm, nesting: u32) -> u32 {
196    arm.guard.as_ref().map_or(0, |(_, expr)| {
197        logical_expr_score(expr) + score_expr(expr, nesting)
198    }) + score_expr(&arm.body, nesting)
199}
200
201fn score_for(expr_for: &ExprForLoop, nesting: u32) -> u32 {
202    1 + nesting
203        + score_expr(&expr_for.expr, nesting)
204        + pattern_complexity(&expr_for.pat)
205        + score_block(&expr_for.body, nesting + 1)
206}
207
208fn score_while(expr_while: &ExprWhile, nesting: u32) -> u32 {
209    1 + nesting
210        + logical_expr_score(&expr_while.cond)
211        + score_expr(&expr_while.cond, nesting)
212        + score_block(&expr_while.body, nesting + 1)
213}
214
215fn score_loop(expr_loop: &ExprLoop, nesting: u32) -> u32 {
216    1 + nesting + score_block(&expr_loop.body, nesting + 1)
217}
218
219fn pattern_complexity(pattern: &Pat) -> u32 {
220    match pattern {
221        Pat::Or(pattern_or) => pattern_or.cases.len().saturating_sub(1) as u32,
222        _ => 0,
223    }
224}
225
226fn logical_expr_score(expr: &Expr) -> u32 {
227    match expr {
228        Expr::Binary(binary) => {
229            let current = logical_ops(binary);
230            current + logical_expr_score(&binary.left) + logical_expr_score(&binary.right)
231        }
232        Expr::Paren(paren) => logical_expr_score(&paren.expr),
233        Expr::Group(group) => logical_expr_score(&group.expr),
234        _ => 0,
235    }
236}
237
238fn logical_ops(expr_binary: &ExprBinary) -> u32 {
239    if matches!(expr_binary.op, syn::BinOp::And(_) | syn::BinOp::Or(_)) {
240        1
241    } else {
242        0
243    }
244}