mirror of https://github.com/astral-sh/ruff
[red-knot] Handle possibly-unbound instance members (#16363)
## Summary Adds support for possibly-unbound/undeclared instance members. ## Test Plan New MD tests.
This commit is contained in:
parent
fa76f6cbb2
commit
f88328eedd
|
|
@ -783,6 +783,9 @@ def _(flag1: bool, flag2: bool):
|
||||||
|
|
||||||
# error: [possibly-unbound-attribute] "Attribute `x` on type `Literal[C1, C2, C3]` is possibly unbound"
|
# error: [possibly-unbound-attribute] "Attribute `x` on type `Literal[C1, C2, C3]` is possibly unbound"
|
||||||
reveal_type(C.x) # revealed: Unknown | Literal[1, 3]
|
reveal_type(C.x) # revealed: Unknown | Literal[1, 3]
|
||||||
|
|
||||||
|
# error: [possibly-unbound-attribute] "Attribute `x` on type `C1 | C2 | C3` is possibly unbound"
|
||||||
|
reveal_type(C().x) # revealed: Unknown | Literal[1, 3]
|
||||||
```
|
```
|
||||||
|
|
||||||
### Possibly-unbound within a class
|
### Possibly-unbound within a class
|
||||||
|
|
@ -806,6 +809,28 @@ def _(flag: bool, flag1: bool, flag2: bool):
|
||||||
|
|
||||||
# error: [possibly-unbound-attribute] "Attribute `x` on type `Literal[C1, C2, C3]` is possibly unbound"
|
# error: [possibly-unbound-attribute] "Attribute `x` on type `Literal[C1, C2, C3]` is possibly unbound"
|
||||||
reveal_type(C.x) # revealed: Unknown | Literal[1, 2, 3]
|
reveal_type(C.x) # revealed: Unknown | Literal[1, 2, 3]
|
||||||
|
|
||||||
|
# Note: we might want to consider ignoring possibly-unbound diagnostics for instance attributes eventually,
|
||||||
|
# see the "Possibly unbound/undeclared instance attribute" section below.
|
||||||
|
# error: [possibly-unbound-attribute] "Attribute `x` on type `C1 | C2 | C3` is possibly unbound"
|
||||||
|
reveal_type(C().x) # revealed: Unknown | Literal[1, 2, 3]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Possibly-unbound within gradual types
|
||||||
|
|
||||||
|
```py
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
def _(flag: bool):
|
||||||
|
class Base:
|
||||||
|
x: Any
|
||||||
|
|
||||||
|
class Derived(Base):
|
||||||
|
if flag:
|
||||||
|
# Redeclaring `x` with a more static type is okay in terms of LSP.
|
||||||
|
x: int
|
||||||
|
|
||||||
|
reveal_type(Derived().x) # revealed: int | Any
|
||||||
```
|
```
|
||||||
|
|
||||||
### Attribute possibly unbound on a subclass but not on a superclass
|
### Attribute possibly unbound on a subclass but not on a superclass
|
||||||
|
|
@ -820,6 +845,8 @@ def _(flag: bool):
|
||||||
x = 2
|
x = 2
|
||||||
|
|
||||||
reveal_type(Bar.x) # revealed: Unknown | Literal[2, 1]
|
reveal_type(Bar.x) # revealed: Unknown | Literal[2, 1]
|
||||||
|
|
||||||
|
reveal_type(Bar().x) # revealed: Unknown | Literal[2, 1]
|
||||||
```
|
```
|
||||||
|
|
||||||
### Attribute possibly unbound on a subclass and on a superclass
|
### Attribute possibly unbound on a subclass and on a superclass
|
||||||
|
|
@ -836,6 +863,41 @@ def _(flag: bool):
|
||||||
|
|
||||||
# error: [possibly-unbound-attribute]
|
# error: [possibly-unbound-attribute]
|
||||||
reveal_type(Bar.x) # revealed: Unknown | Literal[2, 1]
|
reveal_type(Bar.x) # revealed: Unknown | Literal[2, 1]
|
||||||
|
|
||||||
|
# error: [possibly-unbound-attribute]
|
||||||
|
reveal_type(Bar().x) # revealed: Unknown | Literal[2, 1]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Possibly unbound/undeclared instance attribute
|
||||||
|
|
||||||
|
#### Possibly unbound and undeclared
|
||||||
|
|
||||||
|
```py
|
||||||
|
def _(flag: bool):
|
||||||
|
class Foo:
|
||||||
|
if flag:
|
||||||
|
x: int
|
||||||
|
|
||||||
|
def __init(self):
|
||||||
|
if flag:
|
||||||
|
self.x = 1
|
||||||
|
|
||||||
|
# error: [possibly-unbound-attribute]
|
||||||
|
reveal_type(Foo().x) # revealed: int
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Possibly unbound
|
||||||
|
|
||||||
|
```py
|
||||||
|
def _(flag: bool):
|
||||||
|
class Foo:
|
||||||
|
def __init(self):
|
||||||
|
if flag:
|
||||||
|
self.x = 1
|
||||||
|
|
||||||
|
# Emitting a diagnostic in a case like this is not something we support, and it's unclear
|
||||||
|
# if we ever will (or want to)
|
||||||
|
reveal_type(Foo().x) # revealed: Unknown | Literal[1]
|
||||||
```
|
```
|
||||||
|
|
||||||
### Attribute access on `Any`
|
### Attribute access on `Any`
|
||||||
|
|
|
||||||
|
|
@ -64,6 +64,24 @@ c = C()
|
||||||
c.a = 2
|
c.a = 2
|
||||||
```
|
```
|
||||||
|
|
||||||
|
and similarly here:
|
||||||
|
|
||||||
|
```py
|
||||||
|
class Base:
|
||||||
|
a: ClassVar[int] = 1
|
||||||
|
|
||||||
|
class Derived(Base):
|
||||||
|
if flag():
|
||||||
|
a: int
|
||||||
|
|
||||||
|
reveal_type(Derived.a) # revealed: int
|
||||||
|
|
||||||
|
d = Derived()
|
||||||
|
|
||||||
|
# error: [invalid-attribute-access]
|
||||||
|
d.a = 2
|
||||||
|
```
|
||||||
|
|
||||||
## Too many arguments
|
## Too many arguments
|
||||||
|
|
||||||
```py
|
```py
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,10 @@ impl<'db> UnionBuilder<'db> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn is_empty(&self) -> bool {
|
||||||
|
self.elements.is_empty()
|
||||||
|
}
|
||||||
|
|
||||||
/// Collapse the union to a single type: `object`.
|
/// Collapse the union to a single type: `object`.
|
||||||
fn collapse_to_object(mut self) -> Self {
|
fn collapse_to_object(mut self) -> Self {
|
||||||
self.elements.clear();
|
self.elements.clear();
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ use crate::{
|
||||||
},
|
},
|
||||||
symbol::{
|
symbol::{
|
||||||
class_symbol, known_module_symbol, symbol_from_bindings, symbol_from_declarations,
|
class_symbol, known_module_symbol, symbol_from_bindings, symbol_from_declarations,
|
||||||
LookupError, LookupResult, Symbol, SymbolAndQualifiers,
|
Boundness, LookupError, LookupResult, Symbol, SymbolAndQualifiers,
|
||||||
},
|
},
|
||||||
types::{
|
types::{
|
||||||
definition_expression_type, CallArguments, CallError, MetaclassCandidate, TupleType,
|
definition_expression_type, CallArguments, CallError, MetaclassCandidate, TupleType,
|
||||||
|
|
@ -383,6 +383,9 @@ impl<'db> Class<'db> {
|
||||||
///
|
///
|
||||||
/// The attribute might also be defined in a superclass of this class.
|
/// The attribute might also be defined in a superclass of this class.
|
||||||
pub(super) fn instance_member(self, db: &'db dyn Db, name: &str) -> SymbolAndQualifiers<'db> {
|
pub(super) fn instance_member(self, db: &'db dyn Db, name: &str) -> SymbolAndQualifiers<'db> {
|
||||||
|
let mut union = UnionBuilder::new(db);
|
||||||
|
let mut union_qualifiers = TypeQualifiers::empty();
|
||||||
|
|
||||||
for superclass in self.iter_mro(db) {
|
for superclass in self.iter_mro(db) {
|
||||||
match superclass {
|
match superclass {
|
||||||
ClassBase::Dynamic(_) => {
|
ClassBase::Dynamic(_) => {
|
||||||
|
|
@ -391,16 +394,43 @@ impl<'db> Class<'db> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
ClassBase::Class(class) => {
|
ClassBase::Class(class) => {
|
||||||
if let member @ SymbolAndQualifiers(Symbol::Type(_, _), _) =
|
if let member @ SymbolAndQualifiers(Symbol::Type(ty, boundness), qualifiers) =
|
||||||
class.own_instance_member(db, name)
|
class.own_instance_member(db, name)
|
||||||
{
|
{
|
||||||
|
// TODO: We could raise a diagnostic here if there are conflicting type qualifiers
|
||||||
|
union_qualifiers = union_qualifiers.union(qualifiers);
|
||||||
|
|
||||||
|
if boundness == Boundness::Bound {
|
||||||
|
if union.is_empty() {
|
||||||
|
// Short-circuit, no need to allocate inside the union builder
|
||||||
return member;
|
return member;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return SymbolAndQualifiers(
|
||||||
|
Symbol::bound(union.add(ty).build()),
|
||||||
|
union_qualifiers,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we see a possibly-unbound symbol, we need to keep looking
|
||||||
|
// higher up in the MRO.
|
||||||
|
union = union.add(ty);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if union.is_empty() {
|
||||||
SymbolAndQualifiers(Symbol::Unbound, TypeQualifiers::empty())
|
SymbolAndQualifiers(Symbol::Unbound, TypeQualifiers::empty())
|
||||||
|
} else {
|
||||||
|
// If we have reached this point, we know that we have only seen possibly-unbound symbols.
|
||||||
|
// This means that the final result is still possibly-unbound.
|
||||||
|
|
||||||
|
SymbolAndQualifiers(
|
||||||
|
Symbol::Type(union.build(), Boundness::PossiblyUnbound),
|
||||||
|
union_qualifiers,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Tries to find declarations/bindings of an instance attribute named `name` that are only
|
/// Tries to find declarations/bindings of an instance attribute named `name` that are only
|
||||||
|
|
@ -409,16 +439,18 @@ impl<'db> Class<'db> {
|
||||||
db: &'db dyn Db,
|
db: &'db dyn Db,
|
||||||
class_body_scope: ScopeId<'db>,
|
class_body_scope: ScopeId<'db>,
|
||||||
name: &str,
|
name: &str,
|
||||||
inferred_type_from_class_body: Option<Type<'db>>,
|
inferred_from_class_body: &Symbol<'db>,
|
||||||
) -> Symbol<'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 mut union_boundness = Boundness::Bound;
|
||||||
|
|
||||||
if let Some(ty) = inferred_type_from_class_body {
|
if let Symbol::Type(ty, boundness) = inferred_from_class_body {
|
||||||
union_of_inferred_types = union_of_inferred_types.add(ty);
|
union_of_inferred_types = union_of_inferred_types.add(*ty);
|
||||||
|
union_boundness = *boundness;
|
||||||
}
|
}
|
||||||
|
|
||||||
let attribute_assignments = attribute_assignments(db, class_body_scope);
|
let attribute_assignments = attribute_assignments(db, class_body_scope);
|
||||||
|
|
@ -427,10 +459,10 @@ impl<'db> Class<'db> {
|
||||||
.as_deref()
|
.as_deref()
|
||||||
.and_then(|assignments| assignments.get(name))
|
.and_then(|assignments| assignments.get(name))
|
||||||
else {
|
else {
|
||||||
if inferred_type_from_class_body.is_some() {
|
if inferred_from_class_body.is_unbound() {
|
||||||
return Symbol::bound(union_of_inferred_types.build());
|
|
||||||
}
|
|
||||||
return Symbol::Unbound;
|
return Symbol::Unbound;
|
||||||
|
}
|
||||||
|
return Symbol::Type(union_of_inferred_types.build(), union_boundness);
|
||||||
};
|
};
|
||||||
|
|
||||||
for attribute_assignment in attribute_assignments {
|
for attribute_assignment in attribute_assignments {
|
||||||
|
|
@ -484,7 +516,7 @@ impl<'db> Class<'db> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Symbol::bound(union_of_inferred_types.build())
|
Symbol::Type(union_of_inferred_types.build(), union_boundness)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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
|
||||||
|
|
@ -493,7 +525,6 @@ impl<'db> Class<'db> {
|
||||||
// TODO: There are many things that are not yet implemented here:
|
// TODO: There are many things that are not yet implemented here:
|
||||||
// - `typing.Final`
|
// - `typing.Final`
|
||||||
// - Proper diagnostics
|
// - Proper diagnostics
|
||||||
// - Handling of possibly-undeclared/possibly-unbound attributes
|
|
||||||
|
|
||||||
let body_scope = self.body_scope(db);
|
let body_scope = self.body_scope(db);
|
||||||
let table = symbol_table(db, body_scope);
|
let table = symbol_table(db, body_scope);
|
||||||
|
|
@ -504,7 +535,7 @@ impl<'db> Class<'db> {
|
||||||
let declarations = use_def.public_declarations(symbol_id);
|
let declarations = use_def.public_declarations(symbol_id);
|
||||||
|
|
||||||
match symbol_from_declarations(db, declarations) {
|
match symbol_from_declarations(db, declarations) {
|
||||||
Ok(SymbolAndQualifiers(Symbol::Type(declared_ty, _), qualifiers)) => {
|
Ok(SymbolAndQualifiers(declared @ Symbol::Type(declared_ty, _), qualifiers)) => {
|
||||||
// The attribute is declared in the class body.
|
// The attribute is declared in the class body.
|
||||||
|
|
||||||
if let Some(function) = declared_ty.into_function_literal() {
|
if let Some(function) = declared_ty.into_function_literal() {
|
||||||
|
|
@ -514,7 +545,7 @@ impl<'db> Class<'db> {
|
||||||
if function.has_known_class_decorator(db, KnownClass::Classmethod)
|
if function.has_known_class_decorator(db, KnownClass::Classmethod)
|
||||||
&& function.decorators(db).len() == 1
|
&& function.decorators(db).len() == 1
|
||||||
{
|
{
|
||||||
SymbolAndQualifiers(Symbol::bound(declared_ty), qualifiers)
|
SymbolAndQualifiers(declared, qualifiers)
|
||||||
} else if function.has_known_class_decorator(db, KnownClass::Property) {
|
} else if function.has_known_class_decorator(db, KnownClass::Property) {
|
||||||
SymbolAndQualifiers::todo("@property")
|
SymbolAndQualifiers::todo("@property")
|
||||||
} else if function.has_known_function_decorator(db, KnownFunction::Overload)
|
} else if function.has_known_function_decorator(db, KnownFunction::Overload)
|
||||||
|
|
@ -523,10 +554,10 @@ impl<'db> Class<'db> {
|
||||||
} else if !function.decorators(db).is_empty() {
|
} else if !function.decorators(db).is_empty() {
|
||||||
SymbolAndQualifiers::todo("decorated method")
|
SymbolAndQualifiers::todo("decorated method")
|
||||||
} else {
|
} else {
|
||||||
SymbolAndQualifiers(Symbol::bound(declared_ty), qualifiers)
|
SymbolAndQualifiers(declared, qualifiers)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
SymbolAndQualifiers(Symbol::bound(declared_ty), qualifiers)
|
SymbolAndQualifiers(declared, qualifiers)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(SymbolAndQualifiers(Symbol::Unbound, _)) => {
|
Ok(SymbolAndQualifiers(Symbol::Unbound, _)) => {
|
||||||
|
|
@ -535,9 +566,8 @@ impl<'db> Class<'db> {
|
||||||
|
|
||||||
let bindings = use_def.public_bindings(symbol_id);
|
let bindings = use_def.public_bindings(symbol_id);
|
||||||
let inferred = symbol_from_bindings(db, bindings);
|
let inferred = symbol_from_bindings(db, bindings);
|
||||||
let inferred_ty = inferred.ignore_possibly_unbound();
|
|
||||||
|
|
||||||
Self::implicit_instance_attribute(db, body_scope, name, inferred_ty).into()
|
Self::implicit_instance_attribute(db, body_scope, name, &inferred).into()
|
||||||
}
|
}
|
||||||
Err((declared_ty, _conflicting_declarations)) => {
|
Err((declared_ty, _conflicting_declarations)) => {
|
||||||
// There are conflicting declarations for this attribute in the class body.
|
// There are conflicting declarations for this attribute in the class body.
|
||||||
|
|
@ -551,7 +581,7 @@ impl<'db> Class<'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, None).into()
|
Self::implicit_instance_attribute(db, body_scope, name, &Symbol::Unbound).into()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue