wowlab_tidy/languages/rust/rules/complexity/
cyclomatic_complexity.rs1#[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});