Skip to main content

cairo_lint/lints/loops/
loop_for_while.rs

1use cairo_lang_defs::ids::ModuleItemId;
2use cairo_lang_defs::plugin::PluginDiagnostic;
3use cairo_lang_diagnostics::Severity;
4use cairo_lang_semantic::{Arenas, Expr, ExprId, ExprLoop, Statement};
5
6use cairo_lang_syntax::node::{
7    SyntaxNode, TypedStablePtr, TypedSyntaxNode,
8    ast::{Expr as AstExpr, ExprLoop as AstExprLoop, OptionElseClause, Statement as AstStatement},
9};
10use if_chain::if_chain;
11
12use crate::context::{CairoLintKind, Lint};
13
14use crate::fixer::InternalFix;
15use crate::helper::{invert_condition, remove_break_from_block, remove_break_from_else_clause};
16use crate::queries::{get_all_function_bodies, get_all_loop_expressions};
17use salsa::Database;
18
19pub struct LoopForWhile;
20
21/// ## What it does
22///
23/// Checks for `loop` expressions that contain a conditional `if` statement with break inside that
24/// can be simplified to a `while` loop.
25///
26/// ## Example
27///
28/// ```cairo
29/// fn main() {
30///     let mut x: u16 = 0;
31///     loop {
32///         if x == 10 {
33///             break;
34///         }
35///         x += 1;
36///     }
37/// }
38/// ```
39///
40/// Can be simplified to:
41///
42/// ```cairo
43/// fn main() {
44///     let mut x: u16 = 0;
45///     while x != 10 {
46///         x += 1;
47///     }
48/// }
49/// ```
50impl Lint for LoopForWhile {
51    fn allowed_name(&self) -> &'static str {
52        "loop_for_while"
53    }
54
55    fn diagnostic_message(&self) -> &'static str {
56        "you seem to be trying to use `loop`. Consider replacing this `loop` with a `while` \
57                                  loop for clarity and conciseness"
58    }
59
60    fn kind(&self) -> CairoLintKind {
61        CairoLintKind::LoopForWhile
62    }
63
64    fn has_fixer(&self) -> bool {
65        true
66    }
67
68    fn fix<'db>(&self, db: &'db dyn Database, node: SyntaxNode<'db>) -> Option<InternalFix<'db>> {
69        fix_loop_break(db, node)
70    }
71
72    fn fix_message(&self) -> Option<&'static str> {
73        Some("Replace `loop` with `while` for clarity")
74    }
75}
76
77/// Checks for
78/// ```ignore
79/// loop {
80///     ...
81///     if cond {
82///         break;
83///     }
84/// }
85/// ```
86/// Which can be rewritten as a while loop
87/// ```ignore
88/// while cond {
89///     ...
90/// }
91/// ```
92#[tracing::instrument(skip_all, level = "trace")]
93pub fn check_loop_for_while<'db>(
94    db: &'db dyn Database,
95    item: &ModuleItemId<'db>,
96    diagnostics: &mut Vec<PluginDiagnostic<'db>>,
97) {
98    let function_bodies = get_all_function_bodies(db, item);
99    for function_body in function_bodies.iter() {
100        let loop_exprs = get_all_loop_expressions(function_body);
101        let arenas = &function_body.arenas;
102        for loop_expr in loop_exprs.iter() {
103            check_single_loop_for_while(loop_expr, arenas, diagnostics);
104        }
105    }
106}
107
108fn check_single_loop_for_while<'db>(
109    loop_expr: &ExprLoop<'db>,
110    arenas: &Arenas<'db>,
111    diagnostics: &mut Vec<PluginDiagnostic<'db>>,
112) {
113    // Get the else block  expression
114    let Expr::Block(block_expr) = &arenas.exprs[loop_expr.body] else {
115        return;
116    };
117
118    // Checks if the first statement is an if expression that only contains a break instruction.
119    if_chain! {
120        if let Some(statement) = block_expr.statements.first();
121        if let Statement::Expr(ref expr_statement) = arenas.statements[*statement];
122        if check_if_contains_break_with_no_return_value(&expr_statement.expr, arenas);
123        then {
124            diagnostics.push(PluginDiagnostic {
125                stable_ptr: loop_expr.stable_ptr.untyped(),
126                message: LoopForWhile.diagnostic_message().to_string(),
127                severity: Severity::Warning,
128                error_code: None,
129                inner_span: None
130            });
131        }
132    }
133
134    // Do the same thing if the if is in the tail of the block
135    if_chain! {
136        // Loop with single if-else statement will only have a tail expr and the statements will be empty.
137        // We check if the tail if statement is a single one. If it's not, we ignore the loop as a whole.
138        if block_expr.statements.is_empty();
139        if let Some(tail_expr) = block_expr.tail;
140        if check_if_contains_break_with_no_return_value(&tail_expr, arenas);
141        then {
142            diagnostics.push(PluginDiagnostic {
143                stable_ptr: loop_expr.stable_ptr.untyped(),
144                message: LoopForWhile.diagnostic_message().to_string(),
145                severity: Severity::Warning,
146                error_code: None,
147                inner_span: None
148            });
149        }
150    }
151}
152
153fn check_if_contains_break_with_no_return_value(expr: &ExprId, arenas: &Arenas) -> bool {
154    if_chain! {
155        // Is an if expression
156        if let Expr::If(ref if_expr) = arenas.exprs[*expr];
157        // Get the block
158        if let Expr::Block(ref if_block) = arenas.exprs[if_expr.if_block];
159        // Get the first statement of the if
160        if let Some(inner_stmt) = if_block.statements.first();
161        // Is it a break statement
162        if let Statement::Break(break_expr) = &arenas.statements[*inner_stmt];
163        then {
164            // If break also has a return value like `break 1;` then it's not a simple break.
165            return break_expr.expr_option.is_none();
166        }
167    }
168    false
169}
170
171/// Converts a `loop` with a conditionally-breaking `if` statement into a `while` loop.
172///
173/// This function transforms loops that have a conditional `if` statement
174/// followed by a `break` into a `while` loop, which can simplify the logic
175/// and improve readability.
176///
177/// # Arguments
178///
179/// * `db` - Reference to the `SyntaxGroup` for syntax tree access.
180/// * `node` - The `SyntaxNode` representing the loop expression.
181///
182/// # Returns
183///
184/// A `String` containing the transformed loop as a `while` loop, preserving
185/// the original formatting and indentation.
186///
187/// # Example
188///
189/// ```
190/// let mut x = 0;
191/// loop {
192///     if x > 5 {
193///         break;
194///     }
195///     x += 1;
196/// }
197/// ```
198///
199/// Would be converted to:
200///
201/// ```
202/// let mut x = 0;
203/// while x <= 5 {
204///     x += 1;
205/// }
206/// ```
207#[tracing::instrument(skip_all, level = "trace")]
208pub fn fix_loop_break<'db>(
209    db: &'db dyn Database,
210    node: SyntaxNode<'db>,
211) -> Option<InternalFix<'db>> {
212    let loop_expr = AstExprLoop::from_syntax_node(db, node);
213    let indent = node
214        .get_text(db)
215        .chars()
216        .take_while(|c| c.is_whitespace())
217        .collect::<String>();
218    let mut condition_text = String::new();
219    let mut loop_body = String::new();
220
221    let mut loop_span = node.span(db);
222    loop_span.end = node.span_start_without_trivia(db);
223    let trivia = node.get_text_of_span(db, loop_span).trim().to_string();
224    let trivia = if trivia.is_empty() {
225        trivia
226    } else {
227        format!("{indent}{trivia}\n")
228    };
229
230    if let Some(AstStatement::Expr(expr_statement)) =
231        loop_expr.body(db).statements(db).elements(db).next()
232        && let AstExpr::If(if_expr) = expr_statement.expr(db)
233    {
234        condition_text = invert_condition(
235            if_expr
236                .conditions(db)
237                .as_syntax_node()
238                .get_text_without_trivia(db)
239                .long(db)
240                .as_str(),
241        );
242
243        loop_body.push_str(&remove_break_from_block(db, if_expr.if_block(db), &indent));
244
245        if let OptionElseClause::ElseClause(else_clause) = if_expr.else_clause(db) {
246            loop_body.push_str(&remove_break_from_else_clause(db, else_clause, &indent));
247        }
248    }
249
250    for statement in loop_expr.body(db).statements(db).elements(db).skip(1) {
251        loop_body.push_str(&format!(
252            "{}    {}\n",
253            indent,
254            statement.as_syntax_node().get_text(db).trim()
255        ));
256    }
257
258    Some(InternalFix {
259        node,
260        suggestion: format!("{trivia}{indent}while {condition_text} {{\n{loop_body}{indent}}}\n"),
261        description: LoopForWhile.fix_message().unwrap().to_string(),
262        import_addition_paths: None,
263    })
264}