diff --git a/crates/ruff/src/checkers/ast/mod.rs b/crates/ruff/src/checkers/ast/mod.rs index ce34391cdc..73000f4704 100644 --- a/crates/ruff/src/checkers/ast/mod.rs +++ b/crates/ruff/src/checkers/ast/mod.rs @@ -2407,32 +2407,7 @@ where } } } - ExprContext::Del => { - if let Some(binding_id) = self.semantic.scope().get(id) { - // If the name is unbound, then it's an error. - if self.enabled(Rule::UndefinedName) { - let binding = self.semantic.binding(binding_id); - if binding.is_unbound() { - self.diagnostics.push(Diagnostic::new( - pyflakes::rules::UndefinedName { - name: id.to_string(), - }, - expr.range(), - )); - } - } - } else { - // If the name isn't bound at all, then it's an error. - if self.enabled(Rule::UndefinedName) { - self.diagnostics.push(Diagnostic::new( - pyflakes::rules::UndefinedName { - name: id.to_string(), - }, - expr.range(), - )); - } - } - } + ExprContext::Del => {} } if self.enabled(Rule::SixPY3) { flake8_2020::rules::name_or_attribute(self, expr); @@ -4441,7 +4416,7 @@ impl<'a> Checker<'a> { let Expr::Name(ast::ExprName { id, .. }) = expr else { return; }; - self.semantic.resolve_read(id, expr.range()); + self.semantic.resolve_load(id, expr.range()); } fn handle_node_store(&mut self, id: &'a str, expr: &Expr) { @@ -4557,11 +4532,7 @@ impl<'a> Checker<'a> { return; }; - // Treat the deletion of a name as a reference to that name. - if let Some(binding_id) = self.semantic.scope().get(id) { - self.semantic - .add_local_reference(binding_id, expr.range(), ExecutionContext::Runtime); - } + self.semantic.resolve_del(id, expr.range()); if helpers::on_conditional_branch(&mut self.semantic.parents()) { return; @@ -4743,7 +4714,7 @@ impl<'a> Checker<'a> { } for reference in self.semantic.unresolved_references() { - if reference.wildcard_import() { + if reference.is_wildcard_import() { if self.enabled(Rule::UndefinedLocalWithImportStarUsage) { self.diagnostics.push(Diagnostic::new( pyflakes::rules::UndefinedLocalWithImportStarUsage { diff --git a/crates/ruff_python_semantic/src/model.rs b/crates/ruff_python_semantic/src/model.rs index 1682516b68..0d3d9a0e76 100644 --- a/crates/ruff_python_semantic/src/model.rs +++ b/crates/ruff_python_semantic/src/model.rs @@ -251,8 +251,28 @@ impl<'a> SemanticModel<'a> { .map_or(true, |binding| binding.kind.is_builtin()) } - /// Resolve a read reference to `symbol` at `range`. - pub fn resolve_read(&mut self, symbol: &str, range: TextRange) -> ReadResult { + /// Resolve a `del` reference to `symbol` at `range`. + pub fn resolve_del(&mut self, symbol: &str, range: TextRange) { + let is_unbound = self.scopes[self.scope_id] + .get(symbol) + .map_or(true, |binding_id| { + // Treat the deletion of a name as a reference to that name. + self.add_local_reference(binding_id, range, ExecutionContext::Runtime); + self.bindings[binding_id].is_unbound() + }); + + // If the binding is unbound, we need to add an unresolved reference. + if is_unbound { + self.unresolved_references.push( + range, + self.exceptions(), + UnresolvedReferenceFlags::empty(), + ); + } + } + + /// Resolve a `load` reference to `symbol` at `range`. + pub fn resolve_load(&mut self, symbol: &str, range: TextRange) -> ReadResult { // PEP 563 indicates that if a forward reference can be resolved in the module scope, we // should prefer it over local resolutions. if self.in_forward_reference() { @@ -436,7 +456,7 @@ impl<'a> SemanticModel<'a> { } } - /// Lookup a symbol in the current scope. This is a carbon copy of [`Self::resolve_read`], but + /// Lookup a symbol in the current scope. This is a carbon copy of [`Self::resolve_load`], but /// doesn't add any read references to the resolved symbol. pub fn lookup_symbol(&self, symbol: &str) -> Option { if self.in_forward_reference() { diff --git a/crates/ruff_python_semantic/src/reference.rs b/crates/ruff_python_semantic/src/reference.rs index b0cba04f02..2d3047a0f4 100644 --- a/crates/ruff_python_semantic/src/reference.rs +++ b/crates/ruff_python_semantic/src/reference.rs @@ -78,19 +78,23 @@ pub struct UnresolvedReference { } impl UnresolvedReference { + /// The range of the reference in the source code. pub const fn range(&self) -> TextRange { self.range } + /// The set of exceptions that were handled when resolution was attempted. pub const fn exceptions(&self) -> Exceptions { self.exceptions } - pub const fn wildcard_import(&self) -> bool { + /// Returns `true` if the unresolved reference may be resolved by a wildcard import. + pub const fn is_wildcard_import(&self) -> bool { self.flags .contains(UnresolvedReferenceFlags::WILDCARD_IMPORT) } + /// Returns the name of the reference. pub fn name<'a>(&self, locator: &Locator<'a>) -> &'a str { locator.slice(self.range) } @@ -99,7 +103,15 @@ impl UnresolvedReference { bitflags! { #[derive(Copy, Clone, Debug)] pub struct UnresolvedReferenceFlags: u8 { - /// The unresolved reference appeared in a context that includes a wildcard import. + /// The unresolved reference may be resolved by a wildcard import. + /// + /// For example, the reference `x` in the following code may be resolved by the wildcard + /// import of `module`: + /// ```python + /// from module import * + /// + /// print(x) + /// ``` const WILDCARD_IMPORT = 1 << 0; } }