From 963f240e4621e2dfca6758d8f79627885038bf13 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 19 Jul 2023 18:19:55 -0400 Subject: [PATCH] Track unresolved references in the semantic model (#5902) ## Summary As part of my continued quest to separate semantic model-building from diagnostic emission, this PR moves our unresolved-reference rules to a deferred pass. So, rather than emitting diagnostics as we encounter unresolved references, we now track those unresolved references on the semantic model (just like resolved references), and after traversal, emit the relevant rules for any unresolved references. --- crates/ruff/src/checkers/ast/mod.rs | 217 ++++++++---------- .../runtime_import_in_type_checking_block.rs | 4 +- .../rules/typing_only_runtime_import.rs | 4 +- crates/ruff/src/rules/pyflakes/mod.rs | 4 +- .../ruff/src/rules/pyflakes/rules/imports.rs | 11 +- .../rules/pyflakes/rules/undefined_export.rs | 23 +- ..._rules__pyflakes__tests__F405_F405.py.snap | 4 +- ...tests__augmented_assignment_after_del.snap | 16 +- crates/ruff_python_semantic/src/binding.rs | 6 +- crates/ruff_python_semantic/src/model.rs | 83 +++++-- crates/ruff_python_semantic/src/reference.rs | 91 +++++++- 11 files changed, 261 insertions(+), 202 deletions(-) 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