[red-knot] detect unreachable attribute assignments (#16852)

## Summary

This PR closes #15967.

Attribute assignments that are statically known to be unreachable are
excluded from consideration for implicit instance attribute type
inference. If none of the assignments are found to be reachable, an
`unresolved-attribute` error is reported.

## Test Plan

[A test
case](https://github.com/astral-sh/ruff/blob/main/crates/red_knot_python_semantic/resources/mdtest/attributes.md#attributes-defined-in-statically-known-to-be-false-branches)
marked as TODO now work as intended, and new test cases have been added.

---------

Co-authored-by: David Peter <mail@david-peter.de>
This commit is contained in:
Shunsuke Shibayama 2025-04-14 16:23:20 +09:00 committed by GitHub
parent 3aa3ee8b89
commit dfd8eaeb32
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 764 additions and 328 deletions

View file

@ -41,8 +41,7 @@ reveal_type(c_instance.declared_only) # revealed: bytes
reveal_type(c_instance.declared_and_bound) # revealed: bool
# We probably don't want to emit a diagnostic for this being possibly undeclared/unbound.
# mypy and pyright do not show an error here.
# error: [possibly-unbound-attribute]
reveal_type(c_instance.possibly_undeclared_unbound) # revealed: str
# This assignment is fine, as we infer `Unknown | Literal[1, "a"]` for `inferred_from_value`.
@ -339,8 +338,10 @@ class C:
for self.z in NonIterable():
pass
# Iterable might be empty
# error: [possibly-unbound-attribute]
reveal_type(C().x) # revealed: Unknown | int
# error: [possibly-unbound-attribute]
reveal_type(C().y) # revealed: Unknown | str
```
@ -409,8 +410,8 @@ reveal_type(c_instance.a) # revealed: Unknown
#### Conditionally declared / bound attributes
We currently do not raise a diagnostic or change behavior if an attribute is only conditionally
defined. This is consistent with what mypy and pyright do.
Attributes are possibly unbound if they, or the method to which they are added are conditionally
declared / bound.
```py
def flag() -> bool:
@ -428,9 +429,13 @@ class C:
c_instance = C()
# error: [possibly-unbound-attribute]
reveal_type(c_instance.a1) # revealed: str | None
# error: [possibly-unbound-attribute]
reveal_type(c_instance.a2) # revealed: str | None
# error: [possibly-unbound-attribute]
reveal_type(c_instance.b1) # revealed: Unknown | Literal[1]
# error: [possibly-unbound-attribute]
reveal_type(c_instance.b2) # revealed: Unknown | Literal[1]
```
@ -539,10 +544,88 @@ class C:
if (2 + 3) < 4:
self.x: str = "a"
# TODO: Ideally, this would result in a `unresolved-attribute` error. But mypy and pyright
# do not support this either (for conditions that can only be resolved to `False` in type
# inference), so it does not seem to be particularly important.
reveal_type(C().x) # revealed: str
# error: [unresolved-attribute]
reveal_type(C().x) # revealed: Unknown
```
```py
class C:
def __init__(self, cond: bool) -> None:
if True:
self.a = 1
else:
self.a = "a"
if False:
self.b = 2
if cond:
return
self.c = 3
self.d = 4
self.d = 5
def set_c(self, c: str) -> None:
self.c = c
if False:
def set_e(self, e: str) -> None:
self.e = e
reveal_type(C(True).a) # revealed: Unknown | Literal[1]
# error: [unresolved-attribute]
reveal_type(C(True).b) # revealed: Unknown
reveal_type(C(True).c) # revealed: Unknown | Literal[3] | str
# TODO: this attribute is possibly unbound
reveal_type(C(True).d) # revealed: Unknown | Literal[5]
# error: [unresolved-attribute]
reveal_type(C(True).e) # revealed: Unknown
```
#### Attributes considered always bound
```py
class C:
def __init__(self, cond: bool):
self.x = 1
if cond:
raise ValueError("Something went wrong")
# We consider this attribute is always bound.
# This is because, it is not possible to access a partially-initialized object by normal means.
self.y = 2
reveal_type(C(False).x) # revealed: Unknown | Literal[1]
reveal_type(C(False).y) # revealed: Unknown | Literal[2]
class C:
def __init__(self, b: bytes) -> None:
self.b = b
try:
s = b.decode()
except UnicodeDecodeError:
raise ValueError("Invalid UTF-8 sequence")
self.s = s
reveal_type(C(b"abc").b) # revealed: Unknown | bytes
reveal_type(C(b"abc").s) # revealed: Unknown | str
class C:
def __init__(self, iter) -> None:
self.x = 1
for _ in iter:
pass
# The for-loop may not stop,
# but we consider the subsequent attributes to be definitely-bound.
self.y = 2
reveal_type(C([]).x) # revealed: Unknown | Literal[1]
reveal_type(C([]).y) # revealed: Unknown | Literal[2]
```
#### Diagnostics are reported for the right-hand side of attribute assignments
@ -1046,13 +1129,18 @@ def _(flag: bool):
def __init(self):
if flag:
self.x = 1
self.y = "a"
else:
self.y = "b"
# Emitting a diagnostic in a case like this is not something we support, and it's unclear
# if we ever will (or want to)
# error: [possibly-unbound-attribute]
reveal_type(Foo().x) # revealed: Unknown | Literal[1]
# Same here
# error: [possibly-unbound-attribute]
Foo().x = 2
reveal_type(Foo().y) # revealed: Unknown | Literal["a", "b"]
Foo().y = "c"
```
### Unions with all paths unbound

View file

@ -13,7 +13,6 @@ use crate::module_name::ModuleName;
use crate::node_key::NodeKey;
use crate::semantic_index::ast_ids::node_key::ExpressionNodeKey;
use crate::semantic_index::ast_ids::AstIds;
use crate::semantic_index::attribute_assignment::AttributeAssignments;
use crate::semantic_index::builder::SemanticIndexBuilder;
use crate::semantic_index::definition::{Definition, DefinitionNodeKey, Definitions};
use crate::semantic_index::expression::Expression;
@ -24,7 +23,6 @@ use crate::semantic_index::use_def::{EagerBindingsKey, ScopedEagerBindingsId, Us
use crate::Db;
pub mod ast_ids;
pub mod attribute_assignment;
mod builder;
pub mod definition;
pub mod expression;
@ -98,23 +96,25 @@ pub(crate) fn use_def_map<'db>(db: &'db dyn Db, scope: ScopeId<'db>) -> Arc<UseD
index.use_def_map(scope.file_scope_id(db))
}
/// Returns all attribute assignments for a specific class body scope.
///
/// Using [`attribute_assignments`] over [`semantic_index`] has the advantage that
/// Salsa can avoid invalidating dependent queries if this scope's instance attributes
/// are unchanged.
#[salsa::tracked]
pub(crate) fn attribute_assignments<'db>(
/// Returns all attribute assignments (and their method scope IDs) for a specific class body scope.
/// Only call this when doing type inference on the same file as `class_body_scope`, otherwise it
/// introduces a direct dependency on that file's AST.
pub(crate) fn attribute_assignments<'db, 's>(
db: &'db dyn Db,
class_body_scope: ScopeId<'db>,
) -> Option<Arc<AttributeAssignments<'db>>> {
name: &'s str,
) -> impl Iterator<Item = (BindingWithConstraintsIterator<'db, 'db>, FileScopeId)> + use<'s, 'db> {
let file = class_body_scope.file(db);
let index = semantic_index(db, file);
let class_scope_id = class_body_scope.file_scope_id(db);
index
.attribute_assignments
.get(&class_body_scope.file_scope_id(db))
.cloned()
ChildrenIter::new(index, class_scope_id).filter_map(|(file_scope_id, maybe_method)| {
maybe_method.node().as_function()?;
let attribute_table = index.instance_attribute_table(file_scope_id);
let symbol = attribute_table.symbol_id_by_name(name)?;
let use_def = &index.use_def_maps[file_scope_id];
Some((use_def.instance_attribute_bindings(symbol), file_scope_id))
})
}
/// Returns the module global scope of `file`.
@ -137,6 +137,9 @@ pub(crate) struct SemanticIndex<'db> {
/// List of all symbol tables in this file, indexed by scope.
symbol_tables: IndexVec<FileScopeId, Arc<SymbolTable>>,
/// List of all instance attribute tables in this file, indexed by scope.
instance_attribute_tables: IndexVec<FileScopeId, SymbolTable>,
/// List of all scopes in this file.
scopes: IndexVec<FileScopeId, Scope>,
@ -170,10 +173,6 @@ pub(crate) struct SemanticIndex<'db> {
/// Flags about the global scope (code usage impacting inference)
has_future_annotations: bool,
/// Maps from class body scopes to attribute assignments that were found
/// in methods of that class.
attribute_assignments: FxHashMap<FileScopeId, Arc<AttributeAssignments<'db>>>,
/// Map of all of the eager bindings that appear in this file.
eager_bindings: FxHashMap<EagerBindingsKey, ScopedEagerBindingsId>,
}
@ -188,6 +187,10 @@ impl<'db> SemanticIndex<'db> {
self.symbol_tables[scope_id].clone()
}
pub(super) fn instance_attribute_table(&self, scope_id: FileScopeId) -> &SymbolTable {
&self.instance_attribute_tables[scope_id]
}
/// Returns the use-def map for a specific scope.
///
/// Use the Salsa cached [`use_def_map()`] query if you only need the

View file

@ -1,37 +0,0 @@
use crate::{
semantic_index::{ast_ids::ScopedExpressionId, expression::Expression},
unpack::Unpack,
};
use ruff_python_ast::name::Name;
use rustc_hash::FxHashMap;
/// Describes an (annotated) attribute assignment that we discovered in a method
/// body, typically of the form `self.x: int`, `self.x: int = …` or `self.x = …`.
#[derive(Debug, Clone, PartialEq, Eq, salsa::Update)]
pub(crate) enum AttributeAssignment<'db> {
/// An attribute assignment with an explicit type annotation, either
/// `self.x: <annotation>` or `self.x: <annotation> = …`.
Annotated { annotation: Expression<'db> },
/// An attribute assignment without a type annotation, e.g. `self.x = <value>`.
Unannotated { value: Expression<'db> },
/// An attribute assignment where the right-hand side is an iterable, for example
/// `for self.x in <iterable>`.
Iterable { iterable: Expression<'db> },
/// An attribute assignment where the expression to be assigned is a context manager, for example
/// `with <context_manager> as self.x`.
ContextManager { context_manager: Expression<'db> },
/// An attribute assignment where the left-hand side is an unpacking expression,
/// e.g. `self.x, self.y = <value>`.
Unpack {
attribute_expression_id: ScopedExpressionId,
unpack: Unpack<'db>,
},
}
pub(crate) type AttributeAssignments<'db> = FxHashMap<Name, Vec<AttributeAssignment<'db>>>;

View file

@ -16,12 +16,13 @@ use crate::module_resolver::resolve_module;
use crate::node_key::NodeKey;
use crate::semantic_index::ast_ids::node_key::ExpressionNodeKey;
use crate::semantic_index::ast_ids::AstIdsBuilder;
use crate::semantic_index::attribute_assignment::{AttributeAssignment, AttributeAssignments};
use crate::semantic_index::definition::{
AssignmentDefinitionNodeRef, ComprehensionDefinitionNodeRef, Definition, DefinitionCategory,
DefinitionNodeKey, DefinitionNodeRef, Definitions, ExceptHandlerDefinitionNodeRef,
ForStmtDefinitionNodeRef, ImportDefinitionNodeRef, ImportFromDefinitionNodeRef,
MatchPatternDefinitionNodeRef, StarImportDefinitionNodeRef, WithItemDefinitionNodeRef,
AnnotatedAssignmentDefinitionKind, AnnotatedAssignmentDefinitionNodeRef,
AssignmentDefinitionKind, AssignmentDefinitionNodeRef, ComprehensionDefinitionNodeRef,
Definition, DefinitionCategory, DefinitionKind, DefinitionNodeKey, DefinitionNodeRef,
Definitions, ExceptHandlerDefinitionNodeRef, ForStmtDefinitionKind, ForStmtDefinitionNodeRef,
ImportDefinitionNodeRef, ImportFromDefinitionNodeRef, MatchPatternDefinitionNodeRef,
StarImportDefinitionNodeRef, TargetKind, WithItemDefinitionKind, WithItemDefinitionNodeRef,
};
use crate::semantic_index::expression::{Expression, ExpressionKind};
use crate::semantic_index::predicate::{
@ -87,6 +88,7 @@ pub(super) struct SemanticIndexBuilder<'db> {
scopes: IndexVec<FileScopeId, Scope>,
scope_ids_by_scope: IndexVec<FileScopeId, ScopeId<'db>>,
symbol_tables: IndexVec<FileScopeId, SymbolTableBuilder>,
instance_attribute_tables: IndexVec<FileScopeId, SymbolTableBuilder>,
ast_ids: IndexVec<FileScopeId, AstIdsBuilder>,
use_def_maps: IndexVec<FileScopeId, UseDefMapBuilder<'db>>,
scopes_by_node: FxHashMap<NodeWithScopeKey, FileScopeId>,
@ -94,7 +96,6 @@ pub(super) struct SemanticIndexBuilder<'db> {
definitions_by_node: FxHashMap<DefinitionNodeKey, Definitions<'db>>,
expressions_by_node: FxHashMap<ExpressionNodeKey, Expression<'db>>,
imported_modules: FxHashSet<ModuleName>,
attribute_assignments: FxHashMap<FileScopeId, AttributeAssignments<'db>>,
eager_bindings: FxHashMap<EagerBindingsKey, ScopedEagerBindingsId>,
}
@ -114,6 +115,7 @@ impl<'db> SemanticIndexBuilder<'db> {
scopes: IndexVec::new(),
symbol_tables: IndexVec::new(),
instance_attribute_tables: IndexVec::new(),
ast_ids: IndexVec::new(),
scope_ids_by_scope: IndexVec::new(),
use_def_maps: IndexVec::new(),
@ -125,8 +127,6 @@ impl<'db> SemanticIndexBuilder<'db> {
imported_modules: FxHashSet::default(),
attribute_assignments: FxHashMap::default(),
eager_bindings: FxHashMap::default(),
};
@ -222,6 +222,8 @@ impl<'db> SemanticIndexBuilder<'db> {
let file_scope_id = self.scopes.push(scope);
self.symbol_tables.push(SymbolTableBuilder::default());
self.instance_attribute_tables
.push(SymbolTableBuilder::default());
self.use_def_maps.push(UseDefMapBuilder::default());
let ast_id_scope = self.ast_ids.push(AstIdsBuilder::default());
@ -315,6 +317,11 @@ impl<'db> SemanticIndexBuilder<'db> {
&mut self.symbol_tables[scope_id]
}
fn current_attribute_table(&mut self) -> &mut SymbolTableBuilder {
let scope_id = self.current_scope();
&mut self.instance_attribute_tables[scope_id]
}
fn current_use_def_map_mut(&mut self) -> &mut UseDefMapBuilder<'db> {
let scope_id = self.current_scope();
&mut self.use_def_maps[scope_id]
@ -358,6 +365,14 @@ impl<'db> SemanticIndexBuilder<'db> {
(symbol_id, added)
}
fn add_attribute(&mut self, name: Name) -> ScopedSymbolId {
let (symbol_id, added) = self.current_attribute_table().add_symbol(name);
if added {
self.current_use_def_map_mut().add_attribute(symbol_id);
}
symbol_id
}
fn mark_symbol_bound(&mut self, id: ScopedSymbolId) {
self.current_symbol_table().mark_symbol_bound(id);
}
@ -458,6 +473,25 @@ impl<'db> SemanticIndexBuilder<'db> {
(definition, num_definitions)
}
fn add_attribute_definition(
&mut self,
symbol: ScopedSymbolId,
definition_kind: DefinitionKind<'db>,
) -> Definition {
let definition = Definition::new(
self.db,
self.file,
self.current_scope(),
symbol,
definition_kind,
false,
countme::Count::default(),
);
self.current_use_def_map_mut()
.record_attribute_binding(symbol, definition);
definition
}
fn record_expression_narrowing_constraint(
&mut self,
precide_node: &ast::Expr,
@ -626,9 +660,9 @@ impl<'db> SemanticIndexBuilder<'db> {
&mut self,
object: &ast::Expr,
attr: &'db ast::Identifier,
attribute_assignment: AttributeAssignment<'db>,
definition_kind: DefinitionKind<'db>,
) {
if let Some(class_body_scope) = self.is_method_of_class() {
if self.is_method_of_class().is_some() {
// We only care about attribute assignments to the first parameter of a method,
// i.e. typically `self` or `cls`.
let accessed_object_refers_to_first_parameter =
@ -636,12 +670,8 @@ impl<'db> SemanticIndexBuilder<'db> {
== self.current_first_parameter_name;
if accessed_object_refers_to_first_parameter {
self.attribute_assignments
.entry(class_body_scope)
.or_default()
.entry(attr.id().clone())
.or_default()
.push(attribute_assignment);
let symbol = self.add_attribute(attr.id().clone());
self.add_attribute_definition(symbol, definition_kind);
}
}
}
@ -918,18 +948,8 @@ impl<'db> SemanticIndexBuilder<'db> {
));
Some(unpackable.as_current_assignment(unpack))
}
ast::Expr::Name(_) => Some(unpackable.as_current_assignment(None)),
ast::Expr::Attribute(ast::ExprAttribute {
value: object,
attr,
..
}) => {
self.register_attribute_assignment(
object,
attr,
unpackable.as_attribute_assignment(value),
);
None
ast::Expr::Name(_) | ast::Expr::Attribute(_) => {
Some(unpackable.as_current_assignment(None))
}
_ => None,
};
@ -962,6 +982,12 @@ impl<'db> SemanticIndexBuilder<'db> {
.map(|builder| Arc::new(builder.finish()))
.collect();
let mut instance_attribute_tables: IndexVec<_, _> = self
.instance_attribute_tables
.into_iter()
.map(SymbolTableBuilder::finish)
.collect();
let mut use_def_maps: IndexVec<_, _> = self
.use_def_maps
.into_iter()
@ -976,6 +1002,7 @@ impl<'db> SemanticIndexBuilder<'db> {
self.scopes.shrink_to_fit();
symbol_tables.shrink_to_fit();
instance_attribute_tables.shrink_to_fit();
use_def_maps.shrink_to_fit();
ast_ids.shrink_to_fit();
self.scopes_by_expression.shrink_to_fit();
@ -987,6 +1014,7 @@ impl<'db> SemanticIndexBuilder<'db> {
SemanticIndex {
symbol_tables,
instance_attribute_tables,
scopes: self.scopes,
definitions_by_node: self.definitions_by_node,
expressions_by_node: self.expressions_by_node,
@ -997,11 +1025,6 @@ impl<'db> SemanticIndexBuilder<'db> {
use_def_maps,
imported_modules: Arc::new(self.imported_modules),
has_future_annotations: self.has_future_annotations,
attribute_assignments: self
.attribute_assignments
.into_iter()
.map(|(k, v)| (k, Arc::new(v)))
.collect(),
eager_bindings: self.eager_bindings,
}
}
@ -1313,7 +1336,6 @@ where
ast::Stmt::AnnAssign(node) => {
debug_assert_eq!(&self.current_assignments, &[]);
self.visit_expr(&node.annotation);
let annotation = self.add_standalone_type_expression(&node.annotation);
if let Some(value) = &node.value {
self.visit_expr(value);
}
@ -1325,20 +1347,6 @@ where
) {
self.push_assignment(node.into());
self.visit_expr(&node.target);
if let ast::Expr::Attribute(ast::ExprAttribute {
value: object,
attr,
..
}) = &*node.target
{
self.register_attribute_assignment(
object,
attr,
AttributeAssignment::Annotated { annotation },
);
}
self.pop_assignment();
} else {
self.visit_expr(&node.target);
@ -1759,7 +1767,7 @@ where
fn visit_expr(&mut self, expr: &'ast ast::Expr) {
self.scopes_by_expression
.insert(expr.into(), self.current_scope());
let expression_id = self.current_ast_ids().record_expression(expr);
self.current_ast_ids().record_expression(expr);
let node_key = NodeKey::from_node(expr);
@ -1792,12 +1800,20 @@ where
AssignmentDefinitionNodeRef {
unpack,
value: &node.value,
name: name_node,
target: expr,
},
);
}
Some(CurrentAssignment::AnnAssign(ann_assign)) => {
self.add_definition(symbol, ann_assign);
self.add_definition(
symbol,
AnnotatedAssignmentDefinitionNodeRef {
node: ann_assign,
annotation: &ann_assign.annotation,
value: ann_assign.value.as_deref(),
target: expr,
},
);
}
Some(CurrentAssignment::AugAssign(aug_assign)) => {
self.add_definition(symbol, aug_assign);
@ -1808,7 +1824,7 @@ where
ForStmtDefinitionNodeRef {
unpack,
iterable: &node.iter,
name: name_node,
target: expr,
is_async: node.is_async,
},
);
@ -1840,7 +1856,7 @@ where
WithItemDefinitionNodeRef {
unpack,
context_expr: &item.context_expr,
name: name_node,
target: expr,
is_async,
},
);
@ -2026,20 +2042,86 @@ where
range: _,
}) => {
if ctx.is_store() {
if let Some(unpack) = self
.current_assignment()
.as_ref()
.and_then(CurrentAssignment::unpack)
{
match self.current_assignment() {
Some(CurrentAssignment::Assign { node, unpack, .. }) => {
// SAFETY: `value` and `expr` belong to the `self.module` tree
#[allow(unsafe_code)]
let assignment = AssignmentDefinitionKind::new(
TargetKind::from(unpack),
unsafe { AstNodeRef::new(self.module.clone(), &node.value) },
unsafe { AstNodeRef::new(self.module.clone(), expr) },
);
self.register_attribute_assignment(
object,
attr,
AttributeAssignment::Unpack {
attribute_expression_id: expression_id,
unpack,
},
DefinitionKind::Assignment(assignment),
);
}
Some(CurrentAssignment::AnnAssign(ann_assign)) => {
self.add_standalone_type_expression(&ann_assign.annotation);
// SAFETY: `annotation`, `value` and `expr` belong to the `self.module` tree
#[allow(unsafe_code)]
let assignment = AnnotatedAssignmentDefinitionKind::new(
unsafe {
AstNodeRef::new(self.module.clone(), &ann_assign.annotation)
},
ann_assign.value.as_deref().map(|value| unsafe {
AstNodeRef::new(self.module.clone(), value)
}),
unsafe { AstNodeRef::new(self.module.clone(), expr) },
);
self.register_attribute_assignment(
object,
attr,
DefinitionKind::AnnotatedAssignment(assignment),
);
}
Some(CurrentAssignment::For { node, unpack, .. }) => {
// // SAFETY: `iter` and `expr` belong to the `self.module` tree
#[allow(unsafe_code)]
let assignment = ForStmtDefinitionKind::new(
TargetKind::from(unpack),
unsafe { AstNodeRef::new(self.module.clone(), &node.iter) },
unsafe { AstNodeRef::new(self.module.clone(), expr) },
node.is_async,
);
self.register_attribute_assignment(
object,
attr,
DefinitionKind::For(assignment),
);
}
Some(CurrentAssignment::WithItem {
item,
unpack,
is_async,
..
}) => {
// SAFETY: `context_expr` and `expr` belong to the `self.module` tree
#[allow(unsafe_code)]
let assignment = WithItemDefinitionKind::new(
TargetKind::from(unpack),
unsafe { AstNodeRef::new(self.module.clone(), &item.context_expr) },
unsafe { AstNodeRef::new(self.module.clone(), expr) },
is_async,
);
self.register_attribute_assignment(
object,
attr,
DefinitionKind::WithItem(assignment),
);
}
Some(CurrentAssignment::Comprehension { .. }) => {
// TODO:
}
Some(CurrentAssignment::AugAssign(_)) => {
// TODO:
}
Some(CurrentAssignment::Named(_)) => {
// TODO:
}
None => {}
}
}
// Track reachability of attribute expressions to silence `unresolved-attribute`
@ -2138,19 +2220,7 @@ enum CurrentAssignment<'a> {
},
}
impl<'a> CurrentAssignment<'a> {
fn unpack(&self) -> Option<Unpack<'a>> {
match self {
Self::Assign { unpack, .. }
| Self::For { unpack, .. }
| Self::WithItem { unpack, .. } => unpack.map(|(_, unpack)| unpack),
Self::AnnAssign(_)
| Self::AugAssign(_)
| Self::Named(_)
| Self::Comprehension { .. } => None,
}
}
impl CurrentAssignment<'_> {
fn unpack_position_mut(&mut self) -> Option<&mut UnpackPosition> {
match self {
Self::Assign { unpack, .. }
@ -2237,16 +2307,4 @@ impl<'a> Unpackable<'a> {
},
}
}
fn as_attribute_assignment(&self, expression: Expression<'a>) -> AttributeAssignment<'a> {
match self {
Unpackable::Assign(_) => AttributeAssignment::Unannotated { value: expression },
Unpackable::For(_) => AttributeAssignment::Iterable {
iterable: expression,
},
Unpackable::WithItem { .. } => AttributeAssignment::ContextManager {
context_manager: expression,
},
}
}
}

View file

@ -100,7 +100,7 @@ pub(crate) enum DefinitionNodeRef<'a> {
TypeAlias(&'a ast::StmtTypeAlias),
NamedExpression(&'a ast::ExprNamed),
Assignment(AssignmentDefinitionNodeRef<'a>),
AnnotatedAssignment(&'a ast::StmtAnnAssign),
AnnotatedAssignment(AnnotatedAssignmentDefinitionNodeRef<'a>),
AugmentedAssignment(&'a ast::StmtAugAssign),
Comprehension(ComprehensionDefinitionNodeRef<'a>),
VariadicPositionalParameter(&'a ast::Parameter),
@ -138,12 +138,6 @@ impl<'a> From<&'a ast::ExprNamed> for DefinitionNodeRef<'a> {
}
}
impl<'a> From<&'a ast::StmtAnnAssign> for DefinitionNodeRef<'a> {
fn from(node: &'a ast::StmtAnnAssign) -> Self {
Self::AnnotatedAssignment(node)
}
}
impl<'a> From<&'a ast::StmtAugAssign> for DefinitionNodeRef<'a> {
fn from(node: &'a ast::StmtAugAssign) -> Self {
Self::AugmentedAssignment(node)
@ -192,6 +186,12 @@ impl<'a> From<AssignmentDefinitionNodeRef<'a>> for DefinitionNodeRef<'a> {
}
}
impl<'a> From<AnnotatedAssignmentDefinitionNodeRef<'a>> for DefinitionNodeRef<'a> {
fn from(node_ref: AnnotatedAssignmentDefinitionNodeRef<'a>) -> Self {
Self::AnnotatedAssignment(node_ref)
}
}
impl<'a> From<WithItemDefinitionNodeRef<'a>> for DefinitionNodeRef<'a> {
fn from(node_ref: WithItemDefinitionNodeRef<'a>) -> Self {
Self::WithItem(node_ref)
@ -246,14 +246,22 @@ pub(crate) struct ImportFromDefinitionNodeRef<'a> {
pub(crate) struct AssignmentDefinitionNodeRef<'a> {
pub(crate) unpack: Option<(UnpackPosition, Unpack<'a>)>,
pub(crate) value: &'a ast::Expr,
pub(crate) name: &'a ast::ExprName,
pub(crate) target: &'a ast::Expr,
}
#[derive(Copy, Clone, Debug)]
pub(crate) struct AnnotatedAssignmentDefinitionNodeRef<'a> {
pub(crate) node: &'a ast::StmtAnnAssign,
pub(crate) annotation: &'a ast::Expr,
pub(crate) value: Option<&'a ast::Expr>,
pub(crate) target: &'a ast::Expr,
}
#[derive(Copy, Clone, Debug)]
pub(crate) struct WithItemDefinitionNodeRef<'a> {
pub(crate) unpack: Option<(UnpackPosition, Unpack<'a>)>,
pub(crate) context_expr: &'a ast::Expr,
pub(crate) name: &'a ast::ExprName,
pub(crate) target: &'a ast::Expr,
pub(crate) is_async: bool,
}
@ -261,7 +269,7 @@ pub(crate) struct WithItemDefinitionNodeRef<'a> {
pub(crate) struct ForStmtDefinitionNodeRef<'a> {
pub(crate) unpack: Option<(UnpackPosition, Unpack<'a>)>,
pub(crate) iterable: &'a ast::Expr,
pub(crate) name: &'a ast::ExprName,
pub(crate) target: &'a ast::Expr,
pub(crate) is_async: bool,
}
@ -335,27 +343,34 @@ impl<'db> DefinitionNodeRef<'db> {
DefinitionNodeRef::Assignment(AssignmentDefinitionNodeRef {
unpack,
value,
name,
target,
}) => DefinitionKind::Assignment(AssignmentDefinitionKind {
target: TargetKind::from(unpack),
target_kind: TargetKind::from(unpack),
value: AstNodeRef::new(parsed.clone(), value),
name: AstNodeRef::new(parsed, name),
target: AstNodeRef::new(parsed, target),
}),
DefinitionNodeRef::AnnotatedAssignment(AnnotatedAssignmentDefinitionNodeRef {
node: _,
annotation,
value,
target,
}) => DefinitionKind::AnnotatedAssignment(AnnotatedAssignmentDefinitionKind {
target: AstNodeRef::new(parsed.clone(), target),
annotation: AstNodeRef::new(parsed.clone(), annotation),
value: value.map(|v| AstNodeRef::new(parsed, v)),
}),
DefinitionNodeRef::AnnotatedAssignment(assign) => {
DefinitionKind::AnnotatedAssignment(AstNodeRef::new(parsed, assign))
}
DefinitionNodeRef::AugmentedAssignment(augmented_assignment) => {
DefinitionKind::AugmentedAssignment(AstNodeRef::new(parsed, augmented_assignment))
}
DefinitionNodeRef::For(ForStmtDefinitionNodeRef {
unpack,
iterable,
name,
target,
is_async,
}) => DefinitionKind::For(ForStmtDefinitionKind {
target: TargetKind::from(unpack),
target_kind: TargetKind::from(unpack),
iterable: AstNodeRef::new(parsed.clone(), iterable),
name: AstNodeRef::new(parsed, name),
target: AstNodeRef::new(parsed, target),
is_async,
}),
DefinitionNodeRef::Comprehension(ComprehensionDefinitionNodeRef {
@ -381,12 +396,12 @@ impl<'db> DefinitionNodeRef<'db> {
DefinitionNodeRef::WithItem(WithItemDefinitionNodeRef {
unpack,
context_expr,
name,
target,
is_async,
}) => DefinitionKind::WithItem(WithItemDefinitionKind {
target: TargetKind::from(unpack),
target_kind: TargetKind::from(unpack),
context_expr: AstNodeRef::new(parsed.clone(), context_expr),
name: AstNodeRef::new(parsed, name),
target: AstNodeRef::new(parsed, target),
is_async,
}),
DefinitionNodeRef::MatchPattern(MatchPatternDefinitionNodeRef {
@ -449,26 +464,26 @@ impl<'db> DefinitionNodeRef<'db> {
Self::Assignment(AssignmentDefinitionNodeRef {
value: _,
unpack: _,
name,
}) => name.into(),
Self::AnnotatedAssignment(node) => node.into(),
target,
}) => DefinitionNodeKey(NodeKey::from_node(target)),
Self::AnnotatedAssignment(ann_assign) => ann_assign.node.into(),
Self::AugmentedAssignment(node) => node.into(),
Self::For(ForStmtDefinitionNodeRef {
unpack: _,
target,
iterable: _,
name,
unpack: _,
is_async: _,
}) => name.into(),
}) => DefinitionNodeKey(NodeKey::from_node(target)),
Self::Comprehension(ComprehensionDefinitionNodeRef { target, .. }) => target.into(),
Self::VariadicPositionalParameter(node) => node.into(),
Self::VariadicKeywordParameter(node) => node.into(),
Self::Parameter(node) => node.into(),
Self::WithItem(WithItemDefinitionNodeRef {
unpack: _,
context_expr: _,
unpack: _,
is_async: _,
name,
}) => name.into(),
target,
}) => DefinitionNodeKey(NodeKey::from_node(target)),
Self::MatchPattern(MatchPatternDefinitionNodeRef { identifier, .. }) => {
identifier.into()
}
@ -532,7 +547,7 @@ pub enum DefinitionKind<'db> {
TypeAlias(AstNodeRef<ast::StmtTypeAlias>),
NamedExpression(AstNodeRef<ast::ExprNamed>),
Assignment(AssignmentDefinitionKind<'db>),
AnnotatedAssignment(AstNodeRef<ast::StmtAnnAssign>),
AnnotatedAssignment(AnnotatedAssignmentDefinitionKind),
AugmentedAssignment(AstNodeRef<ast::StmtAugAssign>),
For(ForStmtDefinitionKind<'db>),
Comprehension(ComprehensionDefinitionKind),
@ -576,15 +591,15 @@ impl DefinitionKind<'_> {
DefinitionKind::Class(class) => class.name.range(),
DefinitionKind::TypeAlias(type_alias) => type_alias.name.range(),
DefinitionKind::NamedExpression(named) => named.target.range(),
DefinitionKind::Assignment(assignment) => assignment.name().range(),
DefinitionKind::Assignment(assignment) => assignment.target.range(),
DefinitionKind::AnnotatedAssignment(assign) => assign.target.range(),
DefinitionKind::AugmentedAssignment(aug_assign) => aug_assign.target.range(),
DefinitionKind::For(for_stmt) => for_stmt.name().range(),
DefinitionKind::For(for_stmt) => for_stmt.target.range(),
DefinitionKind::Comprehension(comp) => comp.target().range(),
DefinitionKind::VariadicPositionalParameter(parameter) => parameter.name.range(),
DefinitionKind::VariadicKeywordParameter(parameter) => parameter.name.range(),
DefinitionKind::Parameter(parameter) => parameter.parameter.name.range(),
DefinitionKind::WithItem(with_item) => with_item.name().range(),
DefinitionKind::WithItem(with_item) => with_item.target.range(),
DefinitionKind::MatchPattern(match_pattern) => match_pattern.identifier.range(),
DefinitionKind::ExceptHandler(handler) => handler.node().range(),
DefinitionKind::TypeVar(type_var) => type_var.name.range(),
@ -603,15 +618,15 @@ impl DefinitionKind<'_> {
DefinitionKind::Class(class) => class.range(),
DefinitionKind::TypeAlias(type_alias) => type_alias.range(),
DefinitionKind::NamedExpression(named) => named.range(),
DefinitionKind::Assignment(assignment) => assignment.name().range(),
DefinitionKind::AnnotatedAssignment(assign) => assign.range(),
DefinitionKind::Assignment(assignment) => assignment.target.range(),
DefinitionKind::AnnotatedAssignment(assign) => assign.target.range(),
DefinitionKind::AugmentedAssignment(aug_assign) => aug_assign.range(),
DefinitionKind::For(for_stmt) => for_stmt.name().range(),
DefinitionKind::For(for_stmt) => for_stmt.target.range(),
DefinitionKind::Comprehension(comp) => comp.target().range(),
DefinitionKind::VariadicPositionalParameter(parameter) => parameter.range(),
DefinitionKind::VariadicKeywordParameter(parameter) => parameter.range(),
DefinitionKind::Parameter(parameter) => parameter.parameter.range(),
DefinitionKind::WithItem(with_item) => with_item.name().range(),
DefinitionKind::WithItem(with_item) => with_item.target.range(),
DefinitionKind::MatchPattern(match_pattern) => match_pattern.identifier.range(),
DefinitionKind::ExceptHandler(handler) => handler.node().range(),
DefinitionKind::TypeVar(type_var) => type_var.range(),
@ -674,14 +689,14 @@ impl DefinitionKind<'_> {
#[derive(Copy, Clone, Debug, PartialEq, Hash)]
pub(crate) enum TargetKind<'db> {
Sequence(UnpackPosition, Unpack<'db>),
Name,
NameOrAttribute,
}
impl<'db> From<Option<(UnpackPosition, Unpack<'db>)>> for TargetKind<'db> {
fn from(value: Option<(UnpackPosition, Unpack<'db>)>) -> Self {
match value {
Some((unpack_position, unpack)) => TargetKind::Sequence(unpack_position, unpack),
None => TargetKind::Name,
None => TargetKind::NameOrAttribute,
}
}
}
@ -803,44 +818,103 @@ impl ImportFromDefinitionKind {
#[derive(Clone, Debug)]
pub struct AssignmentDefinitionKind<'db> {
target: TargetKind<'db>,
target_kind: TargetKind<'db>,
value: AstNodeRef<ast::Expr>,
name: AstNodeRef<ast::ExprName>,
target: AstNodeRef<ast::Expr>,
}
impl<'db> AssignmentDefinitionKind<'db> {
pub(crate) fn target(&self) -> TargetKind<'db> {
self.target
pub(crate) fn new(
target_kind: TargetKind<'db>,
value: AstNodeRef<ast::Expr>,
target: AstNodeRef<ast::Expr>,
) -> Self {
Self {
target_kind,
value,
target,
}
}
pub(crate) fn target_kind(&self) -> TargetKind<'db> {
self.target_kind
}
pub(crate) fn value(&self) -> &ast::Expr {
self.value.node()
}
pub(crate) fn name(&self) -> &ast::ExprName {
self.name.node()
pub(crate) fn target(&self) -> &ast::Expr {
self.target.node()
}
}
#[derive(Clone, Debug)]
pub struct AnnotatedAssignmentDefinitionKind {
annotation: AstNodeRef<ast::Expr>,
value: Option<AstNodeRef<ast::Expr>>,
target: AstNodeRef<ast::Expr>,
}
impl AnnotatedAssignmentDefinitionKind {
pub(crate) fn new(
annotation: AstNodeRef<ast::Expr>,
value: Option<AstNodeRef<ast::Expr>>,
target: AstNodeRef<ast::Expr>,
) -> Self {
Self {
annotation,
value,
target,
}
}
pub(crate) fn value(&self) -> Option<&ast::Expr> {
self.value.as_deref()
}
pub(crate) fn annotation(&self) -> &ast::Expr {
self.annotation.node()
}
pub(crate) fn target(&self) -> &ast::Expr {
self.target.node()
}
}
#[derive(Clone, Debug)]
pub struct WithItemDefinitionKind<'db> {
target: TargetKind<'db>,
target_kind: TargetKind<'db>,
context_expr: AstNodeRef<ast::Expr>,
name: AstNodeRef<ast::ExprName>,
target: AstNodeRef<ast::Expr>,
is_async: bool,
}
impl<'db> WithItemDefinitionKind<'db> {
pub(crate) fn new(
target_kind: TargetKind<'db>,
context_expr: AstNodeRef<ast::Expr>,
target: AstNodeRef<ast::Expr>,
is_async: bool,
) -> Self {
Self {
target_kind,
context_expr,
target,
is_async,
}
}
pub(crate) fn context_expr(&self) -> &ast::Expr {
self.context_expr.node()
}
pub(crate) fn target(&self) -> TargetKind<'db> {
self.target
pub(crate) fn target_kind(&self) -> TargetKind<'db> {
self.target_kind
}
pub(crate) fn name(&self) -> &ast::ExprName {
self.name.node()
pub(crate) fn target(&self) -> &ast::Expr {
self.target.node()
}
pub(crate) const fn is_async(&self) -> bool {
@ -850,23 +924,37 @@ impl<'db> WithItemDefinitionKind<'db> {
#[derive(Clone, Debug)]
pub struct ForStmtDefinitionKind<'db> {
target: TargetKind<'db>,
target_kind: TargetKind<'db>,
iterable: AstNodeRef<ast::Expr>,
name: AstNodeRef<ast::ExprName>,
target: AstNodeRef<ast::Expr>,
is_async: bool,
}
impl<'db> ForStmtDefinitionKind<'db> {
pub(crate) fn new(
target_kind: TargetKind<'db>,
iterable: AstNodeRef<ast::Expr>,
target: AstNodeRef<ast::Expr>,
is_async: bool,
) -> Self {
Self {
target_kind,
iterable,
target,
is_async,
}
}
pub(crate) fn iterable(&self) -> &ast::Expr {
self.iterable.node()
}
pub(crate) fn target(&self) -> TargetKind<'db> {
self.target
pub(crate) fn target_kind(&self) -> TargetKind<'db> {
self.target_kind
}
pub(crate) fn name(&self) -> &ast::ExprName {
self.name.node()
pub(crate) fn target(&self) -> &ast::Expr {
self.target.node()
}
pub(crate) const fn is_async(&self) -> bool {

View file

@ -259,9 +259,10 @@
use ruff_index::{newtype_index, IndexVec};
use rustc_hash::FxHashMap;
use self::symbol_state::ScopedDefinitionId;
use self::symbol_state::{
LiveBindingsIterator, LiveDeclaration, LiveDeclarationsIterator, ScopedDefinitionId,
SymbolBindings, SymbolDeclarations, SymbolState,
LiveBindingsIterator, LiveDeclaration, LiveDeclarationsIterator, SymbolBindings,
SymbolDeclarations, SymbolState,
};
use crate::node_key::NodeKey;
use crate::semantic_index::ast_ids::ScopedUseId;
@ -276,6 +277,7 @@ use crate::semantic_index::symbol::{FileScopeId, ScopedSymbolId};
use crate::semantic_index::visibility_constraints::{
ScopedVisibilityConstraintId, VisibilityConstraints, VisibilityConstraintsBuilder,
};
use crate::types::Truthiness;
mod symbol_state;
@ -321,6 +323,9 @@ pub(crate) struct UseDefMap<'db> {
/// [`SymbolState`] visible at end of scope for each symbol.
public_symbols: IndexVec<ScopedSymbolId, SymbolState>,
/// [`SymbolState`] for each instance attribute.
instance_attributes: IndexVec<ScopedSymbolId, SymbolState>,
/// Snapshot of bindings in this scope that can be used to resolve a reference in a nested
/// eager scope.
eager_bindings: EagerBindings,
@ -386,6 +391,13 @@ impl<'db> UseDefMap<'db> {
self.bindings_iterator(self.public_symbols[symbol].bindings())
}
pub(crate) fn instance_attribute_bindings(
&self,
symbol: ScopedSymbolId,
) -> BindingWithConstraintsIterator<'_, 'db> {
self.bindings_iterator(self.instance_attributes[symbol].bindings())
}
pub(crate) fn eager_bindings(
&self,
eager_bindings: ScopedEagerBindingsId,
@ -425,6 +437,15 @@ impl<'db> UseDefMap<'db> {
.is_always_false()
}
pub(crate) fn is_binding_visible(
&self,
db: &dyn crate::Db,
binding: &BindingWithConstraints<'_, 'db>,
) -> Truthiness {
self.visibility_constraints
.evaluate(db, &self.predicates, binding.visibility_constraint)
}
fn bindings_iterator<'map>(
&'map self,
bindings: &'map SymbolBindings,
@ -566,6 +587,7 @@ impl std::iter::FusedIterator for DeclarationsIterator<'_, '_> {}
#[derive(Clone, Debug)]
pub(super) struct FlowSnapshot {
symbol_states: IndexVec<ScopedSymbolId, SymbolState>,
instance_attribute_states: IndexVec<ScopedSymbolId, SymbolState>,
scope_start_visibility: ScopedVisibilityConstraintId,
reachability: ScopedVisibilityConstraintId,
}
@ -645,6 +667,9 @@ pub(super) struct UseDefMapBuilder<'db> {
/// Currently live bindings and declarations for each symbol.
symbol_states: IndexVec<ScopedSymbolId, SymbolState>,
/// Currently live bindings for each instance attribute.
instance_attribute_states: IndexVec<ScopedSymbolId, SymbolState>,
/// Snapshot of bindings in this scope that can be used to resolve a reference in a nested
/// eager scope.
eager_bindings: EagerBindings,
@ -665,6 +690,7 @@ impl Default for UseDefMapBuilder<'_> {
bindings_by_declaration: FxHashMap::default(),
symbol_states: IndexVec::new(),
eager_bindings: EagerBindings::default(),
instance_attribute_states: IndexVec::new(),
}
}
}
@ -682,6 +708,13 @@ impl<'db> UseDefMapBuilder<'db> {
debug_assert_eq!(symbol, new_symbol);
}
pub(super) fn add_attribute(&mut self, symbol: ScopedSymbolId) {
let new_symbol = self
.instance_attribute_states
.push(SymbolState::undefined(self.scope_start_visibility));
debug_assert_eq!(symbol, new_symbol);
}
pub(super) fn record_binding(&mut self, symbol: ScopedSymbolId, binding: Definition<'db>) {
let def_id = self.all_definitions.push(Some(binding));
let symbol_state = &mut self.symbol_states[symbol];
@ -690,6 +723,18 @@ impl<'db> UseDefMapBuilder<'db> {
symbol_state.record_binding(def_id, self.scope_start_visibility);
}
pub(super) fn record_attribute_binding(
&mut self,
symbol: ScopedSymbolId,
binding: Definition<'db>,
) {
let def_id = self.all_definitions.push(Some(binding));
let attribute_state = &mut self.instance_attribute_states[symbol];
self.declarations_by_binding
.insert(binding, attribute_state.declarations().clone());
attribute_state.record_binding(def_id, self.scope_start_visibility);
}
pub(super) fn add_predicate(&mut self, predicate: Predicate<'db>) -> ScopedPredicateId {
self.predicates.add_predicate(predicate)
}
@ -700,6 +745,10 @@ impl<'db> UseDefMapBuilder<'db> {
state
.record_narrowing_constraint(&mut self.narrowing_constraints, narrowing_constraint);
}
for state in &mut self.instance_attribute_states {
state
.record_narrowing_constraint(&mut self.narrowing_constraints, narrowing_constraint);
}
}
pub(super) fn record_visibility_constraint(
@ -709,6 +758,9 @@ impl<'db> UseDefMapBuilder<'db> {
for state in &mut self.symbol_states {
state.record_visibility_constraint(&mut self.visibility_constraints, constraint);
}
for state in &mut self.instance_attribute_states {
state.record_visibility_constraint(&mut self.visibility_constraints, constraint);
}
self.scope_start_visibility = self
.visibility_constraints
.add_and_constraint(self.scope_start_visibility, constraint);
@ -762,6 +814,9 @@ impl<'db> UseDefMapBuilder<'db> {
/// of it, as the `if`-`elif`-`elif` chain doesn't include any new bindings of `x`.
pub(super) fn simplify_visibility_constraints(&mut self, snapshot: FlowSnapshot) {
debug_assert!(self.symbol_states.len() >= snapshot.symbol_states.len());
debug_assert!(
self.instance_attribute_states.len() >= snapshot.instance_attribute_states.len()
);
// If there are any control flow paths that have become unreachable between `snapshot` and
// now, then it's not valid to simplify any visibility constraints to `snapshot`.
@ -778,6 +833,13 @@ impl<'db> UseDefMapBuilder<'db> {
for (current, snapshot) in self.symbol_states.iter_mut().zip(snapshot.symbol_states) {
current.simplify_visibility_constraints(snapshot);
}
for (current, snapshot) in self
.instance_attribute_states
.iter_mut()
.zip(snapshot.instance_attribute_states)
{
current.simplify_visibility_constraints(snapshot);
}
}
pub(super) fn record_reachability_constraint(
@ -849,6 +911,7 @@ impl<'db> UseDefMapBuilder<'db> {
pub(super) fn snapshot(&self) -> FlowSnapshot {
FlowSnapshot {
symbol_states: self.symbol_states.clone(),
instance_attribute_states: self.instance_attribute_states.clone(),
scope_start_visibility: self.scope_start_visibility,
reachability: self.reachability,
}
@ -861,9 +924,12 @@ impl<'db> UseDefMapBuilder<'db> {
// greater than the number of known symbols in a previously-taken snapshot.
let num_symbols = self.symbol_states.len();
debug_assert!(num_symbols >= snapshot.symbol_states.len());
let num_attributes = self.instance_attribute_states.len();
debug_assert!(num_attributes >= snapshot.instance_attribute_states.len());
// Restore the current visible-definitions state to the given snapshot.
self.symbol_states = snapshot.symbol_states;
self.instance_attribute_states = snapshot.instance_attribute_states;
self.scope_start_visibility = snapshot.scope_start_visibility;
self.reachability = snapshot.reachability;
@ -874,6 +940,10 @@ impl<'db> UseDefMapBuilder<'db> {
num_symbols,
SymbolState::undefined(self.scope_start_visibility),
);
self.instance_attribute_states.resize(
num_attributes,
SymbolState::undefined(self.scope_start_visibility),
);
}
/// Merge the given snapshot into the current state, reflecting that we might have taken either
@ -899,6 +969,9 @@ impl<'db> UseDefMapBuilder<'db> {
// IDs must line up), so the current number of known symbols must always be equal to or
// greater than the number of known symbols in a previously-taken snapshot.
debug_assert!(self.symbol_states.len() >= snapshot.symbol_states.len());
debug_assert!(
self.instance_attribute_states.len() >= snapshot.instance_attribute_states.len()
);
let mut snapshot_definitions_iter = snapshot.symbol_states.into_iter();
for current in &mut self.symbol_states {
@ -917,6 +990,22 @@ impl<'db> UseDefMapBuilder<'db> {
// Symbol not present in snapshot, so it's unbound/undeclared from that path.
}
}
let mut snapshot_definitions_iter = snapshot.instance_attribute_states.into_iter();
for current in &mut self.instance_attribute_states {
if let Some(snapshot) = snapshot_definitions_iter.next() {
current.merge(
snapshot,
&mut self.narrowing_constraints,
&mut self.visibility_constraints,
);
} else {
current.merge(
SymbolState::undefined(snapshot.scope_start_visibility),
&mut self.narrowing_constraints,
&mut self.visibility_constraints,
);
}
}
self.scope_start_visibility = self
.visibility_constraints
@ -930,6 +1019,7 @@ impl<'db> UseDefMapBuilder<'db> {
pub(super) fn finish(mut self) -> UseDefMap<'db> {
self.all_definitions.shrink_to_fit();
self.symbol_states.shrink_to_fit();
self.instance_attribute_states.shrink_to_fit();
self.bindings_by_use.shrink_to_fit();
self.node_reachability.shrink_to_fit();
self.declarations_by_binding.shrink_to_fit();
@ -944,6 +1034,7 @@ impl<'db> UseDefMapBuilder<'db> {
bindings_by_use: self.bindings_by_use,
node_reachability: self.node_reachability,
public_symbols: self.symbol_states,
instance_attributes: self.instance_attribute_states,
declarations_by_binding: self.declarations_by_binding,
bindings_by_declaration: self.bindings_by_declaration,
eager_bindings: self.eager_bindings,

View file

@ -59,6 +59,10 @@ impl<'db> Symbol<'db> {
Symbol::Type(ty.into(), Boundness::Bound)
}
pub(crate) fn possibly_unbound(ty: impl Into<Type<'db>>) -> Self {
Symbol::Type(ty.into(), Boundness::PossiblyUnbound)
}
/// Constructor that creates a [`Symbol`] with a [`crate::types::TodoType`] type
/// and boundness [`Boundness::Bound`].
#[allow(unused_variables)] // Only unused in release builds

View file

@ -5336,6 +5336,14 @@ impl Truthiness {
}
}
pub(crate) fn and(self, other: Self) -> Self {
match (self, other) {
(Truthiness::AlwaysTrue, Truthiness::AlwaysTrue) => Truthiness::AlwaysTrue,
(Truthiness::AlwaysFalse, _) | (_, Truthiness::AlwaysFalse) => Truthiness::AlwaysFalse,
_ => Truthiness::Ambiguous,
}
}
fn into_type(self, db: &dyn Db) -> Type {
match self {
Self::AlwaysTrue => Type::BooleanLiteral(true),

View file

@ -10,8 +10,12 @@ use crate::types::generics::{GenericContext, Specialization};
use crate::{
module_resolver::file_to_module,
semantic_index::{
attribute_assignment::AttributeAssignment, attribute_assignments, semantic_index,
symbol::ScopeId, symbol_table, use_def_map,
ast_ids::HasScopedExpressionId,
attribute_assignments,
definition::{DefinitionKind, TargetKind},
semantic_index,
symbol::ScopeId,
symbol_table, use_def_map,
},
symbol::{
class_symbol, known_module_symbol, symbol_from_bindings, symbol_from_declarations,
@ -853,81 +857,216 @@ impl<'db> ClassLiteralType<'db> {
db: &'db dyn Db,
class_body_scope: ScopeId<'db>,
name: &str,
) -> Option<Type<'db>> {
) -> Symbol<'db> {
// If we do not see any declarations of an attribute, neither in the class body nor in
// any method, we build a union of `Unknown` with the inferred types of all bindings of
// that attribute. We include `Unknown` in that union to account for the fact that the
// attribute might be externally modified.
let mut union_of_inferred_types = UnionBuilder::new(db).add(Type::unknown());
let attribute_assignments = attribute_assignments(db, class_body_scope);
let mut is_attribute_bound = Truthiness::AlwaysFalse;
let attribute_assignments = attribute_assignments
.as_deref()
.and_then(|assignments| assignments.get(name))?;
let file = class_body_scope.file(db);
let index = semantic_index(db, file);
let class_map = use_def_map(db, class_body_scope);
let class_table = symbol_table(db, class_body_scope);
for (attribute_assignments, method_scope_id) in
attribute_assignments(db, class_body_scope, name)
{
let method_scope = method_scope_id.to_scope_id(db, file);
let method_map = use_def_map(db, method_scope);
// The attribute assignment inherits the visibility of the method which contains it
let is_method_visible = if let Some(method_def) = method_scope.node(db).as_function() {
let method = index.expect_single_definition(method_def);
let method_symbol = class_table.symbol_id_by_name(&method_def.name).unwrap();
class_map
.public_bindings(method_symbol)
.find_map(|bind| {
(bind.binding == Some(method))
.then(|| class_map.is_binding_visible(db, &bind))
})
.unwrap_or(Truthiness::AlwaysFalse)
} else {
Truthiness::AlwaysFalse
};
if is_method_visible.is_always_false() {
continue;
}
let mut attribute_assignments = attribute_assignments.peekable();
let unbound_visibility = attribute_assignments
.peek()
.map(|attribute_assignment| {
if attribute_assignment.binding.is_none() {
method_map.is_binding_visible(db, attribute_assignment)
} else {
Truthiness::AlwaysFalse
}
})
.unwrap_or(Truthiness::AlwaysFalse);
for attribute_assignment in attribute_assignments {
match attribute_assignment {
AttributeAssignment::Annotated { annotation } => {
let Some(binding) = attribute_assignment.binding else {
continue;
};
match method_map
.is_binding_visible(db, &attribute_assignment)
.and(is_method_visible)
{
Truthiness::AlwaysTrue => {
is_attribute_bound = Truthiness::AlwaysTrue;
}
Truthiness::Ambiguous => {
if is_attribute_bound.is_always_false() {
is_attribute_bound = Truthiness::Ambiguous;
}
}
Truthiness::AlwaysFalse => {
continue;
}
}
// There is at least one attribute assignment that may be visible,
// so if `unbound_visibility` is always false then this attribute is considered bound.
// TODO: this is incomplete logic since the attributes bound after termination are considered visible.
if unbound_visibility
.negate()
.and(is_method_visible)
.is_always_true()
{
is_attribute_bound = Truthiness::AlwaysTrue;
}
match binding.kind(db) {
DefinitionKind::AnnotatedAssignment(ann_assign) => {
// We found an annotated assignment of one of the following forms (using 'self' in these
// examples, but we support arbitrary names for the first parameters of methods):
//
// self.name: <annotation>
// self.name: <annotation> = …
let annotation_ty = infer_expression_type(db, *annotation);
let annotation_ty =
infer_expression_type(db, index.expression(ann_assign.annotation()));
// TODO: check if there are conflicting declarations
return Some(annotation_ty);
match is_attribute_bound {
Truthiness::AlwaysTrue => {
return Symbol::bound(annotation_ty);
}
AttributeAssignment::Unannotated { value } => {
// We found an un-annotated attribute assignment of the form:
//
// self.name = <value>
let inferred_ty = infer_expression_type(db, *value);
union_of_inferred_types = union_of_inferred_types.add(inferred_ty);
Truthiness::Ambiguous => {
return Symbol::possibly_unbound(annotation_ty);
}
AttributeAssignment::Iterable { iterable } => {
// We found an attribute assignment like:
//
// for self.name in <iterable>:
let iterable_ty = infer_expression_type(db, *iterable);
// TODO: Potential diagnostics resulting from the iterable are currently not reported.
let inferred_ty = iterable_ty.iterate(db);
union_of_inferred_types = union_of_inferred_types.add(inferred_ty);
Truthiness::AlwaysFalse => unreachable!("If the attribute assignments are all invisible, inference of their types should be skipped"),
}
AttributeAssignment::ContextManager { context_manager } => {
// We found an attribute assignment like:
//
// with <context_manager> as self.name:
let context_ty = infer_expression_type(db, *context_manager);
let inferred_ty = context_ty.enter(db);
union_of_inferred_types = union_of_inferred_types.add(inferred_ty);
}
AttributeAssignment::Unpack {
attribute_expression_id,
unpack,
} => {
DefinitionKind::Assignment(assign) => {
match assign.target_kind() {
TargetKind::Sequence(_, unpack) => {
// We found an unpacking assignment like:
//
// .., self.name, .. = <value>
// (.., self.name, ..) = <value>
// [.., self.name, ..] = <value>
let unpacked = infer_unpack_types(db, unpack);
let target_ast_id =
assign.target().scoped_expression_id(db, method_scope);
let inferred_ty = unpacked.expression_type(target_ast_id);
union_of_inferred_types = union_of_inferred_types.add(inferred_ty);
}
TargetKind::NameOrAttribute => {
// We found an un-annotated attribute assignment of the form:
//
// self.name = <value>
let inferred_ty =
infer_unpack_types(db, *unpack).expression_type(*attribute_expression_id);
infer_expression_type(db, index.expression(assign.value()));
union_of_inferred_types = union_of_inferred_types.add(inferred_ty);
}
}
}
DefinitionKind::For(for_stmt) => {
match for_stmt.target_kind() {
TargetKind::Sequence(_, unpack) => {
// We found an unpacking assignment like:
//
// for .., self.name, .. in <iterable>:
Some(union_of_inferred_types.build())
let unpacked = infer_unpack_types(db, unpack);
let target_ast_id =
for_stmt.target().scoped_expression_id(db, method_scope);
let inferred_ty = unpacked.expression_type(target_ast_id);
union_of_inferred_types = union_of_inferred_types.add(inferred_ty);
}
TargetKind::NameOrAttribute => {
// We found an attribute assignment like:
//
// for self.name in <iterable>:
let iterable_ty = infer_expression_type(
db,
index.expression(for_stmt.iterable()),
);
// TODO: Potential diagnostics resulting from the iterable are currently not reported.
let inferred_ty = iterable_ty.iterate(db);
union_of_inferred_types = union_of_inferred_types.add(inferred_ty);
}
}
}
DefinitionKind::WithItem(with_item) => {
match with_item.target_kind() {
TargetKind::Sequence(_, unpack) => {
// We found an unpacking assignment like:
//
// with <context_manager> as .., self.name, ..:
let unpacked = infer_unpack_types(db, unpack);
let target_ast_id =
with_item.target().scoped_expression_id(db, method_scope);
let inferred_ty = unpacked.expression_type(target_ast_id);
union_of_inferred_types = union_of_inferred_types.add(inferred_ty);
}
TargetKind::NameOrAttribute => {
// We found an attribute assignment like:
//
// with <context_manager> as self.name:
let context_ty = infer_expression_type(
db,
index.expression(with_item.context_expr()),
);
let inferred_ty = context_ty.enter(db);
union_of_inferred_types = union_of_inferred_types.add(inferred_ty);
}
}
}
DefinitionKind::Comprehension(_) => {
// TODO:
}
DefinitionKind::AugmentedAssignment(_) => {
// TODO:
}
DefinitionKind::NamedExpression(_) => {
// TODO:
}
_ => {}
}
}
}
match is_attribute_bound {
Truthiness::AlwaysTrue => Symbol::bound(union_of_inferred_types.build()),
Truthiness::Ambiguous => Symbol::possibly_unbound(union_of_inferred_types.build()),
Truthiness::AlwaysFalse => Symbol::Unbound,
}
}
/// A helper function for `instance_member` that looks up the `name` attribute only on
@ -961,6 +1100,7 @@ impl<'db> ClassLiteralType<'db> {
if let Some(implicit_ty) =
Self::implicit_instance_attribute(db, body_scope, name)
.ignore_possibly_unbound()
{
if declaredness == Boundness::Bound {
// If a symbol is definitely declared, and we see
@ -995,6 +1135,7 @@ impl<'db> ClassLiteralType<'db> {
} else {
if let Some(implicit_ty) =
Self::implicit_instance_attribute(db, body_scope, name)
.ignore_possibly_unbound()
{
Symbol::Type(
UnionType::from_elements(db, [declared_ty, implicit_ty]),
@ -1015,9 +1156,7 @@ impl<'db> ClassLiteralType<'db> {
// The attribute is not *declared* in the class body. It could still be declared/bound
// in a method.
Self::implicit_instance_attribute(db, body_scope, name)
.map_or(Symbol::Unbound, Symbol::bound)
.into()
Self::implicit_instance_attribute(db, body_scope, name).into()
}
Err((declared, _conflicting_declarations)) => {
// There are conflicting declarations for this attribute in the class body.
@ -1028,9 +1167,7 @@ impl<'db> ClassLiteralType<'db> {
// This attribute is neither declared nor bound in the class body.
// It could still be implicitly defined in a method.
Self::implicit_instance_attribute(db, body_scope, name)
.map_or(Symbol::Unbound, Symbol::bound)
.into()
Self::implicit_instance_attribute(db, body_scope, name).into()
}
}

View file

@ -49,8 +49,9 @@ use crate::module_resolver::resolve_module;
use crate::node_key::NodeKey;
use crate::semantic_index::ast_ids::{HasScopedExpressionId, HasScopedUseId, ScopedExpressionId};
use crate::semantic_index::definition::{
AssignmentDefinitionKind, Definition, DefinitionKind, DefinitionNodeKey,
ExceptHandlerDefinitionKind, ForStmtDefinitionKind, TargetKind, WithItemDefinitionKind,
AnnotatedAssignmentDefinitionKind, AssignmentDefinitionKind, Definition, DefinitionKind,
DefinitionNodeKey, ExceptHandlerDefinitionKind, ForStmtDefinitionKind, TargetKind,
WithItemDefinitionKind,
};
use crate::semantic_index::expression::{Expression, ExpressionKind};
use crate::semantic_index::symbol::{
@ -918,7 +919,7 @@ impl<'db> TypeInferenceBuilder<'db> {
self.infer_assignment_definition(assignment, definition);
}
DefinitionKind::AnnotatedAssignment(annotated_assignment) => {
self.infer_annotated_assignment_definition(annotated_assignment.node(), definition);
self.infer_annotated_assignment_definition(annotated_assignment, definition);
}
DefinitionKind::AugmentedAssignment(augmented_assignment) => {
self.infer_augment_assignment_definition(augmented_assignment.node(), definition);
@ -1928,23 +1929,23 @@ impl<'db> TypeInferenceBuilder<'db> {
definition: Definition<'db>,
) {
let context_expr = with_item.context_expr();
let name = with_item.name();
let target = with_item.target();
let context_expr_ty = self.infer_standalone_expression(context_expr);
let target_ty = if with_item.is_async() {
todo_type!("async `with` statement")
} else {
match with_item.target() {
match with_item.target_kind() {
TargetKind::Sequence(unpack_position, unpack) => {
let unpacked = infer_unpack_types(self.db(), unpack);
let name_ast_id = name.scoped_expression_id(self.db(), self.scope());
let target_ast_id = target.scoped_expression_id(self.db(), self.scope());
if unpack_position == UnpackPosition::First {
self.context.extend(unpacked.diagnostics());
}
unpacked.expression_type(name_ast_id)
unpacked.expression_type(target_ast_id)
}
TargetKind::Name => self.infer_context_expression(
TargetKind::NameOrAttribute => self.infer_context_expression(
context_expr,
context_expr_ty,
with_item.is_async(),
@ -1952,8 +1953,8 @@ impl<'db> TypeInferenceBuilder<'db> {
}
};
self.store_expression_type(name, target_ty);
self.add_binding(name.into(), definition, target_ty);
self.store_expression_type(target, target_ty);
self.add_binding(target.into(), definition, target_ty);
}
/// Infers the type of a context expression (`with expr`) and returns the target's type
@ -2791,11 +2792,11 @@ impl<'db> TypeInferenceBuilder<'db> {
definition: Definition<'db>,
) {
let value = assignment.value();
let name = assignment.name();
let target = assignment.target();
let value_ty = self.infer_standalone_expression(value);
let mut target_ty = match assignment.target() {
let mut target_ty = match assignment.target_kind() {
TargetKind::Sequence(unpack_position, unpack) => {
let unpacked = infer_unpack_types(self.db(), unpack);
// Only copy the diagnostics if this is the first assignment to avoid duplicating the
@ -2804,22 +2805,19 @@ impl<'db> TypeInferenceBuilder<'db> {
self.context.extend(unpacked.diagnostics());
}
let name_ast_id = name.scoped_expression_id(self.db(), self.scope());
unpacked.expression_type(name_ast_id)
let target_ast_id = target.scoped_expression_id(self.db(), self.scope());
unpacked.expression_type(target_ast_id)
}
TargetKind::Name => {
TargetKind::NameOrAttribute => {
// `TYPE_CHECKING` is a special variable that should only be assigned `False`
// at runtime, but is always considered `True` in type checking.
// See mdtest/known_constants.md#user-defined-type_checking for details.
if &name.id == "TYPE_CHECKING" {
if target.as_name_expr().map(|name| name.id.as_str()) == Some("TYPE_CHECKING") {
if !matches!(
value.as_boolean_literal_expr(),
Some(ast::ExprBooleanLiteral { value: false, .. })
) {
report_invalid_type_checking_constant(
&self.context,
assignment.name().into(),
);
report_invalid_type_checking_constant(&self.context, target.into());
}
Type::BooleanLiteral(true)
} else if self.in_stub() && value.is_ellipsis_literal_expr() {
@ -2830,14 +2828,14 @@ impl<'db> TypeInferenceBuilder<'db> {
}
};
if let Some(known_instance) =
if let Some(known_instance) = target.as_name_expr().and_then(|name| {
KnownInstanceType::try_from_file_and_name(self.db(), self.file(), &name.id)
{
}) {
target_ty = Type::KnownInstance(known_instance);
}
self.store_expression_type(name, target_ty);
self.add_binding(name.into(), definition, target_ty);
self.store_expression_type(target, target_ty);
self.add_binding(target.into(), definition, target_ty);
}
fn infer_annotated_assignment_statement(&mut self, assignment: &ast::StmtAnnAssign) {
@ -2861,16 +2859,12 @@ impl<'db> TypeInferenceBuilder<'db> {
/// Infer the types in an annotated assignment definition.
fn infer_annotated_assignment_definition(
&mut self,
assignment: &ast::StmtAnnAssign,
assignment: &'db AnnotatedAssignmentDefinitionKind,
definition: Definition<'db>,
) {
let ast::StmtAnnAssign {
range: _,
target,
annotation,
value,
simple: _,
} = assignment;
let annotation = assignment.annotation();
let target = assignment.target();
let value = assignment.value();
let mut declared_ty = self.infer_annotation_expression(
annotation,
@ -2886,7 +2880,7 @@ impl<'db> TypeInferenceBuilder<'db> {
.is_assignable_to(self.db(), declared_ty.inner_type())
{
// annotation not assignable from `bool` is an error
report_invalid_type_checking_constant(&self.context, assignment.into());
report_invalid_type_checking_constant(&self.context, target.into());
} else if self.in_stub()
&& value
.as_ref()
@ -2900,7 +2894,7 @@ impl<'db> TypeInferenceBuilder<'db> {
Some(ast::ExprBooleanLiteral { value: false, .. })
) {
// otherwise, assigning something other than `False` is an error
report_invalid_type_checking_constant(&self.context, assignment.into());
report_invalid_type_checking_constant(&self.context, target.into());
}
declared_ty.inner = Type::BooleanLiteral(true);
}
@ -2920,7 +2914,7 @@ impl<'db> TypeInferenceBuilder<'db> {
}
}
if let Some(value) = value.as_deref() {
if let Some(value) = value {
let inferred_ty = self.infer_expression(value);
let inferred_ty = if target
.as_name_expr()
@ -2933,7 +2927,7 @@ impl<'db> TypeInferenceBuilder<'db> {
inferred_ty
};
self.add_declaration_with_binding(
assignment.into(),
target.into(),
definition,
&DeclaredAndInferredType::MightBeDifferent {
declared_ty,
@ -2943,12 +2937,12 @@ impl<'db> TypeInferenceBuilder<'db> {
} else {
if self.in_stub() {
self.add_declaration_with_binding(
assignment.into(),
target.into(),
definition,
&DeclaredAndInferredType::AreTheSame(declared_ty.inner_type()),
);
} else {
self.add_declaration(assignment.into(), definition, declared_ty);
self.add_declaration(target.into(), definition, declared_ty);
}
}
@ -3087,31 +3081,33 @@ impl<'db> TypeInferenceBuilder<'db> {
definition: Definition<'db>,
) {
let iterable = for_stmt.iterable();
let name = for_stmt.name();
let target = for_stmt.target();
let iterable_type = self.infer_standalone_expression(iterable);
let loop_var_value_type = if for_stmt.is_async() {
todo_type!("async iterables/iterators")
} else {
match for_stmt.target() {
match for_stmt.target_kind() {
TargetKind::Sequence(unpack_position, unpack) => {
let unpacked = infer_unpack_types(self.db(), unpack);
if unpack_position == UnpackPosition::First {
self.context.extend(unpacked.diagnostics());
}
let name_ast_id = name.scoped_expression_id(self.db(), self.scope());
unpacked.expression_type(name_ast_id)
let target_ast_id = target.scoped_expression_id(self.db(), self.scope());
unpacked.expression_type(target_ast_id)
}
TargetKind::Name => iterable_type.try_iterate(self.db()).unwrap_or_else(|err| {
TargetKind::NameOrAttribute => {
iterable_type.try_iterate(self.db()).unwrap_or_else(|err| {
err.report_diagnostic(&self.context, iterable_type, iterable.into());
err.fallback_element_type(self.db())
}),
})
}
}
};
self.store_expression_type(name, loop_var_value_type);
self.add_binding(name.into(), definition, loop_var_value_type);
self.store_expression_type(target, loop_var_value_type);
self.add_binding(target.into(), definition, loop_var_value_type);
}
fn infer_while_statement(&mut self, while_statement: &ast::StmtWhile) {