use crate::linter::diagnostic::{Diagnostic, Severity};
use crate::linter::rules::{Example, Rule, RuleContext};
use crate::syntax::{SyntaxElement, SyntaxKind, SyntaxNode};
pub struct BreakOutsideLoop;
fn is_function_boundary(node: &SyntaxNode, from: &SyntaxNode) -> bool {
match node.kind() {
SyntaxKind::FUNCTION_DEF | SyntaxKind::MACRO_DEF | SyntaxKind::ARROW_EXPR => true,
SyntaxKind::DO_EXPR => {
from.kind() == SyntaxKind::BLOCK
&& !node.children().any(|c| c.kind() == SyntaxKind::MACRO_CALL)
}
SyntaxKind::COMPREHENSION
| SyntaxKind::BRACES_COMPREHENSION
| SyntaxKind::TYPED_COMPREHENSION
| SyntaxKind::GENERATOR => from.kind() != SyntaxKind::FOR_BINDING,
_ => false,
}
}
impl Rule for BreakOutsideLoop {
fn id(&self) -> &'static str {
"break-outside-loop"
}
fn default_severity(&self) -> Severity {
Severity::Error
}
fn description(&self) -> &'static str {
"Flag a `break` or `continue` with no enclosing `for` or `while` loop. \
The code parses but always fails at lowering with \"break or continue \
outside loop\" — including inside a closure, do-block, or \
comprehension body defined within a loop, since `break` cannot cross \
a function boundary."
}
fn examples(&self) -> &'static [Example] {
&[
Example {
caption: "`break` with no loop in sight:",
source: "function process(x)\n if x < 0\n break\n end\n x\nend\n",
},
Example {
caption: "A do-block body is an anonymous function, so the outer loop is out of reach:",
source: "for i in 1:3\n foreach(1:2) do x\n continue\n end\nend\n",
},
]
}
fn interests(&self) -> &'static [SyntaxKind] {
&[SyntaxKind::BREAK_EXPR, SyntaxKind::CONTINUE_EXPR]
}
fn check(&self, el: &SyntaxElement, _ctx: &RuleContext<'_>, sink: &mut Vec<Diagnostic>) {
let Some(node) = el.as_node() else {
return;
};
let mut from = node.clone();
for ancestor in node.ancestors().skip(1) {
match ancestor.kind() {
SyntaxKind::FOR_EXPR | SyntaxKind::WHILE_EXPR => return,
SyntaxKind::QUOTE_EXPR | SyntaxKind::QUOTE_SYM | SyntaxKind::MACRO_CALL => {
return;
}
_ if is_function_boundary(&ancestor, &from) => break,
_ => {}
}
from = ancestor;
}
let keyword = if node.kind() == SyntaxKind::BREAK_EXPR {
"break"
} else {
"continue"
};
sink.push(Diagnostic::new(
self.id(),
node.text_range(),
format!("`{keyword}` outside of a `for` or `while` loop"),
));
}
}