Skip to content

Commit f88328e

Browse files
authored
[red-knot] Handle possibly-unbound instance members (#16363)
## Summary Adds support for possibly-unbound/undeclared instance members. ## Test Plan New MD tests.
1 parent fa76f6c commit f88328e

File tree

4 files changed

+133
-19
lines changed

4 files changed

+133
-19
lines changed

crates/red_knot_python_semantic/resources/mdtest/attributes.md

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -783,6 +783,9 @@ def _(flag1: bool, flag2: bool):
783783

784784
# error: [possibly-unbound-attribute] "Attribute `x` on type `Literal[C1, C2, C3]` is possibly unbound"
785785
reveal_type(C.x) # revealed: Unknown | Literal[1, 3]
786+
787+
# error: [possibly-unbound-attribute] "Attribute `x` on type `C1 | C2 | C3` is possibly unbound"
788+
reveal_type(C().x) # revealed: Unknown | Literal[1, 3]
786789
```
787790

788791
### Possibly-unbound within a class
@@ -806,6 +809,28 @@ def _(flag: bool, flag1: bool, flag2: bool):
806809

807810
# error: [possibly-unbound-attribute] "Attribute `x` on type `Literal[C1, C2, C3]` is possibly unbound"
808811
reveal_type(C.x) # revealed: Unknown | Literal[1, 2, 3]
812+
813+
# Note: we might want to consider ignoring possibly-unbound diagnostics for instance attributes eventually,
814+
# see the "Possibly unbound/undeclared instance attribute" section below.
815+
# error: [possibly-unbound-attribute] "Attribute `x` on type `C1 | C2 | C3` is possibly unbound"
816+
reveal_type(C().x) # revealed: Unknown | Literal[1, 2, 3]
817+
```
818+
819+
### Possibly-unbound within gradual types
820+
821+
```py
822+
from typing import Any
823+
824+
def _(flag: bool):
825+
class Base:
826+
x: Any
827+
828+
class Derived(Base):
829+
if flag:
830+
# Redeclaring `x` with a more static type is okay in terms of LSP.
831+
x: int
832+
833+
reveal_type(Derived().x) # revealed: int | Any
809834
```
810835

811836
### Attribute possibly unbound on a subclass but not on a superclass
@@ -820,6 +845,8 @@ def _(flag: bool):
820845
x = 2
821846

822847
reveal_type(Bar.x) # revealed: Unknown | Literal[2, 1]
848+
849+
reveal_type(Bar().x) # revealed: Unknown | Literal[2, 1]
823850
```
824851

825852
### Attribute possibly unbound on a subclass and on a superclass
@@ -836,6 +863,41 @@ def _(flag: bool):
836863

837864
# error: [possibly-unbound-attribute]
838865
reveal_type(Bar.x) # revealed: Unknown | Literal[2, 1]
866+
867+
# error: [possibly-unbound-attribute]
868+
reveal_type(Bar().x) # revealed: Unknown | Literal[2, 1]
869+
```
870+
871+
### Possibly unbound/undeclared instance attribute
872+
873+
#### Possibly unbound and undeclared
874+
875+
```py
876+
def _(flag: bool):
877+
class Foo:
878+
if flag:
879+
x: int
880+
881+
def __init(self):
882+
if flag:
883+
self.x = 1
884+
885+
# error: [possibly-unbound-attribute]
886+
reveal_type(Foo().x) # revealed: int
887+
```
888+
889+
#### Possibly unbound
890+
891+
```py
892+
def _(flag: bool):
893+
class Foo:
894+
def __init(self):
895+
if flag:
896+
self.x = 1
897+
898+
# Emitting a diagnostic in a case like this is not something we support, and it's unclear
899+
# if we ever will (or want to)
900+
reveal_type(Foo().x) # revealed: Unknown | Literal[1]
839901
```
840902

841903
### Attribute access on `Any`

crates/red_knot_python_semantic/resources/mdtest/type_qualifiers/classvar.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,24 @@ c = C()
6464
c.a = 2
6565
```
6666

67+
and similarly here:
68+
69+
```py
70+
class Base:
71+
a: ClassVar[int] = 1
72+
73+
class Derived(Base):
74+
if flag():
75+
a: int
76+
77+
reveal_type(Derived.a) # revealed: int
78+
79+
d = Derived()
80+
81+
# error: [invalid-attribute-access]
82+
d.a = 2
83+
```
84+
6785
## Too many arguments
6886

6987
```py

crates/red_knot_python_semantic/src/types/builder.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@ impl<'db> UnionBuilder<'db> {
4343
}
4444
}
4545

