Skip to main content

wowlab_tidy/languages/rust/rules/style/
loop_to_while.rs

1use ra_ap_syntax::{ast, ast::HasLoopBody};
2
3use crate::{AstCtx, Example, Violation};
4
5#[rustfmt::skip]
6const EXAMPLES: &[Example] = &[
7    Example {
8        label: "loop with break-if at start",
9        code: "fn f() { loop { if !cond() { break; } do_work(); } }",
10        pass: false,
11    },
12    Example {
13        label: "loop with break-if-not at start",
14        code: "fn f() { loop { if done() { break; } do_work(); } }",
15        pass: false,
16    },
17    Example {
18        label: "while loop is fine",
19        code: "fn f() { while cond() { do_work(); } }",
20        pass: true,
21    },
22    Example {
23        label: "loop with break in middle",
24        code: "fn f() { loop { do_work(); if done() { break; } more_work(); } }",
25        pass: true,
26    },
27    Example {
28        label: "loop with no break-if",
29        code: "fn f() { loop { do_work(); break; } }",
30        pass: true,
31    },
32    Example {
33        label: "break-if after a let statement",
34        code: "fn f() { loop { let value = next(); if value == 0 { break; } } }",
35        pass: true,
36    },
37    Example {
38        label: "break-if body has another statement",
39        code: "fn f() { loop { if done() { note(); break; } do_work(); } }",
40        pass: true,
41    },
42    Example {
43        label: "loop with break value",
44        code: "fn f() -> i32 { loop { if done() { break 42; } } }",
45        pass: true,
46    },
47    Example {
48        label: "loop in test module",
49        code: "#[cfg(test)]\nmod tests {\n    fn f() { loop { if !cond() { break; } do_work(); } }\n}",
50        pass: true,
51    },
52];
53
54crate::ast_rule!(
55    loop_to_while,
56    "Flag `loop { if cond { break; } ... }` — use `while` instead.",
57    "A loop with a break-if guard as the first statement is a while loop in disguise. while is clearer and less error-prone.",
58    Low,
59);
60
61fn check_loop_to_while(ctx: &AstCtx<'_>) -> Vec<Violation> {
62    ctx.nodes::<ast::LoopExpr>()
63        .filter(|loop_expr| {
64            !ctx.is_in_test(loop_expr)
65                && loop_expr.label().is_none()
66                && loop_expr
67                    .loop_body()
68                    .is_some_and(|body| is_break_guard(&body))
69        })
70        .map(|loop_expr| {
71            ctx.violation(&loop_expr, "loop with break-if guard — use `while` instead")
72        })
73        .collect()
74}
75
76fn is_bare_break(expr: &ast::Expr) -> bool {
77    matches!(expr, ast::Expr::BreakExpr(expr) if expr.lifetime().is_none() && expr.expr().is_none())
78}
79
80fn is_break_guard(block: &ast::BlockExpr) -> bool {
81    let Some(statements) = block.stmt_list() else {
82        return false;
83    };
84    let Some(ast::Expr::IfExpr(if_expr)) = first_expr(&statements) else {
85        return false;
86    };
87
88    if if_expr.else_branch().is_some() {
89        return false;
90    }
91
92    let Some(then_statements) = if_expr.then_branch().and_then(|body| body.stmt_list()) else {
93        return false;
94    };
95
96    sole_expr(&then_statements).is_some_and(|expr| is_bare_break(&expr))
97}
98
99fn first_expr(statements: &ast::StmtList) -> Option<ast::Expr> {
100    match statements.statements().next() {
101        Some(ast::Stmt::ExprStmt(statement)) => statement.expr(),
102        Some(_) => None,
103        None => statements.tail_expr(),
104    }
105}
106
107fn sole_expr(statements: &ast::StmtList) -> Option<ast::Expr> {
108    let mut body = statements.statements();
109
110    match (body.next(), body.next(), statements.tail_expr()) {
111        (Some(ast::Stmt::ExprStmt(statement)), None, None) => statement.expr(),
112        (None, None, Some(expression)) => Some(expression),
113        _ => None,
114    }
115}
116
117crate::tidy_ast_test!(check_loop_to_while, {
118    crate::example_tests!(EXAMPLES, check_loop_to_while);
119});