Skip to content

Commit 102c2ee

Browse files
authored
[red-knot] Implicit instance attributes (#15811)
## Summary Add support for implicitly-defined instance attributes, i.e. support type inference for cases like this: ```py class C: def __init__(self) -> None: self.x: int = 1 self.y = None reveal_type(C().x) # int reveal_type(C().y) # Unknown | None ``` ## Benchmarks Codspeed reports no change in a cold-cache benchmark, and a -1% regression in the incremental benchmark. On `black`'s `src` folder, I don't see a statistically significant difference between the branches: | Command | Mean [ms] | Min [ms] | Max [ms] | Relative | |:---|---:|---:|---:|---:| | `./red_knot_main check --project /home/shark/black/src` | 133.7 ± 9.5 | 126.7 | 164.7 | 1.01 ± 0.08 | | `./red_knot_feature check --project /home/shark/black/src` | 132.2 ± 5.1 | 118.1 | 140.9 | 1.00 | ## Test Plan Updated and new Markdown tests
1 parent dc5e922 commit 102c2ee

File tree

8 files changed

+657
-53
lines changed

8 files changed

+657
-53
lines changed

crates/red_knot_python_semantic/resources/mdtest/attributes.md

Lines changed: 290 additions & 27 deletions
Large diffs are not rendered by default.

crates/red_knot_python_semantic/src/semantic_index.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ use ruff_index::{IndexSlice, IndexVec};
1111
use crate::module_name::ModuleName;
1212
use crate::semantic_index::ast_ids::node_key::ExpressionNodeKey;
1313
use crate::semantic_index::ast_ids::AstIds;
14+
use crate::semantic_index::attribute_assignment::AttributeAssignments;
1415
use crate::semantic_index::builder::SemanticIndexBuilder;
1516
use crate::semantic_index::definition::{Definition, DefinitionNodeKey};
1617
use crate::semantic_index::expression::Expression;
@@ -21,6 +22,7 @@ use crate::semantic_index::use_def::UseDefMap;
2122
use crate::Db;
2223

2324
pub mod ast_ids;
25+
pub mod attribute_assignment;
2426
mod builder;
2527
pub(crate) mod constraint;
2628
pub mod definition;
@@ -93,6 +95,25 @@ pub(crate) fn use_def_map<'db>(db: &'db dyn Db, scope: ScopeId<'db>) -> Arc<UseD
9395
index.use_def_map(scope.file_scope_id(db))
9496
}
9597

98+
/// Returns all attribute assignments for a specific class body scope.
99+
///
100+
/// Using [`attribute_assignments`] over [`semantic_index`] has the advantage that
101+
/// Salsa can avoid invalidating dependent queries if this scope's instance attributes
102+
/// are unchanged.
103+
#[salsa::tracked]
104+
pub(crate) fn attribute_assignments<'db>(
105+
db: &'db dyn Db,
106+
class_body_scope: ScopeId<'db>,
107+
) -> Option<Arc<AttributeAssignments<'db>>> {
108+
let file = class_body_scope.file(db);
109+
let index = semantic_index(db, file);
110+
111+
index
112+
.attribute_assignments
113+
.get(&class_body_scope.file_scope_id(db))
114+
.cloned()
115+
}
116+
96117
/// Returns the module global scope of `file`.
97118
#[salsa::tracked]
98119
pub(crate) fn global_scope(db: &dyn Db, file: File) -> ScopeId<'_> {
@@ -139,6 +160,10 @@ pub(crate) struct SemanticIndex<'db> {
139160

140161
/// Flags about the global scope (code usage impacting inference)
141162
has_future_annotations: bool,
163+
164+
/// Maps from class body scopes to attribute assignments that were found
165+
/// in methods of that class.
166+
attribute_assignments: FxHashMap<FileScopeId, Arc<AttributeAssignments<'db>>>,
142167
}
143168

