wowlab_tidy/languages/rust/rules/complexity/
max_nesting.rs1#[cfg(test)]
2use googletest::prelude::*;
3use ra_ap_syntax::{
4 AstNode, SyntaxKind, SyntaxNode,
5 ast::{self, HasLoopBody, HasName},
6};
7
8use crate::{AstCtx, Example, Violation};
9
10#[rustfmt::skip]
11const EXAMPLES: &[Example] = &[
12 Example {
13 label: "shallow function",
14 code: "fn f() { if true { if true { } } }",
15 pass: true,
16 },
17 Example {
18 label: "match/loop/else nested to depth 6 fails",
19 code: "fn f() {\n loop {\n while c {\n if a {\n } else {\n match x {\n _ => loop {\n match y {\n _ => loop { }\n }\n }\n }\n }\n }\n }\n}",
20 pass: false,
21 },
22 Example {
23 label: "match/loop nested to depth 5 passes",
24 code: "fn f() {\n loop {\n while c {\n match x {\n _ => loop {\n match y {\n _ => ()\n }\n }\n }\n }\n }\n}",
25 pass: true,
26 },
27];
28
29crate::ast_rule!(
30 max_nesting,
31 "Flag nesting depth > threshold levels.",
32 "Deeply nested code is hard to follow. Use early returns, guard clauses, or extract helper functions.",
33 Medium,
34 params { threshold: i64 = 5 },
35);
36
37fn check_max_nesting(ctx: &AstCtx<'_>) -> Vec<Violation> {
38 let max_depth = ctx.file.config.get_usize("rust_max_nesting", &PARAMS[0]);
39
40 ctx.nodes::<ast::Fn>()
41 .filter(|function| !ctx.is_in_test(function))
42 .filter_map(|function| {
43 let body = function.body()?;
44 let mut depth = NestingDepth::default();
45
46 depth.visit_node(body.syntax(), 0);
47
48 (depth.max > max_depth).then(|| {
49 let name = function.name()?;
50
51 Some(ctx.violation(
52 &name,
53 format!(
54 "function `{name}` has nesting depth {} (max {max_depth})",
55 depth.max
56 ),
57 ))
58 })?
59 })
60 .collect()
61}
62
63#[derive(Default)]
64struct NestingDepth {
65 max: usize,
66}
67
68impl NestingDepth {
69 fn visit_node(&mut self, node: &SyntaxNode, current: usize) {
70 for child in node.children() {
71 if child.kind() == SyntaxKind::FN {
72 continue;
73 }
74
75 if ast::Expr::can_cast(child.kind()) {
76 let Some(expression) = ast::Expr::cast(child) else {
77 continue;
78 };
79
80 self.visit_expression(&expression, current);
81 } else {
82 self.visit_node(&child, current);
83 }
84 }
85 }
86
87 fn visit_expression(&mut self, expression: &ast::Expr, current: usize) {
89 match expression {
90 ast::Expr::IfExpr(expression) => {
91 if let Some(condition) = expression.condition() {
92 self.visit_expression(&condition, current);
93 }
94
95 if let Some(branch) = expression.then_branch() {
96 self.visit_nested(branch.syntax(), current);
97 }
98
99 if let Some(branch) = expression.else_branch() {
100 match branch {
101 ast::ElseBranch::Block(branch) => self.visit_node(branch.syntax(), current),
102 ast::ElseBranch::IfExpr(branch) => {
103 self.visit_expression(&ast::Expr::IfExpr(branch), current);
104 }
105 }
106 }
107 }
108 ast::Expr::MatchExpr(expression) => {
109 if let Some(scrutinee) = expression.expr() {
110 self.visit_expression(&scrutinee, current);
111 }
112
113 let nested = current + 1;
114
115 self.max = self.max.max(nested);
116
117 if let Some(arms) = expression.match_arm_list() {
118 for arm in arms.arms() {
119 if let Some(condition) = arm.guard().and_then(|guard| guard.condition()) {
120 self.visit_expression(&condition, nested);
121 }
122
123 if let Some(body) = arm.expr() {
124 self.visit_expression(&body, nested);
125 }
126 }
127 }
128 }
129 ast::Expr::ForExpr(expression) => {
130 if let Some(iterable) = expression.iterable() {
131 self.visit_expression(&iterable, current);
132 }
133
134 if let Some(body) = expression.loop_body() {
135 self.visit_nested(body.syntax(), current);
136 }
137 }
138 ast::Expr::WhileExpr(expression) => {
139 if let Some(condition) = expression.condition() {
140 self.visit_expression(&condition, current);
141 }
142
143 if let Some(body) = expression.loop_body() {
144 self.visit_nested(body.syntax(), current);
145 }
146 }
147 ast::Expr::LoopExpr(expression) => {
148 if let Some(body) = expression.loop_body() {
149 self.visit_nested(body.syntax(), current);
150 }
151 }
152 _ => self.visit_node(expression.syntax(), current),
153 }
154 }
155
156 fn visit_nested(&mut self, body: &SyntaxNode, current: usize) {
157 let nested = current + 1;
158
159 self.max = self.max.max(nested);
160 self.visit_node(body, nested);
161 }
162}
163
164crate::tidy_ast_test!(check_max_nesting, {
165 crate::example_tests!(EXAMPLES, check_max_nesting);
166
167 #[gtest]
168 fn deeply_nested_fails() -> Result<()> {
169 let src = "fn f() {
170 if true { // 1
171 if true { // 2
172 if true { // 3
173 if true { // 4
174 if true { // 5
175 if true { } // 6
176 }
177 }
178 }
179 }
180 }
181 }";
182 let v = run(src);
183 verify_eq!(v.len(), 1)?;
184 verify_true!(v[0].message.contains("nesting depth 6"))?;
185
186 Ok(())
187 }
188
189 #[gtest]
190 fn at_threshold_passes() -> Result<()> {
191 let src = "fn f() {
192 if true { // 1
193 if true { // 2
194 if true { // 3
195 if true { // 4
196 if true { } // 5
197 }
198 }
199 }
200 }
201 }";
202 verify_true!(run(src).is_empty())?;
203
204 Ok(())
205 }
206});