Skip to main content

wowlab_tidy/languages/rust/rules/complexity/
cyclomatic_complexity.rs

1#[cfg(test)]
2use googletest::prelude::*;
3use ra_ap_syntax::{
4    AstNode,
5    ast::{self, BinaryOp, HasName},
6};
7
8use crate::{AstCtx, Example, Violation};
9
10#[rustfmt::skip]
11const EXAMPLES: &[Example] = &[
12    Example {
13        label: "simple function",
14        code: "fn f() { let x = 1; }",
15        pass: true,
16    },
17    Example {
18        label: "moderate branching is fine",
19        code: "fn f(x: bool) { if x {} if x {} if x {} if x {} if x {} if x {} }",
20        pass: true,
21    },
22    Example {
23        label: "too many branches",
24        code: "fn f(x: bool) { if x {} if x {} if x {} if x {} if x {} if x {} if x {} if x {} if x {} if x {} if x {} if x {} if x {} if x {} if x {} if x {} }",
25        pass: false,
26    },
27    Example {
28        label: "mixed constructs over threshold",
29        code: "fn f(x: u8) -> u8 {\n    match x { 0 => 1, 1 => 2, 2 => 3, 3 => 4, _ => 5 }\n    for _ in it { }\n    while a && b { }\n    loop { break v && w; }\n    let _ = c || d || e;\n    let _ = |z: bool| if z { 1 } else { 2 };\n    let _ = h()?;\n    let _ = p && q;\n    return x;\n}",
30        pass: false,
31    },
32    Example {
33        label: "mixed constructs at threshold",
34        code: "fn f(x: u8) -> u8 {\n    match x { 0 => 1, 1 => 2, 2 => 3, 3 => 4, _ => 5 }\n    for _ in it { }\n    while a && b { }\n    loop { break v && w; }\n    let _ = c || d || e;\n    let _ = |z: bool| if z { 1 } else { 2 };\n    let _ = h()?;\n    return x;\n}",
35        pass: true,
36    },
37];
38
39crate::ast_rule!(
40    cyclomatic_complexity,
41    "Flag functions with cyclomatic complexity > threshold.",
42    "High cyclomatic complexity means many execution paths, making the function hard to test and prone to bugs.",
43    Medium,
44    params {
45        threshold: i64 = 15
46    },
47);
48
49fn check_cyclomatic_complexity(ctx: &AstCtx<'_>) -> Vec<Violation> {
50    let threshold = ctx
51        .file
52        .config
53        .get_usize("rust_cyclomatic_complexity", &PARAMS[0]);
54
55    ctx.nodes::<ast::Fn>()
56        .filter(|function| !ctx.is_in_test(function))
57        .filter_map(|function| {
58            function.body()?;
59            let complexity = function
60                .syntax()
61                .descendants()
62                .filter_map(ast::Expr::cast)
63                .filter(|expression| enclosing_function(expression).as_ref() == Some(&function))
64                .map(expression_complexity)
65                .sum::<usize>();
66
67            (complexity > threshold).then(|| {
68                let name = function.name()?;
69
70                Some(ctx.violation(
71                    &name,
72                    format!(
73                        "function `{name}` has cyclomatic complexity {complexity} (max {threshold})"
74                    ),
75                ))
76            })?
77        })
78        .collect()
79}
80
81fn enclosing_function<N>(node: &N) -> Option<ast::Fn>
82where
83    N: AstNode,
84{
85    node.syntax().ancestors().skip(1).find_map(ast::Fn::cast)
86}
87
88fn expression_complexity(expression: ast::Expr) -> usize {
89    match expression {
90        ast::Expr::IfExpr(_)
91        | ast::Expr::ForExpr(_)
92        | ast::Expr::WhileExpr(_)
93        | ast::Expr::LoopExpr(_)
94        | ast::Expr::TryExpr(_)
95        | ast::Expr::ReturnExpr(_)
96        | ast::Expr::LetExpr(_) => 1,
97        ast::Expr::MatchExpr(expression) => expression
98            .match_arm_list()
99            .map_or(0, |arms| arms.arms().count().saturating_sub(1)),
100        ast::Expr::BinExpr(expression) => {
101            usize::from(matches!(expression.op_kind(), Some(BinaryOp::LogicOp(_))))
102        }
103        ast::Expr::BreakExpr(expression) => usize::from(expression.expr().is_some()),
104        _ => 0,
105    }
106}
107
108crate::tidy_ast_test!(check_cyclomatic_complexity, {
109    crate::example_tests!(EXAMPLES, check_cyclomatic_complexity);
110
111    #[gtest]
112    fn complex_fn_fails() -> Result<()> {
113        let src = "fn f(x: bool) {
114            if x { }
115            if x { }
116            if x { }
117            if x { }
118            if x { }
119            if x { }
120            if x { }
121            if x { }
122            if x { }
123            if x { }
124            if x { }
125            if x { }
126            if x { }
127            if x { }
128            if x { }
129            if x { }
130        }";
131        let v = run(src);
132        verify_eq!(v.len(), 1)?;
133        verify_true!(v[0].message.contains("cyclomatic complexity 16"))?;
134
135        Ok(())
136    }
137
138    #[gtest]
139    fn at_threshold_passes() -> Result<()> {
140        let src = "fn f(x: bool) {
141            if x { }
142            if x { }
143            if x { }
144            if x { }
145            if x { }
146            if x { }
147            if x { }
148            if x { }
149            if x { }
150            if x { }
151            if x { }
152            if x { }
153            if x { }
154            if x { }
155            if x { }
156        }";
157        verify_true!(run(src).is_empty())?;
158
159        Ok(())
160    }
161
162    #[gtest]
163    fn test_code_passes() -> Result<()> {
164        let src = "#[cfg(test)]
165        mod tests {
166            fn f(x: bool) {
167                if x { } if x { } if x { } if x { }
168                if x { } if x { } if x { } if x { }
169                if x { } if x { } if x { } if x { }
170                if x { } if x { } if x { } if x { }
171            }
172        }";
173        verify_true!(run(src).is_empty())?;
174
175        Ok(())
176    }
177});