144169
impl<'db> SemanticIndex<'db> {
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
use crate::semantic_index::expression::Expression;
2+
3+
use ruff_python_ast::name::Name;
4+
5+
use rustc_hash::FxHashMap;
6+
7+
/// Describes an (annotated) attribute assignment that we discovered in a method
8+
/// body, typically of the form `self.x: int`, `self.x: int = …` or `self.x = …`.
9+
#[derive(Debug, Clone, PartialEq, Eq)]
10+
pub(crate) enum AttributeAssignment<'db> {
11+
/// An attribute assignment with an explicit type annotation, either
12+
/// `self.x: <annotation>` or `self.x: <annotation> = …`.
13+
Annotated { annotation: Expression<'db> },
14+
15+
/// An attribute assignment without a type annotation, e.g. `self.x = <value>`.
16+
Unannotated { value: Expression<'db> },
17+
}
18+
19+
pub(crate) type AttributeAssignments<'db> = FxHashMap<Name, Vec<AttributeAssignment<'db>>>;

crates/red_knot_python_semantic/src/semantic_index/builder.rs

Lines changed: 128 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,15 @@ use crate::ast_node_ref::AstNodeRef;
1414
use crate::module_name::ModuleName;
1515
use crate::semantic_index::ast_ids::node_key::ExpressionNodeKey;
1616
use crate::semantic_index::ast_ids::AstIdsBuilder;
17+
use crate::semantic_index::attribute_assignment::{AttributeAssignment, AttributeAssignments};
1718
use crate::semantic_index::constraint::PatternConstraintKind;
1819
use crate::semantic_index::definition::{
1920
AssignmentDefinitionNodeRef, ComprehensionDefinitionNodeRef, Definition, DefinitionNodeKey,
2021
DefinitionNodeRef, ForStmtDefinitionNodeRef, ImportFromDefinitionNodeRef,
2122
};
22-
use crate::semantic_index::expression::Expression;
23+
use crate::semantic_index::expression::{Expression, ExpressionKind};
2324
use crate::semantic_index::symbol::{
24-
FileScopeId, NodeWithScopeKey, NodeWithScopeRef, Scope, ScopeId, ScopedSymbolId,
25+
FileScopeId, NodeWithScopeKey, NodeWithScopeRef, Scope, ScopeId, ScopeKind, ScopedSymbolId,
2526
SymbolTableBuilder,
2627
};
2728
use crate::semantic_index::use_def::{
@@ -53,17 +54,24 @@ impl LoopState {
5354
}
5455
}
5556

57+
struct ScopeInfo {
58+
file_scope_id: FileScopeId,
59+
loop_state: LoopState,
60+
}
61+
5662
pub(super) struct SemanticIndexBuilder<'db> {
5763
// Builder state
5864
db: &'db dyn Db,
5965
file: File,
6066
module: &'db ParsedModule,
61-
scope_stack: Vec<(FileScopeId, LoopState)>,
67+
scope_stack: Vec<ScopeInfo>,
6268
/// The assignments we're currently visiting, with
6369
/// the most recent visit at the end of the Vec
6470
current_assignments: Vec<CurrentAssignment<'db>>,
6571
/// The match case we're currently visiting.
6672
current_match_case: Option<CurrentMatchCase<'db>>,
73+
/// The name of the first function parameter of the innermost function that we're currently visiting.
74+
current_first_parameter_name: Option<&'db str>,
6775

6876
/// Flow states at each `break` in the current loop.
6977
loop_break_states: Vec<FlowSnapshot>,
@@ -84,6 +92,7 @@ pub(super) struct SemanticIndexBuilder<'db> {
8492
definitions_by_node: FxHashMap<DefinitionNodeKey, Definition<'db>>,
8593
expressions_by_node: FxHashMap<ExpressionNodeKey, Expression<'db>>,
8694
imported_modules: FxHashSet<ModuleName>,
95+
attribute_assignments: FxHashMap<FileScopeId, AttributeAssignments<'db>>,
8796
}
8897

8998
impl<'db> SemanticIndexBuilder<'db> {
@@ -95,6 +104,7 @@ impl<'db> SemanticIndexBuilder<'db> {
95104
scope_stack: Vec::new(),
96105
current_assignments: vec![],
97106
current_match_case: None,
107+
current_first_parameter_name: None,
98108
loop_break_states: vec![],
99109
try_node_context_stack_manager: TryNodeContextStackManager::default(),
100110

@@ -112,6 +122,8 @@ impl<'db> SemanticIndexBuilder<'db> {
112122
expressions_by_node: FxHashMap::default(),
113123

114124
imported_modules: FxHashSet::default(),
125+
126+
attribute_assignments: FxHashMap::default(),
115127
};
116128

117129
builder.push_scope_with_parent(NodeWithScopeRef::Module, None);
@@ -123,22 +135,40 @@ impl<'db> SemanticIndexBuilder<'db> {
123135
*self
124136
.scope_stack
125137
.last()
126-
.map(|(scope, _)| scope)
138+
.map(|ScopeInfo { file_scope_id, .. }| file_scope_id)
127139
.expect("Always to have a root scope")
128140
}
129141

130142
fn loop_state(&self) -> LoopState {
131143
self.scope_stack
132144
.last()
133145
.expect("Always to have a root scope")
134-
.1
146+
.loop_state
147+
}
148+
149+
/// Returns the scope ID of the surrounding class body scope if the current scope
150+
/// is a method inside a class body. Returns `None` otherwise, e.g. if the current
151+
/// scope is a function body outside of a class, or if the current scope is not a
152+
/// function body.
153+
fn is_method_of_class(&self) -> Option<FileScopeId> {
154+
let mut scopes_rev = self.scope_stack.iter().rev();
155+
let current = scopes_rev.next()?;
156+
let parent = scopes_rev.next()?;
157+
158+
match (
159+
self.scopes[current.file_scope_id].kind(),
160+
self.scopes[parent.file_scope_id].kind(),
161+
) {
162+
(ScopeKind::Function, ScopeKind::Class) => Some(parent.file_scope_id),
163+
_ => None,
164+
}
135165
}
136166

137167
fn set_inside_loop(&mut self, state: LoopState) {
138168
self.scope_stack
139169
.last_mut()
140170
.expect("Always to have a root scope")
141-
.1 = state;
171+
.loop_state = state;
142172
}
143173

144174
fn push_scope(&mut self, node: NodeWithScopeRef) {
@@ -171,16 +201,20 @@ impl<'db> SemanticIndexBuilder<'db> {
171201

172202
debug_assert_eq!(ast_id_scope, file_scope_id);
173203

174-
self.scope_stack.push((file_scope_id, LoopState::NotInLoop));
204+
self.scope_stack.push(ScopeInfo {
205+
file_scope_id,
206+
loop_state: LoopState::NotInLoop,
207+
});
175208
}
176209

177210
fn pop_scope(&mut self) -> FileScopeId {
178-
let (id, _) = self.scope_stack.pop().expect("Root scope to be present");
211+
let ScopeInfo { file_scope_id, .. } =
212+
self.scope_stack.pop().expect("Root scope to be present");
179213
let children_end = self.scopes.next_index();
180-
let scope = &mut self.scopes[id];
214+
let scope = &mut self.scopes[file_scope_id];
181215
scope.descendents = scope.descendents.start..children_end;
182216
self.try_node_context_stack_manager.exit_scope();
183-
id
217+
file_scope_id
184218
}
185219

186220
fn current_symbol_table(&mut self) -> &mut SymbolTableBuilder {
@@ -404,6 +438,32 @@ impl<'db> SemanticIndexBuilder<'db> {
404438
self.current_assignments.last_mut()
405439
}
406440

441+
/// Records the fact that we saw an attribute assignment of the form
442+
/// `object.attr: <annotation>( = …)` or `object.attr = <value>`.
443+
fn register_attribute_assignment(
444+
&mut self,
445+
object: &ast::Expr,
446+
attr: &'db ast::Identifier,
447+
attribute_assignment: AttributeAssignment<'db>,
448+
) {
449+
if let Some(class_body_scope) = self.is_method_of_class() {
450+
// We only care about attribute assignments to the first parameter of a method,
451+
// i.e. typically `self` or `cls`.
452+
let accessed_object_refers_to_first_parameter =
453+
object.as_name_expr().map(|name| name.id.as_str())
454+
== self.current_first_parameter_name;
455+
456+
if accessed_object_refers_to_first_parameter {
457+
self.attribute_assignments
458+
.entry(class_body_scope)
459+
.or_default()
460+
.entry(attr.id().clone())
461+
.or_default()
462+
.push(attribute_assignment);
463+
}
464+
}
465+
}
466+
407467
fn add_pattern_constraint(
408468
&mut self,
409469
subject: Expression<'db>,
@@ -457,6 +517,20 @@ impl<'db> SemanticIndexBuilder<'db> {
457517
/// Record an expression that needs to be a Salsa ingredient, because we need to infer its type
458518
/// standalone (type narrowing tests, RHS of an assignment.)
459519
fn add_standalone_expression(&mut self, expression_node: &ast::Expr) -> Expression<'db> {
520+
self.add_standalone_expression_impl(expression_node, ExpressionKind::Normal)
521+
}
522+
523+
/// Same as [`SemanticIndexBuilder::add_standalone_expression`], but marks the expression as a
524+
/// *type* expression, which makes sure that it will later be inferred as such.
525+
fn add_standalone_type_expression(&mut self, expression_node: &ast::Expr) -> Expression<'db> {
526+
self.add_standalone_expression_impl(expression_node, ExpressionKind::TypeExpression)
527+
}
528+
529+
fn add_standalone_expression_impl(
530+
&mut self,
531+
expression_node: &ast::Expr,
532+
expression_kind: ExpressionKind,
533+
) -> Expression<'db> {
460534
let expression = Expression::new(
461535
self.db,
462536
self.file,
@@ -465,6 +539,7 @@ impl<'db> SemanticIndexBuilder<'db> {
465539
unsafe {
466540
AstNodeRef::new(self.module.clone(), expression_node)
467541
},
542+
expression_kind,
468543
countme::Count::default(),
469544
);
470545
self.expressions_by_node
@@ -668,6 +743,11 @@ impl<'db> SemanticIndexBuilder<'db> {
668743
use_def_maps,
669744
imported_modules: Arc::new(self.imported_modules),
670745
has_future_annotations: self.has_future_annotations,
746+
attribute_assignments: self
747+
.attribute_assignments
748+
.into_iter()
749+
.map(|(k, v)| (k, Arc::new(v)))
750+
.collect(),
671751
}
672752
}
673753
}
@@ -706,7 +786,17 @@ where
706786

707787
builder.declare_parameters(parameters);
708788

789+
let mut first_parameter_name = parameters
790+
.iter_non_variadic_params()
791+
.next()
792+
.map(|first_param| first_param.parameter.name.id().as_str());
793+
std::mem::swap(
794+
&mut builder.current_first_parameter_name,
795+
&mut first_parameter_name,
796+
);
709797
builder.visit_body(body);
798+
builder.current_first_parameter_name = first_parameter_name;
799+
710800
builder.pop_scope()
711801
},
712802
);
@@ -840,6 +930,19 @@ where
840930
unpack: None,
841931
first: false,
842932
}),
933+
ast::Expr::Attribute(ast::ExprAttribute {
934+
value: object,
935+
attr,
936+
..
937+
}) => {
938+
self.register_attribute_assignment(
939+
object,
940+
attr,
941+
AttributeAssignment::Unannotated { value },
942+
);
943+
944+
None
945+
}
843946
_ => None,
844947
};
845948

