diff --git a/crates/ruff/src/checkers/ast/mod.rs b/crates/ruff/src/checkers/ast/mod.rs index f1809dda7b..e5bbc47130 100644 --- a/crates/ruff/src/checkers/ast/mod.rs +++ b/crates/ruff/src/checkers/ast/mod.rs @@ -50,8 +50,8 @@ use ruff_python_ast::{cast, helpers, str, visitor}; use ruff_python_semantic::analyze::{branch_detection, typing, visibility}; use ruff_python_semantic::{ Binding, BindingFlags, BindingId, BindingKind, ContextualizedDefinition, Exceptions, - ExecutionContext, Export, FromImport, Globals, Import, Module, ModuleKind, ResolvedRead, Scope, - ScopeId, ScopeKind, SemanticModel, SemanticModelFlags, StarImport, SubmoduleImport, + ExecutionContext, Export, FromImport, Globals, Import, Module, ModuleKind, ScopeId, ScopeKind, + SemanticModel, SemanticModelFlags, StarImport, SubmoduleImport, }; use ruff_python_stdlib::builtins::{BUILTINS, MAGIC_GLOBALS}; use ruff_python_stdlib::path::is_python_stub_file; @@ -4428,55 +4428,7 @@ impl<'a> Checker<'a> { let Expr::Name(ast::ExprName { id, .. }) = expr else { return; }; - match self.semantic.resolve_read(id, expr.range()) { - ResolvedRead::Resolved(_) | ResolvedRead::ImplicitGlobal => { - // Nothing to do. - } - ResolvedRead::WildcardImport => { - // F405 - if self.enabled(Rule::UndefinedLocalWithImportStarUsage) { - let sources: Vec = self - .semantic - .scopes - .iter() - .flat_map(Scope::star_imports) - .map(|StarImport { level, module }| { - helpers::format_import_from(*level, *module) - }) - .sorted() - .dedup() - .collect(); - self.diagnostics.push(Diagnostic::new( - pyflakes::rules::UndefinedLocalWithImportStarUsage { - name: id.to_string(), - sources, - }, - expr.range(), - )); - } - } - ResolvedRead::NotFound | ResolvedRead::UnboundLocal(_) => { - // F821 - if self.enabled(Rule::UndefinedName) { - // Allow __path__. - if self.path.ends_with("__init__.py") && id == "__path__" { - return; - } - - // Avoid flagging if `NameError` is handled. - if self.semantic.exceptions().contains(Exceptions::NAME_ERROR) { - return; - } - - self.diagnostics.push(Diagnostic::new( - pyflakes::rules::UndefinedName { - name: id.to_string(), - }, - expr.range(), - )); - } - } - } + self.semantic.resolve_read(id, expr.range()); } fn handle_node_store(&mut self, id: &'a str, expr: &Expr) { @@ -4771,6 +4723,47 @@ impl<'a> Checker<'a> { } } + /// Run any lint rules that operate over a single [`UnresolvedReference`]. + fn check_unresolved_references(&mut self) { + if !self.any_enabled(&[Rule::UndefinedLocalWithImportStarUsage, Rule::UndefinedName]) { + return; + } + + for reference in self.semantic.unresolved_references() { + if reference.wildcard_import() { + if self.enabled(Rule::UndefinedLocalWithImportStarUsage) { + self.diagnostics.push(Diagnostic::new( + pyflakes::rules::UndefinedLocalWithImportStarUsage { + name: reference.name(self.locator).to_string(), + }, + reference.range(), + )); + } + } else { + if self.enabled(Rule::UndefinedName) { + // Avoid flagging if `NameError` is handled. + if reference.exceptions().contains(Exceptions::NAME_ERROR) { + continue; + } + + // Allow __path__. + if self.path.ends_with("__init__.py") { + if reference.name(self.locator) == "__path__" { + continue; + } + } + + self.diagnostics.push(Diagnostic::new( + pyflakes::rules::UndefinedName { + name: reference.name(self.locator).to_string(), + }, + reference.range(), + )); + } + } + } + } + /// Run any lint rules that operate over a single [`Binding`]. fn check_bindings(&mut self) { if !self.any_enabled(&[ @@ -4838,6 +4831,55 @@ impl<'a> Checker<'a> { } } + /// Run any lint rules that operate over the module exports (i.e., members of `__all__`). + fn check_exports(&mut self) { + let exports: Vec<(&str, TextRange)> = self + .semantic + .global_scope() + .get_all("__all__") + .map(|binding_id| &self.semantic.bindings[binding_id]) + .filter_map(|binding| match &binding.kind { + BindingKind::Export(Export { names }) => { + Some(names.iter().map(|name| (*name, binding.range))) + } + _ => None, + }) + .flatten() + .collect(); + + for (name, range) in exports { + if let Some(binding_id) = self.semantic.global_scope().get(name) { + // Mark anything referenced in `__all__` as used. + self.semantic + .add_global_reference(binding_id, range, ExecutionContext::Runtime); + } else { + if self.semantic.global_scope().uses_star_imports() { + // F405 + if self.enabled(Rule::UndefinedLocalWithImportStarUsage) { + self.diagnostics.push(Diagnostic::new( + pyflakes::rules::UndefinedLocalWithImportStarUsage { + name: (*name).to_string(), + }, + range, + )); + } + } else { + // F822 + if self.enabled(Rule::UndefinedExport) { + if !self.path.ends_with("__init__.py") { + self.diagnostics.push(Diagnostic::new( + pyflakes::rules::UndefinedExport { + name: (*name).to_string(), + }, + range, + )); + } + } + } + } + } + } + fn check_deferred_scopes(&mut self) { if !self.any_enabled(&[ Rule::GlobalVariableNotAssigned, @@ -4847,37 +4889,11 @@ impl<'a> Checker<'a> { Rule::TypingOnlyFirstPartyImport, Rule::TypingOnlyStandardLibraryImport, Rule::TypingOnlyThirdPartyImport, - Rule::UndefinedExport, - Rule::UndefinedLocalWithImportStarUsage, - Rule::UndefinedLocalWithImportStarUsage, Rule::UnusedImport, ]) { return; } - // Mark anything referenced in `__all__` as used. - let exports: Vec<(&str, TextRange)> = { - self.semantic - .global_scope() - .get_all("__all__") - .map(|binding_id| &self.semantic.bindings[binding_id]) - .filter_map(|binding| match &binding.kind { - BindingKind::Export(Export { names }) => { - Some(names.iter().map(|name| (*name, binding.range))) - } - _ => None, - }) - .flatten() - .collect() - }; - - for (name, range) in &exports { - if let Some(binding_id) = self.semantic.global_scope().get(name) { - self.semantic - .add_global_reference(binding_id, *range, ExecutionContext::Runtime); - } - } - // Identify any valid runtime imports. If a module is imported at runtime, and // used at runtime, then by default, we avoid flagging any other // imports from that model as typing-only. @@ -4920,43 +4936,6 @@ impl<'a> Checker<'a> { for scope_id in self.deferred.scopes.iter().rev().copied() { let scope = &self.semantic.scopes[scope_id]; - if scope.kind.is_module() { - // F822 - if self.enabled(Rule::UndefinedExport) { - if !self.path.ends_with("__init__.py") { - for (name, range) in &exports { - diagnostics - .extend(pyflakes::rules::undefined_export(name, *range, scope)); - } - } - } - - // F405 - if self.enabled(Rule::UndefinedLocalWithImportStarUsage) { - let sources: Vec = scope - .star_imports() - .map(|StarImport { level, module }| { - helpers::format_import_from(*level, *module) - }) - .sorted() - .dedup() - .collect(); - if !sources.is_empty() { - for (name, range) in &exports { - if !scope.has(name) { - diagnostics.push(Diagnostic::new( - pyflakes::rules::UndefinedLocalWithImportStarUsage { - name: (*name).to_string(), - sources: sources.clone(), - }, - *range, - )); - } - } - } - } - } - // PLW0602 if self.enabled(Rule::GlobalVariableNotAssigned) { for (name, binding_id) in scope.bindings() { @@ -5226,8 +5205,8 @@ impl<'a> Checker<'a> { // Compute visibility of all definitions. let exports: Option> = { - let global_scope = self.semantic.global_scope(); - global_scope + self.semantic + .global_scope() .get_all("__all__") .map(|binding_id| &self.semantic.bindings[binding_id]) .filter_map(|binding| match &binding.kind { @@ -5490,14 +5469,16 @@ pub(crate) fn check_ast( checker.check_deferred_assignments(); checker.check_deferred_for_loops(); - // Check docstrings. + // Check docstrings, exports, bindings, and unresolved references. checker.check_definitions(); + checker.check_exports(); + checker.check_bindings(); + checker.check_unresolved_references(); // Reset the scope to module-level, and check all consumed scopes. checker.semantic.scope_id = ScopeId::global(); checker.deferred.scopes.push(ScopeId::global()); checker.check_deferred_scopes(); - checker.check_bindings(); checker.diagnostics } diff --git a/crates/ruff/src/rules/flake8_type_checking/rules/runtime_import_in_type_checking_block.rs b/crates/ruff/src/rules/flake8_type_checking/rules/runtime_import_in_type_checking_block.rs index da9795bec7..c20460e8a7 100644 --- a/crates/ruff/src/rules/flake8_type_checking/rules/runtime_import_in_type_checking_block.rs +++ b/crates/ruff/src/rules/flake8_type_checking/rules/runtime_import_in_type_checking_block.rs @@ -4,7 +4,7 @@ use rustc_hash::FxHashMap; use ruff_diagnostics::{AutofixKind, Diagnostic, Fix, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_semantic::{NodeId, ReferenceId, Scope}; +use ruff_python_semantic::{NodeId, ResolvedReferenceId, Scope}; use crate::autofix; use crate::checkers::ast::Checker; @@ -180,7 +180,7 @@ struct Import<'a> { /// The qualified name of the import (e.g., `typing.List` for `from typing import List`). qualified_name: &'a str, /// The first reference to the imported symbol. - reference_id: ReferenceId, + reference_id: ResolvedReferenceId, /// The trimmed range of the import (e.g., `List` in `from typing import List`). range: TextRange, /// The range of the import's parent statement. diff --git a/crates/ruff/src/rules/flake8_type_checking/rules/typing_only_runtime_import.rs b/crates/ruff/src/rules/flake8_type_checking/rules/typing_only_runtime_import.rs index 5d1ee695bd..3b82d5e6df 100644 --- a/crates/ruff/src/rules/flake8_type_checking/rules/typing_only_runtime_import.rs +++ b/crates/ruff/src/rules/flake8_type_checking/rules/typing_only_runtime_import.rs @@ -4,7 +4,7 @@ use rustc_hash::FxHashMap; use ruff_diagnostics::{AutofixKind, Diagnostic, DiagnosticKind, Fix, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_semantic::{Binding, NodeId, ReferenceId, Scope}; +use ruff_python_semantic::{Binding, NodeId, ResolvedReferenceId, Scope}; use crate::autofix; use crate::checkers::ast::Checker; @@ -357,7 +357,7 @@ struct Import<'a> { /// The qualified name of the import (e.g., `typing.List` for `from typing import List`). qualified_name: &'a str, /// The first reference to the imported symbol. - reference_id: ReferenceId, + reference_id: ResolvedReferenceId, /// The trimmed range of the import (e.g., `List` in `from typing import List`). range: TextRange, /// The range of the import's parent statement. diff --git a/crates/ruff/src/rules/pyflakes/mod.rs b/crates/ruff/src/rules/pyflakes/mod.rs index 885b4baf6b..1853cba50c 100644 --- a/crates/ruff/src/rules/pyflakes/mod.rs +++ b/crates/ruff/src/rules/pyflakes/mod.rs @@ -1024,11 +1024,11 @@ mod tests { e[any] = 5 "#, &[ - Rule::UndefinedName, Rule::UndefinedName, Rule::UndefinedName, Rule::UnusedVariable, Rule::UndefinedName, + Rule::UndefinedName, ], ); } @@ -2115,7 +2115,7 @@ mod tests { try: pass except Exception as fu: pass "#, - &[Rule::RedefinedWhileUnused, Rule::UnusedVariable], + &[Rule::UnusedVariable, Rule::RedefinedWhileUnused], ); } diff --git a/crates/ruff/src/rules/pyflakes/rules/imports.rs b/crates/ruff/src/rules/pyflakes/rules/imports.rs index b53b9394f1..4f6be6c9ab 100644 --- a/crates/ruff/src/rules/pyflakes/rules/imports.rs +++ b/crates/ruff/src/rules/pyflakes/rules/imports.rs @@ -1,5 +1,3 @@ -use itertools::Itertools; - use ruff_diagnostics::Violation; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::source_code::OneIndexed; @@ -157,18 +155,13 @@ impl Violation for LateFutureImport { #[violation] pub struct UndefinedLocalWithImportStarUsage { pub(crate) name: String, - pub(crate) sources: Vec, } impl Violation for UndefinedLocalWithImportStarUsage { #[derive_message_formats] fn message(&self) -> String { - let UndefinedLocalWithImportStarUsage { name, sources } = self; - let sources = sources - .iter() - .map(|source| format!("`{source}`")) - .join(", "); - format!("`{name}` may be undefined, or defined from star imports: {sources}") + let UndefinedLocalWithImportStarUsage { name } = self; + format!("`{name}` may be undefined, or defined from star imports") } } diff --git a/crates/ruff/src/rules/pyflakes/rules/undefined_export.rs b/crates/ruff/src/rules/pyflakes/rules/undefined_export.rs index 46e9cc5a87..6858f1124c 100644 --- a/crates/ruff/src/rules/pyflakes/rules/undefined_export.rs +++ b/crates/ruff/src/rules/pyflakes/rules/undefined_export.rs @@ -1,8 +1,5 @@ -use ruff_text_size::TextRange; - -use ruff_diagnostics::{Diagnostic, Violation}; +use ruff_diagnostics::Violation; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_semantic::Scope; /// ## What it does /// Checks for undefined names in `__all__`. @@ -36,7 +33,7 @@ use ruff_python_semantic::Scope; /// - [Python documentation: `__all__`](https://docs.python.org/3/tutorial/modules.html#importing-from-a-package) #[violation] pub struct UndefinedExport { - name: String, + pub name: String, } impl Violation for UndefinedExport { @@ -46,19 +43,3 @@ impl Violation for UndefinedExport { format!("Undefined name `{name}` in `__all__`") } } - -/// F822 -pub(crate) fn undefined_export(name: &str, range: TextRange, scope: &Scope) -> Vec { - let mut diagnostics = Vec::new(); - if !scope.uses_star_imports() { - if !scope.has(name) { - diagnostics.push(Diagnostic::new( - UndefinedExport { - name: (*name).to_string(), - }, - range, - )); - } - } - diagnostics -} diff --git a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F405_F405.py.snap b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F405_F405.py.snap index 867f503836..218b8dc5e2 100644 --- a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F405_F405.py.snap +++ b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F405_F405.py.snap @@ -1,14 +1,14 @@ --- source: crates/ruff/src/rules/pyflakes/mod.rs --- -F405.py:5:11: F405 `name` may be undefined, or defined from star imports: `mymodule` +F405.py:5:11: F405 `name` may be undefined, or defined from star imports | 4 | def print_name(): 5 | print(name) | ^^^^ F405 | -F405.py:11:1: F405 `a` may be undefined, or defined from star imports: `mymodule` +F405.py:11:1: F405 `a` may be undefined, or defined from star imports | 9 | print(name) 10 | diff --git a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__augmented_assignment_after_del.snap b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__augmented_assignment_after_del.snap index 57a88f04c4..bae62655e8 100644 --- a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__augmented_assignment_after_del.snap +++ b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__augmented_assignment_after_del.snap @@ -1,14 +1,6 @@ --- source: crates/ruff/src/rules/pyflakes/mod.rs --- -:10:5: F821 Undefined name `x` - | - 8 | # entirely after the `del` statement. However, it should be an F821 - 9 | # error, because the name is defined in the scope, but unbound. -10 | x += 1 - | ^ F821 - | - :10:5: F841 Local variable `x` is assigned to but never used | 8 | # entirely after the `del` statement. However, it should be an F821 @@ -18,4 +10,12 @@ source: crates/ruff/src/rules/pyflakes/mod.rs | = help: Remove assignment to unused variable `x` +:10:5: F821 Undefined name `x` + | + 8 | # entirely after the `del` statement. However, it should be an F821 + 9 | # error, because the name is defined in the scope, but unbound. +10 | x += 1 + | ^ F821 + | + diff --git a/crates/ruff_python_semantic/src/binding.rs b/crates/ruff_python_semantic/src/binding.rs index 50bd684fe5..1f47db394f 100644 --- a/crates/ruff_python_semantic/src/binding.rs +++ b/crates/ruff_python_semantic/src/binding.rs @@ -10,7 +10,7 @@ use ruff_python_ast::source_code::Locator; use crate::context::ExecutionContext; use crate::model::SemanticModel; use crate::node::NodeId; -use crate::reference::ReferenceId; +use crate::reference::ResolvedReferenceId; use crate::ScopeId; #[derive(Debug, Clone)] @@ -24,7 +24,7 @@ pub struct Binding<'a> { /// The statement in which the [`Binding`] was defined. pub source: Option, /// The references to the [`Binding`]. - pub references: Vec, + pub references: Vec, /// The exceptions that were handled when the [`Binding`] was defined. pub exceptions: Exceptions, /// Flags for the [`Binding`]. @@ -38,7 +38,7 @@ impl<'a> Binding<'a> { } /// Returns an iterator over all references for the current [`Binding`]. - pub fn references(&self) -> impl Iterator + '_ { + pub fn references(&self) -> impl Iterator + '_ { self.references.iter().copied() } diff --git a/crates/ruff_python_semantic/src/model.rs b/crates/ruff_python_semantic/src/model.rs index d1aba8460d..1682516b68 100644 --- a/crates/ruff_python_semantic/src/model.rs +++ b/crates/ruff_python_semantic/src/model.rs @@ -20,8 +20,11 @@ use crate::context::ExecutionContext; use crate::definition::{Definition, DefinitionId, Definitions, Member, Module}; use crate::globals::{Globals, GlobalsArena}; use crate::node::{NodeId, Nodes}; -use crate::reference::{Reference, ReferenceId, References}; +use crate::reference::{ + ResolvedReference, ResolvedReferenceId, ResolvedReferences, UnresolvedReferences, +}; use crate::scope::{Scope, ScopeId, ScopeKind, Scopes}; +use crate::{UnresolvedReference, UnresolvedReferenceFlags}; /// A semantic model for a Python module, to enable querying the module's semantic information. pub struct SemanticModel<'a> { @@ -51,7 +54,10 @@ pub struct SemanticModel<'a> { pub bindings: Bindings<'a>, /// Stack of all references created in any scope, at any point in execution. - references: References, + resolved_references: ResolvedReferences, + + /// Stack of all unresolved references created in any scope, at any point in execution. + unresolved_references: UnresolvedReferences, /// Arena of global bindings. globals: GlobalsArena<'a>, @@ -128,7 +134,8 @@ impl<'a> SemanticModel<'a> { definitions: Definitions::for_module(module), definition_id: DefinitionId::module(), bindings: Bindings::default(), - references: References::default(), + resolved_references: ResolvedReferences::default(), + unresolved_references: UnresolvedReferences::default(), globals: GlobalsArena::default(), shadowed_bindings: IntMap::default(), delayed_annotations: IntMap::default(), @@ -144,10 +151,10 @@ impl<'a> SemanticModel<'a> { &self.bindings[id] } - /// Resolve the [`Reference`] for the given [`ReferenceId`]. + /// Resolve the [`ResolvedReference`] for the given [`ResolvedReferenceId`]. #[inline] - pub fn reference(&self, id: ReferenceId) -> &Reference { - &self.references[id] + pub fn reference(&self, id: ResolvedReferenceId) -> &ResolvedReference { + &self.resolved_references[id] } /// Return `true` if the `Expr` is a reference to `typing.${target}`. @@ -245,7 +252,7 @@ impl<'a> SemanticModel<'a> { } /// Resolve a read reference to `symbol` at `range`. - pub fn resolve_read(&mut self, symbol: &str, range: TextRange) -> ResolvedRead { + pub fn resolve_read(&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() { @@ -253,18 +260,22 @@ impl<'a> SemanticModel<'a> { if !self.bindings[binding_id].is_unbound() { // Mark the binding as used. let context = self.execution_context(); - let reference_id = self.references.push(ScopeId::global(), range, context); + let reference_id = + self.resolved_references + .push(ScopeId::global(), range, context); self.bindings[binding_id].references.push(reference_id); // Mark any submodule aliases as used. if let Some(binding_id) = self.resolve_submodule(symbol, ScopeId::global(), binding_id) { - let reference_id = self.references.push(ScopeId::global(), range, context); + let reference_id = + self.resolved_references + .push(ScopeId::global(), range, context); self.bindings[binding_id].references.push(reference_id); } - return ResolvedRead::Resolved(binding_id); + return ReadResult::Resolved(binding_id); } } } @@ -282,7 +293,7 @@ impl<'a> SemanticModel<'a> { // print(__class__) // ``` if seen_function && matches!(symbol, "__class__") { - return ResolvedRead::ImplicitGlobal; + return ReadResult::ImplicitGlobal; } if index > 0 { continue; @@ -292,12 +303,12 @@ impl<'a> SemanticModel<'a> { if let Some(binding_id) = scope.get(symbol) { // Mark the binding as used. let context = self.execution_context(); - let reference_id = self.references.push(self.scope_id, range, context); + let reference_id = self.resolved_references.push(self.scope_id, range, context); self.bindings[binding_id].references.push(reference_id); // Mark any submodule aliases as used. if let Some(binding_id) = self.resolve_submodule(symbol, scope_id, binding_id) { - let reference_id = self.references.push(self.scope_id, range, context); + let reference_id = self.resolved_references.push(self.scope_id, range, context); self.bindings[binding_id].references.push(reference_id); } @@ -336,7 +347,12 @@ impl<'a> SemanticModel<'a> { // // The `x` in `print(x)` should be treated as unresolved. BindingKind::Deletion | BindingKind::UnboundException(None) => { - return ResolvedRead::UnboundLocal(binding_id) + self.unresolved_references.push( + range, + self.exceptions(), + UnresolvedReferenceFlags::empty(), + ); + return ReadResult::UnboundLocal(binding_id); } // If we hit an unbound exception that shadowed a bound name, resole to the @@ -357,22 +373,24 @@ impl<'a> SemanticModel<'a> { BindingKind::UnboundException(Some(binding_id)) => { // Mark the binding as used. let context = self.execution_context(); - let reference_id = self.references.push(self.scope_id, range, context); + let reference_id = + self.resolved_references.push(self.scope_id, range, context); self.bindings[binding_id].references.push(reference_id); // Mark any submodule aliases as used. if let Some(binding_id) = self.resolve_submodule(symbol, scope_id, binding_id) { - let reference_id = self.references.push(self.scope_id, range, context); + let reference_id = + self.resolved_references.push(self.scope_id, range, context); self.bindings[binding_id].references.push(reference_id); } - return ResolvedRead::Resolved(binding_id); + return ReadResult::Resolved(binding_id); } // Otherwise, treat it as resolved. - _ => return ResolvedRead::Resolved(binding_id), + _ => return ReadResult::Resolved(binding_id), } } @@ -393,7 +411,7 @@ impl<'a> SemanticModel<'a> { // ``` if index == 0 && scope.kind.is_class() { if matches!(symbol, "__module__" | "__qualname__") { - return ResolvedRead::ImplicitGlobal; + return ReadResult::ImplicitGlobal; } } @@ -402,9 +420,19 @@ impl<'a> SemanticModel<'a> { } if import_starred { - ResolvedRead::WildcardImport + self.unresolved_references.push( + range, + self.exceptions(), + UnresolvedReferenceFlags::WILDCARD_IMPORT, + ); + ReadResult::WildcardImport } else { - ResolvedRead::NotFound + self.unresolved_references.push( + range, + self.exceptions(), + UnresolvedReferenceFlags::empty(), + ); + ReadResult::NotFound } } @@ -875,7 +903,7 @@ impl<'a> SemanticModel<'a> { range: TextRange, context: ExecutionContext, ) { - let reference_id = self.references.push(self.scope_id, range, context); + let reference_id = self.resolved_references.push(self.scope_id, range, context); self.bindings[binding_id].references.push(reference_id); } @@ -886,7 +914,9 @@ impl<'a> SemanticModel<'a> { range: TextRange, context: ExecutionContext, ) { - let reference_id = self.references.push(ScopeId::global(), range, context); + let reference_id = self + .resolved_references + .push(ScopeId::global(), range, context); self.bindings[binding_id].references.push(reference_id); } @@ -918,6 +948,11 @@ impl<'a> SemanticModel<'a> { self.rebinding_scopes.get(&binding_id).map(Vec::as_slice) } + /// Return an iterator over all [`UnresolvedReference`]s in the semantic model. + pub fn unresolved_references(&self) -> impl Iterator { + self.unresolved_references.iter() + } + /// Return the [`ExecutionContext`] of the current scope. pub const fn execution_context(&self) -> ExecutionContext { if self.in_type_checking_block() @@ -1360,7 +1395,7 @@ pub struct Snapshot { } #[derive(Debug)] -pub enum ResolvedRead { +pub enum ReadResult { /// The read reference is resolved to a specific binding. /// /// For example, given: diff --git a/crates/ruff_python_semantic/src/reference.rs b/crates/ruff_python_semantic/src/reference.rs index d19b03194a..b0cba04f02 100644 --- a/crates/ruff_python_semantic/src/reference.rs +++ b/crates/ruff_python_semantic/src/reference.rs @@ -1,13 +1,17 @@ +use bitflags::bitflags; use ruff_text_size::TextRange; use std::ops::Deref; use ruff_index::{newtype_index, IndexSlice, IndexVec}; +use ruff_python_ast::source_code::Locator; use crate::context::ExecutionContext; use crate::scope::ScopeId; +use crate::Exceptions; +/// A resolved read reference to a name in a program. #[derive(Debug, Clone)] -pub struct Reference { +pub struct ResolvedReference { /// The scope in which the reference is defined. scope_id: ScopeId, /// The range of the reference in the source code. @@ -16,7 +20,7 @@ pub struct Reference { context: ExecutionContext, } -impl Reference { +impl ResolvedReference { pub const fn scope_id(&self) -> ScopeId { self.scope_id } @@ -32,21 +36,21 @@ impl Reference { /// Id uniquely identifying a read reference in a program. #[newtype_index] -pub struct ReferenceId; +pub struct ResolvedReferenceId; -/// The references of a program indexed by [`ReferenceId`]. +/// The references of a program indexed by [`ResolvedReferenceId`]. #[derive(Debug, Default)] -pub(crate) struct References(IndexVec); +pub(crate) struct ResolvedReferences(IndexVec); -impl References { - /// Pushes a new [`Reference`] and returns its [`ReferenceId`]. +impl ResolvedReferences { + /// Pushes a new [`ResolvedReference`] and returns its [`ResolvedReferenceId`]. pub(crate) fn push( &mut self, scope_id: ScopeId, range: TextRange, context: ExecutionContext, - ) -> ReferenceId { - self.0.push(Reference { + ) -> ResolvedReferenceId { + self.0.push(ResolvedReference { scope_id, range, context, @@ -54,8 +58,73 @@ impl References { } } -impl Deref for References { - type Target = IndexSlice; +impl Deref for ResolvedReferences { + type Target = IndexSlice; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +/// An unresolved read reference to a name in a program. +#[derive(Debug, Clone)] +pub struct UnresolvedReference { + /// The range of the reference in the source code. + range: TextRange, + /// The set of exceptions that were handled when resolution was attempted. + exceptions: Exceptions, + /// Flags indicating the context in which the reference occurs. + flags: UnresolvedReferenceFlags, +} + +impl UnresolvedReference { + pub const fn range(&self) -> TextRange { + self.range + } + + pub const fn exceptions(&self) -> Exceptions { + self.exceptions + } + + pub const fn wildcard_import(&self) -> bool { + self.flags + .contains(UnresolvedReferenceFlags::WILDCARD_IMPORT) + } + + pub fn name<'a>(&self, locator: &Locator<'a>) -> &'a str { + locator.slice(self.range) + } +} + +bitflags! { + #[derive(Copy, Clone, Debug)] + pub struct UnresolvedReferenceFlags: u8 { + /// The unresolved reference appeared in a context that includes a wildcard import. + const WILDCARD_IMPORT = 1 << 0; + } +} + +#[derive(Debug, Default)] +pub(crate) struct UnresolvedReferences(Vec); + +impl UnresolvedReferences { + /// Pushes a new [`UnresolvedReference`]. + pub(crate) fn push( + &mut self, + range: TextRange, + exceptions: Exceptions, + flags: UnresolvedReferenceFlags, + ) { + self.0.push(UnresolvedReference { + range, + exceptions, + flags, + }); + } +} + +impl Deref for UnresolvedReferences { + type Target = Vec; fn deref(&self) -> &Self::Target { &self.0