wowlab_tidy/languages/rust/rules/style/
loop_to_while.rs1use 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});