ruff/crates/ruff_python_semantic/src/model.rs
David Salvisberg 8fcac0ff36
Recognize all symbols named TYPE_CHECKING for in_type_checking_block (#15719)
Closes #15681

## Summary

This changes `analyze::typing::is_type_checking_block` to recognize all
symbols named "TYPE_CHECKING".
This matches the current behavior of mypy and pyright as well as
`flake8-type-checking`.

It also drops support for detecting `if False:` and `if 0:` as type
checking blocks. This used to be an option for
providing backwards compatibility with Python versions that did not have
a `typing` module, but has since
been removed from the typing spec and is no longer supported by any of
the mainstream type checkers.

## Test Plan

`cargo nextest run`

---------

Co-authored-by: Micha Reiser <micha@reiser.io>
2025-02-06 14:45:12 +01:00

2713 lines
104 KiB
Rust

use std::path::Path;
use bitflags::bitflags;
use rustc_hash::FxHashMap;
use ruff_python_ast::helpers::from_relative_import;
use ruff_python_ast::name::{QualifiedName, UnqualifiedName};
use ruff_python_ast::{self as ast, Expr, ExprContext, PySourceType, Stmt};
use ruff_text_size::{Ranged, 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;
pub mod all;
/// A semantic model for a Python module, to enable querying the module's semantic information.
pub struct SemanticModel<'a> {
typing_modules: &'a [String],
module: Module<'a>,
/// 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,
/// Modules that have been seen by the semantic model.
pub seen: Modules,
/// Exceptions that are handled by the current `try` block.
///
/// For example, if we're visiting the `x = 1` assignment below,
/// `AttributeError` is considered to be a "handled exception",
/// but `TypeError` is not:
///
/// ```py
/// try:
/// try:
/// foo()
/// except TypeError:
/// pass
/// except AttributeError:
/// pass
/// ```
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: &Path, module: Module<'a>) -> Self {
Self {
typing_modules,
module,
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),
seen: Modules::empty(),
handled_exceptions: Vec::default(),
resolved_names: FxHashMap::default(),
}
}
/// Return the [`Binding`] for the given [`BindingId`].
#[inline]
pub fn binding(&self, id: BindingId) -> &Binding<'a> {
&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.seen_typing()
&& self
.resolve_qualified_name(expr)
.is_some_and(|qualified_name| {
self.match_typing_qualified_name(&qualified_name, target)
})
}
/// Return `true` if the call path is a reference to `typing.${target}`.
pub fn match_typing_qualified_name(
&self,
qualified_name: &QualifiedName,
target: &str,
) -> bool {
if matches!(
qualified_name.segments(),
["typing" | "_typeshed" | "typing_extensions", member] if *member == target
) {
return true;
}
if self.typing_modules.iter().any(|module| {
let module = QualifiedName::from_dotted_name(module);
qualified_name == &module.append_member(target)
}) {
return true;
}
false
}
/// Return an iterator over the set of `typing` modules allowed in the semantic model.
pub fn typing_modules(&self) -> impl Iterator<Item = &'a str> {
["typing", "_typeshed", "typing_extensions"]
.iter()
.copied()
.chain(self.typing_modules.iter().map(String::as_str))
}
/// 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 *in the scope we are currently visiting*.
///
/// Note that a "builtin binding" does *not* include explicit lookups via the `builtins`
/// module, e.g. `import builtins; builtins.open`. It *only* includes the bindings
/// that are pre-populated in Python's global scope before any imports have taken place.
pub fn has_builtin_binding(&self, member: &str) -> bool {
self.has_builtin_binding_in_scope(member, self.scope_id)
}
/// Return `true` if `member` is bound as a builtin *in a given scope*.
///
/// Note that a "builtin binding" does *not* include explicit lookups via the `builtins`
/// module, e.g. `import builtins; builtins.open`. It *only* includes the bindings
/// that are pre-populated in Python's global scope before any imports have taken place.
pub fn has_builtin_binding_in_scope(&self, member: &str, scope: ScopeId) -> bool {
self.lookup_symbol_in_scope(member, scope, false)
.map(|binding_id| &self.bindings[binding_id])
.is_some_and(|binding| binding.kind.is_builtin())
}
/// If `expr` is a reference to a builtins symbol,
/// return the name of that symbol. Else, return `None`.
///
/// This method returns `true` both for "builtin bindings"
/// (present even without any imports, e.g. `open()`), and for explicit lookups
/// via the `builtins` module (e.g. `import builtins; builtins.open()`).
pub fn resolve_builtin_symbol<'expr>(&'a self, expr: &'expr Expr) -> Option<&'a str>
where
'expr: 'a,
{
// Fast path: we only need to worry about name expressions
if !self.seen_module(Modules::BUILTINS) {
let name = &expr.as_name_expr()?.id;
return if self.has_builtin_binding(name) {
Some(name)
} else {
None
};
}
// Slow path: we have to consider names and attributes
let qualified_name = self.resolve_qualified_name(expr)?;
match qualified_name.segments() {
["" | "builtins", name] => Some(*name),
_ => None,
}
}
/// Return `true` if `expr` is a reference to `builtins.$target`,
/// i.e. either `object` (where `object` is not overridden in the global scope),
/// or `builtins.object` (where `builtins` is imported as a module at the top level)
pub fn match_builtin_expr(&self, expr: &Expr, symbol: &str) -> bool {
debug_assert!(!symbol.contains('.'));
// fast path with more short-circuiting
if !self.seen_module(Modules::BUILTINS) {
let Expr::Name(ast::ExprName { id, .. }) = expr else {
return false;
};
return id == symbol && self.has_builtin_binding(symbol);
}
// slow path: we need to consider attribute accesses and aliased imports
let Some(qualified_name) = self.resolve_qualified_name(expr) else {
return false;
};
matches!(qualified_name.segments(), ["" | "builtins", name] if *name == symbol)
}
/// Return `true` if `member` is an "available" symbol, i.e., a symbol that has not been bound
/// in the current scope currently being visited, or in any containing scope.
pub fn is_available(&self, member: &str) -> bool {
self.is_available_in_scope(member, self.scope_id)
}
/// Return `true` if `member` is an "available" symbol in a given scope, i.e.,
/// a symbol that has not been bound in that current scope, or in any containing scope.
pub fn is_available_in_scope(&self, member: &str, scope_id: ScopeId) -> bool {
self.lookup_symbol_in_scope(member, scope_id, false)
.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, ExprContext::Del, 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(),
self.node_id,
ExprContext::Load,
self.flags,
name.range,
);
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(),
self.node_id,
ExprContext::Load,
self.flags,
name.range,
);
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,
self.node_id,
ExprContext::Load,
self.flags,
name.range,
);
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,
self.node_id,
ExprContext::Load,
self.flags,
name.range,
);
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.
//
// Stub files are an exception. In a stub file, it _is_ considered valid to
// resolve to a type annotation.
BindingKind::Annotation if !self.in_stub_file() => 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);
}
BindingKind::ConditionalDeletion(binding_id) => {
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,
self.node_id,
ExprContext::Load,
self.flags,
name.range,
);
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,
self.node_id,
ExprContext::Load,
self.flags,
name.range,
);
self.bindings[binding_id].references.push(reference_id);
}
self.resolved_names.insert(name.into(), binding_id);
return ReadResult::Resolved(binding_id);
}
BindingKind::Global(Some(binding_id))
| BindingKind::Nonlocal(binding_id, _) => {
// Mark the shadowed binding as used.
let reference_id = self.resolved_references.push(
self.scope_id,
self.node_id,
ExprContext::Load,
self.flags,
name.range,
);
self.bindings[binding_id].references.push(reference_id);
// Treat it as resolved.
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.
pub fn lookup_symbol(&self, symbol: &str) -> Option<BindingId> {
self.lookup_symbol_in_scope(symbol, self.scope_id, self.in_forward_reference())
}
/// Lookup a symbol in a certain 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_in_scope(
&self,
symbol: &str,
scope_id: ScopeId,
in_forward_reference: bool,
) -> Option<BindingId> {
if 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(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;
seen_function |= scope.kind.is_function();
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::ConditionalDeletion(binding_id) => return Some(binding_id),
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;
}
}
}
None
}
/// Simulates a runtime load of a given [`ast::ExprName`].
///
/// This should not be run until after all the bindings have been visited.
///
/// The main purpose of this method and what makes this different
/// from methods like [`SemanticModel::lookup_symbol`] and
/// [`SemanticModel::resolve_name`] is that it may be used
/// to perform speculative name lookups.
///
/// In most cases a load can be accurately modeled simply by calling
/// [`SemanticModel::resolve_name`] at the right time during semantic
/// analysis, however for speculative lookups this is not the case,
/// since we're aiming to change the semantic meaning of our load.
/// E.g. we want to check what would happen if we changed a forward
/// reference to an immediate load or vice versa.
///
/// Use caution when utilizing this method, since it was primarily designed
/// to work for speculative lookups from within type definitions, which
/// happen to share some nice properties, where attaching each binding
/// to a range in the source code and ordering those bindings based on
/// that range is a good enough approximation of which bindings are
/// available at runtime for which reference.
///
/// References from within an [`ast::Comprehension`] can produce incorrect
/// results when referring to a [`BindingKind::NamedExprAssignment`].
pub fn simulate_runtime_load(
&self,
name: &ast::ExprName,
typing_only_bindings_status: TypingOnlyBindingsStatus,
) -> Option<BindingId> {
self.simulate_runtime_load_at_location_in_scope(
name.id.as_str(),
name.range,
self.scope_id,
typing_only_bindings_status,
)
}
/// Simulates a runtime load of the given symbol.
///
/// This should not be run until after all the bindings have been visited.
///
/// The main purpose of this method and what makes this different from
/// [`SemanticModel::lookup_symbol_in_scope`] is that it may be used to
/// perform speculative name lookups.
///
/// In most cases a load can be accurately modeled simply by calling
/// [`SemanticModel::lookup_symbol`] at the right time during semantic
/// analysis, however for speculative lookups this is not the case,
/// since we're aiming to change the semantic meaning of our load.
/// E.g. we want to check what would happen if we changed a forward
/// reference to an immediate load or vice versa.
///
/// Use caution when utilizing this method, since it was primarily designed
/// to work for speculative lookups from within type definitions, which
/// happen to share some nice properties, where attaching each binding
/// to a range in the source code and ordering those bindings based on
/// that range is a good enough approximation of which bindings are
/// available at runtime for which reference.
///
/// References from within an [`ast::Comprehension`] can produce incorrect
/// results when referring to a [`BindingKind::NamedExprAssignment`].
pub fn simulate_runtime_load_at_location_in_scope(
&self,
symbol: &str,
symbol_range: TextRange,
scope_id: ScopeId,
typing_only_bindings_status: TypingOnlyBindingsStatus,
) -> Option<BindingId> {
let mut seen_function = false;
let mut class_variables_visible = true;
let mut source_order_sensitive_lookup = true;
for (index, scope_id) in self.scopes.ancestor_ids(scope_id).enumerate() {
let scope = &self.scopes[scope_id];
// Only once we leave a function scope and its enclosing type scope should
// we stop doing source-order lookups. We could e.g. have nested classes
// where we lookup symbols from the innermost class scope, which can only see
// things from the outer class(es) that have been defined before the inner
// class. Source-order lookups take advantage of the fact that most of the
// bindings are created sequentially in source order, so if we want to
// determine whether or not a given reference can refer to another binding
// we can look at their text ranges to check whether or not the binding
// could actually be referred to. This is not as robust as back-tracking
// the AST, since that can properly take care of the few out-of order
// corner-cases, but back-tracking the AST from the reference to the binding
// is a lot more expensive than comparing a pair of text ranges.
if seen_function && !scope.kind.is_type() {
source_order_sensitive_lookup = false;
}
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;
seen_function |= scope.kind.is_function();
if let Some(binding_id) = scope.get(symbol) {
if source_order_sensitive_lookup {
// we need to look through all the shadowed bindings
// since we may be shadowing a source-order accurate
// runtime binding with a source-order inaccurate one
for shadowed_id in scope.shadowed_bindings(binding_id) {
let binding = &self.bindings[shadowed_id];
if typing_only_bindings_status.is_disallowed()
&& binding.context.is_typing()
{
continue;
}
if let BindingKind::Annotation
| BindingKind::Deletion
| BindingKind::UnboundException(..)
| BindingKind::ConditionalDeletion(..) = binding.kind
{
continue;
}
// This ensures we perform the correct source-order lookup,
// since the ranges for these two types of bindings are trimmed
// to just the target, but the name is not available until the
// end of the entire statement
let binding_range = match binding.statement(self) {
Some(Stmt::Assign(stmt)) => stmt.range(),
Some(Stmt::AnnAssign(stmt)) => stmt.range(),
Some(Stmt::ClassDef(stmt)) => stmt.range(),
_ => binding.range,
};
if binding_range.ordering(symbol_range).is_lt() {
return Some(shadowed_id);
}
}
} else {
let candidate_id = match self.bindings[binding_id].kind {
BindingKind::Annotation => continue,
BindingKind::Deletion | BindingKind::UnboundException(None) => return None,
BindingKind::ConditionalDeletion(binding_id) => binding_id,
BindingKind::UnboundException(Some(binding_id)) => binding_id,
_ => binding_id,
};
if typing_only_bindings_status.is_disallowed()
&& self.bindings[candidate_id].context.is_typing()
{
continue;
}
return Some(candidate_id);
}
}
if index == 0 && scope.kind.is_class() {
if matches!(symbol, "__module__" | "__qualname__") {
return None;
}
}
}
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(&self, value: &Expr) -> Option<BindingId> {
let unqualified_name = UnqualifiedName::from_expr(value)?;
// Find the symbol in the current scope.
let (symbol, attribute) = unqualified_name.segments().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.qualified_name();
let segment = call_path.segments().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 [`ast::ExprName`] to the [`BindingId`] of the symbol it refers to, if it's the
/// only binding to that name in its scope.
pub fn only_binding(&self, name: &ast::ExprName) -> Option<BindingId> {
self.resolve_name(name).filter(|id| {
let binding = self.binding(*id);
let scope = &self.scopes[binding.scope];
scope.shadowed_binding(*id).is_none()
})
}
/// 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_qualified_name(${python_version})` will resolve to `sys.version_info`.
pub fn resolve_qualified_name<'name, 'expr: 'name>(
&self,
value: &'expr Expr,
) -> Option<QualifiedName<'name>>
where
'a: 'name,
{
/// 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 { qualified_name }) => {
let unqualified_name = UnqualifiedName::from_expr(value)?;
let (_, tail) = unqualified_name.segments().split_first()?;
let resolved: QualifiedName = qualified_name
.segments()
.iter()
.chain(tail)
.copied()
.collect();
Some(resolved)
}
BindingKind::SubmoduleImport(SubmoduleImport { qualified_name }) => {
let value_name = UnqualifiedName::from_expr(value)?;
let (_, tail) = value_name.segments().split_first()?;
Some(
qualified_name
.segments()
.iter()
.take(1)
.chain(tail)
.copied()
.collect(),
)
}
BindingKind::FromImport(FromImport { qualified_name }) => {
let value_name = UnqualifiedName::from_expr(value)?;
let (_, tail) = value_name.segments().split_first()?;
let resolved: QualifiedName =
if qualified_name.segments().first().copied() == Some(".") {
from_relative_import(
self.module.qualified_name()?,
qualified_name.segments(),
tail,
)?
} else {
qualified_name
.segments()
.iter()
.chain(tail)
.copied()
.collect()
};
Some(resolved)
}
BindingKind::Builtin => {
if value.is_name_expr() {
// Ex) `dict`
Some(QualifiedName::builtin(head.id.as_str()))
} else {
// Ex) `dict.__dict__`
let value_name = UnqualifiedName::from_expr(value)?;
Some(
std::iter::once("")
.chain(value_name.segments().iter().copied())
.collect(),
)
}
}
BindingKind::ClassDefinition(_) | BindingKind::FunctionDefinition(_) => {
// If we have a fully-qualified path for the module, use it.
if let Some(path) = self.module.qualified_name() {
Some(
path.iter()
.map(String::as_str)
.chain(
UnqualifiedName::from_expr(value)?
.segments()
.iter()
.copied(),
)
.collect(),
)
} else {
// Otherwise, if we're in (e.g.) a script, use the module name.
Some(
std::iter::once(self.module.name()?)
.chain(
UnqualifiedName::from_expr(value)?
.segments()
.iter()
.copied(),
)
.collect(),
)
}
}
_ => 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)| {
let mut imported_names = scope.bindings().filter_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 { qualified_name }) => {
if qualified_name.segments() == 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}"),
source,
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 { qualified_name }) => {
if let Some((target_member, target_module)) =
qualified_name.segments().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(),
source,
range: self.nodes[source].range(),
context: binding.context,
});
}
}
}
}
}
// Ex) Given `module="os"` and `object="name"`:
// `import os.path ` -> `os.name`
// Ex) Given `module="os.path"` and `object="join"`:
// `import os.path ` -> `os.path.join`
BindingKind::SubmoduleImport(SubmoduleImport { qualified_name }) => {
if qualified_name.segments().starts_with(&module_path) {
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!("{module}.{member}"),
source,
range: self.nodes[source].range(),
context: binding.context,
});
}
}
}
}
// Non-imports.
_ => {}
}
None
});
let first = imported_names.next()?;
if let Some(second) = imported_names.next() {
// Multiple candidates. We need to sort them because `scope.bindings()` is a HashMap
// which doesn't have a stable iteration order.
let mut imports: Vec<_> =
[first, second].into_iter().chain(imported_names).collect();
imports.sort_unstable_by_key(|import| import.range.start());
// Return the binding that was imported last.
imports.pop()
} else {
Some(first)
}
})
}
/// 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)
.map_while(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`], if any.
pub fn current_statement_id(&self) -> Option<NodeId> {
self.current_statement_ids().next()
}
/// 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<'a>> {
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 ID of the parent of the given [`ScopeId`], if any.
pub fn parent_scope_id(&self, scope_id: ScopeId) -> Option<ScopeId> {
self.scopes[scope_id].parent
}
/// 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
}
/// Returns the first parent of the given [`ScopeId`] that is not of [`ScopeKind::Type`], if any.
pub fn first_non_type_parent_scope_id(&self, scope_id: ScopeId) -> Option<ScopeId> {
let mut current_scope_id = scope_id;
while let Some(parent_id) = self.parent_scope_id(current_scope_id) {
if self.scopes[parent_id].kind.is_type() {
current_scope_id = parent_id;
} else {
return Some(parent_id);
}
}
None
}
/// Return the [`Stmt`] corresponding to the given [`NodeId`].
#[inline]
pub fn node(&self, node_id: NodeId) -> &NodeRef<'a> {
&self.nodes[node_id]
}
/// Given a [`NodeId`], return its parent, if any.
#[inline]
pub fn parent_expression(&self, node_id: NodeId) -> Option<&'a Expr> {
let parent_node_id = self.nodes.ancestor_ids(node_id).nth(1)?;
self.nodes[parent_node_id].as_expression()
}
/// Given a [`NodeId`], return the [`NodeId`] of the parent expression, if any.
pub fn parent_expression_id(&self, node_id: NodeId) -> Option<NodeId> {
let parent_node_id = self.nodes.ancestor_ids(node_id).nth(1)?;
self.nodes[parent_node_id]
.is_expression()
.then_some(parent_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")
}
/// Returns an [`Iterator`] over the statements, starting from the given [`NodeId`].
/// through to any parents.
pub fn statements(&self, node_id: NodeId) -> impl Iterator<Item = &'a Stmt> + '_ {
self.nodes
.ancestor_ids(node_id)
.filter_map(move |id| self.nodes[id].as_statement())
}
/// 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)
}
/// Return the [`Expr`] corresponding to the given [`NodeId`].
#[inline]
pub fn expression(&self, node_id: NodeId) -> Option<&'a Expr> {
self.nodes[node_id].as_expression()
}
/// Returns an [`Iterator`] over the expressions, starting from the given [`NodeId`].
/// through to any parents.
pub fn expressions(&self, node_id: NodeId) -> impl Iterator<Item = &'a Expr> + '_ {
self.nodes
.ancestor_ids(node_id)
.map_while(move |id| self.nodes[id].as_expression())
}
/// Mark a Python module as "seen" by the semantic model. Future callers can quickly discount
/// the need to resolve symbols from these modules if they haven't been seen.
pub fn add_module(&mut self, module: &str) {
match module {
"_typeshed" => self.seen.insert(Modules::TYPESHED),
"anyio" => self.seen.insert(Modules::ANYIO),
"builtins" => self.seen.insert(Modules::BUILTINS),
"collections" => self.seen.insert(Modules::COLLECTIONS),
"copy" => self.seen.insert(Modules::COPY),
"contextvars" => self.seen.insert(Modules::CONTEXTVARS),
"dataclasses" => self.seen.insert(Modules::DATACLASSES),
"datetime" => self.seen.insert(Modules::DATETIME),
"django" => self.seen.insert(Modules::DJANGO),
"fastapi" => self.seen.insert(Modules::FASTAPI),
"flask" => self.seen.insert(Modules::FLASK),
"logging" => self.seen.insert(Modules::LOGGING),
"markupsafe" => self.seen.insert(Modules::MARKUPSAFE),
"mock" => self.seen.insert(Modules::MOCK),
"numpy" => self.seen.insert(Modules::NUMPY),
"os" => self.seen.insert(Modules::OS),
"pandas" => self.seen.insert(Modules::PANDAS),
"pytest" => self.seen.insert(Modules::PYTEST),
"re" => self.seen.insert(Modules::RE),
"regex" => self.seen.insert(Modules::REGEX),
"six" => self.seen.insert(Modules::SIX),
"subprocess" => self.seen.insert(Modules::SUBPROCESS),
"tarfile" => self.seen.insert(Modules::TARFILE),
"trio" => self.seen.insert(Modules::TRIO),
"typing" => self.seen.insert(Modules::TYPING),
"typing_extensions" => self.seen.insert(Modules::TYPING_EXTENSIONS),
"attr" | "attrs" => self.seen.insert(Modules::ATTRS),
"airflow" => self.seen.insert(Modules::AIRFLOW),
"hashlib" => self.seen.insert(Modules::HASHLIB),
"crypt" => self.seen.insert(Modules::CRYPT),
_ => {}
}
}
/// Return `true` if the [`Module`] was "seen" anywhere in the semantic model. This is used as
/// a fast path to avoid unnecessary work when resolving symbols.
///
/// Callers should still verify that the module is available in the current scope, as visiting
/// an import of the relevant module _anywhere_ in the file will cause this method to return
/// `true`.
pub fn seen_module(&self, module: Modules) -> bool {
self.seen.intersects(module)
}
pub fn seen_typing(&self) -> bool {
self.seen_module(Modules::TYPING | Modules::TYPESHED | Modules::TYPING_EXTENSIONS)
|| !self.typing_modules.is_empty()
}
/// 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: ScopeId::global(),
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)
}
/// 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 {
let mut parent_expressions = self.current_expressions().skip(1);
match parent_expressions.next() {
// The parent expression is of the inner union is a single `typing.Union`.
// Ex) `Union[Union[a, b]]`
Some(Expr::Subscript(parent)) => self.match_typing_expr(&parent.value, "Union"),
// The parent expression is of the inner union is a tuple with two or more
// comma-separated elements and the parent of that tuple is a `typing.Union`.
// Ex) `Union[Union[a, b], Union[c, d]]`
Some(Expr::Tuple(_)) => parent_expressions
.next()
.and_then(Expr::as_subscript_expr)
.is_some_and(|grandparent| self.match_typing_expr(&grandparent.value, "Union")),
// The parent expression of the inner union is a PEP604-style union.
// Ex) `a | b | c` or `Union[a, b] | c`
// In contrast to `typing.Union`, PEP604-style unions are always binary operations, e.g.
// the expression `a | b | c` is represented by two binary unions: `(a | b) | c`.
Some(Expr::BinOp(bin_op)) => bin_op.op.is_bit_or(),
// Not a nested union otherwise.
_ => false,
}
}
/// Return `true` if the model is in a nested literal expression (e.g., the inner `Literal` in
/// `Literal[Literal[int, str], float]`).
pub fn in_nested_literal(&self) -> bool {
let mut parent_expressions = self.current_expressions().skip(1);
match parent_expressions.next() {
// The parent expression of the current `Literal` is a tuple, and the
// grandparent is a `Literal`.
// Ex) `Literal[Literal[str], Literal[int]]`
Some(Expr::Tuple(_)) => parent_expressions
.next()
.and_then(Expr::as_subscript_expr)
.is_some_and(|grandparent| self.match_typing_expr(&grandparent.value, "Literal")),
// The parent expression of the current `Literal` is also a `Literal`.
// Ex) `Literal[Literal[str]]`
Some(Expr::Subscript(parent)) => self.match_typing_expr(&parent.value, "Literal"),
// Not a nested literal otherwise
_ => false,
}
}
/// Returns `true` if `left` and `right` are in the same branches of an `if`, `match`, or
/// `try` statement.
///
/// This implementation assumes that the statements are in the same scope.
pub fn same_branch(&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 == right
}
/// Returns `true` if the given expression is an unused variable, or consists solely of
/// references to other unused variables. This method is conservative in that it considers a
/// variable to be "used" if it's shadowed by another variable with usages.
pub fn is_unused(&self, expr: &Expr) -> bool {
match expr {
Expr::Tuple(tuple) => tuple.iter().all(|expr| self.is_unused(expr)),
Expr::Name(ast::ExprName { id, .. }) => {
// Treat a variable as used if it has any usages, _or_ it's shadowed by another variable
// with usages.
//
// If we don't respect shadowing, we'll incorrectly flag `bar` as unused in:
// ```python
// from random import random
//
// for bar in range(10):
// if random() > 0.5:
// break
// else:
// bar = 1
//
// print(bar)
// ```
self.current_scope()
.get_all(id)
.map(|binding_id| self.binding(binding_id))
.filter(|binding| binding.start() >= expr.start())
.all(Binding::is_unused)
}
_ => false,
}
}
/// Add a reference to the given [`BindingId`] in the local scope.
pub fn add_local_reference(
&mut self,
binding_id: BindingId,
ctx: ExprContext,
range: TextRange,
) {
let reference_id =
self.resolved_references
.push(self.scope_id, self.node_id, ctx, self.flags, range);
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,
ctx: ExprContext,
range: TextRange,
) {
let reference_id =
self.resolved_references
.push(ScopeId::global(), self.node_id, ctx, self.flags, range);
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_default()
.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_default()
.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 context is in a runtime-evaluated type annotation.
pub const fn in_runtime_evaluated_annotation(&self) -> bool {
self.flags
.intersects(SemanticModelFlags::RUNTIME_EVALUATED_ANNOTATION)
}
/// Return `true` if the context is in a runtime-required type annotation.
pub const fn in_runtime_required_annotation(&self) -> bool {
self.flags
.intersects(SemanticModelFlags::RUNTIME_REQUIRED_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 visiting a "string type definition"
/// that was previously deferred when initially traversing the AST
pub const fn in_string_type_definition(&self) -> bool {
self.flags
.intersects(SemanticModelFlags::STRING_TYPE_DEFINITION)
}
/// Return `true` if the model is visiting a "simple string type definition"
/// that was previously deferred when initially traversing the AST
pub const fn in_simple_string_type_definition(&self) -> bool {
self.flags
.intersects(SemanticModelFlags::SIMPLE_STRING_TYPE_DEFINITION)
}
/// Return `true` if the model is visiting a "complex string type definition"
/// that was previously deferred when initially traversing the AST
pub const fn in_complex_string_type_definition(&self) -> bool {
self.flags
.intersects(SemanticModelFlags::COMPLEX_STRING_TYPE_DEFINITION)
}
/// Return `true` if the model is visiting a "`__future__` type definition"
/// that was previously deferred when initially traversing the AST
pub const fn in_future_type_definition(&self) -> bool {
self.flags
.intersects(SemanticModelFlags::FUTURE_TYPE_DEFINITION)
}
/// Return `true` if the model is visiting any kind of type definition
/// that was previously deferred when initially traversing the AST
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 visiting the value expression
/// of a [PEP 613] type alias.
///
/// For example:
/// ```python
/// from typing import TypeAlias
///
/// OptStr: TypeAlias = str | None # We're visiting the RHS
/// ```
///
/// [PEP 613]: https://peps.python.org/pep-0613/
pub const fn in_annotated_type_alias_value(&self) -> bool {
self.flags
.intersects(SemanticModelFlags::ANNOTATED_TYPE_ALIAS)
}
/// Return `true` if the model is visiting the value expression
/// of a [PEP 695] type alias.
///
/// For example:
/// ```python
/// type OptStr = str | None # We're visiting the RHS
/// ```
///
/// [PEP 695]: https://peps.python.org/pep-0695/#generic-type-alias
pub const fn in_deferred_type_alias_value(&self) -> bool {
self.flags
.intersects(SemanticModelFlags::DEFERRED_TYPE_ALIAS)
}
/// Return `true` if the model is visiting the value expression of
/// either kind of type alias.
pub const fn in_type_alias_value(&self) -> bool {
self.flags.intersects(SemanticModelFlags::TYPE_ALIAS)
}
/// 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 `assert` statement.
pub const fn in_assert_statement(&self) -> bool {
self.flags.intersects(SemanticModelFlags::ASSERT_STATEMENT)
}
/// 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 an f-string replacement field.
pub const fn in_f_string_replacement_field(&self) -> bool {
self.flags
.intersects(SemanticModelFlags::F_STRING_REPLACEMENT_FIELD)
}
/// 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_typing_literal(&self) -> bool {
self.flags.intersects(SemanticModelFlags::TYPING_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 is in a docstring as described in [PEP 257].
///
/// [PEP 257]: https://peps.python.org/pep-0257/#what-is-a-docstring
pub const fn in_pep_257_docstring(&self) -> bool {
self.flags.intersects(SemanticModelFlags::PEP_257_DOCSTRING)
}
/// Return `true` if the model is in an attribute docstring.
pub const fn in_attribute_docstring(&self) -> bool {
self.flags
.intersects(SemanticModelFlags::ATTRIBUTE_DOCSTRING)
}
/// Return `true` if the model is in a `@no_type_check` context.
pub const fn in_no_type_check(&self) -> bool {
self.flags.intersects(SemanticModelFlags::NO_TYPE_CHECK)
}
/// 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 the model has traversed past the module docstring boundary.
pub const fn seen_module_docstring_boundary(&self) -> bool {
self.flags
.intersects(SemanticModelFlags::MODULE_DOCSTRING_BOUNDARY)
}
/// Return `true` if `__future__`-style type annotations are enabled.
pub const fn future_annotations_or_stub(&self) -> bool {
self.flags
.intersects(SemanticModelFlags::FUTURE_ANNOTATIONS_OR_STUB)
}
/// Return `true` if the model is in a stub file (i.e., a file with a `.pyi` extension).
pub const fn in_stub_file(&self) -> bool {
self.flags.intersects(SemanticModelFlags::STUB_FILE)
}
/// Return `true` if the model is in a named expression assignment (e.g., `x := 1`).
pub const fn in_named_expression_assignment(&self) -> bool {
self.flags
.intersects(SemanticModelFlags::NAMED_EXPRESSION_ASSIGNMENT)
}
/// Return `true` if the model is visiting the r.h.s. of an `__all__` definition
/// (e.g. `"foo"` in `__all__ = ["foo"]`)
pub const fn in_dunder_all_definition(&self) -> bool {
self.flags
.intersects(SemanticModelFlags::DUNDER_ALL_DEFINITION)
}
/// Return `true` if the model is visiting an item in a class's bases tuple
/// (e.g. `Foo` in `class Bar(Foo): ...`)
pub const fn in_class_base(&self) -> bool {
self.flags.intersects(SemanticModelFlags::CLASS_BASE)
}
/// Return `true` if the model is visiting an item in a class's bases tuple
/// that was initially deferred while traversing the AST.
/// (This only happens in stub files.)
pub const fn in_deferred_class_base(&self) -> bool {
self.flags
.intersects(SemanticModelFlags::DEFERRED_CLASS_BASE)
}
/// Return `true` if we should use the new semantics to recognize
/// type checking blocks. Previously we only recognized type checking
/// blocks if `TYPE_CHECKING` was imported from a typing module.
///
/// With this feature flag enabled we recognize any symbol named
/// `TYPE_CHECKING`, regardless of where it comes from to mirror
/// what mypy and pyright do.
pub const fn use_new_type_checking_block_detection_semantics(&self) -> bool {
self.flags
.intersects(SemanticModelFlags::NEW_TYPE_CHECKING_BLOCK_DETECTION)
}
/// 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
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TypingOnlyBindingsStatus {
Allowed,
Disallowed,
}
impl TypingOnlyBindingsStatus {
pub const fn is_allowed(self) -> bool {
matches!(self, TypingOnlyBindingsStatus::Allowed)
}
pub const fn is_disallowed(self) -> bool {
matches!(self, TypingOnlyBindingsStatus::Disallowed)
}
}
impl From<bool> for TypingOnlyBindingsStatus {
fn from(value: bool) -> Self {
if value {
TypingOnlyBindingsStatus::Allowed
} else {
TypingOnlyBindingsStatus::Disallowed
}
}
}
bitflags! {
/// A select list of Python modules that the semantic model can explicitly track.
#[derive(Debug)]
pub struct Modules: u32 {
const COLLECTIONS = 1 << 0;
const DATETIME = 1 << 1;
const DJANGO = 1 << 2;
const LOGGING = 1 << 3;
const MOCK = 1 << 4;
const NUMPY = 1 << 5;
const OS = 1 << 6;
const PANDAS = 1 << 7;
const PYTEST = 1 << 8;
const RE = 1 << 9;
const SIX = 1 << 10;
const SUBPROCESS = 1 << 11;
const TARFILE = 1 << 12;
const TRIO = 1 << 13;
const TYPING = 1 << 14;
const TYPING_EXTENSIONS = 1 << 15;
const TYPESHED = 1 << 16;
const DATACLASSES = 1 << 17;
const BUILTINS = 1 << 18;
const CONTEXTVARS = 1 << 19;
const ANYIO = 1 << 20;
const FASTAPI = 1 << 21;
const COPY = 1 << 22;
const MARKUPSAFE = 1 << 23;
const FLASK = 1 << 24;
const ATTRS = 1 << 25;
const REGEX = 1 << 26;
const AIRFLOW = 1 << 27;
const HASHLIB = 1 << 28;
const CRYPT = 1 << 29;
}
}
bitflags! {
/// Flags indicating the current model state.
#[derive(Debug, Default, Copy, Clone, Eq, PartialEq)]
pub struct SemanticModelFlags: u32 {
/// The model is in a type annotation that will only be evaluated when running a type
/// checker.
///
/// 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 type annotation that will be evaluated at runtime.
///
/// 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_EVALUATED_ANNOTATION = 1 << 1;
/// The model is in a type annotation that is _required_ to be available at runtime.
///
/// For example, the context could be visiting `int` in:
/// ```python
/// from pydantic import BaseModel
///
/// class Foo(BaseModel):
/// x: int
/// ```
///
/// In this case, Pydantic requires that the type annotation be available at runtime
/// in order to perform runtime type-checking.
///
/// Unlike [`RUNTIME_EVALUATED_ANNOTATION`], annotations that are marked as
/// [`RUNTIME_REQUIRED_ANNOTATION`] cannot be deferred to typing time via conversion to a
/// forward reference (e.g., by wrapping the type in quotes), as the annotations are not
/// only required by the Python interpreter, but by runtime type checkers too.
const RUNTIME_REQUIRED_ANNOTATION = 1 << 2;
/// 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 << 3;
/// 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.
///
/// Note that this flag is only set when we are actually *visiting* the deferred definition,
/// not when we "pass by" it when initially traversing the source tree.
const SIMPLE_STRING_TYPE_DEFINITION = 1 << 4;
/// 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.
///
/// Note that this flag is only set when we are actually *visiting* the deferred definition,
/// not when we "pass by" it when initially traversing the source tree.
const COMPLEX_STRING_TYPE_DEFINITION = 1 << 5;
/// 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`.
///
/// This flag should only be set in contexts where PEP-563 semantics are relevant to
/// resolution of the type definition. For example, the flag should not be set
/// in the following context, because the type definition is not inside a type annotation,
/// so whether or not `from __future__ import annotations` is active has no relevance:
/// ```python
/// from __future__ import annotations
/// from typing import TypeAlias
///
/// X: TypeAlias = list[int]
/// ```
///
/// Note also that this flag is only set when we are actually *visiting* the deferred definition,
/// not when we "pass by" it when initially traversing the source tree.
const FUTURE_TYPE_DEFINITION = 1 << 6;
/// 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 << 7;
/// The model is in an f-string.
///
/// For example, the model could be visiting `x` in:
/// ```python
/// f'{x}'
/// ```
const F_STRING = 1 << 8;
/// 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 << 9;
/// 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 TYPING_LITERAL = 1 << 10;
/// The model is in a subscript expression.
///
/// For example, the model could be visiting `x["a"]` in:
/// ```python
/// x["a"]["b"]
/// ```
const SUBSCRIPT = 1 << 11;
/// 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 << 12;
/// 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 << 13;
/// 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 << 14;
/// The model is in a file that has `from __future__ import annotations`
/// at the top of the module.
///
/// For example, the model could be visiting `x` in:
/// ```python
/// from __future__ import annotations
///
///
/// def f(x: int) -> int:
/// ...
/// ```
const FUTURE_ANNOTATIONS = 1 << 15;
/// The model is in a Python stub file (i.e., a `.pyi` file).
const STUB_FILE = 1 << 16;
/// `__future__`-style type annotations are enabled in this model.
/// That could be because it's a stub file,
/// or it could be because it's a non-stub file that has `from __future__ import annotations`
/// at the top of the module.
const FUTURE_ANNOTATIONS_OR_STUB = Self::FUTURE_ANNOTATIONS.bits() | Self::STUB_FILE.bits();
/// The model has traversed past the module docstring.
///
/// For example, the model could be visiting `x` in:
/// ```python
/// """Module docstring."""
///
/// x: int = 1
/// ```
const MODULE_DOCSTRING_BOUNDARY = 1 << 17;
/// The model is in a (deferred) [type parameter definition].
///
/// For example, the model could be visiting `T`, `P` or `Ts` in:
/// ```python
/// class Foo[T, *Ts, **P]: pass
/// ```
///
/// Note that this flag is *not* set for "pre-PEP-695" TypeVars, ParamSpecs or TypeVarTuples.
/// None of the following would lead to the flag being set:
///
/// ```python
/// from typing import TypeVar, ParamSpec, TypeVarTuple
///
/// T = TypeVar("T")
/// P = ParamSpec("P")
/// Ts = TypeVarTuple("Ts")
/// ```
///
/// Note also that this flag is only set when we are actually *visiting* the deferred definition,
/// not when we "pass by" it when initially traversing the source tree.
///
/// [type parameter definition]: https://docs.python.org/3/reference/executionmodel.html#annotation-scopes
const TYPE_PARAM_DEFINITION = 1 << 18;
/// The model is in a named expression assignment.
///
/// For example, the model could be visiting `x` in:
/// ```python
/// if (x := 1): ...
/// ```
const NAMED_EXPRESSION_ASSIGNMENT = 1 << 19;
/// The model is in a docstring as described in [PEP 257].
///
/// For example, the model could be visiting either the module, class,
/// or function docstring in:
/// ```python
/// """Module docstring."""
///
///
/// class Foo:
/// """Class docstring."""
/// pass
///
///
/// def foo():
/// """Function docstring."""
/// pass
/// ```
///
/// [PEP 257]: https://peps.python.org/pep-0257/#what-is-a-docstring
const PEP_257_DOCSTRING = 1 << 20;
/// The model is visiting the r.h.s. of a module-level `__all__` definition.
///
/// This could be any module-level statement that assigns or alters `__all__`,
/// for example:
/// ```python
/// __all__ = ["foo"]
/// __all__: str = ["foo"]
/// __all__ = ("bar",)
/// __all__ += ("baz,")
/// ```
const DUNDER_ALL_DEFINITION = 1 << 21;
/// The model is in an f-string replacement field.
///
/// For example, the model could be visiting `x` or `y` in:
///
/// ```python
/// f"first {x} second {y}"
/// ```
const F_STRING_REPLACEMENT_FIELD = 1 << 22;
/// The model is visiting the bases tuple of a class.
///
/// For example, the model could be visiting `Foo` or `Bar` in:
///
/// ```python
/// class Baz(Foo, Bar):
/// pass
/// ```
const CLASS_BASE = 1 << 23;
/// The model is visiting a class base that was initially deferred
/// while traversing the AST. (This only happens in stub files.)
const DEFERRED_CLASS_BASE = 1 << 24;
/// The model is in an attribute docstring.
///
/// An attribute docstring is a string literal immediately following an assignment or an
/// annotated assignment statement. The context in which this is valid are:
/// 1. At the top level of a module
/// 2. At the top level of a class definition i.e., a class attribute
///
/// For example:
/// ```python
/// a = 1
/// """This is an attribute docstring for `a` variable"""
///
///
/// class Foo:
/// b = 1
/// """This is an attribute docstring for `Foo.b` class variable"""
/// ```
///
/// Unlike other kinds of docstrings as described in [PEP 257], attribute docstrings are
/// discarded at runtime. However, they are used by some documentation renderers and
/// static-analysis tools.
///
/// [PEP 257]: https://peps.python.org/pep-0257/#what-is-a-docstring
const ATTRIBUTE_DOCSTRING = 1 << 25;
/// The model is in the value expression of a [PEP 613] explicit type alias.
///
/// For example:
/// ```python
/// from typing import TypeAlias
///
/// OptStr: TypeAlias = str | None # We're visiting the RHS
/// ```
///
/// [PEP 613]: https://peps.python.org/pep-0613/
const ANNOTATED_TYPE_ALIAS = 1 << 27;
/// The model is in the value expression of a [PEP 695] type statement.
///
/// For example:
/// ```python
/// type OptStr = str | None # We're visiting the RHS
/// ```
///
/// [PEP 695]: https://peps.python.org/pep-0695/#generic-type-alias
const DEFERRED_TYPE_ALIAS = 1 << 28;
/// The model is visiting an `assert` statement.
///
/// For example, the model might be visiting `y` in
/// ```python
/// assert (y := x**2) > 42, y
/// ```
const ASSERT_STATEMENT = 1 << 29;
/// The model is in a [`@no_type_check`] context.
///
/// This is used to skip type checking when the `@no_type_check` decorator is found.
///
/// For example (adapted from [#13824]):
/// ```python
/// from typing import no_type_check
///
/// @no_type_check
/// def fn(arg: "A") -> "R":
/// pass
/// ```
///
/// [no_type_check]: https://docs.python.org/3/library/typing.html#typing.no_type_check
/// [#13824]: https://github.com/astral-sh/ruff/issues/13824
const NO_TYPE_CHECK = 1 << 30;
/// The model special-cases any symbol named `TYPE_CHECKING`.
///
/// Previously we only recognized `TYPE_CHECKING` if it was part of
/// one of the configured `typing` modules. This flag exists to
/// test out the semantic change only in preview. This flag will go
/// away once this change has been stabilized.
const NEW_TYPE_CHECKING_BLOCK_DETECTION = 1 << 31;
/// The context is in any type annotation.
const ANNOTATION = Self::TYPING_ONLY_ANNOTATION.bits() | Self::RUNTIME_EVALUATED_ANNOTATION.bits() | Self::RUNTIME_REQUIRED_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()
| Self::TYPE_PARAM_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() | Self::TYPE_PARAM_DEFINITION.bits();
/// The context is in any type alias.
const TYPE_ALIAS = Self::ANNOTATED_TYPE_ALIAS.bits() | Self::DEFERRED_TYPE_ALIAS.bits();
}
}
impl SemanticModelFlags {
pub fn new(path: &Path) -> Self {
if PySourceType::from(path).is_stub() {
Self::STUB_FILE
} else {
Self::default()
}
}
}
/// 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 statement from which the symbol is imported.
source: NodeId,
/// 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
}
pub fn statement<'a>(&self, semantic: &SemanticModel<'a>) -> &'a Stmt {
semantic.statement(self.source)
}
}
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())
}
}