diff --git a/crates/ruff_linter/src/checkers/ast/analyze/statement.rs b/crates/ruff_linter/src/checkers/ast/analyze/statement.rs index 155836b3bd..89873737a6 100644 --- a/crates/ruff_linter/src/checkers/ast/analyze/statement.rs +++ b/crates/ruff_linter/src/checkers/ast/analyze/statement.rs @@ -50,15 +50,6 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) { pylint::rules::nonlocal_and_global(checker, nonlocal); } } - Stmt::Break(_) => { - if checker.is_rule_enabled(Rule::BreakOutsideLoop) { - pyflakes::rules::break_outside_loop( - checker, - stmt, - &mut checker.semantic.current_statements().skip(1), - ); - } - } Stmt::Continue(_) => { if checker.is_rule_enabled(Rule::ContinueOutsideLoop) { pyflakes::rules::continue_outside_loop( diff --git a/crates/ruff_linter/src/checkers/ast/mod.rs b/crates/ruff_linter/src/checkers/ast/mod.rs index 1a1f462e8e..d6f5b81199 100644 --- a/crates/ruff_linter/src/checkers/ast/mod.rs +++ b/crates/ruff_linter/src/checkers/ast/mod.rs @@ -697,6 +697,7 @@ impl SemanticSyntaxContext for Checker<'_> { } } SemanticSyntaxErrorKind::FutureFeatureNotDefined(name) => { + // F407 if self.is_rule_enabled(Rule::FutureFeatureNotDefined) { self.report_diagnostic( pyflakes::rules::FutureFeatureNotDefined { name }, @@ -704,6 +705,12 @@ impl SemanticSyntaxContext for Checker<'_> { ); } } + SemanticSyntaxErrorKind::BreakOutsideLoop => { + // F701 + if self.is_rule_enabled(Rule::BreakOutsideLoop) { + self.report_diagnostic(pyflakes::rules::BreakOutsideLoop, error.range); + } + } SemanticSyntaxErrorKind::ReboundComprehensionVariable | SemanticSyntaxErrorKind::DuplicateTypeParameter | SemanticSyntaxErrorKind::MultipleCaseAssignment(_) @@ -811,19 +818,40 @@ impl SemanticSyntaxContext for Checker<'_> { } ) } + + fn in_loop_context(&self) -> bool { + let mut child = self.semantic.current_statement(); + + for parent in self.semantic.current_statements().skip(1) { + match parent { + Stmt::For(ast::StmtFor { orelse, .. }) + | Stmt::While(ast::StmtWhile { orelse, .. }) => { + if !orelse.contains(child) { + return true; + } + } + Stmt::FunctionDef(_) | Stmt::ClassDef(_) => { + break; + } + _ => {} + } + child = parent; + } + false + } } impl<'a> Visitor<'a> for Checker<'a> { fn visit_stmt(&mut self, stmt: &'a Stmt) { + // Step 0: Pre-processing + self.semantic.push_node(stmt); + // For functions, defer semantic syntax error checks until the body of the function is // visited if !stmt.is_function_def_stmt() { self.with_semantic_checker(|semantic, context| semantic.visit_stmt(stmt, context)); } - // Step 0: Pre-processing - self.semantic.push_node(stmt); - // For Jupyter Notebooks, we'll reset the `IMPORT_BOUNDARY` flag when // we encounter a cell boundary. if self.source_type.is_ipynb() diff --git a/crates/ruff_linter/src/rules/pyflakes/rules/break_outside_loop.rs b/crates/ruff_linter/src/rules/pyflakes/rules/break_outside_loop.rs index 88b77c2cb4..0309c0047e 100644 --- a/crates/ruff_linter/src/rules/pyflakes/rules/break_outside_loop.rs +++ b/crates/ruff_linter/src/rules/pyflakes/rules/break_outside_loop.rs @@ -1,9 +1,6 @@ -use ruff_python_ast::{self as ast, Stmt}; - use ruff_macros::{ViolationMetadata, derive_message_formats}; -use ruff_text_size::Ranged; -use crate::{Violation, checkers::ast::Checker}; +use crate::Violation; /// ## What it does /// Checks for `break` statements outside of loops. @@ -29,28 +26,3 @@ impl Violation for BreakOutsideLoop { "`break` outside loop".to_string() } } - -/// F701 -pub(crate) fn break_outside_loop<'a>( - checker: &Checker, - stmt: &'a Stmt, - parents: &mut impl Iterator, -) { - let mut child = stmt; - for parent in parents { - match parent { - Stmt::For(ast::StmtFor { orelse, .. }) | Stmt::While(ast::StmtWhile { orelse, .. }) => { - if !orelse.contains(child) { - return; - } - } - Stmt::FunctionDef(_) | Stmt::ClassDef(_) => { - break; - } - _ => {} - } - child = parent; - } - - checker.report_diagnostic(BreakOutsideLoop, stmt.range()); -} diff --git a/crates/ruff_python_parser/src/semantic_errors.rs b/crates/ruff_python_parser/src/semantic_errors.rs index 8913c5d0c1..51949f8bb4 100644 --- a/crates/ruff_python_parser/src/semantic_errors.rs +++ b/crates/ruff_python_parser/src/semantic_errors.rs @@ -224,6 +224,11 @@ impl SemanticSyntaxChecker { ); } } + Stmt::Break(ast::StmtBreak { range, .. }) => { + if !ctx.in_loop_context() { + Self::add_error(ctx, SemanticSyntaxErrorKind::BreakOutsideLoop, *range); + } + } _ => {} } @@ -1125,6 +1130,7 @@ impl Display for SemanticSyntaxError { SemanticSyntaxErrorKind::FutureFeatureNotDefined(name) => { write!(f, "Future feature `{name}` is not defined") } + SemanticSyntaxErrorKind::BreakOutsideLoop => f.write_str("`break` outside loop"), } } } @@ -1498,6 +1504,9 @@ pub enum SemanticSyntaxErrorKind { /// Represents the use of a `__future__` feature that is not defined. FutureFeatureNotDefined(String), + + /// Represents the use of a `break` statement outside of a loop. + BreakOutsideLoop, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, get_size2::GetSize)] @@ -1979,6 +1988,8 @@ pub trait SemanticSyntaxContext { fn in_notebook(&self) -> bool; fn report_semantic_error(&self, error: SemanticSyntaxError); + + fn in_loop_context(&self) -> bool; } /// Modified version of [`std::str::EscapeDefault`] that does not escape single or double quotes. diff --git a/crates/ruff_python_parser/tests/fixtures.rs b/crates/ruff_python_parser/tests/fixtures.rs index e3679ea50a..9837d9d873 100644 --- a/crates/ruff_python_parser/tests/fixtures.rs +++ b/crates/ruff_python_parser/tests/fixtures.rs @@ -571,6 +571,10 @@ impl SemanticSyntaxContext for SemanticSyntaxCheckerVisitor<'_> { fn in_generator_scope(&self) -> bool { true } + + fn in_loop_context(&self) -> bool { + true + } } impl Visitor<'_> for SemanticSyntaxCheckerVisitor<'_> { diff --git a/crates/ty_python_semantic/src/semantic_index/builder.rs b/crates/ty_python_semantic/src/semantic_index/builder.rs index b7d19d075b..01de16b8a6 100644 --- a/crates/ty_python_semantic/src/semantic_index/builder.rs +++ b/crates/ty_python_semantic/src/semantic_index/builder.rs @@ -2785,6 +2785,10 @@ impl SemanticSyntaxContext for SemanticIndexBuilder<'_, '_> { self.semantic_syntax_errors.borrow_mut().push(error); } } + + fn in_loop_context(&self) -> bool { + true + } } #[derive(Copy, Clone, Debug, PartialEq)]