46+
pub(crate) fn is_empty(&self) -> bool {
47+
self.elements.is_empty()
48+
}
49+
4650
/// Collapse the union to a single type: `object`.
4751
fn collapse_to_object(mut self) -> Self {
4852
self.elements.clear();

crates/red_knot_python_semantic/src/types/class.rs

Lines changed: 49 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use crate::{
66
},
77
symbol::{
88
class_symbol, known_module_symbol, symbol_from_bindings, symbol_from_declarations,
9-
LookupError, LookupResult, Symbol, SymbolAndQualifiers,
9+
Boundness, LookupError, LookupResult, Symbol, SymbolAndQualifiers,
1010
},
1111
types::{
1212
definition_expression_type, CallArguments, CallError, MetaclassCandidate, TupleType,
@@ -383,6 +383,9 @@ impl<'db> Class<'db> {
383383
///
384384
/// The attribute might also be defined in a superclass of this class.
385385
pub(super) fn instance_member(self, db: &'db dyn Db, name: &str) -> SymbolAndQualifiers<'db> {
386+
let mut union = UnionBuilder::new(db);
387+
let mut union_qualifiers = TypeQualifiers::empty();
388+
386389
for superclass in self.iter_mro(db) {
387390
match superclass {
388391
ClassBase::Dynamic(_) => {
@@ -391,16 +394,43 @@ impl<'db> Class<'db> {
391394
);
392395
}
393396
ClassBase::Class(class) => {
394-
if let member @ SymbolAndQualifiers(Symbol::Type(_, _), _) =
397+
if let member @ SymbolAndQualifiers(Symbol::Type(ty, boundness), qualifiers) =
395398
class.own_instance_member(db, name)
396399
{
397-
return member;
400+
// TODO: We could raise a diagnostic here if there are conflicting type qualifiers
401+
union_qualifiers = union_qualifiers.union(qualifiers);
402+
403+
if boundness == Boundness::Bound {
404+
if union.is_empty() {
405+
// Short-circuit, no need to allocate inside the union builder
406+
return member;
407+
}
408+
409+
return SymbolAndQualifiers(
410+
Symbol::bound(union.add(ty).build()),
411+
union_qualifiers,
412+
);
413+
}
414+
415+
// If we see a possibly-unbound symbol, we need to keep looking
416+
// higher up in the MRO.
417+
union = union.add(ty);
398418
}
399419
}
400420
}
401421
}
402422

403-
SymbolAndQualifiers(Symbol::Unbound, TypeQualifiers::empty())
423+
if union.is_empty() {
424+
SymbolAndQualifiers(Symbol::Unbound, TypeQualifiers::empty())
425+
} else {
426+
// If we have reached this point, we know that we have only seen possibly-unbound symbols.
427+
// This means that the final result is still possibly-unbound.
428+
429+
SymbolAndQualifiers(
430+
Symbol::Type(union.build(), Boundness::PossiblyUnbound),
431+
union_qualifiers,
432+
)
433+
}
404434
}
405435

406436
/// Tries to find declarations/bindings of an instance attribute named `name` that are only
@@ -409,16 +439,18 @@ impl<'db> Class<'db> {
409439
db: &'db dyn Db,
410440
class_body_scope: ScopeId<'db>,
411441
name: &str,
412-
inferred_type_from_class_body: Option<Type<'db>>,
442+
inferred_from_class_body: &Symbol<'db>,
413443
) -> Symbol<'db> {
414444
// If we do not see any declarations of an attribute, neither in the class body nor in
415445
// any method, we build a union of `Unknown` with the inferred types of all bindings of
416446
// that attribute. We include `Unknown` in that union to account for the fact that the
417447
// attribute might be externally modified.
418448
let mut union_of_inferred_types = UnionBuilder::new(db).add(Type::unknown());
449+
let mut union_boundness = Boundness::Bound;
419450

420-
if let Some(ty) = inferred_type_from_class_body {
421-
union_of_inferred_types = union_of_inferred_types.add(ty);
451+
if let Symbol::Type(ty, boundness) = inferred_from_class_body {
452+
union_of_inferred_types = union_of_inferred_types.add(*ty);
453+
union_boundness = *boundness;
422454
}
423455

424456
let attribute_assignments = attribute_assignments(db, class_body_scope);
@@ -427,10 +459,10 @@ impl<'db> Class<'db> {
427459
.as_deref()
428460
.and_then(|assignments| assignments.get(name))
429461
else {
430-
if inferred_type_from_class_body.is_some() {
431-
return Symbol::bound(union_of_inferred_types.build());
462+
if inferred_from_class_body.is_unbound() {
463+
return Symbol::Unbound;
432464
}
433-
return Symbol::Unbound;
465+
return Symbol::Type(union_of_inferred_types.build(), union_boundness);
434466
};
435467

436468
for attribute_assignment in attribute_assignments {
@@ -484,7 +516,7 @@ impl<'db> Class<'db> {
484516
}
485517
}
486518

487-
Symbol::bound(union_of_inferred_types.build())
519+
Symbol::Type(union_of_inferred_types.build(), union_boundness)
488520
}
489521

490522
/// A helper function for `instance_member` that looks up the `name` attribute only on
@@ -493,7 +525,6 @@ impl<'db> Class<'db> {
493525
// TODO: There are many things that are not yet implemented here:
494526
// - `typing.Final`
495527
// - Proper diagnostics
496-
// - Handling of possibly-undeclared/possibly-unbound attributes
497528

498529
let body_scope = self.body_scope(db);
499530
let table = symbol_table(db, body_scope);
@@ -504,7 +535,7 @@ impl<'db> Class<'db> {
504535
let declarations = use_def.public_declarations(symbol_id);
505536

506537
match symbol_from_declarations(db, declarations) {
507-
Ok(SymbolAndQualifiers(Symbol::Type(declared_ty, _), qualifiers)) => {
538+
Ok(SymbolAndQualifiers(declared @ Symbol::Type(declared_ty, _), qualifiers)) => {
508539
// The attribute is declared in the class body.
509540

510541
if let Some(function) = declared_ty.into_function_literal() {
@@ -514,7 +545,7 @@ impl<'db> Class<'db> {
514545
if function.has_known_class_decorator(db, KnownClass::Classmethod)
515546
&& function.decorators(db).len() == 1
516547
{
517-
SymbolAndQualifiers(Symbol::bound(declared_ty), qualifiers)
548+
SymbolAndQualifiers(declared, qualifiers)
518549
} else if function.has_known_class_decorator(db, KnownClass::Property) {
519550
SymbolAndQualifiers::todo("@property")
520551
} else if function.has_known_function_decorator(db, KnownFunction::Overload)
@@ -523,10 +554,10 @@ impl<'db> Class<'db> {
523554
} else if !function.decorators(db).is_empty() {
524555
SymbolAndQualifiers::todo("decorated method")
525556
} else {
526-
SymbolAndQualifiers(Symbol::bound(declared_ty), qualifiers)
557+
SymbolAndQualifiers(declared, qualifiers)
527558
}
528559
} else {
529-
SymbolAndQualifiers(Symbol::bound(declared_ty), qualifiers)
560+
SymbolAndQualifiers(declared, qualifiers)
530561
}
531562
}
532563
Ok(SymbolAndQualifiers(Symbol::Unbound, _)) => {
@@ -535,9 +566,8 @@ impl<'db> Class<'db> {
535566

536567
let bindings = use_def.public_bindings(symbol_id);
537568
let inferred = symbol_from_bindings(db, bindings);
538-
let inferred_ty = inferred.ignore_possibly_unbound();
539569

540-
Self::implicit_instance_attribute(db, body_scope, name, inferred_ty).into()
570+
Self::implicit_instance_attribute(db, body_scope, name, &inferred).into()
541571
}
542572
Err((declared_ty, _conflicting_declarations)) => {
543573
// There are conflicting declarations for this attribute in the class body.
@@ -551,7 +581,7 @@ impl<'db> Class<'db> {
551581
// This attribute is neither declared nor bound in the class body.
552582
// It could still be implicitly defined in a method.
553583

554-
Self::implicit_instance_attribute(db, body_scope, name, None).into()
584+
Self::implicit_instance_attribute(db, body_scope, name, &Symbol::Unbound).into()
555585
}
556586
}
557587

0 commit comments

Comments
 (0)