mirror of
https://github.com/astral-sh/ruff.git
synced 2025-08-09 21:28:21 +00:00
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.
This commit is contained in:
parent
23cde4d1f5
commit
963f240e46
11 changed files with 261 additions and 202 deletions
|
@ -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<NodeId>,
|
||||
/// The references to the [`Binding`].
|
||||
pub references: Vec<ReferenceId>,
|
||||
pub references: Vec<ResolvedReferenceId>,
|
||||
/// 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<Item = ReferenceId> + '_ {
|
||||
pub fn references(&self) -> impl Iterator<Item = ResolvedReferenceId> + '_ {
|
||||
self.references.iter().copied()
|
||||
}
|
||||
|
||||
|
|
|
@ -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<Item = &UnresolvedReference> {
|
||||
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:
|
||||
|
|
|
@ -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<ReferenceId, Reference>);
|
||||
pub(crate) struct ResolvedReferences(IndexVec<ResolvedReferenceId, ResolvedReference>);
|
||||
|
||||
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<ReferenceId, Reference>;
|
||||
impl Deref for ResolvedReferences {
|
||||
type Target = IndexSlice<ResolvedReferenceId, ResolvedReference>;
|
||||
|
||||
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<UnresolvedReference>);
|
||||
|
||||
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<UnresolvedReference>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue