cairo_lint/lints/loops/
loop_for_while.rs1use 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
21impl 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#[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 let Expr::Block(block_expr) = &arenas.exprs[loop_expr.body] else {
115 return;
116 };
117
118 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 if_chain! {
136 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 if let Expr::If(ref if_expr) = arenas.exprs[*expr];
157 if let Expr::Block(ref if_block) = arenas.exprs[if_expr.if_block];
159 if let Some(inner_stmt) = if_block.statements.first();
161 if let Statement::Break(break_expr) = &arenas.statements[*inner_stmt];
163 then {
164 return break_expr.expr_option.is_none();
166 }
167 }
168 false
169}
170
171#[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}