Skip to content

Commit

Permalink
feat(linter): add loop does not iterate rule
Browse files Browse the repository at this point in the history
Signed-off-by: azjezz <azjezz@protonmail.com>
  • Loading branch information
azjezz committed Dec 10, 2024
1 parent 263ae70 commit b566180
Show file tree
Hide file tree
Showing 4 changed files with 111 additions and 0 deletions.
2 changes: 2 additions & 0 deletions crates/linter/src/plugin/best_practices/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use crate::plugin::best_practices::rules::disallowed_functions::DisallowedFunctionsRule;
use crate::plugin::best_practices::rules::excessive_nesting::ExcessiveNesting;
use crate::plugin::best_practices::rules::loop_does_not_iterate::LoopDoesNotIterateRule;
use crate::plugin::best_practices::rules::no_debug_symbols::NoDebugSymbolsRule;
use crate::plugin::best_practices::rules::no_empty_loop::NoEmptyLoopRule;
use crate::plugin::best_practices::rules::no_goto::NoGotoRule;
Expand Down Expand Up @@ -28,6 +29,7 @@ impl Plugin for BestPracticesPlugin {
Box::new(DisallowedFunctionsRule),
Box::new(NoUnusedParameterRule),
Box::new(ExcessiveNesting),
Box::new(LoopDoesNotIterateRule),
Box::new(NoGotoRule),
Box::new(NoDebugSymbolsRule),
Box::new(NoEmptyLoopRule),
Expand Down
104 changes: 104 additions & 0 deletions crates/linter/src/plugin/best_practices/rules/loop_does_not_iterate.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
use mago_ast::*;
use mago_reporting::*;
use mago_span::HasSpan;
use mago_walker::Walker;

use crate::context::LintContext;
use crate::rule::Rule;

#[derive(Clone, Debug)]
pub struct LoopDoesNotIterateRule;

impl Rule for LoopDoesNotIterateRule {
fn get_name(&self) -> &'static str {
"loop-does-not-iterate"
}

fn get_default_level(&self) -> Option<Level> {
Some(Level::Warning)
}
}

impl LoopDoesNotIterateRule {
fn report(&self, r#loop: impl HasSpan, terminator: LoopTerminator<'_>, context: &mut LintContext<'_>) {
let loop_span = r#loop.span();
let terminator_span = match terminator {
LoopTerminator::Break(break_stmt) => break_stmt.span(),
LoopTerminator::Return(return_stmt) => return_stmt.span(),
};

let issue = Issue::new(context.level(), "loop does not iterate")
.with_annotations([
Annotation::primary(terminator_span)
.with_message("this statement unconditionally terminates the loop."),
Annotation::secondary(loop_span).with_message("this loop does not iterate."),
])
.with_help("remove or refactor the loop to avoid redundant or misleading code.");

context.report(issue);
}
}

impl<'a> Walker<LintContext<'a>> for LoopDoesNotIterateRule {
fn walk_in_foreach(&self, foreach: &Foreach, context: &mut LintContext<'a>) {
if let Some(terminator) = match &foreach.body {
ForeachBody::Statement(stmt) => get_loop_terminator_from_statement(stmt),
ForeachBody::ColonDelimited(block) => get_loop_terminator_from_statements(block.statements.as_slice()),
} {
self.report(foreach, terminator, context);
}
}

fn walk_in_for(&self, for_loop: &For, context: &mut LintContext<'a>) {
if let Some(terminator) = match &for_loop.body {
ForBody::Statement(stmt) => get_loop_terminator_from_statement(stmt),
ForBody::ColonDelimited(block) => get_loop_terminator_from_statements(block.statements.as_slice()),
} {
self.report(for_loop, terminator, context);
}
}

fn walk_in_while(&self, while_loop: &While, context: &mut LintContext<'a>) {
if let Some(terminator) = match &while_loop.body {
WhileBody::Statement(stmt) => get_loop_terminator_from_statement(stmt),
WhileBody::ColonDelimited(block) => get_loop_terminator_from_statements(block.statements.as_slice()),
} {
self.report(while_loop, terminator, context);
}
}

fn walk_in_do_while(&self, do_while: &DoWhile, context: &mut LintContext<'a>) {
if let Some(terminator) = get_loop_terminator_from_statement(&do_while.statement) {
self.report(do_while, terminator, context);
}
}
}

enum LoopTerminator<'a> {
Break(&'a Break),
Return(&'a Return),
}

fn get_loop_terminator_from_statements(statements: &[Statement]) -> Option<LoopTerminator<'_>> {
for statement in statements.iter().rev() {
if let Some(terminator) = get_loop_terminator_from_statement(statement) {
return Some(terminator);
}
}

None
}

fn get_loop_terminator_from_statement(statement: &Statement) -> Option<LoopTerminator<'_>> {
match statement {
Statement::Block(block) => get_loop_terminator_from_statements(block.statements.as_slice()),
Statement::Break(break_stmt) => match break_stmt.level {
None | Some(Expression::Literal(Literal::Integer(LiteralInteger { value: Some(1), .. }))) => {
Some(LoopTerminator::Break(break_stmt))
}
Some(_) => None,
},
Statement::Return(return_stmt) => Some(LoopTerminator::Return(return_stmt)),
_ => None,
}
}
1 change: 1 addition & 0 deletions crates/linter/src/plugin/best_practices/rules/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
pub mod disallowed_functions;
pub mod excessive_nesting;
pub mod loop_does_not_iterate;
pub mod no_debug_symbols;
pub mod no_empty_loop;
pub mod no_goto;
Expand Down
4 changes: 4 additions & 0 deletions examples/src/main.php
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,10 @@ function Main(): never
break;
}

foreach ($files as $file) {
return;
}

assert($var === 1);

return;
Expand Down

0 comments on commit b566180

Please sign in to comment.