diff --git a/crates/ruff_linter/src/checkers/ast/analyze/statement.rs b/crates/ruff_linter/src/checkers/ast/analyze/statement.rs index cded9e44e6002..f85a322aa65a8 100644 --- a/crates/ruff_linter/src/checkers/ast/analyze/statement.rs +++ b/crates/ruff_linter/src/checkers/ast/analyze/statement.rs @@ -43,9 +43,6 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) { pycodestyle::rules::ambiguous_variable_name(checker, name, name.range()); } } - if checker.is_rule_enabled(Rule::NonlocalWithoutBinding) { - pylint::rules::nonlocal_without_binding(checker, nonlocal); - } if checker.is_rule_enabled(Rule::NonlocalAndGlobal) { pylint::rules::nonlocal_and_global(checker, nonlocal); } diff --git a/crates/ruff_linter/src/checkers/ast/mod.rs b/crates/ruff_linter/src/checkers/ast/mod.rs index 7750d29f34d8a..a7139ab55de2d 100644 --- a/crates/ruff_linter/src/checkers/ast/mod.rs +++ b/crates/ruff_linter/src/checkers/ast/mod.rs @@ -73,7 +73,8 @@ use crate::rules::pyflakes::rules::{ UndefinedLocalWithNestedImportStarUsage, YieldOutsideFunction, }; use crate::rules::pylint::rules::{ - AwaitOutsideAsync, LoadBeforeGlobalDeclaration, YieldFromInAsyncFunction, + AwaitOutsideAsync, LoadBeforeGlobalDeclaration, NonlocalWithoutBinding, + YieldFromInAsyncFunction, }; use crate::rules::{flake8_pyi, flake8_type_checking, pyflakes, pyupgrade}; use crate::settings::rule_table::RuleTable; @@ -641,6 +642,10 @@ impl SemanticSyntaxContext for Checker<'_> { self.semantic.global(name) } + fn has_nonlocal_binding(&self, name: &str) -> bool { + self.semantic.nonlocal(name).is_some() + } + fn report_semantic_error(&self, error: SemanticSyntaxError) { match error.kind { SemanticSyntaxErrorKind::LateFutureImport => { @@ -717,6 +722,12 @@ impl SemanticSyntaxContext for Checker<'_> { self.report_diagnostic(pyflakes::rules::ContinueOutsideLoop, error.range); } } + SemanticSyntaxErrorKind::NonlocalWithoutBinding(name) => { + // PLE0117 + if self.is_rule_enabled(Rule::NonlocalWithoutBinding) { + self.report_diagnostic(NonlocalWithoutBinding { name }, error.range); + } + } SemanticSyntaxErrorKind::ReboundComprehensionVariable | SemanticSyntaxErrorKind::DuplicateTypeParameter | SemanticSyntaxErrorKind::MultipleCaseAssignment(_) diff --git a/crates/ruff_linter/src/rules/pylint/rules/nonlocal_without_binding.rs b/crates/ruff_linter/src/rules/pylint/rules/nonlocal_without_binding.rs index ff2bd1a2ab0de..c6e82e3958c25 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/nonlocal_without_binding.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/nonlocal_without_binding.rs @@ -1,9 +1,6 @@ use ruff_macros::{ViolationMetadata, derive_message_formats}; -use ruff_python_ast as ast; -use ruff_text_size::Ranged; use crate::Violation; -use crate::checkers::ast::Checker; /// ## What it does /// Checks for `nonlocal` names without bindings. @@ -45,19 +42,3 @@ impl Violation for NonlocalWithoutBinding { format!("Nonlocal name `{name}` found without binding") } } - -/// PLE0117 -pub(crate) fn nonlocal_without_binding(checker: &Checker, nonlocal: &ast::StmtNonlocal) { - if !checker.semantic().scope_id.is_global() { - for name in &nonlocal.names { - if checker.semantic().nonlocal(name).is_none() { - checker.report_diagnostic( - NonlocalWithoutBinding { - name: name.to_string(), - }, - name.range(), - ); - } - } - } -} diff --git a/crates/ruff_python_parser/src/semantic_errors.rs b/crates/ruff_python_parser/src/semantic_errors.rs index aba6e5ed7cd70..c475ea9d288c3 100644 --- a/crates/ruff_python_parser/src/semantic_errors.rs +++ b/crates/ruff_python_parser/src/semantic_errors.rs @@ -219,7 +219,7 @@ impl SemanticSyntaxChecker { AwaitOutsideAsyncFunctionKind::AsyncWith, ); } - Stmt::Nonlocal(ast::StmtNonlocal { range, .. }) => { + Stmt::Nonlocal(ast::StmtNonlocal { names, range, .. }) => { // test_ok nonlocal_declaration_at_module_level // def _(): // nonlocal x @@ -234,6 +234,18 @@ impl SemanticSyntaxChecker { *range, ); } + + if !ctx.in_module_scope() { + for name in names { + if !ctx.has_nonlocal_binding(name) { + Self::add_error( + ctx, + SemanticSyntaxErrorKind::NonlocalWithoutBinding(name.to_string()), + name.range, + ); + } + } + } } Stmt::Break(ast::StmtBreak { range, .. }) => { if !ctx.in_loop_context() { @@ -1154,6 +1166,9 @@ impl Display for SemanticSyntaxError { SemanticSyntaxErrorKind::DifferentMatchPatternBindings => { write!(f, "alternative patterns bind different names") } + SemanticSyntaxErrorKind::NonlocalWithoutBinding(name) => { + write!(f, "no binding for nonlocal `{name}` found") + } } } } @@ -1554,6 +1569,9 @@ pub enum SemanticSyntaxErrorKind { /// ... /// ``` DifferentMatchPatternBindings, + + /// Represents a nonlocal statement for a name that has no binding in an enclosing scope. + NonlocalWithoutBinding(String), } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, get_size2::GetSize)] @@ -1994,6 +2012,9 @@ pub trait SemanticSyntaxContext { /// Return the [`TextRange`] at which a name is declared as `global` in the current scope. fn global(&self, name: &str) -> Option; + /// Returns `true` if `name` has a binding in an enclosing scope. + fn has_nonlocal_binding(&self, name: &str) -> bool; + /// Returns `true` if the visitor is currently in an async context, i.e. an async function. fn in_async_context(&self) -> bool; diff --git a/crates/ruff_python_parser/tests/fixtures.rs b/crates/ruff_python_parser/tests/fixtures.rs index c646fe525b9c2..2de49e6d68410 100644 --- a/crates/ruff_python_parser/tests/fixtures.rs +++ b/crates/ruff_python_parser/tests/fixtures.rs @@ -527,6 +527,10 @@ impl SemanticSyntaxContext for SemanticSyntaxCheckerVisitor<'_> { None } + fn has_nonlocal_binding(&self, _name: &str) -> bool { + true + } + fn in_async_context(&self) -> bool { if let Some(scope) = self.scopes.iter().next_back() { match scope { diff --git a/crates/ty_python_semantic/src/semantic_index/builder.rs b/crates/ty_python_semantic/src/semantic_index/builder.rs index 8107f9c122ece..72f262b405bbb 100644 --- a/crates/ty_python_semantic/src/semantic_index/builder.rs +++ b/crates/ty_python_semantic/src/semantic_index/builder.rs @@ -2697,6 +2697,12 @@ impl SemanticSyntaxContext for SemanticIndexBuilder<'_, '_> { None } + // We handle the one syntax error that relies on this method (`NonlocalWithoutBinding`) directly + // in `TypeInferenceBuilder::infer_nonlocal_statement`, so this just returns `true`. + fn has_nonlocal_binding(&self, _name: &str) -> bool { + true + } + fn in_async_context(&self) -> bool { for scope_info in self.scope_stack.iter().rev() { let scope = &self.scopes[scope_info.file_scope_id];