Skip to main content

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

1#[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    // #t(fn: rust_cyclomatic_complexity) structural dispatch mirrors the five nesting constructs
88    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});