@@ -858,6 +961,7 @@ where
858961
ast::Stmt::AnnAssign(node) => {
859962
debug_assert_eq!(&self.current_assignments, &[]);
860963
self.visit_expr(&node.annotation);
964+
let annotation = self.add_standalone_type_expression(&node.annotation);
861965
if let Some(value) = &node.value {
862966
self.visit_expr(value);
863967
}
@@ -869,6 +973,20 @@ where
869973
) {
870974
self.push_assignment(node.into());
871975
self.visit_expr(&node.target);
976+
977+
if let ast::Expr::Attribute(ast::ExprAttribute {
978+
value: object,
979+
attr,
980+
..
981+
}) = &*node.target
982+
{
983+
self.register_attribute_assignment(
984+
object,
985+
attr,
986+
AttributeAssignment::Annotated { annotation },
987+
);
988+
}
989+
872990
self.pop_assignment();
873991
} else {
874992
self.visit_expr(&node.target);

crates/red_knot_python_semantic/src/semantic_index/expression.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,16 @@ use ruff_db::files::File;
55
use ruff_python_ast as ast;
66
use salsa;
77

8+
/// Whether or not this expression should be inferred as a normal expression or
9+
/// a type expression. For example, in `self.x: <annotation> = <value>`, the
10+
/// `<annotation>` is inferred as a type expression, while `<value>` is inferred
11+
/// as a normal expression.
12+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
13+
pub(crate) enum ExpressionKind {
14+
Normal,
15+
TypeExpression,
16+
}
17+
818
/// An independently type-inferable expression.
919
///
1020
/// Includes constraint expressions (e.g. if tests) and the RHS of an unpacking assignment.
@@ -35,6 +45,10 @@ pub(crate) struct Expression<'db> {
3545
#[return_ref]
3646
pub(crate) node_ref: AstNodeRef<ast::Expr>,
3747

48+
/// Should this expression be inferred as a normal expression or a type expression?
49+
#[id]
50+
pub(crate) kind: ExpressionKind,
51+
3852
#[no_eq]
3953
count: countme::Count<Expression<'static>>,
4054
}

0 commit comments

Comments
 (0)