diff --git a/crates/ruff_linter/src/checkers/ast/mod.rs b/crates/ruff_linter/src/checkers/ast/mod.rs index 30c48d23441df..f45fc86591f9c 100644 --- a/crates/ruff_linter/src/checkers/ast/mod.rs +++ b/crates/ruff_linter/src/checkers/ast/mod.rs @@ -1461,7 +1461,10 @@ impl<'a> Visitor<'a> for Checker<'a> { } self.semantic.set_branch(branch); + let flags_snapshot = self.semantic.flags; + self.semantic.flags |= SemanticModelFlags::ORELSE; self.visit_body(orelse); + self.semantic.flags = flags_snapshot; self.semantic.pop_branch(); self.semantic.push_branch(); @@ -1557,7 +1560,27 @@ impl<'a> Visitor<'a> for Checker<'a> { }) => { self.visit_boolean_test(test); self.visit_body(body); + let flags_snapshot = self.semantic.flags; + self.semantic.flags |= SemanticModelFlags::ORELSE; self.visit_body(orelse); + self.semantic.flags = flags_snapshot; + } + Stmt::For(ast::StmtFor { + node_index: _, + range: _, + is_async: _, + target, + iter, + body, + orelse, + }) => { + self.visit_expr(iter); + self.visit_expr(target); + self.visit_body(body); + let flags_snapshot = self.semantic.flags; + self.semantic.flags |= SemanticModelFlags::ORELSE; + self.visit_body(orelse); + self.semantic.flags = flags_snapshot; } Stmt::If( stmt_if @ ast::StmtIf { @@ -1786,7 +1809,10 @@ impl<'a> Visitor<'a> for Checker<'a> { }) => { self.visit_boolean_test(test); self.visit_expr(body); + let flags_snapshot = self.semantic.flags; + self.semantic.flags |= SemanticModelFlags::ORELSE; self.visit_expr(orelse); + self.semantic.flags = flags_snapshot; } Expr::UnaryOp(ast::ExprUnaryOp { op: UnaryOp::Not, @@ -2866,7 +2892,10 @@ impl<'a> Checker<'a> { self.semantic.resolve_del(id, expr.range()); - if helpers::on_conditional_branch(&mut self.semantic.current_statements()) { + if helpers::on_conditional_branch(&mut self.semantic.current_statements()) + || self.semantic.in_exception_handler() + || self.semantic.in_orelse() + { return; } diff --git a/crates/ruff_linter/src/renamer.rs b/crates/ruff_linter/src/renamer.rs index d4c587142dbeb..c5b93711e27ef 100644 --- a/crates/ruff_linter/src/renamer.rs +++ b/crates/ruff_linter/src/renamer.rs @@ -372,7 +372,6 @@ impl Renamer { | BindingKind::ClassDefinition(_) | BindingKind::FunctionDefinition(_) | BindingKind::Deletion - | BindingKind::ConditionalDeletion(_) | BindingKind::UnboundException(_) => { Some(Edit::range_replacement(target.to_string(), binding.range())) } diff --git a/crates/ruff_linter/src/rules/pyflakes/mod.rs b/crates/ruff_linter/src/rules/pyflakes/mod.rs index 6ce7faa99e756..bffe7db9128b1 100644 --- a/crates/ruff_linter/src/rules/pyflakes/mod.rs +++ b/crates/ruff_linter/src/rules/pyflakes/mod.rs @@ -1372,6 +1372,43 @@ mod tests { ); } + #[test] + fn del_conditional_except() { + // Ignores conditional bindings deletion. + flakes( + r" + context = None + test = True + try: + ... + except Exception: + del(test) + else: + assert(test) + ", + &[], + ); + } + + #[test] + fn del_conditional_orelse() { + // Ignores conditional bindings deletion. + flakes( + r" + context = None + test = True + try: + ... + except Exception: + print(test) + else: + del test + assert(test) + ", + &[], + ); + } + #[test] fn del_conditional_nested() { // Ignored conditional bindings deletion even if they are nested in other diff --git a/crates/ruff_linter/src/rules/pylint/rules/non_ascii_name.rs b/crates/ruff_linter/src/rules/pylint/rules/non_ascii_name.rs index 9be3a4e377c2a..97584abf9ef37 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/non_ascii_name.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/non_ascii_name.rs @@ -73,7 +73,6 @@ pub(crate) fn non_ascii_name(checker: &Checker, binding: &Binding) { | BindingKind::FromImport(_) | BindingKind::SubmoduleImport(_) | BindingKind::Deletion - | BindingKind::ConditionalDeletion(_) | BindingKind::UnboundException(_) | BindingKind::DunderClassCell => { return; diff --git a/crates/ruff_linter/src/rules/ruff/rules/used_dummy_variable.rs b/crates/ruff_linter/src/rules/ruff/rules/used_dummy_variable.rs index 88f408164af94..343249a9a7f07 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/used_dummy_variable.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/used_dummy_variable.rs @@ -149,7 +149,6 @@ pub(crate) fn used_dummy_variable(checker: &Checker, binding: &Binding, binding_ | BindingKind::FromImport(_) | BindingKind::SubmoduleImport(_) | BindingKind::Deletion - | BindingKind::ConditionalDeletion(_) | BindingKind::DunderClassCell => return, } diff --git a/crates/ruff_python_semantic/src/binding.rs b/crates/ruff_python_semantic/src/binding.rs index 9254a5b528560..5375ef6d85d10 100644 --- a/crates/ruff_python_semantic/src/binding.rs +++ b/crates/ruff_python_semantic/src/binding.rs @@ -230,7 +230,6 @@ impl<'a> Binding<'a> { // Deletions, annotations, `__future__` imports, and builtins are never considered // redefinitions. BindingKind::Deletion - | BindingKind::ConditionalDeletion(_) | BindingKind::Annotation | BindingKind::FutureImport | BindingKind::Builtin => { @@ -642,13 +641,6 @@ pub enum BindingKind<'a> { /// ``` Deletion, - /// A binding for a deletion, like `x` in: - /// ```python - /// if x > 0: - /// del x - /// ``` - ConditionalDeletion(BindingId), - /// A binding to bind an exception to a local variable, like `x` in: /// ```python /// try: diff --git a/crates/ruff_python_semantic/src/model.rs b/crates/ruff_python_semantic/src/model.rs index a64a413866263..4366d042615ba 100644 --- a/crates/ruff_python_semantic/src/model.rs +++ b/crates/ruff_python_semantic/src/model.rs @@ -552,15 +552,6 @@ impl<'a> SemanticModel<'a> { return ReadResult::UnboundLocal(binding_id); } - BindingKind::ConditionalDeletion(binding_id) => { - self.unresolved_references.push( - name.range, - self.exceptions(), - UnresolvedReferenceFlags::empty(), - ); - return ReadResult::UnboundLocal(binding_id); - } - // If we hit an unbound exception that shadowed a bound name, resole to the // bound name. For example, given: // @@ -712,7 +703,6 @@ impl<'a> SemanticModel<'a> { match self.bindings[binding_id].kind { BindingKind::Annotation => continue, BindingKind::Deletion | BindingKind::UnboundException(None) => return None, - BindingKind::ConditionalDeletion(binding_id) => return Some(binding_id), BindingKind::UnboundException(Some(binding_id)) => return Some(binding_id), _ => return Some(binding_id), } @@ -845,8 +835,7 @@ impl<'a> SemanticModel<'a> { } if let BindingKind::Annotation | BindingKind::Deletion - | BindingKind::UnboundException(..) - | BindingKind::ConditionalDeletion(..) = binding.kind + | BindingKind::UnboundException(..) = binding.kind { continue; } @@ -870,7 +859,6 @@ impl<'a> SemanticModel<'a> { let candidate_id = match self.bindings[binding_id].kind { BindingKind::Annotation => continue, BindingKind::Deletion | BindingKind::UnboundException(None) => return None, - BindingKind::ConditionalDeletion(binding_id) => binding_id, BindingKind::UnboundException(Some(binding_id)) => binding_id, _ => binding_id, }; @@ -2033,6 +2021,11 @@ impl<'a> SemanticModel<'a> { self.flags.intersects(SemanticModelFlags::EXCEPTION_HANDLER) } + /// Return `true` if the model is in an exception handler. + pub const fn in_orelse(&self) -> bool { + self.flags.intersects(SemanticModelFlags::ORELSE) + } + /// Return `true` if the model is in an `assert` statement. pub const fn in_assert_statement(&self) -> bool { self.flags.intersects(SemanticModelFlags::ASSERT_STATEMENT) @@ -2700,6 +2693,19 @@ bitflags! { /// ``` const T_STRING = 1 << 29; + /// The model is in the body of an `else` clause. + /// + /// For example, the model could be visiting `x` in: + /// ```python + /// try: + /// ... + /// except Exception: + /// ... + /// else: + /// print(x) + /// ``` + const ORELSE = 1 << 30; + /// The context is in any type annotation. const ANNOTATION = Self::TYPING_ONLY_ANNOTATION.bits() | Self::RUNTIME_EVALUATED_ANNOTATION.bits() | Self::RUNTIME_REQUIRED_ANNOTATION.bits();