[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 reveal_type(c_instance.declared_and_bound) # revealed: bool
# We probably don't want to emit a diagnostic for this being possibly undeclared/unbound. # error: [possibly-unbound-attribute]
# mypy and pyright do not show an error here.
reveal_type(c_instance.possibly_undeclared_unbound) # revealed: str reveal_type(c_instance.possibly_undeclared_unbound) # revealed: str
# This assignment is fine, as we infer `Unknown | Literal[1, "a"]` for `inferred_from_value`. # 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(): for self.z in NonIterable():
pass pass
# Iterable might be empty
# error: [possibly-unbound-attribute]
reveal_type(C().x) # revealed: Unknown | int reveal_type(C().x) # revealed: Unknown | int
# error: [possibly-unbound-attribute]
reveal_type(C().y) # revealed: Unknown | str reveal_type(C().y) # revealed: Unknown | str
``` ```
@ -409,8 +410,8 @@ reveal_type(c_instance.a) # revealed: Unknown
#### Conditionally declared / bound attributes #### Conditionally declared / bound attributes
We currently do not raise a diagnostic or change behavior if an attribute is only conditionally Attributes are possibly unbound if they, or the method to which they are added are conditionally
defined. This is consistent with what mypy and pyright do. declared / bound.
```py ```py
def flag() -> bool: def flag() -> bool:
@ -428,9 +429,13 @@ class C:
c_instance = C() c_instance = C()
# error: [possibly-unbound-attribute]
reveal_type(c_instance.a1) # revealed: str | None reveal_type(c_instance.a1) # revealed: str | None
# error: [possibly-unbound-attribute]
reveal_type(c_instance.a2) # revealed: str | None reveal_type(c_instance.a2) # revealed: str | None
# error: [possibly-unbound-attribute]
reveal_type(c_instance.b1) # revealed: Unknown | Literal[1] reveal_type(c_instance.b1) # revealed: Unknown | Literal[1]
# error: [possibly-unbound-attribute]
reveal_type(c_instance.b2) # revealed: Unknown | Literal[1] reveal_type(c_instance.b2) # revealed: Unknown | Literal[1]
``` ```
@ -539,10 +544,88 @@ class C:
if (2 + 3) < 4: if (2 + 3) < 4:
self.x: str = "a" self.x: str = "a"
# TODO: Ideally, this would result in a `unresolved-attribute` error. But mypy and pyright # error: [unresolved-attribute]
# do not support this either (for conditions that can only be resolved to `False` in type reveal_type(C().x) # revealed: Unknown
# inference), so it does not seem to be particularly important. ```
reveal_type(C().x) # revealed: str
```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 #### Diagnostics are reported for the right-hand side of attribute assignments
@ -1046,13 +1129,18 @@ def _(flag: bool):
def __init(self): def __init(self):
if flag: if flag:
self.x = 1 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 # error: [possibly-unbound-attribute]
# if we ever will (or want to)
reveal_type(Foo().x) # revealed: Unknown | Literal[1] reveal_type(Foo().x) # revealed: Unknown | Literal[1]
# Same here # error: [possibly-unbound-attribute]
Foo().x = 2 Foo().x = 2
reveal_type(Foo().y) # revealed: Unknown | Literal["a", "b"]
Foo().y = "c"
``` ```
### Unions with all paths unbound ### Unions with all paths unbound

View file

@ -13,7 +13,6 @@ use crate::module_name::ModuleName;
use crate::node_key::NodeKey; use crate::node_key::NodeKey;
use crate::semantic_index::ast_ids::node_key::ExpressionNodeKey; use crate::semantic_index::ast_ids::node_key::ExpressionNodeKey;
use crate::semantic_index::ast_ids::AstIds; use crate::semantic_index::ast_ids::AstIds;
use crate::semantic_index::attribute_assignment::AttributeAssignments;
use crate::semantic_index::builder::SemanticIndexBuilder; use crate::semantic_index::builder::SemanticIndexBuilder;
use crate::semantic_index::definition::{Definition, DefinitionNodeKey, Definitions}; use crate::semantic_index::definition::{Definition, DefinitionNodeKey, Definitions};
use crate::semantic_index::expression::Expression; use crate::semantic_index::expression::Expression;
@ -24,7 +23,6 @@ use crate::semantic_index::use_def::{EagerBindingsKey, ScopedEagerBindingsId, Us
use crate::Db; use crate::Db;
pub mod ast_ids; pub mod ast_ids;
pub mod attribute_assignment;
mod builder; mod builder;
pub mod definition; pub mod definition;
pub mod expression; 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)) index.use_def_map(scope.file_scope_id(db))
} }
/// Returns all attribute assignments for a specific class body scope. /// 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
/// Using [`attribute_assignments`] over [`semantic_index`] has the advantage that /// introduces a direct dependency on that file's AST.
/// Salsa can avoid invalidating dependent queries if this scope's instance attributes pub(crate) fn attribute_assignments<'db, 's>(
/// are unchanged.
#[salsa::tracked]
pub(crate) fn attribute_assignments<'db>(
db: &'db dyn Db, db: &'db dyn Db,
class_body_scope: ScopeId<'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 file = class_body_scope.file(db);
let index = semantic_index(db, file); let index = semantic_index(db, file);
let class_scope_id = class_body_scope.file_scope_id(db);
index ChildrenIter::new(index, class_scope_id).filter_map(|(file_scope_id, maybe_method)| {
.attribute_assignments maybe_method.node().as_function()?;
.get(&class_body_scope.file_scope_id(db)) let attribute_table = index.instance_attribute_table(file_scope_id);
.cloned() 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`. /// 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. /// List of all symbol tables in this file, indexed by scope.
symbol_tables: IndexVec<FileScopeId, Arc<SymbolTable>>, 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. /// List of all scopes in this file.
scopes: IndexVec<FileScopeId, Scope>, scopes: IndexVec<FileScopeId, Scope>,
@ -170,10 +173,6 @@ pub(crate) struct SemanticIndex<'db> {
/// Flags about the global scope (code usage impacting inference) /// Flags about the global scope (code usage impacting inference)
has_future_annotations: bool, 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. /// Map of all of the eager bindings that appear in this file.
eager_bindings: FxHashMap<EagerBindingsKey, ScopedEagerBindingsId>, eager_bindings: FxHashMap<EagerBindingsKey, ScopedEagerBindingsId>,
} }
@ -188,6 +187,10 @@ impl<'db> SemanticIndex<'db> {
self.symbol_tables[scope_id].clone() 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. /// Returns the use-def map for a specific scope.
/// ///
/// Use the Salsa cached [`use_def_map()`] query if you only need the /// 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::node_key::NodeKey;
use crate::semantic_index::ast_ids::node_key::ExpressionNodeKey; use crate::semantic_index::ast_ids::node_key::ExpressionNodeKey;
use crate::semantic_index::ast_ids::AstIdsBuilder; use crate::semantic_index::ast_ids::AstIdsBuilder;
use crate::semantic_index::attribute_assignment::{AttributeAssignment, AttributeAssignments};
use crate::semantic_index::definition::{ use crate::semantic_index::definition::{
AssignmentDefinitionNodeRef, ComprehensionDefinitionNodeRef, Definition, DefinitionCategory, AnnotatedAssignmentDefinitionKind, AnnotatedAssignmentDefinitionNodeRef,
DefinitionNodeKey, DefinitionNodeRef, Definitions, ExceptHandlerDefinitionNodeRef, AssignmentDefinitionKind, AssignmentDefinitionNodeRef, ComprehensionDefinitionNodeRef,
ForStmtDefinitionNodeRef, ImportDefinitionNodeRef, ImportFromDefinitionNodeRef, Definition, DefinitionCategory, DefinitionKind, DefinitionNodeKey, DefinitionNodeRef,
MatchPatternDefinitionNodeRef, StarImportDefinitionNodeRef, WithItemDefinitionNodeRef, Definitions, ExceptHandlerDefinitionNodeRef, ForStmtDefinitionKind, ForStmtDefinitionNodeRef,
ImportDefinitionNodeRef, ImportFromDefinitionNodeRef, MatchPatternDefinitionNodeRef,
StarImportDefinitionNodeRef, TargetKind, WithItemDefinitionKind, WithItemDefinitionNodeRef,
}; };
use crate::semantic_index::expression::{Expression, ExpressionKind}; use crate::semantic_index::expression::{Expression, ExpressionKind};
use crate::semantic_index::predicate::{ use crate::semantic_index::predicate::{
@ -87,6 +88,7 @@ pub(super) struct SemanticIndexBuilder<'db> {
scopes: IndexVec<FileScopeId, Scope>, scopes: IndexVec<FileScopeId, Scope>,
scope_ids_by_scope: IndexVec<FileScopeId, ScopeId<'db>>, scope_ids_by_scope: IndexVec<FileScopeId, ScopeId<'db>>,
symbol_tables: IndexVec<FileScopeId, SymbolTableBuilder>, symbol_tables: IndexVec<FileScopeId, SymbolTableBuilder>,
instance_attribute_tables: IndexVec<FileScopeId, SymbolTableBuilder>,
ast_ids: IndexVec<FileScopeId, AstIdsBuilder>, ast_ids: IndexVec<FileScopeId, AstIdsBuilder>,
use_def_maps: IndexVec<FileScopeId, UseDefMapBuilder<'db>>, use_def_maps: IndexVec<FileScopeId, UseDefMapBuilder<'db>>,
scopes_by_node: FxHashMap<NodeWithScopeKey, FileScopeId>, scopes_by_node: FxHashMap<NodeWithScopeKey, FileScopeId>,
@ -94,7 +96,6 @@ pub(super) struct SemanticIndexBuilder<'db> {
definitions_by_node: FxHashMap<DefinitionNodeKey, Definitions<'db>>, definitions_by_node: FxHashMap<DefinitionNodeKey, Definitions<'db>>,
expressions_by_node: FxHashMap<ExpressionNodeKey, Expression<'db>>, expressions_by_node: FxHashMap<ExpressionNodeKey, Expression<'db>>,
imported_modules: FxHashSet<ModuleName>, imported_modules: FxHashSet<ModuleName>,
attribute_assignments: FxHashMap<FileScopeId, AttributeAssignments<'db>>,
eager_bindings: FxHashMap<EagerBindingsKey, ScopedEagerBindingsId>, eager_bindings: FxHashMap<EagerBindingsKey, ScopedEagerBindingsId>,
} }
@ -114,6 +115,7 @@ impl<'db> SemanticIndexBuilder<'db> {
scopes: IndexVec::new(), scopes: IndexVec::new(),
symbol_tables: IndexVec::new(), symbol_tables: IndexVec::new(),
instance_attribute_tables: IndexVec::new(),
ast_ids: IndexVec::new(), ast_ids: IndexVec::new(),
scope_ids_by_scope: IndexVec::new(), scope_ids_by_scope: IndexVec::new(),
use_def_maps: IndexVec::new(), use_def_maps: IndexVec::new(),
@ -125,8 +127,6 @@ impl<'db> SemanticIndexBuilder<'db> {
imported_modules: FxHashSet::default(), imported_modules: FxHashSet::default(),
attribute_assignments: FxHashMap::default(),
eager_bindings: FxHashMap::default(), eager_bindings: FxHashMap::default(),
}; };
@ -222,6 +222,8 @@ impl<'db> SemanticIndexBuilder<'db> {
let file_scope_id = self.scopes.push(scope); let file_scope_id = self.scopes.push(scope);
self.symbol_tables.push(SymbolTableBuilder::default()); self.symbol_tables.push(SymbolTableBuilder::default());
self.instance_attribute_tables
.push(SymbolTableBuilder::default());
self.use_def_maps.push(UseDefMapBuilder::default()); self.use_def_maps.push(UseDefMapBuilder::default());
let ast_id_scope = self.ast_ids.push(AstIdsBuilder::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] &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> { fn current_use_def_map_mut(&mut self) -> &mut UseDefMapBuilder<'db> {
let scope_id = self.current_scope(); let scope_id = self.current_scope();
&mut self.use_def_maps[scope_id] &mut self.use_def_maps[scope_id]
@ -358,6 +365,14 @@ impl<'db> SemanticIndexBuilder<'db> {
(symbol_id, added) (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) { fn mark_symbol_bound(&mut self, id: ScopedSymbolId) {
self.current_symbol_table().mark_symbol_bound(id); self.current_symbol_table().mark_symbol_bound(id);
} }
@ -458,6 +473,25 @@ impl<'db> SemanticIndexBuilder<'db> {
(definition, num_definitions) (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( fn record_expression_narrowing_constraint(
&mut self, &mut self,
precide_node: &ast::Expr, precide_node: &ast::Expr,
@ -626,9 +660,9 @@ impl<'db> SemanticIndexBuilder<'db> {
&mut self, &mut self,
object: &ast::Expr, object: &ast::Expr,
attr: &'db ast::Identifier, 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, // We only care about attribute assignments to the first parameter of a method,
// i.e. typically `self` or `cls`. // i.e. typically `self` or `cls`.
let accessed_object_refers_to_first_parameter = let accessed_object_refers_to_first_parameter =
@ -636,12 +670,8 @@ impl<'db> SemanticIndexBuilder<'db> {
== self.current_first_parameter_name; == self.current_first_parameter_name;
if accessed_object_refers_to_first_parameter { if accessed_object_refers_to_first_parameter {
self.attribute_assignments let symbol = self.add_attribute(attr.id().clone());
.entry(class_body_scope) self.add_attribute_definition(symbol, definition_kind);
.or_default()
.entry(attr.id().clone())
.or_default()
.push(attribute_assignment);
} }
} }
} }
@ -918,18 +948,8 @@ impl<'db> SemanticIndexBuilder<'db> {
)); ));
Some(unpackable.as_current_assignment(unpack)) Some(unpackable.as_current_assignment(unpack))
} }
ast::Expr::Name(_) => Some(unpackable.as_current_assignment(None)), ast::Expr::Name(_) | ast::Expr::Attribute(_) => {
ast::Expr::Attribute(ast::ExprAttribute { Some(unpackable.as_current_assignment(None))
value: object,
attr,
..
}) => {
self.register_attribute_assignment(
object,
attr,
unpackable.as_attribute_assignment(value),
);
None
} }
_ => None, _ => None,
}; };
@ -962,6 +982,12 @@ impl<'db> SemanticIndexBuilder<'db> {
.map(|builder| Arc::new(builder.finish())) .map(|builder| Arc::new(builder.finish()))
.collect(); .collect();
let mut instance_attribute_tables: IndexVec<_, _> = self
.instance_attribute_tables
.into_iter()
.map(SymbolTableBuilder::finish)
.collect();
let mut use_def_maps: IndexVec<_, _> = self let mut use_def_maps: IndexVec<_, _> = self
.use_def_maps .use_def_maps
.into_iter() .into_iter()
@ -976,6 +1002,7 @@ impl<'db> SemanticIndexBuilder<'db> {
self.scopes.shrink_to_fit(); self.scopes.shrink_to_fit();
symbol_tables.shrink_to_fit(); symbol_tables.shrink_to_fit();
instance_attribute_tables.shrink_to_fit();
use_def_maps.shrink_to_fit(); use_def_maps.shrink_to_fit();
ast_ids.shrink_to_fit(); ast_ids.shrink_to_fit();
self.scopes_by_expression.shrink_to_fit(); self.scopes_by_expression.shrink_to_fit();
@ -987,6 +1014,7 @@ impl<'db> SemanticIndexBuilder<'db> {
SemanticIndex { SemanticIndex {
symbol_tables, symbol_tables,
instance_attribute_tables,
scopes: self.scopes, scopes: self.scopes,
definitions_by_node: self.definitions_by_node, definitions_by_node: self.definitions_by_node,
expressions_by_node: self.expressions_by_node, expressions_by_node: self.expressions_by_node,
@ -997,11 +1025,6 @@ impl<'db> SemanticIndexBuilder<'db> {
use_def_maps, use_def_maps,
imported_modules: Arc::new(self.imported_modules), imported_modules: Arc::new(self.imported_modules),
has_future_annotations: self.has_future_annotations, 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, eager_bindings: self.eager_bindings,
} }
} }
@ -1313,7 +1336,6 @@ where
ast::Stmt::AnnAssign(node) => { ast::Stmt::AnnAssign(node) => {
debug_assert_eq!(&self.current_assignments, &[]); debug_assert_eq!(&self.current_assignments, &[]);
self.visit_expr(&node.annotation); self.visit_expr(&node.annotation);
let annotation = self.add_standalone_type_expression(&node.annotation);
if let Some(value) = &node.value { if let Some(value) = &node.value {
self.visit_expr(value); self.visit_expr(value);
} }
@ -1325,20 +1347,6 @@ where
) { ) {
self.push_assignment(node.into()); self.push_assignment(node.into());
self.visit_expr(&node.target); 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(); self.pop_assignment();
} else { } else {
self.visit_expr(&node.target); self.visit_expr(&node.target);
@ -1759,7 +1767,7 @@ where
fn visit_expr(&mut self, expr: &'ast ast::Expr) { fn visit_expr(&mut self, expr: &'ast ast::Expr) {
self.scopes_by_expression self.scopes_by_expression
.insert(expr.into(), self.current_scope()); .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); let node_key = NodeKey::from_node(expr);
@ -1792,12 +1800,20 @@ where
AssignmentDefinitionNodeRef { AssignmentDefinitionNodeRef {
unpack, unpack,
value: &node.value, value: &node.value,
name: name_node, target: expr,
}, },
); );
} }
Some(CurrentAssignment::AnnAssign(ann_assign)) => { 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)) => { Some(CurrentAssignment::AugAssign(aug_assign)) => {
self.add_definition(symbol, aug_assign); self.add_definition(symbol, aug_assign);
@ -1808,7 +1824,7 @@ where
ForStmtDefinitionNodeRef { ForStmtDefinitionNodeRef {
unpack, unpack,
iterable: &node.iter, iterable: &node.iter,
name: name_node, target: expr,
is_async: node.is_async, is_async: node.is_async,
}, },
); );
@ -1840,7 +1856,7 @@ where
WithItemDefinitionNodeRef { WithItemDefinitionNodeRef {
unpack, unpack,
context_expr: &item.context_expr, context_expr: &item.context_expr,
name: name_node, target: expr,
is_async, is_async,
}, },
); );
@ -2026,20 +2042,86 @@ where
range: _, range: _,
}) => { }) => {
if ctx.is_store() { if ctx.is_store() {
if let Some(unpack) = self match self.current_assignment() {
.current_assignment() Some(CurrentAssignment::Assign { node, unpack, .. }) => {
.as_ref() // SAFETY: `value` and `expr` belong to the `self.module` tree
.and_then(CurrentAssignment::unpack) #[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( self.register_attribute_assignment(
object, object,
attr, attr,
AttributeAssignment::Unpack { DefinitionKind::Assignment(assignment),
attribute_expression_id: expression_id,
unpack,
},
); );
} }
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` // Track reachability of attribute expressions to silence `unresolved-attribute`
@ -2138,19 +2220,7 @@ enum CurrentAssignment<'a> {
}, },
} }
impl<'a> CurrentAssignment<'a> { impl CurrentAssignment<'_> {
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,
}
}
fn unpack_position_mut(&mut self) -> Option<&mut UnpackPosition> { fn unpack_position_mut(&mut self) -> Option<&mut UnpackPosition> {
match self { match self {
Self::Assign { unpack, .. } 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), TypeAlias(&'a ast::StmtTypeAlias),
NamedExpression(&'a ast::ExprNamed), NamedExpression(&'a ast::ExprNamed),
Assignment(AssignmentDefinitionNodeRef<'a>), Assignment(AssignmentDefinitionNodeRef<'a>),
AnnotatedAssignment(&'a ast::StmtAnnAssign), AnnotatedAssignment(AnnotatedAssignmentDefinitionNodeRef<'a>),
AugmentedAssignment(&'a ast::StmtAugAssign), AugmentedAssignment(&'a ast::StmtAugAssign),
Comprehension(ComprehensionDefinitionNodeRef<'a>), Comprehension(ComprehensionDefinitionNodeRef<'a>),
VariadicPositionalParameter(&'a ast::Parameter), 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> { impl<'a> From<&'a ast::StmtAugAssign> for DefinitionNodeRef<'a> {
fn from(node: &'a ast::StmtAugAssign) -> Self { fn from(node: &'a ast::StmtAugAssign) -> Self {
Self::AugmentedAssignment(node) 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> { impl<'a> From<WithItemDefinitionNodeRef<'a>> for DefinitionNodeRef<'a> {
fn from(node_ref: WithItemDefinitionNodeRef<'a>) -> Self { fn from(node_ref: WithItemDefinitionNodeRef<'a>) -> Self {
Self::WithItem(node_ref) Self::WithItem(node_ref)
@ -246,14 +246,22 @@ pub(crate) struct ImportFromDefinitionNodeRef<'a> {
pub(crate) struct AssignmentDefinitionNodeRef<'a> { pub(crate) struct AssignmentDefinitionNodeRef<'a> {
pub(crate) unpack: Option<(UnpackPosition, Unpack<'a>)>, pub(crate) unpack: Option<(UnpackPosition, Unpack<'a>)>,
pub(crate) value: &'a ast::Expr, 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)] #[derive(Copy, Clone, Debug)]
pub(crate) struct WithItemDefinitionNodeRef<'a> { pub(crate) struct WithItemDefinitionNodeRef<'a> {
pub(crate) unpack: Option<(UnpackPosition, Unpack<'a>)>, pub(crate) unpack: Option<(UnpackPosition, Unpack<'a>)>,
pub(crate) context_expr: &'a ast::Expr, pub(crate) context_expr: &'a ast::Expr,
pub(crate) name: &'a ast::ExprName, pub(crate) target: &'a ast::Expr,
pub(crate) is_async: bool, pub(crate) is_async: bool,
} }
@ -261,7 +269,7 @@ pub(crate) struct WithItemDefinitionNodeRef<'a> {
pub(crate) struct ForStmtDefinitionNodeRef<'a> { pub(crate) struct ForStmtDefinitionNodeRef<'a> {
pub(crate) unpack: Option<(UnpackPosition, Unpack<'a>)>, pub(crate) unpack: Option<(UnpackPosition, Unpack<'a>)>,
pub(crate) iterable: &'a ast::Expr, pub(crate) iterable: &'a ast::Expr,
pub(crate) name: &'a ast::ExprName, pub(crate) target: &'a ast::Expr,
pub(crate) is_async: bool, pub(crate) is_async: bool,
} }
@ -335,27 +343,34 @@ impl<'db> DefinitionNodeRef<'db> {
DefinitionNodeRef::Assignment(AssignmentDefinitionNodeRef { DefinitionNodeRef::Assignment(AssignmentDefinitionNodeRef {
unpack, unpack,
value, value,
name, target,
}) => DefinitionKind::Assignment(AssignmentDefinitionKind { }) => DefinitionKind::Assignment(AssignmentDefinitionKind {
target: TargetKind::from(unpack), target_kind: TargetKind::from(unpack),
value: AstNodeRef::new(parsed.clone(), value), 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) => { DefinitionNodeRef::AugmentedAssignment(augmented_assignment) => {
DefinitionKind::AugmentedAssignment(AstNodeRef::new(parsed, augmented_assignment)) DefinitionKind::AugmentedAssignment(AstNodeRef::new(parsed, augmented_assignment))
} }
DefinitionNodeRef::For(ForStmtDefinitionNodeRef { DefinitionNodeRef::For(ForStmtDefinitionNodeRef {
unpack, unpack,
iterable, iterable,
name, target,
is_async, is_async,
}) => DefinitionKind::For(ForStmtDefinitionKind { }) => DefinitionKind::For(ForStmtDefinitionKind {
target: TargetKind::from(unpack), target_kind: TargetKind::from(unpack),
iterable: AstNodeRef::new(parsed.clone(), iterable), iterable: AstNodeRef::new(parsed.clone(), iterable),
name: AstNodeRef::new(parsed, name), target: AstNodeRef::new(parsed, target),
is_async, is_async,
}), }),
DefinitionNodeRef::Comprehension(ComprehensionDefinitionNodeRef { DefinitionNodeRef::Comprehension(ComprehensionDefinitionNodeRef {
@ -381,12 +396,12 @@ impl<'db> DefinitionNodeRef<'db> {
DefinitionNodeRef::WithItem(WithItemDefinitionNodeRef { DefinitionNodeRef::WithItem(WithItemDefinitionNodeRef {
unpack, unpack,
context_expr, context_expr,
name, target,
is_async, is_async,
}) => DefinitionKind::WithItem(WithItemDefinitionKind { }) => DefinitionKind::WithItem(WithItemDefinitionKind {
target: TargetKind::from(unpack), target_kind: TargetKind::from(unpack),
context_expr: AstNodeRef::new(parsed.clone(), context_expr), context_expr: AstNodeRef::new(parsed.clone(), context_expr),
name: AstNodeRef::new(parsed, name), target: AstNodeRef::new(parsed, target),
is_async, is_async,
}), }),
DefinitionNodeRef::MatchPattern(MatchPatternDefinitionNodeRef { DefinitionNodeRef::MatchPattern(MatchPatternDefinitionNodeRef {
@ -449,26 +464,26 @@ impl<'db> DefinitionNodeRef<'db> {
Self::Assignment(AssignmentDefinitionNodeRef { Self::Assignment(AssignmentDefinitionNodeRef {
value: _, value: _,
unpack: _, unpack: _,
name, target,
}) => name.into(), }) => DefinitionNodeKey(NodeKey::from_node(target)),
Self::AnnotatedAssignment(node) => node.into(), Self::AnnotatedAssignment(ann_assign) => ann_assign.node.into(),
Self::AugmentedAssignment(node) => node.into(), Self::AugmentedAssignment(node) => node.into(),
Self::For(ForStmtDefinitionNodeRef { Self::For(ForStmtDefinitionNodeRef {
unpack: _, target,
iterable: _, iterable: _,
name, unpack: _,
is_async: _, is_async: _,
}) => name.into(), }) => DefinitionNodeKey(NodeKey::from_node(target)),
Self::Comprehension(ComprehensionDefinitionNodeRef { target, .. }) => target.into(), Self::Comprehension(ComprehensionDefinitionNodeRef { target, .. }) => target.into(),
Self::VariadicPositionalParameter(node) => node.into(), Self::VariadicPositionalParameter(node) => node.into(),
Self::VariadicKeywordParameter(node) => node.into(), Self::VariadicKeywordParameter(node) => node.into(),
Self::Parameter(node) => node.into(), Self::Parameter(node) => node.into(),
Self::WithItem(WithItemDefinitionNodeRef { Self::WithItem(WithItemDefinitionNodeRef {
unpack: _,
context_expr: _, context_expr: _,
unpack: _,
is_async: _, is_async: _,
name, target,
}) => name.into(), }) => DefinitionNodeKey(NodeKey::from_node(target)),
Self::MatchPattern(MatchPatternDefinitionNodeRef { identifier, .. }) => { Self::MatchPattern(MatchPatternDefinitionNodeRef { identifier, .. }) => {
identifier.into() identifier.into()
} }
@ -532,7 +547,7 @@ pub enum DefinitionKind<'db> {
TypeAlias(AstNodeRef<ast::StmtTypeAlias>), TypeAlias(AstNodeRef<ast::StmtTypeAlias>),
NamedExpression(AstNodeRef<ast::ExprNamed>), NamedExpression(AstNodeRef<ast::ExprNamed>),
Assignment(AssignmentDefinitionKind<'db>), Assignment(AssignmentDefinitionKind<'db>),
AnnotatedAssignment(AstNodeRef<ast::StmtAnnAssign>), AnnotatedAssignment(AnnotatedAssignmentDefinitionKind),
AugmentedAssignment(AstNodeRef<ast::StmtAugAssign>), AugmentedAssignment(AstNodeRef<ast::StmtAugAssign>),
For(ForStmtDefinitionKind<'db>), For(ForStmtDefinitionKind<'db>),
Comprehension(ComprehensionDefinitionKind), Comprehension(ComprehensionDefinitionKind),
@ -576,15 +591,15 @@ impl DefinitionKind<'_> {
DefinitionKind::Class(class) => class.name.range(), DefinitionKind::Class(class) => class.name.range(),
DefinitionKind::TypeAlias(type_alias) => type_alias.name.range(), DefinitionKind::TypeAlias(type_alias) => type_alias.name.range(),
DefinitionKind::NamedExpression(named) => named.target.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::AnnotatedAssignment(assign) => assign.target.range(),
DefinitionKind::AugmentedAssignment(aug_assign) => aug_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::Comprehension(comp) => comp.target().range(),
DefinitionKind::VariadicPositionalParameter(parameter) => parameter.name.range(), DefinitionKind::VariadicPositionalParameter(parameter) => parameter.name.range(),
DefinitionKind::VariadicKeywordParameter(parameter) => parameter.name.range(), DefinitionKind::VariadicKeywordParameter(parameter) => parameter.name.range(),
DefinitionKind::Parameter(parameter) => 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::MatchPattern(match_pattern) => match_pattern.identifier.range(),
DefinitionKind::ExceptHandler(handler) => handler.node().range(), DefinitionKind::ExceptHandler(handler) => handler.node().range(),
DefinitionKind::TypeVar(type_var) => type_var.name.range(), DefinitionKind::TypeVar(type_var) => type_var.name.range(),
@ -603,15 +618,15 @@ impl DefinitionKind<'_> {
DefinitionKind::Class(class) => class.range(), DefinitionKind::Class(class) => class.range(),
DefinitionKind::TypeAlias(type_alias) => type_alias.range(), DefinitionKind::TypeAlias(type_alias) => type_alias.range(),
DefinitionKind::NamedExpression(named) => named.range(), DefinitionKind::NamedExpression(named) => named.range(),
DefinitionKind::Assignment(assignment) => assignment.name().range(), DefinitionKind::Assignment(assignment) => assignment.target.range(),
DefinitionKind::AnnotatedAssignment(assign) => assign.range(), DefinitionKind::AnnotatedAssignment(assign) => assign.target.range(),
DefinitionKind::AugmentedAssignment(aug_assign) => aug_assign.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::Comprehension(comp) => comp.target().range(),
DefinitionKind::VariadicPositionalParameter(parameter) => parameter.range(), DefinitionKind::VariadicPositionalParameter(parameter) => parameter.range(),
DefinitionKind::VariadicKeywordParameter(parameter) => parameter.range(), DefinitionKind::VariadicKeywordParameter(parameter) => parameter.range(),
DefinitionKind::Parameter(parameter) => 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::MatchPattern(match_pattern) => match_pattern.identifier.range(),
DefinitionKind::ExceptHandler(handler) => handler.node().range(), DefinitionKind::ExceptHandler(handler) => handler.node().range(),
DefinitionKind::TypeVar(type_var) => type_var.range(), DefinitionKind::TypeVar(type_var) => type_var.range(),
@ -674,14 +689,14 @@ impl DefinitionKind<'_> {
#[derive(Copy, Clone, Debug, PartialEq, Hash)] #[derive(Copy, Clone, Debug, PartialEq, Hash)]
pub(crate) enum TargetKind<'db> { pub(crate) enum TargetKind<'db> {
Sequence(UnpackPosition, Unpack<'db>), Sequence(UnpackPosition, Unpack<'db>),
Name, NameOrAttribute,
} }
impl<'db> From<Option<(UnpackPosition, Unpack<'db>)>> for TargetKind<'db> { impl<'db> From<Option<(UnpackPosition, Unpack<'db>)>> for TargetKind<'db> {
fn from(value: Option<(UnpackPosition, Unpack<'db>)>) -> Self { fn from(value: Option<(UnpackPosition, Unpack<'db>)>) -> Self {
match value { match value {
Some((unpack_position, unpack)) => TargetKind::Sequence(unpack_position, unpack), Some((unpack_position, unpack)) => TargetKind::Sequence(unpack_position, unpack),
None => TargetKind::Name, None => TargetKind::NameOrAttribute,
} }
} }
} }
@ -803,44 +818,103 @@ impl ImportFromDefinitionKind {
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct AssignmentDefinitionKind<'db> { pub struct AssignmentDefinitionKind<'db> {
target: TargetKind<'db>, target_kind: TargetKind<'db>,
value: AstNodeRef<ast::Expr>, value: AstNodeRef<ast::Expr>,
name: AstNodeRef<ast::ExprName>, target: AstNodeRef<ast::Expr>,
} }
impl<'db> AssignmentDefinitionKind<'db> { impl<'db> AssignmentDefinitionKind<'db> {
pub(crate) fn target(&self) -> TargetKind<'db> { pub(crate) fn new(
self.target 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 { pub(crate) fn value(&self) -> &ast::Expr {
self.value.node() self.value.node()
} }
pub(crate) fn name(&self) -> &ast::ExprName { pub(crate) fn target(&self) -> &ast::Expr {
self.name.node() 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)] #[derive(Clone, Debug)]
pub struct WithItemDefinitionKind<'db> { pub struct WithItemDefinitionKind<'db> {
target: TargetKind<'db>, target_kind: TargetKind<'db>,
context_expr: AstNodeRef<ast::Expr>, context_expr: AstNodeRef<ast::Expr>,
name: AstNodeRef<ast::ExprName>, target: AstNodeRef<ast::Expr>,
is_async: bool, is_async: bool,
} }
impl<'db> WithItemDefinitionKind<'db> { 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 { pub(crate) fn context_expr(&self) -> &ast::Expr {
self.context_expr.node() self.context_expr.node()
} }
pub(crate) fn target(&self) -> TargetKind<'db> { pub(crate) fn target_kind(&self) -> TargetKind<'db> {
self.target self.target_kind
} }
pub(crate) fn name(&self) -> &ast::ExprName { pub(crate) fn target(&self) -> &ast::Expr {
self.name.node() self.target.node()
} }
pub(crate) const fn is_async(&self) -> bool { pub(crate) const fn is_async(&self) -> bool {
@ -850,23 +924,37 @@ impl<'db> WithItemDefinitionKind<'db> {
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct ForStmtDefinitionKind<'db> { pub struct ForStmtDefinitionKind<'db> {
target: TargetKind<'db>, target_kind: TargetKind<'db>,
iterable: AstNodeRef<ast::Expr>, iterable: AstNodeRef<ast::Expr>,
name: AstNodeRef<ast::ExprName>, target: AstNodeRef<ast::Expr>,
is_async: bool, is_async: bool,
} }
impl<'db> ForStmtDefinitionKind<'db> { 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 { pub(crate) fn iterable(&self) -> &ast::Expr {
self.iterable.node() self.iterable.node()
} }
pub(crate) fn target(&self) -> TargetKind<'db> { pub(crate) fn target_kind(&self) -> TargetKind<'db> {
self.target self.target_kind
} }
pub(crate) fn name(&self) -> &ast::ExprName { pub(crate) fn target(&self) -> &ast::Expr {
self.name.node() self.target.node()
} }
pub(crate) const fn is_async(&self) -> bool { pub(crate) const fn is_async(&self) -> bool {

View file

@ -259,9 +259,10 @@
use ruff_index::{newtype_index, IndexVec}; use ruff_index::{newtype_index, IndexVec};
use rustc_hash::FxHashMap; use rustc_hash::FxHashMap;
use self::symbol_state::ScopedDefinitionId;
use self::symbol_state::{ use self::symbol_state::{
LiveBindingsIterator, LiveDeclaration, LiveDeclarationsIterator, ScopedDefinitionId, LiveBindingsIterator, LiveDeclaration, LiveDeclarationsIterator, SymbolBindings,
SymbolBindings, SymbolDeclarations, SymbolState, SymbolDeclarations, SymbolState,
}; };
use crate::node_key::NodeKey; use crate::node_key::NodeKey;
use crate::semantic_index::ast_ids::ScopedUseId; use crate::semantic_index::ast_ids::ScopedUseId;
@ -276,6 +277,7 @@ use crate::semantic_index::symbol::{FileScopeId, ScopedSymbolId};
use crate::semantic_index::visibility_constraints::{ use crate::semantic_index::visibility_constraints::{
ScopedVisibilityConstraintId, VisibilityConstraints, VisibilityConstraintsBuilder, ScopedVisibilityConstraintId, VisibilityConstraints, VisibilityConstraintsBuilder,
}; };
use crate::types::Truthiness;
mod symbol_state; mod symbol_state;
@ -321,6 +323,9 @@ pub(crate) struct UseDefMap<'db> {
/// [`SymbolState`] visible at end of scope for each symbol. /// [`SymbolState`] visible at end of scope for each symbol.
public_symbols: IndexVec<ScopedSymbolId, SymbolState>, 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 /// Snapshot of bindings in this scope that can be used to resolve a reference in a nested
/// eager scope. /// eager scope.
eager_bindings: EagerBindings, eager_bindings: EagerBindings,
@ -386,6 +391,13 @@ impl<'db> UseDefMap<'db> {
self.bindings_iterator(self.public_symbols[symbol].bindings()) 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( pub(crate) fn eager_bindings(
&self, &self,
eager_bindings: ScopedEagerBindingsId, eager_bindings: ScopedEagerBindingsId,
@ -425,6 +437,15 @@ impl<'db> UseDefMap<'db> {
.is_always_false() .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>( fn bindings_iterator<'map>(
&'map self, &'map self,
bindings: &'map SymbolBindings, bindings: &'map SymbolBindings,
@ -566,6 +587,7 @@ impl std::iter::FusedIterator for DeclarationsIterator<'_, '_> {}
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub(super) struct FlowSnapshot { pub(super) struct FlowSnapshot {
symbol_states: IndexVec<ScopedSymbolId, SymbolState>, symbol_states: IndexVec<ScopedSymbolId, SymbolState>,
instance_attribute_states: IndexVec<ScopedSymbolId, SymbolState>,
scope_start_visibility: ScopedVisibilityConstraintId, scope_start_visibility: ScopedVisibilityConstraintId,
reachability: ScopedVisibilityConstraintId, reachability: ScopedVisibilityConstraintId,
} }
@ -645,6 +667,9 @@ pub(super) struct UseDefMapBuilder<'db> {
/// Currently live bindings and declarations for each symbol. /// Currently live bindings and declarations for each symbol.
symbol_states: IndexVec<ScopedSymbolId, SymbolState>, 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 /// Snapshot of bindings in this scope that can be used to resolve a reference in a nested
/// eager scope. /// eager scope.
eager_bindings: EagerBindings, eager_bindings: EagerBindings,
@ -665,6 +690,7 @@ impl Default for UseDefMapBuilder<'_> {
bindings_by_declaration: FxHashMap::default(), bindings_by_declaration: FxHashMap::default(),
symbol_states: IndexVec::new(), symbol_states: IndexVec::new(),
eager_bindings: EagerBindings::default(), eager_bindings: EagerBindings::default(),
instance_attribute_states: IndexVec::new(),
} }
} }
} }
@ -682,6 +708,13 @@ impl<'db> UseDefMapBuilder<'db> {
debug_assert_eq!(symbol, new_symbol); 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>) { pub(super) fn record_binding(&mut self, symbol: ScopedSymbolId, binding: Definition<'db>) {
let def_id = self.all_definitions.push(Some(binding)); let def_id = self.all_definitions.push(Some(binding));
let symbol_state = &mut self.symbol_states[symbol]; 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); 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 { pub(super) fn add_predicate(&mut self, predicate: Predicate<'db>) -> ScopedPredicateId {
self.predicates.add_predicate(predicate) self.predicates.add_predicate(predicate)
} }
@ -700,6 +745,10 @@ impl<'db> UseDefMapBuilder<'db> {
state state
.record_narrowing_constraint(&mut self.narrowing_constraints, narrowing_constraint); .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( pub(super) fn record_visibility_constraint(
@ -709,6 +758,9 @@ impl<'db> UseDefMapBuilder<'db> {
for state in &mut self.symbol_states { for state in &mut self.symbol_states {
state.record_visibility_constraint(&mut self.visibility_constraints, constraint); 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 self.scope_start_visibility = self
.visibility_constraints .visibility_constraints
.add_and_constraint(self.scope_start_visibility, constraint); .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`. /// 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) { pub(super) fn simplify_visibility_constraints(&mut self, snapshot: FlowSnapshot) {
debug_assert!(self.symbol_states.len() >= snapshot.symbol_states.len()); 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 // 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`. // 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) { for (current, snapshot) in self.symbol_states.iter_mut().zip(snapshot.symbol_states) {
current.simplify_visibility_constraints(snapshot); 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( pub(super) fn record_reachability_constraint(
@ -849,6 +911,7 @@ impl<'db> UseDefMapBuilder<'db> {
pub(super) fn snapshot(&self) -> FlowSnapshot { pub(super) fn snapshot(&self) -> FlowSnapshot {
FlowSnapshot { FlowSnapshot {
symbol_states: self.symbol_states.clone(), symbol_states: self.symbol_states.clone(),
instance_attribute_states: self.instance_attribute_states.clone(),
scope_start_visibility: self.scope_start_visibility, scope_start_visibility: self.scope_start_visibility,
reachability: self.reachability, reachability: self.reachability,
} }
@ -861,9 +924,12 @@ impl<'db> UseDefMapBuilder<'db> {
// greater than the number of known symbols in a previously-taken snapshot. // greater than the number of known symbols in a previously-taken snapshot.
let num_symbols = self.symbol_states.len(); let num_symbols = self.symbol_states.len();
debug_assert!(num_symbols >= snapshot.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. // Restore the current visible-definitions state to the given snapshot.
self.symbol_states = snapshot.symbol_states; self.symbol_states = snapshot.symbol_states;
self.instance_attribute_states = snapshot.instance_attribute_states;
self.scope_start_visibility = snapshot.scope_start_visibility; self.scope_start_visibility = snapshot.scope_start_visibility;
self.reachability = snapshot.reachability; self.reachability = snapshot.reachability;
@ -874,6 +940,10 @@ impl<'db> UseDefMapBuilder<'db> {
num_symbols, num_symbols,
SymbolState::undefined(self.scope_start_visibility), 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 /// 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 // 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. // 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.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(); let mut snapshot_definitions_iter = snapshot.symbol_states.into_iter();
for current in &mut self.symbol_states { 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. // 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 self.scope_start_visibility = self
.visibility_constraints .visibility_constraints
@ -930,6 +1019,7 @@ impl<'db> UseDefMapBuilder<'db> {
pub(super) fn finish(mut self) -> UseDefMap<'db> { pub(super) fn finish(mut self) -> UseDefMap<'db> {
self.all_definitions.shrink_to_fit(); self.all_definitions.shrink_to_fit();
self.symbol_states.shrink_to_fit(); self.symbol_states.shrink_to_fit();
self.instance_attribute_states.shrink_to_fit();
self.bindings_by_use.shrink_to_fit(); self.bindings_by_use.shrink_to_fit();
self.node_reachability.shrink_to_fit(); self.node_reachability.shrink_to_fit();
self.declarations_by_binding.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, bindings_by_use: self.bindings_by_use,
node_reachability: self.node_reachability, node_reachability: self.node_reachability,
public_symbols: self.symbol_states, public_symbols: self.symbol_states,
instance_attributes: self.instance_attribute_states,
declarations_by_binding: self.declarations_by_binding, declarations_by_binding: self.declarations_by_binding,
bindings_by_declaration: self.bindings_by_declaration, bindings_by_declaration: self.bindings_by_declaration,
eager_bindings: self.eager_bindings, eager_bindings: self.eager_bindings,

View file

@ -59,6 +59,10 @@ impl<'db> Symbol<'db> {
Symbol::Type(ty.into(), Boundness::Bound) 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 /// Constructor that creates a [`Symbol`] with a [`crate::types::TodoType`] type
/// and boundness [`Boundness::Bound`]. /// and boundness [`Boundness::Bound`].
#[allow(unused_variables)] // Only unused in release builds #[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 { fn into_type(self, db: &dyn Db) -> Type {
match self { match self {
Self::AlwaysTrue => Type::BooleanLiteral(true), Self::AlwaysTrue => Type::BooleanLiteral(true),

View file

@ -10,8 +10,12 @@ use crate::types::generics::{GenericContext, Specialization};
use crate::{ use crate::{
module_resolver::file_to_module, module_resolver::file_to_module,
semantic_index::{ semantic_index::{
attribute_assignment::AttributeAssignment, attribute_assignments, semantic_index, ast_ids::HasScopedExpressionId,
symbol::ScopeId, symbol_table, use_def_map, attribute_assignments,
definition::{DefinitionKind, TargetKind},
semantic_index,
symbol::ScopeId,
symbol_table, use_def_map,
}, },
symbol::{ symbol::{
class_symbol, known_module_symbol, symbol_from_bindings, symbol_from_declarations, class_symbol, known_module_symbol, symbol_from_bindings, symbol_from_declarations,
@ -853,81 +857,216 @@ impl<'db> ClassLiteralType<'db> {
db: &'db dyn Db, db: &'db dyn Db,
class_body_scope: ScopeId<'db>, class_body_scope: ScopeId<'db>,
name: &str, name: &str,
) -> Option<Type<'db>> { ) -> Symbol<'db> {
// If we do not see any declarations of an attribute, neither in the class body nor in // 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 // 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 // that attribute. We include `Unknown` in that union to account for the fact that the
// attribute might be externally modified. // attribute might be externally modified.
let mut union_of_inferred_types = UnionBuilder::new(db).add(Type::unknown()); 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 let file = class_body_scope.file(db);
.as_deref() let index = semantic_index(db, file);
.and_then(|assignments| assignments.get(name))?; 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 { for attribute_assignment in attribute_assignments {
match attribute_assignment { let Some(binding) = attribute_assignment.binding else {
AttributeAssignment::Annotated { annotation } => { 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 // 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): // examples, but we support arbitrary names for the first parameters of methods):
// //
// self.name: <annotation> // self.name: <annotation>
// 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 // 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 } => { Truthiness::Ambiguous => {
// We found an un-annotated attribute assignment of the form: return Symbol::possibly_unbound(annotation_ty);
//
// self.name = <value>
let inferred_ty = infer_expression_type(db, *value);
union_of_inferred_types = union_of_inferred_types.add(inferred_ty);
} }
AttributeAssignment::Iterable { iterable } => { Truthiness::AlwaysFalse => unreachable!("If the attribute assignments are all invisible, inference of their types should be skipped"),
// 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);
} }
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 { DefinitionKind::Assignment(assign) => {
attribute_expression_id, match assign.target_kind() {
unpack, TargetKind::Sequence(_, unpack) => {
} => {
// We found an unpacking assignment like: // We found an unpacking assignment like:
// //
// .., self.name, .. = <value> // .., self.name, .. = <value>
// (.., self.name, ..) = <value> // (.., 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 = 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); 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 /// 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) = if let Some(implicit_ty) =
Self::implicit_instance_attribute(db, body_scope, name) Self::implicit_instance_attribute(db, body_scope, name)
.ignore_possibly_unbound()
{ {
if declaredness == Boundness::Bound { if declaredness == Boundness::Bound {
// If a symbol is definitely declared, and we see // If a symbol is definitely declared, and we see
@ -995,6 +1135,7 @@ impl<'db> ClassLiteralType<'db> {
} else { } else {
if let Some(implicit_ty) = if let Some(implicit_ty) =
Self::implicit_instance_attribute(db, body_scope, name) Self::implicit_instance_attribute(db, body_scope, name)
.ignore_possibly_unbound()
{ {
Symbol::Type( Symbol::Type(
UnionType::from_elements(db, [declared_ty, implicit_ty]), 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 // The attribute is not *declared* in the class body. It could still be declared/bound
// in a method. // in a method.
Self::implicit_instance_attribute(db, body_scope, name) Self::implicit_instance_attribute(db, body_scope, name).into()
.map_or(Symbol::Unbound, Symbol::bound)
.into()
} }
Err((declared, _conflicting_declarations)) => { Err((declared, _conflicting_declarations)) => {
// There are conflicting declarations for this attribute in the class body. // 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. // This attribute is neither declared nor bound in the class body.
// It could still be implicitly defined in a method. // It could still be implicitly defined in a method.
Self::implicit_instance_attribute(db, body_scope, name) Self::implicit_instance_attribute(db, body_scope, name).into()
.map_or(Symbol::Unbound, Symbol::bound)
.into()
} }
} }

View file

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