mirror of
https://github.com/astral-sh/ruff.git
synced 2025-08-08 04:38:26 +00:00

## Summary This PR is a follow-up to the suggestion in https://github.com/astral-sh/ruff/pull/6345#discussion_r1285470953 to use a single stack to store all statements and expressions, rather than using separate vectors for each, which gives us something closer to a full-fidelity chain. (We can then generalize this concept to include all other AST nodes too.) This is in part made possible by the removal of the hash map from `&Stmt` to `StatementId` (#6694), which makes it much cheaper to store these using a single interface (since doing so no longer introduces the requirement that we hash all expressions). I'll follow-up with some profiling, but a few notes on how the data requirements have changed: - We now store a `BranchId` for every expression, not just every statement, so that's an extra `u32`. - We now store a single `NodeId` on every snapshot, rather than separate `StatementId` and `ExpressionId` IDs, so that's one fewer `u32` for each snapshot. - We're probably doing a few more lookups in general, since any calls to `current_statement()` etc. now have to iterate up the node hierarchy until they identify the first statement. ## Test Plan `cargo test`
1711 lines
62 KiB
Rust
1711 lines
62 KiB
Rust
use std::path::Path;
|
|
|
|
use bitflags::bitflags;
|
|
use rustc_hash::FxHashMap;
|
|
use smallvec::smallvec;
|
|
|
|
use ruff_python_ast::call_path::{collect_call_path, from_unqualified_name, CallPath};
|
|
use ruff_python_ast::helpers::from_relative_import;
|
|
use ruff_python_ast::{self as ast, Expr, Operator, Ranged, Stmt};
|
|
use ruff_python_stdlib::path::is_python_stub_file;
|
|
use ruff_python_stdlib::typing::is_typing_extension;
|
|
use ruff_text_size::{TextRange, TextSize};
|
|
|
|
use crate::binding::{
|
|
Binding, BindingFlags, BindingId, BindingKind, Bindings, Exceptions, FromImport, Import,
|
|
SubmoduleImport,
|
|
};
|
|
use crate::branches::{BranchId, Branches};
|
|
use crate::context::ExecutionContext;
|
|
use crate::definition::{Definition, DefinitionId, Definitions, Member, Module};
|
|
use crate::globals::{Globals, GlobalsArena};
|
|
use crate::nodes::{NodeId, NodeRef, Nodes};
|
|
use crate::reference::{
|
|
ResolvedReference, ResolvedReferenceId, ResolvedReferences, UnresolvedReference,
|
|
UnresolvedReferenceFlags, UnresolvedReferences,
|
|
};
|
|
use crate::scope::{Scope, ScopeId, ScopeKind, Scopes};
|
|
use crate::Imported;
|
|
|
|
/// A semantic model for a Python module, to enable querying the module's semantic information.
|
|
pub struct SemanticModel<'a> {
|
|
typing_modules: &'a [String],
|
|
module_path: Option<&'a [String]>,
|
|
|
|
/// Stack of all AST nodes in the program.
|
|
nodes: Nodes<'a>,
|
|
|
|
/// The ID of the current AST node.
|
|
node_id: Option<NodeId>,
|
|
|
|
/// Stack of all branches in the program.
|
|
branches: Branches,
|
|
|
|
/// The ID of the current branch.
|
|
branch_id: Option<BranchId>,
|
|
|
|
/// Stack of all scopes, along with the identifier of the current scope.
|
|
pub scopes: Scopes<'a>,
|
|
pub scope_id: ScopeId,
|
|
|
|
/// Stack of all definitions created in any scope, at any point in execution.
|
|
pub definitions: Definitions<'a>,
|
|
|
|
/// The ID of the current definition.
|
|
pub definition_id: DefinitionId,
|
|
|
|
/// A stack of all bindings created in any scope, at any point in execution.
|
|
pub bindings: Bindings<'a>,
|
|
|
|
/// Stack of all references created in any scope, at any point in execution.
|
|
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>,
|
|
|
|
/// Map from binding ID to binding ID that it shadows (in another scope).
|
|
///
|
|
/// For example, given:
|
|
/// ```python
|
|
/// import x
|
|
///
|
|
/// def f():
|
|
/// x = 1
|
|
/// ```
|
|
///
|
|
/// In this case, the binding created by `x = 1` shadows the binding created by `import x`,
|
|
/// despite the fact that they're in different scopes.
|
|
pub shadowed_bindings: FxHashMap<BindingId, BindingId>,
|
|
|
|
/// Map from binding index to indexes of bindings that annotate it (in the same scope).
|
|
///
|
|
/// For example, given:
|
|
/// ```python
|
|
/// x = 1
|
|
/// x: int
|
|
/// ```
|
|
///
|
|
/// In this case, the binding created by `x = 1` is annotated by the binding created by
|
|
/// `x: int`. We don't consider the latter binding to _shadow_ the former, because it doesn't
|
|
/// change the value of the binding, and so we don't store in on the scope. But we _do_ want to
|
|
/// track the annotation in some form, since it's a reference to `x`.
|
|
///
|
|
/// Note that, given:
|
|
/// ```python
|
|
/// x: int
|
|
/// ```
|
|
///
|
|
/// In this case, we _do_ store the binding created by `x: int` directly on the scope, and not
|
|
/// as a delayed annotation. Annotations are thus treated as bindings only when they are the
|
|
/// first binding in a scope; any annotations that follow are treated as "delayed" annotations.
|
|
delayed_annotations: FxHashMap<BindingId, Vec<BindingId>>,
|
|
|
|
/// Map from binding ID to the IDs of all scopes in which it is declared a `global` or
|
|
/// `nonlocal`.
|
|
///
|
|
/// For example, given:
|
|
/// ```python
|
|
/// x = 1
|
|
///
|
|
/// def f():
|
|
/// global x
|
|
/// ```
|
|
///
|
|
/// In this case, the binding created by `x = 1` is rebound within the scope created by `f`
|
|
/// by way of the `global x` statement.
|
|
rebinding_scopes: FxHashMap<BindingId, Vec<ScopeId>>,
|
|
|
|
/// Flags for the semantic model.
|
|
pub flags: SemanticModelFlags,
|
|
|
|
/// Exceptions that have been handled by the current scope.
|
|
pub handled_exceptions: Vec<Exceptions>,
|
|
|
|
/// Map from [`ast::ExprName`] node (represented as a [`NameId`]) to the [`Binding`] to which
|
|
/// it resolved (represented as a [`BindingId`]).
|
|
resolved_names: FxHashMap<NameId, BindingId>,
|
|
}
|
|
|
|
impl<'a> SemanticModel<'a> {
|
|
pub fn new(typing_modules: &'a [String], path: &'a Path, module: Module<'a>) -> Self {
|
|
Self {
|
|
typing_modules,
|
|
module_path: module.path(),
|
|
nodes: Nodes::default(),
|
|
node_id: None,
|
|
branches: Branches::default(),
|
|
branch_id: None,
|
|
scopes: Scopes::default(),
|
|
scope_id: ScopeId::global(),
|
|
definitions: Definitions::for_module(module),
|
|
definition_id: DefinitionId::module(),
|
|
bindings: Bindings::default(),
|
|
resolved_references: ResolvedReferences::default(),
|
|
unresolved_references: UnresolvedReferences::default(),
|
|
globals: GlobalsArena::default(),
|
|
shadowed_bindings: FxHashMap::default(),
|
|
delayed_annotations: FxHashMap::default(),
|
|
rebinding_scopes: FxHashMap::default(),
|
|
flags: SemanticModelFlags::new(path),
|
|
handled_exceptions: Vec::default(),
|
|
resolved_names: FxHashMap::default(),
|
|
}
|
|
}
|
|
|
|
/// Return the [`Binding`] for the given [`BindingId`].
|
|
#[inline]
|
|
pub fn binding(&self, id: BindingId) -> &Binding {
|
|
&self.bindings[id]
|
|
}
|
|
|
|
/// Resolve the [`ResolvedReference`] for the given [`ResolvedReferenceId`].
|
|
#[inline]
|
|
pub fn reference(&self, id: ResolvedReferenceId) -> &ResolvedReference {
|
|
&self.resolved_references[id]
|
|
}
|
|
|
|
/// Return `true` if the `Expr` is a reference to `typing.${target}`.
|
|
pub fn match_typing_expr(&self, expr: &Expr, target: &str) -> bool {
|
|
self.resolve_call_path(expr)
|
|
.is_some_and(|call_path| self.match_typing_call_path(&call_path, target))
|
|
}
|
|
|
|
/// Return `true` if the call path is a reference to `typing.${target}`.
|
|
pub fn match_typing_call_path(&self, call_path: &CallPath, target: &str) -> bool {
|
|
if call_path.as_slice() == ["typing", target] {
|
|
return true;
|
|
}
|
|
|
|
if call_path.as_slice() == ["_typeshed", target] {
|
|
return true;
|
|
}
|
|
|
|
if is_typing_extension(target) {
|
|
if call_path.as_slice() == ["typing_extensions", target] {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
if self.typing_modules.iter().any(|module| {
|
|
let mut module: CallPath = from_unqualified_name(module);
|
|
module.push(target);
|
|
*call_path == module
|
|
}) {
|
|
return true;
|
|
}
|
|
|
|
false
|
|
}
|
|
|
|
/// Create a new [`Binding`] for a builtin.
|
|
pub fn push_builtin(&mut self) -> BindingId {
|
|
self.bindings.push(Binding {
|
|
range: TextRange::default(),
|
|
kind: BindingKind::Builtin,
|
|
scope: ScopeId::global(),
|
|
references: Vec::new(),
|
|
flags: BindingFlags::empty(),
|
|
source: None,
|
|
context: ExecutionContext::Runtime,
|
|
exceptions: Exceptions::empty(),
|
|
})
|
|
}
|
|
|
|
/// Create a new [`Binding`] for the given `name` and `range`.
|
|
pub fn push_binding(
|
|
&mut self,
|
|
range: TextRange,
|
|
kind: BindingKind<'a>,
|
|
flags: BindingFlags,
|
|
) -> BindingId {
|
|
self.bindings.push(Binding {
|
|
range,
|
|
kind,
|
|
flags,
|
|
references: Vec::new(),
|
|
scope: self.scope_id,
|
|
source: self.node_id,
|
|
context: self.execution_context(),
|
|
exceptions: self.exceptions(),
|
|
})
|
|
}
|
|
|
|
/// Return the [`BindingId`] that the given [`BindingId`] shadows, if any.
|
|
///
|
|
/// Note that this will only return bindings that are shadowed by a binding in a parent scope.
|
|
pub fn shadowed_binding(&self, binding_id: BindingId) -> Option<BindingId> {
|
|
self.shadowed_bindings.get(&binding_id).copied()
|
|
}
|
|
|
|
/// Return `true` if `member` is bound as a builtin.
|
|
pub fn is_builtin(&self, member: &str) -> bool {
|
|
self.lookup_symbol(member)
|
|
.map(|binding_id| &self.bindings[binding_id])
|
|
.is_some_and(|binding| binding.kind.is_builtin())
|
|
}
|
|
|
|
/// Return `true` if `member` is an "available" symbol, i.e., a symbol that has not been bound
|
|
/// in the current scope, or in any containing scope.
|
|
pub fn is_available(&self, member: &str) -> bool {
|
|
self.lookup_symbol(member)
|
|
.map(|binding_id| &self.bindings[binding_id])
|
|
.map_or(true, |binding| binding.kind.is_builtin())
|
|
}
|
|
|
|
/// 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);
|
|
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 an [`ast::ExprName`].
|
|
pub fn resolve_load(&mut self, name: &ast::ExprName) -> 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() {
|
|
if let Some(binding_id) = self.scopes.global().get(name.id.as_str()) {
|
|
if !self.bindings[binding_id].is_unbound() {
|
|
// Mark the binding as used.
|
|
let reference_id =
|
|
self.resolved_references
|
|
.push(ScopeId::global(), name.range, self.flags);
|
|
self.bindings[binding_id].references.push(reference_id);
|
|
|
|
// Mark any submodule aliases as used.
|
|
if let Some(binding_id) =
|
|
self.resolve_submodule(name.id.as_str(), ScopeId::global(), binding_id)
|
|
{
|
|
let reference_id = self.resolved_references.push(
|
|
ScopeId::global(),
|
|
name.range,
|
|
self.flags,
|
|
);
|
|
self.bindings[binding_id].references.push(reference_id);
|
|
}
|
|
|
|
self.resolved_names.insert(name.into(), binding_id);
|
|
return ReadResult::Resolved(binding_id);
|
|
}
|
|
}
|
|
}
|
|
|
|
let mut seen_function = false;
|
|
let mut import_starred = false;
|
|
let mut class_variables_visible = true;
|
|
for (index, scope_id) in self.scopes.ancestor_ids(self.scope_id).enumerate() {
|
|
let scope = &self.scopes[scope_id];
|
|
if scope.kind.is_class() {
|
|
// Allow usages of `__class__` within methods, e.g.:
|
|
//
|
|
// ```python
|
|
// class Foo:
|
|
// def __init__(self):
|
|
// print(__class__)
|
|
// ```
|
|
if seen_function && matches!(name.id.as_str(), "__class__") {
|
|
return ReadResult::ImplicitGlobal;
|
|
}
|
|
// Do not allow usages of class symbols unless it is the immediate parent
|
|
// (excluding type scopes), e.g.:
|
|
//
|
|
// ```python
|
|
// class Foo:
|
|
// a = 0
|
|
//
|
|
// b = a # allowed
|
|
// def c(self, arg=a): # allowed
|
|
// print(arg)
|
|
//
|
|
// def d(self):
|
|
// print(a) # not allowed
|
|
// ```
|
|
if !class_variables_visible {
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// Allow class variables to be visible for an additional scope level
|
|
// when a type scope is seen — this covers the type scope present between
|
|
// function and class definitions and their parent class scope.
|
|
class_variables_visible = scope.kind.is_type() && index == 0;
|
|
|
|
if let Some(binding_id) = scope.get(name.id.as_str()) {
|
|
// Mark the binding as used.
|
|
let reference_id =
|
|
self.resolved_references
|
|
.push(self.scope_id, name.range, self.flags);
|
|
self.bindings[binding_id].references.push(reference_id);
|
|
|
|
// Mark any submodule aliases as used.
|
|
if let Some(binding_id) =
|
|
self.resolve_submodule(name.id.as_str(), scope_id, binding_id)
|
|
{
|
|
let reference_id =
|
|
self.resolved_references
|
|
.push(self.scope_id, name.range, self.flags);
|
|
self.bindings[binding_id].references.push(reference_id);
|
|
}
|
|
|
|
match self.bindings[binding_id].kind {
|
|
// If it's a type annotation, don't treat it as resolved. For example, given:
|
|
//
|
|
// ```python
|
|
// name: str
|
|
// print(name)
|
|
// ```
|
|
//
|
|
// The `name` in `print(name)` should be treated as unresolved, but the `name` in
|
|
// `name: str` should be treated as used.
|
|
BindingKind::Annotation => continue,
|
|
|
|
// If it's a deletion, don't treat it as resolved, since the name is now
|
|
// unbound. For example, given:
|
|
//
|
|
// ```python
|
|
// x = 1
|
|
// del x
|
|
// print(x)
|
|
// ```
|
|
//
|
|
// The `x` in `print(x)` should be treated as unresolved.
|
|
//
|
|
// Similarly, given:
|
|
//
|
|
// ```python
|
|
// try:
|
|
// pass
|
|
// except ValueError as x:
|
|
// pass
|
|
//
|
|
// print(x)
|
|
//
|
|
// The `x` in `print(x)` should be treated as unresolved.
|
|
BindingKind::Deletion | BindingKind::UnboundException(None) => {
|
|
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:
|
|
//
|
|
// ```python
|
|
// x = 1
|
|
//
|
|
// try:
|
|
// pass
|
|
// except ValueError as x:
|
|
// pass
|
|
//
|
|
// print(x)
|
|
// ```
|
|
//
|
|
// The `x` in `print(x)` should resolve to the `x` in `x = 1`.
|
|
BindingKind::UnboundException(Some(binding_id)) => {
|
|
// Mark the binding as used.
|
|
let reference_id =
|
|
self.resolved_references
|
|
.push(self.scope_id, name.range, self.flags);
|
|
self.bindings[binding_id].references.push(reference_id);
|
|
|
|
// Mark any submodule aliases as used.
|
|
if let Some(binding_id) =
|
|
self.resolve_submodule(name.id.as_str(), scope_id, binding_id)
|
|
{
|
|
let reference_id = self.resolved_references.push(
|
|
self.scope_id,
|
|
name.range,
|
|
self.flags,
|
|
);
|
|
self.bindings[binding_id].references.push(reference_id);
|
|
}
|
|
|
|
self.resolved_names.insert(name.into(), binding_id);
|
|
return ReadResult::Resolved(binding_id);
|
|
}
|
|
|
|
_ => {
|
|
// Otherwise, treat it as resolved.
|
|
self.resolved_names.insert(name.into(), binding_id);
|
|
return ReadResult::Resolved(binding_id);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Allow usages of `__module__` and `__qualname__` within class scopes, e.g.:
|
|
//
|
|
// ```python
|
|
// class Foo:
|
|
// print(__qualname__)
|
|
// ```
|
|
//
|
|
// Intentionally defer this check to _after_ the standard `scope.get` logic, so that
|
|
// we properly attribute reads to overridden class members, e.g.:
|
|
//
|
|
// ```python
|
|
// class Foo:
|
|
// __qualname__ = "Bar"
|
|
// print(__qualname__)
|
|
// ```
|
|
if index == 0 && scope.kind.is_class() {
|
|
if matches!(name.id.as_str(), "__module__" | "__qualname__") {
|
|
return ReadResult::ImplicitGlobal;
|
|
}
|
|
}
|
|
|
|
seen_function |= scope.kind.is_function();
|
|
import_starred = import_starred || scope.uses_star_imports();
|
|
}
|
|
|
|
if import_starred {
|
|
self.unresolved_references.push(
|
|
name.range,
|
|
self.exceptions(),
|
|
UnresolvedReferenceFlags::WILDCARD_IMPORT,
|
|
);
|
|
ReadResult::WildcardImport
|
|
} else {
|
|
self.unresolved_references.push(
|
|
name.range,
|
|
self.exceptions(),
|
|
UnresolvedReferenceFlags::empty(),
|
|
);
|
|
ReadResult::NotFound
|
|
}
|
|
}
|
|
|
|
/// 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<BindingId> {
|
|
if self.in_forward_reference() {
|
|
if let Some(binding_id) = self.scopes.global().get(symbol) {
|
|
if !self.bindings[binding_id].is_unbound() {
|
|
return Some(binding_id);
|
|
}
|
|
}
|
|
}
|
|
|
|
let mut seen_function = false;
|
|
let mut class_variables_visible = true;
|
|
for (index, scope_id) in self.scopes.ancestor_ids(self.scope_id).enumerate() {
|
|
let scope = &self.scopes[scope_id];
|
|
if scope.kind.is_class() {
|
|
if seen_function && matches!(symbol, "__class__") {
|
|
return None;
|
|
}
|
|
if !class_variables_visible {
|
|
continue;
|
|
}
|
|
}
|
|
|
|
class_variables_visible = scope.kind.is_type() && index == 0;
|
|
|
|
if let Some(binding_id) = scope.get(symbol) {
|
|
match self.bindings[binding_id].kind {
|
|
BindingKind::Annotation => continue,
|
|
BindingKind::Deletion | BindingKind::UnboundException(None) => return None,
|
|
BindingKind::UnboundException(Some(binding_id)) => return Some(binding_id),
|
|
_ => return Some(binding_id),
|
|
}
|
|
}
|
|
|
|
if index == 0 && scope.kind.is_class() {
|
|
if matches!(symbol, "__module__" | "__qualname__") {
|
|
return None;
|
|
}
|
|
}
|
|
|
|
seen_function |= scope.kind.is_function();
|
|
}
|
|
|
|
None
|
|
}
|
|
|
|
/// Lookup a qualified attribute in the current scope.
|
|
///
|
|
/// For example, given `["Class", "method"`], resolve the `BindingKind::ClassDefinition`
|
|
/// associated with `Class`, then the `BindingKind::FunctionDefinition` associated with
|
|
/// `Class#method`.
|
|
pub fn lookup_attribute(&'a self, value: &'a Expr) -> Option<BindingId> {
|
|
let call_path = collect_call_path(value)?;
|
|
|
|
// Find the symbol in the current scope.
|
|
let (symbol, attribute) = call_path.split_first()?;
|
|
let mut binding_id = self.lookup_symbol(symbol)?;
|
|
|
|
// Recursively resolve class attributes, e.g., `foo.bar.baz` in.
|
|
let mut tail = attribute;
|
|
while let Some((symbol, rest)) = tail.split_first() {
|
|
// Find the next symbol in the class scope.
|
|
let BindingKind::ClassDefinition(scope_id) = self.binding(binding_id).kind else {
|
|
return None;
|
|
};
|
|
binding_id = self.scopes[scope_id].get(symbol)?;
|
|
tail = rest;
|
|
}
|
|
|
|
Some(binding_id)
|
|
}
|
|
|
|
/// Given a `BindingId`, return the `BindingId` of the submodule import that it aliases.
|
|
fn resolve_submodule(
|
|
&self,
|
|
symbol: &str,
|
|
scope_id: ScopeId,
|
|
binding_id: BindingId,
|
|
) -> Option<BindingId> {
|
|
// If the name of a submodule import is the same as an alias of another import, and the
|
|
// alias is used, then the submodule import should be marked as used too.
|
|
//
|
|
// For example, mark `pyarrow.csv` as used in:
|
|
//
|
|
// ```python
|
|
// import pyarrow as pa
|
|
// import pyarrow.csv
|
|
// print(pa.csv.read_csv("test.csv"))
|
|
// ```
|
|
let import = self.bindings[binding_id].as_any_import()?;
|
|
if !import.is_import() {
|
|
return None;
|
|
}
|
|
|
|
// Grab, e.g., `pyarrow` from `import pyarrow as pa`.
|
|
let call_path = import.call_path();
|
|
let segment = call_path.last()?;
|
|
if *segment == symbol {
|
|
return None;
|
|
}
|
|
|
|
// Locate the submodule import (e.g., `pyarrow.csv`) that `pa` aliases.
|
|
let binding_id = self.scopes[scope_id].get(segment)?;
|
|
let submodule = &self.bindings[binding_id].as_any_import()?;
|
|
if !submodule.is_submodule_import() {
|
|
return None;
|
|
}
|
|
|
|
// Ensure that the submodule import and the aliased import are from the same module.
|
|
if import.module_name() != submodule.module_name() {
|
|
return None;
|
|
}
|
|
|
|
Some(binding_id)
|
|
}
|
|
|
|
/// Resolves the [`ast::ExprName`] to the [`BindingId`] of the symbol it refers to, if any.
|
|
pub fn resolve_name(&self, name: &ast::ExprName) -> Option<BindingId> {
|
|
self.resolved_names.get(&name.into()).copied()
|
|
}
|
|
|
|
/// Resolves the [`Expr`] to a fully-qualified symbol-name, if `value` resolves to an imported
|
|
/// or builtin symbol.
|
|
///
|
|
/// E.g., given:
|
|
///
|
|
///
|
|
/// ```python
|
|
/// from sys import version_info as python_version
|
|
/// print(python_version)
|
|
/// ```
|
|
///
|
|
/// ...then `resolve_call_path(${python_version})` will resolve to `sys.version_info`.
|
|
pub fn resolve_call_path(&'a self, value: &'a Expr) -> Option<CallPath<'a>> {
|
|
/// Return the [`ast::ExprName`] at the head of the expression, if any.
|
|
const fn match_head(value: &Expr) -> Option<&ast::ExprName> {
|
|
match value {
|
|
Expr::Attribute(ast::ExprAttribute { value, .. }) => match_head(value),
|
|
Expr::Name(name) => Some(name),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
// If the name was already resolved, look it up; otherwise, search for the symbol.
|
|
let head = match_head(value)?;
|
|
let binding = self
|
|
.resolve_name(head)
|
|
.or_else(|| self.lookup_symbol(&head.id))
|
|
.map(|id| self.binding(id))?;
|
|
|
|
match &binding.kind {
|
|
BindingKind::Import(Import { call_path }) => {
|
|
let value_path = collect_call_path(value)?;
|
|
let (_, tail) = value_path.split_first()?;
|
|
let resolved: CallPath = call_path.iter().chain(tail.iter()).copied().collect();
|
|
Some(resolved)
|
|
}
|
|
BindingKind::SubmoduleImport(SubmoduleImport { call_path }) => {
|
|
let value_path = collect_call_path(value)?;
|
|
let (_, tail) = value_path.split_first()?;
|
|
let resolved: CallPath = call_path
|
|
.iter()
|
|
.take(1)
|
|
.chain(tail.iter())
|
|
.copied()
|
|
.collect();
|
|
Some(resolved)
|
|
}
|
|
BindingKind::FromImport(FromImport { call_path }) => {
|
|
let value_path = collect_call_path(value)?;
|
|
let (_, tail) = value_path.split_first()?;
|
|
|
|
let resolved: CallPath =
|
|
if call_path.first().map_or(false, |segment| *segment == ".") {
|
|
from_relative_import(self.module_path?, call_path, tail)?
|
|
} else {
|
|
call_path.iter().chain(tail.iter()).copied().collect()
|
|
};
|
|
Some(resolved)
|
|
}
|
|
BindingKind::Builtin => Some(smallvec!["", head.id.as_str()]),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
/// Given a `module` and `member`, return the fully-qualified name of the binding in the current
|
|
/// scope, if it exists.
|
|
///
|
|
/// E.g., given:
|
|
///
|
|
/// ```python
|
|
/// from sys import version_info as python_version
|
|
/// print(python_version)
|
|
/// ```
|
|
///
|
|
/// ...then `resolve_qualified_import_name("sys", "version_info")` will return
|
|
/// `Some("python_version")`.
|
|
pub fn resolve_qualified_import_name(
|
|
&self,
|
|
module: &str,
|
|
member: &str,
|
|
) -> Option<ImportedName> {
|
|
// TODO(charlie): Pass in a slice.
|
|
let module_path: Vec<&str> = module.split('.').collect();
|
|
self.current_scopes()
|
|
.enumerate()
|
|
.find_map(|(scope_index, scope)| {
|
|
scope.bindings().find_map(|(name, binding_id)| {
|
|
let binding = &self.bindings[binding_id];
|
|
match &binding.kind {
|
|
// Ex) Given `module="sys"` and `object="exit"`:
|
|
// `import sys` -> `sys.exit`
|
|
// `import sys as sys2` -> `sys2.exit`
|
|
BindingKind::Import(Import { call_path }) => {
|
|
if call_path.as_ref() == module_path.as_slice() {
|
|
if let Some(source) = binding.source {
|
|
// Verify that `sys` isn't bound in an inner scope.
|
|
if self
|
|
.current_scopes()
|
|
.take(scope_index)
|
|
.all(|scope| !scope.has(name))
|
|
{
|
|
return Some(ImportedName {
|
|
name: format!("{name}.{member}"),
|
|
range: self.nodes[source].range(),
|
|
context: binding.context,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// Ex) Given `module="os.path"` and `object="join"`:
|
|
// `from os.path import join` -> `join`
|
|
// `from os.path import join as join2` -> `join2`
|
|
BindingKind::FromImport(FromImport { call_path }) => {
|
|
if let Some((target_member, target_module)) = call_path.split_last() {
|
|
if target_module == module_path.as_slice()
|
|
&& target_member == &member
|
|
{
|
|
if let Some(source) = binding.source {
|
|
// Verify that `join` isn't bound in an inner scope.
|
|
if self
|
|
.current_scopes()
|
|
.take(scope_index)
|
|
.all(|scope| !scope.has(name))
|
|
{
|
|
return Some(ImportedName {
|
|
name: (*name).to_string(),
|
|
range: self.nodes[source].range(),
|
|
context: binding.context,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// Ex) Given `module="os"` and `object="name"`:
|
|
// `import os.path ` -> `os.name`
|
|
BindingKind::SubmoduleImport(SubmoduleImport { .. }) => {
|
|
if name == module {
|
|
if let Some(source) = binding.source {
|
|
// Verify that `os` isn't bound in an inner scope.
|
|
if self
|
|
.current_scopes()
|
|
.take(scope_index)
|
|
.all(|scope| !scope.has(name))
|
|
{
|
|
return Some(ImportedName {
|
|
name: format!("{name}.{member}"),
|
|
range: self.nodes[source].range(),
|
|
context: binding.context,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// Non-imports.
|
|
_ => {}
|
|
}
|
|
None
|
|
})
|
|
})
|
|
}
|
|
|
|
/// Push an AST node [`NodeRef`] onto the stack.
|
|
pub fn push_node<T: Into<NodeRef<'a>>>(&mut self, node: T) {
|
|
self.node_id = Some(self.nodes.insert(node.into(), self.node_id, self.branch_id));
|
|
}
|
|
|
|
/// Pop the current AST node [`NodeRef`] off the stack.
|
|
pub fn pop_node(&mut self) {
|
|
let node_id = self.node_id.expect("Attempted to pop without node");
|
|
self.node_id = self.nodes.parent_id(node_id);
|
|
}
|
|
|
|
/// Push a [`Scope`] with the given [`ScopeKind`] onto the stack.
|
|
pub fn push_scope(&mut self, kind: ScopeKind<'a>) {
|
|
let id = self.scopes.push_scope(kind, self.scope_id);
|
|
self.scope_id = id;
|
|
}
|
|
|
|
/// Pop the current [`Scope`] off the stack.
|
|
pub fn pop_scope(&mut self) {
|
|
self.scope_id = self.scopes[self.scope_id]
|
|
.parent
|
|
.expect("Attempted to pop without scope");
|
|
}
|
|
|
|
/// Push a [`Member`] onto the stack.
|
|
pub fn push_definition(&mut self, definition: Member<'a>) {
|
|
self.definition_id = self.definitions.push_member(definition);
|
|
}
|
|
|
|
/// Pop the current [`Member`] off the stack.
|
|
pub fn pop_definition(&mut self) {
|
|
let Definition::Member(member) = &self.definitions[self.definition_id] else {
|
|
panic!("Attempted to pop without member definition");
|
|
};
|
|
self.definition_id = member.parent;
|
|
}
|
|
|
|
/// Push a new branch onto the stack, returning its [`BranchId`].
|
|
pub fn push_branch(&mut self) -> Option<BranchId> {
|
|
self.branch_id = Some(self.branches.insert(self.branch_id));
|
|
self.branch_id
|
|
}
|
|
|
|
/// Pop the current [`BranchId`] off the stack.
|
|
pub fn pop_branch(&mut self) {
|
|
let node_id = self.branch_id.expect("Attempted to pop without branch");
|
|
self.branch_id = self.branches.parent_id(node_id);
|
|
}
|
|
|
|
/// Set the current [`BranchId`].
|
|
pub fn set_branch(&mut self, branch_id: Option<BranchId>) {
|
|
self.branch_id = branch_id;
|
|
}
|
|
|
|
/// Returns an [`Iterator`] over the current statement hierarchy, from the current [`Stmt`]
|
|
/// through to any parents.
|
|
pub fn current_statements(&self) -> impl Iterator<Item = &'a Stmt> + '_ {
|
|
let id = self.node_id.expect("No current node");
|
|
self.nodes
|
|
.ancestor_ids(id)
|
|
.filter_map(move |id| self.nodes[id].as_statement())
|
|
}
|
|
|
|
/// Return the current [`Stmt`].
|
|
pub fn current_statement(&self) -> &'a Stmt {
|
|
self.current_statements()
|
|
.next()
|
|
.expect("No current statement")
|
|
}
|
|
|
|
/// Return the parent [`Stmt`] of the current [`Stmt`], if any.
|
|
pub fn current_statement_parent(&self) -> Option<&'a Stmt> {
|
|
self.current_statements().nth(1)
|
|
}
|
|
|
|
/// Returns an [`Iterator`] over the current expression hierarchy, from the current [`Expr`]
|
|
/// through to any parents.
|
|
pub fn current_expressions(&self) -> impl Iterator<Item = &'a Expr> + '_ {
|
|
let id = self.node_id.expect("No current node");
|
|
self.nodes
|
|
.ancestor_ids(id)
|
|
.filter_map(move |id| self.nodes[id].as_expression())
|
|
}
|
|
|
|
/// Return the current [`Expr`].
|
|
pub fn current_expression(&self) -> Option<&'a Expr> {
|
|
self.current_expressions().next()
|
|
}
|
|
|
|
/// Return the parent [`Expr`] of the current [`Expr`], if any.
|
|
pub fn current_expression_parent(&self) -> Option<&'a Expr> {
|
|
self.current_expressions().nth(1)
|
|
}
|
|
|
|
/// Return the grandparent [`Expr`] of the current [`Expr`], if any.
|
|
pub fn current_expression_grandparent(&self) -> Option<&'a Expr> {
|
|
self.current_expressions().nth(2)
|
|
}
|
|
|
|
/// Returns an [`Iterator`] over the current statement hierarchy represented as [`NodeId`],
|
|
/// from the current [`NodeId`] through to any parents.
|
|
pub fn current_statement_ids(&self) -> impl Iterator<Item = NodeId> + '_ {
|
|
self.node_id
|
|
.iter()
|
|
.flat_map(|id| self.nodes.ancestor_ids(*id))
|
|
.filter(|id| self.nodes[*id].is_statement())
|
|
}
|
|
|
|
/// Return the [`NodeId`] of the current [`Stmt`].
|
|
pub fn current_statement_id(&self) -> NodeId {
|
|
self.current_statement_ids()
|
|
.next()
|
|
.expect("No current statement")
|
|
}
|
|
|
|
/// Return the [`NodeId`] of the current [`Stmt`] parent, if any.
|
|
pub fn current_statement_parent_id(&self) -> Option<NodeId> {
|
|
self.current_statement_ids().nth(1)
|
|
}
|
|
|
|
/// Returns a reference to the global [`Scope`].
|
|
pub fn global_scope(&self) -> &Scope<'a> {
|
|
self.scopes.global()
|
|
}
|
|
|
|
/// Returns a mutable reference to the global [`Scope`].
|
|
pub fn global_scope_mut(&mut self) -> &mut Scope<'a> {
|
|
self.scopes.global_mut()
|
|
}
|
|
|
|
/// Returns the current top-most [`Scope`].
|
|
pub fn current_scope(&self) -> &Scope<'a> {
|
|
&self.scopes[self.scope_id]
|
|
}
|
|
|
|
/// Returns a mutable reference to the current top-most [`Scope`].
|
|
pub fn current_scope_mut(&mut self) -> &mut Scope<'a> {
|
|
&mut self.scopes[self.scope_id]
|
|
}
|
|
|
|
/// Returns an iterator over all scopes, starting from the current [`Scope`].
|
|
pub fn current_scopes(&self) -> impl Iterator<Item = &Scope> {
|
|
self.scopes.ancestors(self.scope_id)
|
|
}
|
|
|
|
/// Returns an iterator over all scopes IDs, starting from the current [`Scope`].
|
|
pub fn current_scope_ids(&self) -> impl Iterator<Item = ScopeId> + '_ {
|
|
self.scopes.ancestor_ids(self.scope_id)
|
|
}
|
|
|
|
/// Returns the parent of the given [`Scope`], if any.
|
|
pub fn parent_scope(&self, scope: &Scope) -> Option<&Scope<'a>> {
|
|
scope.parent.map(|scope_id| &self.scopes[scope_id])
|
|
}
|
|
|
|
/// Returns the first parent of the given [`Scope`] that is not of [`ScopeKind::Type`], if any.
|
|
pub fn first_non_type_parent_scope(&self, scope: &Scope) -> Option<&Scope<'a>> {
|
|
let mut current_scope = scope;
|
|
while let Some(parent) = self.parent_scope(current_scope) {
|
|
if parent.kind.is_type() {
|
|
current_scope = parent;
|
|
} else {
|
|
return Some(parent);
|
|
}
|
|
}
|
|
None
|
|
}
|
|
|
|
/// Return the [`Stmt`] corresponding to the given [`NodeId`].
|
|
#[inline]
|
|
pub fn node(&self, node_id: NodeId) -> &NodeRef<'a> {
|
|
&self.nodes[node_id]
|
|
}
|
|
|
|
/// Return the [`Stmt`] corresponding to the given [`NodeId`].
|
|
#[inline]
|
|
pub fn statement(&self, node_id: NodeId) -> &'a Stmt {
|
|
self.nodes
|
|
.ancestor_ids(node_id)
|
|
.find_map(|id| self.nodes[id].as_statement())
|
|
.expect("No statement found")
|
|
}
|
|
|
|
/// Given a [`Stmt`], return its parent, if any.
|
|
#[inline]
|
|
pub fn parent_statement(&self, node_id: NodeId) -> Option<&'a Stmt> {
|
|
self.nodes
|
|
.ancestor_ids(node_id)
|
|
.filter_map(|id| self.nodes[id].as_statement())
|
|
.nth(1)
|
|
}
|
|
|
|
/// Given a [`NodeId`], return the [`NodeId`] of the parent statement, if any.
|
|
pub fn parent_statement_id(&self, node_id: NodeId) -> Option<NodeId> {
|
|
self.nodes
|
|
.ancestor_ids(node_id)
|
|
.filter(|id| self.nodes[*id].is_statement())
|
|
.nth(1)
|
|
}
|
|
|
|
/// Set the [`Globals`] for the current [`Scope`].
|
|
pub fn set_globals(&mut self, globals: Globals<'a>) {
|
|
// If any global bindings don't already exist in the global scope, add them.
|
|
for (name, range) in globals.iter() {
|
|
if self
|
|
.global_scope()
|
|
.get(name)
|
|
.map_or(true, |binding_id| self.bindings[binding_id].is_unbound())
|
|
{
|
|
let id = self.bindings.push(Binding {
|
|
kind: BindingKind::Assignment,
|
|
range: *range,
|
|
references: Vec::new(),
|
|
scope: self.scope_id,
|
|
source: self.node_id,
|
|
context: self.execution_context(),
|
|
exceptions: self.exceptions(),
|
|
flags: BindingFlags::empty(),
|
|
});
|
|
self.global_scope_mut().add(name, id);
|
|
}
|
|
}
|
|
|
|
self.scopes[self.scope_id].set_globals_id(self.globals.push(globals));
|
|
}
|
|
|
|
/// Return the [`TextRange`] at which a name is declared as global in the current [`Scope`].
|
|
pub fn global(&self, name: &str) -> Option<TextRange> {
|
|
let global_id = self.scopes[self.scope_id].globals_id()?;
|
|
self.globals[global_id].get(name).copied()
|
|
}
|
|
|
|
/// Given a `name` that has been declared `nonlocal`, return the [`ScopeId`] and [`BindingId`]
|
|
/// to which it refers.
|
|
///
|
|
/// Unlike `global` declarations, for which the scope is unambiguous, Python requires that
|
|
/// `nonlocal` declarations refer to the closest enclosing scope that contains a binding for
|
|
/// the given name.
|
|
pub fn nonlocal(&self, name: &str) -> Option<(ScopeId, BindingId)> {
|
|
self.scopes
|
|
.ancestor_ids(self.scope_id)
|
|
.skip(1)
|
|
.find_map(|scope_id| {
|
|
let scope = &self.scopes[scope_id];
|
|
if scope.kind.is_module() || scope.kind.is_class() {
|
|
None
|
|
} else {
|
|
scope.get(name).map(|binding_id| (scope_id, binding_id))
|
|
}
|
|
})
|
|
}
|
|
|
|
/// Return `true` if the given [`ScopeId`] matches that of the current scope.
|
|
pub fn is_current_scope(&self, scope_id: ScopeId) -> bool {
|
|
self.scope_id == scope_id
|
|
}
|
|
|
|
/// Return `true` if the model is at the top level of the module (i.e., in the module scope,
|
|
/// and not nested within any statements).
|
|
pub fn at_top_level(&self) -> bool {
|
|
self.scope_id.is_global() && self.current_statement_parent_id().is_none()
|
|
}
|
|
|
|
/// Return `true` if the model is in an async context.
|
|
pub fn in_async_context(&self) -> bool {
|
|
for scope in self.current_scopes() {
|
|
if let ScopeKind::Function(ast::StmtFunctionDef { is_async, .. }) = scope.kind {
|
|
return *is_async;
|
|
}
|
|
}
|
|
false
|
|
}
|
|
|
|
/// Return `true` if the model is in a nested union expression (e.g., the inner `Union` in
|
|
/// `Union[Union[int, str], float]`).
|
|
pub fn in_nested_union(&self) -> bool {
|
|
// Ex) `Union[Union[int, str], float]`
|
|
if self
|
|
.current_expression_grandparent()
|
|
.and_then(Expr::as_subscript_expr)
|
|
.is_some_and(|parent| self.match_typing_expr(&parent.value, "Union"))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
// Ex) `int | Union[str, float]`
|
|
if self.current_expression_parent().is_some_and(|parent| {
|
|
matches!(
|
|
parent,
|
|
Expr::BinOp(ast::ExprBinOp {
|
|
op: Operator::BitOr,
|
|
..
|
|
})
|
|
)
|
|
}) {
|
|
return true;
|
|
}
|
|
|
|
false
|
|
}
|
|
|
|
/// Returns `true` if `left` and `right` are on different branches of an `if`, `match`, or
|
|
/// `try` statement.
|
|
///
|
|
/// This implementation assumes that the statements are in the same scope.
|
|
pub fn different_branches(&self, left: NodeId, right: NodeId) -> bool {
|
|
// Collect the branch path for the left statement.
|
|
let left = self
|
|
.nodes
|
|
.branch_id(left)
|
|
.iter()
|
|
.flat_map(|branch_id| self.branches.ancestor_ids(*branch_id))
|
|
.collect::<Vec<_>>();
|
|
|
|
// Collect the branch path for the right statement.
|
|
let right = self
|
|
.nodes
|
|
.branch_id(right)
|
|
.iter()
|
|
.flat_map(|branch_id| self.branches.ancestor_ids(*branch_id))
|
|
.collect::<Vec<_>>();
|
|
|
|
!left
|
|
.iter()
|
|
.zip(right.iter())
|
|
.all(|(left, right)| left == right)
|
|
}
|
|
|
|
/// Returns `true` if the given [`BindingId`] is used.
|
|
pub fn is_used(&self, binding_id: BindingId) -> bool {
|
|
self.bindings[binding_id].is_used()
|
|
}
|
|
|
|
/// Add a reference to the given [`BindingId`] in the local scope.
|
|
pub fn add_local_reference(&mut self, binding_id: BindingId, range: TextRange) {
|
|
let reference_id = self
|
|
.resolved_references
|
|
.push(self.scope_id, range, self.flags);
|
|
self.bindings[binding_id].references.push(reference_id);
|
|
}
|
|
|
|
/// Add a reference to the given [`BindingId`] in the global scope.
|
|
pub fn add_global_reference(&mut self, binding_id: BindingId, range: TextRange) {
|
|
let reference_id = self
|
|
.resolved_references
|
|
.push(ScopeId::global(), range, self.flags);
|
|
self.bindings[binding_id].references.push(reference_id);
|
|
}
|
|
|
|
/// Add a [`BindingId`] to the list of delayed annotations for the given [`BindingId`].
|
|
pub fn add_delayed_annotation(&mut self, binding_id: BindingId, annotation_id: BindingId) {
|
|
self.delayed_annotations
|
|
.entry(binding_id)
|
|
.or_insert_with(Vec::new)
|
|
.push(annotation_id);
|
|
}
|
|
|
|
/// Return the list of delayed annotations for the given [`BindingId`].
|
|
pub fn delayed_annotations(&self, binding_id: BindingId) -> Option<&[BindingId]> {
|
|
self.delayed_annotations.get(&binding_id).map(Vec::as_slice)
|
|
}
|
|
|
|
/// Mark the given [`BindingId`] as rebound in the given [`ScopeId`] (i.e., declared as
|
|
/// `global` or `nonlocal`).
|
|
pub fn add_rebinding_scope(&mut self, binding_id: BindingId, scope_id: ScopeId) {
|
|
self.rebinding_scopes
|
|
.entry(binding_id)
|
|
.or_insert_with(Vec::new)
|
|
.push(scope_id);
|
|
}
|
|
|
|
/// Return the list of [`ScopeId`]s in which the given [`BindingId`] is rebound (i.e., declared
|
|
/// as `global` or `nonlocal`).
|
|
pub fn rebinding_scopes(&self, binding_id: BindingId) -> Option<&[ScopeId]> {
|
|
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 union of all handled exceptions as an [`Exceptions`] bitflag.
|
|
pub fn exceptions(&self) -> Exceptions {
|
|
let mut exceptions = Exceptions::empty();
|
|
for exception in &self.handled_exceptions {
|
|
exceptions.insert(*exception);
|
|
}
|
|
exceptions
|
|
}
|
|
|
|
/// Generate a [`Snapshot`] of the current semantic model.
|
|
pub fn snapshot(&self) -> Snapshot {
|
|
Snapshot {
|
|
scope_id: self.scope_id,
|
|
node_id: self.node_id,
|
|
branch_id: self.branch_id,
|
|
definition_id: self.definition_id,
|
|
flags: self.flags,
|
|
}
|
|
}
|
|
|
|
/// Restore the semantic model to the given [`Snapshot`].
|
|
pub fn restore(&mut self, snapshot: Snapshot) {
|
|
let Snapshot {
|
|
scope_id,
|
|
node_id,
|
|
branch_id,
|
|
definition_id,
|
|
flags,
|
|
} = snapshot;
|
|
self.scope_id = scope_id;
|
|
self.node_id = node_id;
|
|
self.branch_id = branch_id;
|
|
self.definition_id = definition_id;
|
|
self.flags = flags;
|
|
}
|
|
|
|
/// Return the [`ExecutionContext`] of the current scope.
|
|
pub const fn execution_context(&self) -> ExecutionContext {
|
|
if self.flags.intersects(SemanticModelFlags::TYPING_CONTEXT) {
|
|
ExecutionContext::Typing
|
|
} else {
|
|
ExecutionContext::Runtime
|
|
}
|
|
}
|
|
|
|
/// Return `true` if the model is in a type annotation.
|
|
pub const fn in_annotation(&self) -> bool {
|
|
self.flags.intersects(SemanticModelFlags::ANNOTATION)
|
|
}
|
|
|
|
/// Return `true` if the model is in a typing-only type annotation.
|
|
pub const fn in_typing_only_annotation(&self) -> bool {
|
|
self.flags
|
|
.intersects(SemanticModelFlags::TYPING_ONLY_ANNOTATION)
|
|
}
|
|
|
|
/// Return `true` if the model is in a runtime-required type annotation.
|
|
pub const fn in_runtime_annotation(&self) -> bool {
|
|
self.flags
|
|
.intersects(SemanticModelFlags::RUNTIME_ANNOTATION)
|
|
}
|
|
|
|
/// Return `true` if the model is in a type definition.
|
|
pub const fn in_type_definition(&self) -> bool {
|
|
self.flags.intersects(SemanticModelFlags::TYPE_DEFINITION)
|
|
}
|
|
|
|
/// Return `true` if the model is in a string type definition.
|
|
pub const fn in_string_type_definition(&self) -> bool {
|
|
self.flags
|
|
.intersects(SemanticModelFlags::STRING_TYPE_DEFINITION)
|
|
}
|
|
|
|
/// Return `true` if the model is in a "simple" string type definition.
|
|
pub const fn in_simple_string_type_definition(&self) -> bool {
|
|
self.flags
|
|
.intersects(SemanticModelFlags::SIMPLE_STRING_TYPE_DEFINITION)
|
|
}
|
|
|
|
/// Return `true` if the model is in a "complex" string type definition.
|
|
pub const fn in_complex_string_type_definition(&self) -> bool {
|
|
self.flags
|
|
.intersects(SemanticModelFlags::COMPLEX_STRING_TYPE_DEFINITION)
|
|
}
|
|
|
|
/// Return `true` if the model is in a `__future__` type definition.
|
|
pub const fn in_future_type_definition(&self) -> bool {
|
|
self.flags
|
|
.intersects(SemanticModelFlags::FUTURE_TYPE_DEFINITION)
|
|
}
|
|
|
|
/// Return `true` if the model is in any kind of deferred type definition.
|
|
pub const fn in_deferred_type_definition(&self) -> bool {
|
|
self.flags
|
|
.intersects(SemanticModelFlags::DEFERRED_TYPE_DEFINITION)
|
|
}
|
|
|
|
/// Return `true` if the model is in a forward type reference.
|
|
///
|
|
/// Includes deferred string types, and future types in annotations.
|
|
///
|
|
/// ## Examples
|
|
/// ```python
|
|
/// from __future__ import annotations
|
|
///
|
|
/// from threading import Thread
|
|
///
|
|
///
|
|
/// x: Thread # Forward reference
|
|
/// cast("Thread", x) # Forward reference
|
|
/// cast(Thread, x) # Non-forward reference
|
|
/// ```
|
|
pub const fn in_forward_reference(&self) -> bool {
|
|
self.in_string_type_definition()
|
|
|| (self.in_future_type_definition() && self.in_typing_only_annotation())
|
|
}
|
|
|
|
/// Return `true` if the model is in an exception handler.
|
|
pub const fn in_exception_handler(&self) -> bool {
|
|
self.flags.intersects(SemanticModelFlags::EXCEPTION_HANDLER)
|
|
}
|
|
|
|
/// Return `true` if the model is in an f-string.
|
|
pub const fn in_f_string(&self) -> bool {
|
|
self.flags.intersects(SemanticModelFlags::F_STRING)
|
|
}
|
|
|
|
/// Return `true` if the model is in boolean test.
|
|
pub const fn in_boolean_test(&self) -> bool {
|
|
self.flags.intersects(SemanticModelFlags::BOOLEAN_TEST)
|
|
}
|
|
|
|
/// Return `true` if the model is in a `typing::Literal` annotation.
|
|
pub const fn in_literal(&self) -> bool {
|
|
self.flags.intersects(SemanticModelFlags::LITERAL)
|
|
}
|
|
|
|
/// Return `true` if the model is in a subscript expression.
|
|
pub const fn in_subscript(&self) -> bool {
|
|
self.flags.intersects(SemanticModelFlags::SUBSCRIPT)
|
|
}
|
|
|
|
/// Return `true` if the model is in a type-checking block.
|
|
pub const fn in_type_checking_block(&self) -> bool {
|
|
self.flags
|
|
.intersects(SemanticModelFlags::TYPE_CHECKING_BLOCK)
|
|
}
|
|
|
|
/// Return `true` if the model has traversed past the "top-of-file" import boundary.
|
|
pub const fn seen_import_boundary(&self) -> bool {
|
|
self.flags.intersects(SemanticModelFlags::IMPORT_BOUNDARY)
|
|
}
|
|
|
|
/// Return `true` if the model has traverse past the `__future__` import boundary.
|
|
pub const fn seen_futures_boundary(&self) -> bool {
|
|
self.flags.intersects(SemanticModelFlags::FUTURES_BOUNDARY)
|
|
}
|
|
|
|
/// Return `true` if `__future__`-style type annotations are enabled.
|
|
pub const fn future_annotations(&self) -> bool {
|
|
self.flags
|
|
.intersects(SemanticModelFlags::FUTURE_ANNOTATIONS)
|
|
}
|
|
|
|
/// Return an iterator over all bindings shadowed by the given [`BindingId`], within the
|
|
/// containing scope, and across scopes.
|
|
pub fn shadowed_bindings(
|
|
&self,
|
|
scope_id: ScopeId,
|
|
binding_id: BindingId,
|
|
) -> impl Iterator<Item = ShadowedBinding> + '_ {
|
|
let mut first = true;
|
|
let mut binding_id = binding_id;
|
|
std::iter::from_fn(move || {
|
|
// First, check whether this binding is shadowing another binding in a different scope.
|
|
if std::mem::take(&mut first) {
|
|
if let Some(shadowed_id) = self.shadowed_bindings.get(&binding_id).copied() {
|
|
return Some(ShadowedBinding {
|
|
binding_id,
|
|
shadowed_id,
|
|
same_scope: false,
|
|
});
|
|
}
|
|
}
|
|
|
|
// Otherwise, check whether this binding is shadowing another binding in the same scope.
|
|
if let Some(shadowed_id) = self.scopes[scope_id].shadowed_binding(binding_id) {
|
|
let next = ShadowedBinding {
|
|
binding_id,
|
|
shadowed_id,
|
|
same_scope: true,
|
|
};
|
|
|
|
// Advance to the next binding in the scope.
|
|
first = true;
|
|
binding_id = shadowed_id;
|
|
|
|
return Some(next);
|
|
}
|
|
|
|
None
|
|
})
|
|
}
|
|
}
|
|
|
|
pub struct ShadowedBinding {
|
|
/// The binding that is shadowing another binding.
|
|
binding_id: BindingId,
|
|
/// The binding that is being shadowed.
|
|
shadowed_id: BindingId,
|
|
/// Whether the shadowing and shadowed bindings are in the same scope.
|
|
same_scope: bool,
|
|
}
|
|
|
|
impl ShadowedBinding {
|
|
pub const fn binding_id(&self) -> BindingId {
|
|
self.binding_id
|
|
}
|
|
|
|
pub const fn shadowed_id(&self) -> BindingId {
|
|
self.shadowed_id
|
|
}
|
|
|
|
pub const fn same_scope(&self) -> bool {
|
|
self.same_scope
|
|
}
|
|
}
|
|
|
|
bitflags! {
|
|
/// Flags indicating the current model state.
|
|
#[derive(Debug, Default, Copy, Clone, Eq, PartialEq)]
|
|
pub struct SemanticModelFlags: u16 {
|
|
/// The model is in a typing-time-only type annotation.
|
|
///
|
|
/// For example, the model could be visiting `int` in:
|
|
/// ```python
|
|
/// def foo() -> int:
|
|
/// x: int = 1
|
|
/// ```
|
|
///
|
|
/// In this case, Python doesn't require that the type annotation be evaluated at runtime.
|
|
///
|
|
/// If `from __future__ import annotations` is used, all annotations are evaluated at
|
|
/// typing time. Otherwise, all function argument annotations are evaluated at runtime, as
|
|
/// are any annotated assignments in module or class scopes.
|
|
const TYPING_ONLY_ANNOTATION = 1 << 0;
|
|
|
|
/// The model is in a runtime type annotation.
|
|
///
|
|
/// For example, the model could be visiting `int` in:
|
|
/// ```python
|
|
/// def foo(x: int) -> int:
|
|
/// ...
|
|
/// ```
|
|
///
|
|
/// In this case, Python requires that the type annotation be evaluated at runtime,
|
|
/// as it needs to be available on the function's `__annotations__` attribute.
|
|
///
|
|
/// If `from __future__ import annotations` is used, all annotations are evaluated at
|
|
/// typing time. Otherwise, all function argument annotations are evaluated at runtime, as
|
|
/// are any annotated assignments in module or class scopes.
|
|
const RUNTIME_ANNOTATION = 1 << 1;
|
|
|
|
/// The model is in a type definition.
|
|
///
|
|
/// For example, the model could be visiting `int` in:
|
|
/// ```python
|
|
/// from typing import NewType
|
|
///
|
|
/// UserId = NewType("UserId", int)
|
|
/// ```
|
|
///
|
|
/// All type annotations are also type definitions, but the converse is not true.
|
|
/// In our example, `int` is a type definition but not a type annotation, as it
|
|
/// doesn't appear in a type annotation context, but rather in a type definition.
|
|
const TYPE_DEFINITION = 1 << 2;
|
|
|
|
/// The model is in a (deferred) "simple" string type definition.
|
|
///
|
|
/// For example, the model could be visiting `list[int]` in:
|
|
/// ```python
|
|
/// x: "list[int]" = []
|
|
/// ```
|
|
///
|
|
/// "Simple" string type definitions are those that consist of a single string literal,
|
|
/// as opposed to an implicitly concatenated string literal.
|
|
const SIMPLE_STRING_TYPE_DEFINITION = 1 << 3;
|
|
|
|
/// The model is in a (deferred) "complex" string type definition.
|
|
///
|
|
/// For example, the model could be visiting `list[int]` in:
|
|
/// ```python
|
|
/// x: ("list" "[int]") = []
|
|
/// ```
|
|
///
|
|
/// "Complex" string type definitions are those that consist of a implicitly concatenated
|
|
/// string literals. These are uncommon but valid.
|
|
const COMPLEX_STRING_TYPE_DEFINITION = 1 << 4;
|
|
|
|
/// The model is in a (deferred) `__future__` type definition.
|
|
///
|
|
/// For example, the model could be visiting `list[int]` in:
|
|
/// ```python
|
|
/// from __future__ import annotations
|
|
///
|
|
/// x: list[int] = []
|
|
/// ```
|
|
///
|
|
/// `__future__`-style type annotations are only enabled if the `annotations` feature
|
|
/// is enabled via `from __future__ import annotations`.
|
|
const FUTURE_TYPE_DEFINITION = 1 << 5;
|
|
|
|
/// The model is in an exception handler.
|
|
///
|
|
/// For example, the model could be visiting `x` in:
|
|
/// ```python
|
|
/// try:
|
|
/// ...
|
|
/// except Exception:
|
|
/// x: int = 1
|
|
/// ```
|
|
const EXCEPTION_HANDLER = 1 << 6;
|
|
|
|
/// The model is in an f-string.
|
|
///
|
|
/// For example, the model could be visiting `x` in:
|
|
/// ```python
|
|
/// f'{x}'
|
|
/// ```
|
|
const F_STRING = 1 << 7;
|
|
|
|
/// The model is in a boolean test.
|
|
///
|
|
/// For example, the model could be visiting `x` in:
|
|
/// ```python
|
|
/// if x:
|
|
/// ...
|
|
/// ```
|
|
///
|
|
/// The implication is that the actual value returned by the current expression is
|
|
/// not used, only its truthiness.
|
|
const BOOLEAN_TEST = 1 << 8;
|
|
|
|
/// The model is in a `typing::Literal` annotation.
|
|
///
|
|
/// For example, the model could be visiting any of `"A"`, `"B"`, or `"C"` in:
|
|
/// ```python
|
|
/// def f(x: Literal["A", "B", "C"]):
|
|
/// ...
|
|
/// ```
|
|
const LITERAL = 1 << 9;
|
|
|
|
/// The model is in a subscript expression.
|
|
///
|
|
/// For example, the model could be visiting `x["a"]` in:
|
|
/// ```python
|
|
/// x["a"]["b"]
|
|
/// ```
|
|
const SUBSCRIPT = 1 << 10;
|
|
|
|
/// The model is in a type-checking block.
|
|
///
|
|
/// For example, the model could be visiting `x` in:
|
|
/// ```python
|
|
/// from typing import TYPE_CHECKING
|
|
///
|
|
///
|
|
/// if TYPE_CHECKING:
|
|
/// x: int = 1
|
|
/// ```
|
|
const TYPE_CHECKING_BLOCK = 1 << 11;
|
|
|
|
/// The model has traversed past the "top-of-file" import boundary.
|
|
///
|
|
/// For example, the model could be visiting `x` in:
|
|
/// ```python
|
|
/// import os
|
|
///
|
|
/// def f() -> None:
|
|
/// ...
|
|
///
|
|
/// x: int = 1
|
|
/// ```
|
|
const IMPORT_BOUNDARY = 1 << 12;
|
|
|
|
/// The model has traversed past the `__future__` import boundary.
|
|
///
|
|
/// For example, the model could be visiting `x` in:
|
|
/// ```python
|
|
/// from __future__ import annotations
|
|
///
|
|
/// import os
|
|
///
|
|
/// x: int = 1
|
|
/// ```
|
|
///
|
|
/// Python considers it a syntax error to import from `__future__` after
|
|
/// any other non-`__future__`-importing statements.
|
|
const FUTURES_BOUNDARY = 1 << 13;
|
|
|
|
/// `__future__`-style type annotations are enabled in this model.
|
|
///
|
|
/// For example, the model could be visiting `x` in:
|
|
/// ```python
|
|
/// from __future__ import annotations
|
|
///
|
|
///
|
|
/// def f(x: int) -> int:
|
|
/// ...
|
|
/// ```
|
|
const FUTURE_ANNOTATIONS = 1 << 14;
|
|
|
|
/// The context is in any type annotation.
|
|
const ANNOTATION = Self::TYPING_ONLY_ANNOTATION.bits() | Self::RUNTIME_ANNOTATION.bits();
|
|
|
|
/// The context is in any string type definition.
|
|
const STRING_TYPE_DEFINITION = Self::SIMPLE_STRING_TYPE_DEFINITION.bits()
|
|
| Self::COMPLEX_STRING_TYPE_DEFINITION.bits();
|
|
|
|
/// The context is in any deferred type definition.
|
|
const DEFERRED_TYPE_DEFINITION = Self::SIMPLE_STRING_TYPE_DEFINITION.bits()
|
|
| Self::COMPLEX_STRING_TYPE_DEFINITION.bits()
|
|
| Self::FUTURE_TYPE_DEFINITION.bits();
|
|
|
|
/// The context is in a typing-only context.
|
|
const TYPING_CONTEXT = Self::TYPE_CHECKING_BLOCK.bits() | Self::TYPING_ONLY_ANNOTATION.bits() |
|
|
Self::STRING_TYPE_DEFINITION.bits();
|
|
}
|
|
}
|
|
|
|
impl SemanticModelFlags {
|
|
pub fn new(path: &Path) -> Self {
|
|
let mut flags = Self::default();
|
|
if is_python_stub_file(path) {
|
|
flags |= Self::FUTURE_ANNOTATIONS;
|
|
}
|
|
flags
|
|
}
|
|
}
|
|
|
|
/// A snapshot of the [`SemanticModel`] at a given point in the AST traversal.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub struct Snapshot {
|
|
scope_id: ScopeId,
|
|
node_id: Option<NodeId>,
|
|
branch_id: Option<BranchId>,
|
|
definition_id: DefinitionId,
|
|
flags: SemanticModelFlags,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub enum ReadResult {
|
|
/// The read reference is resolved to a specific binding.
|
|
///
|
|
/// For example, given:
|
|
/// ```python
|
|
/// x = 1
|
|
/// print(x)
|
|
/// ```
|
|
///
|
|
/// The `x` in `print(x)` is resolved to the binding of `x` in `x = 1`.
|
|
Resolved(BindingId),
|
|
|
|
/// The read reference is resolved to a context-specific, implicit global (e.g., `__class__`
|
|
/// within a class scope).
|
|
///
|
|
/// For example, given:
|
|
/// ```python
|
|
/// class C:
|
|
/// print(__class__)
|
|
/// ```
|
|
///
|
|
/// The `__class__` in `print(__class__)` is resolved to the implicit global `__class__`.
|
|
ImplicitGlobal,
|
|
|
|
/// The read reference is unresolved, but at least one of the containing scopes contains a
|
|
/// wildcard import.
|
|
///
|
|
/// For example, given:
|
|
/// ```python
|
|
/// from x import *
|
|
///
|
|
/// print(y)
|
|
/// ```
|
|
///
|
|
/// The `y` in `print(y)` is unresolved, but the containing scope contains a wildcard import,
|
|
/// so `y` _may_ be resolved to a symbol imported by the wildcard import.
|
|
WildcardImport,
|
|
|
|
/// The read reference is resolved, but to an unbound local variable.
|
|
///
|
|
/// For example, given:
|
|
/// ```python
|
|
/// x = 1
|
|
/// del x
|
|
/// print(x)
|
|
/// ```
|
|
///
|
|
/// The `x` in `print(x)` is an unbound local.
|
|
UnboundLocal(BindingId),
|
|
|
|
/// The read reference is definitively unresolved.
|
|
///
|
|
/// For example, given:
|
|
/// ```python
|
|
/// print(x)
|
|
/// ```
|
|
///
|
|
/// The `x` in `print(x)` is definitively unresolved.
|
|
NotFound,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub struct ImportedName {
|
|
/// The name to which the imported symbol is bound.
|
|
name: String,
|
|
/// The range at which the symbol is imported.
|
|
range: TextRange,
|
|
/// The context in which the symbol is imported.
|
|
context: ExecutionContext,
|
|
}
|
|
|
|
impl ImportedName {
|
|
pub fn into_name(self) -> String {
|
|
self.name
|
|
}
|
|
|
|
pub const fn context(&self) -> ExecutionContext {
|
|
self.context
|
|
}
|
|
}
|
|
|
|
impl Ranged for ImportedName {
|
|
fn range(&self) -> TextRange {
|
|
self.range
|
|
}
|
|
}
|
|
|
|
/// A unique identifier for an [`ast::ExprName`]. No two names can even appear at the same location
|
|
/// in the source code, so the starting offset is a cheap and sufficient unique identifier.
|
|
#[derive(Debug, Hash, PartialEq, Eq)]
|
|
struct NameId(TextSize);
|
|
|
|
impl From<&ast::ExprName> for NameId {
|
|
fn from(name: &ast::ExprName) -> Self {
|
|
Self(name.start())
|
|
}
|
|
}
|