mirror of
https://github.com/astral-sh/ruff.git
synced 2025-08-04 18:58:04 +00:00
[red-knot] add fixpoint iteration for Type::member_lookup_with_policy (#17464)
## Summary Member lookup can be cyclic, with type inference of implicit members. A sample case is shown in the added mdtest. There's no clear way to handle such cases other than to fixpoint-iterate the cycle. Fixes #17457. ## Test Plan Added test.
This commit is contained in:
parent
08221454f6
commit
27a315b740
2 changed files with 97 additions and 5 deletions
|
@ -1820,6 +1820,78 @@ def f(never: Never):
|
|||
never.another_attribute = never
|
||||
```
|
||||
|
||||
### Cyclic implicit attributes
|
||||
|
||||
Inferring types for undeclared implicit attributes can be cyclic:
|
||||
|
||||
```py
|
||||
class C:
|
||||
def __init__(self):
|
||||
self.x = 1
|
||||
|
||||
def copy(self, other: "C"):
|
||||
self.x = other.x
|
||||
|
||||
reveal_type(C().x) # revealed: Unknown | Literal[1]
|
||||
```
|
||||
|
||||
If the only assignment to a name is cyclic, we just infer `Unknown` for that attribute:
|
||||
|
||||
```py
|
||||
class D:
|
||||
def copy(self, other: "D"):
|
||||
self.x = other.x
|
||||
|
||||
reveal_type(D().x) # revealed: Unknown
|
||||
```
|
||||
|
||||
If there is an annotation for a name, we don't try to infer any type from the RHS of assignments to
|
||||
that name, so these cases don't trigger any cycle:
|
||||
|
||||
```py
|
||||
class E:
|
||||
def __init__(self):
|
||||
self.x: int = 1
|
||||
|
||||
def copy(self, other: "E"):
|
||||
self.x = other.x
|
||||
|
||||
reveal_type(E().x) # revealed: int
|
||||
|
||||
class F:
|
||||
def __init__(self):
|
||||
self.x = 1
|
||||
|
||||
def copy(self, other: "F"):
|
||||
self.x: int = other.x
|
||||
|
||||
reveal_type(F().x) # revealed: int
|
||||
|
||||
class G:
|
||||
def copy(self, other: "G"):
|
||||
self.x: int = other.x
|
||||
|
||||
reveal_type(G().x) # revealed: int
|
||||
```
|
||||
|
||||
We can even handle cycles involving multiple classes:
|
||||
|
||||
```py
|
||||
class A:
|
||||
def __init__(self):
|
||||
self.x = 1
|
||||
|
||||
def copy(self, other: "B"):
|
||||
self.x = other.x
|
||||
|
||||
class B:
|
||||
def copy(self, other: "A"):
|
||||
self.x = other.x
|
||||
|
||||
reveal_type(B().x) # revealed: Unknown | Literal[1]
|
||||
reveal_type(A().x) # revealed: Unknown | Literal[1]
|
||||
```
|
||||
|
||||
### Builtin types attributes
|
||||
|
||||
This test can probably be removed eventually, but we currently include it because we do not yet
|
||||
|
|
|
@ -147,6 +147,12 @@ enum AttributeKind {
|
|||
NormalOrNonDataDescriptor,
|
||||
}
|
||||
|
||||
impl AttributeKind {
|
||||
const fn is_data(self) -> bool {
|
||||
matches!(self, Self::DataDescriptor)
|
||||
}
|
||||
}
|
||||
|
||||
/// This enum is used to control the behavior of the descriptor protocol implementation.
|
||||
/// When invoked on a class object, the fallback type (a class attribute) can shadow a
|
||||
/// non-data descriptor of the meta-type (the class's metaclass). However, this is not
|
||||
|
@ -217,10 +223,24 @@ impl Default for MemberLookupPolicy {
|
|||
}
|
||||
}
|
||||
|
||||
impl AttributeKind {
|
||||
const fn is_data(self) -> bool {
|
||||
matches!(self, Self::DataDescriptor)
|
||||
}
|
||||
fn member_lookup_cycle_recover<'db>(
|
||||
_db: &'db dyn Db,
|
||||
_value: &SymbolAndQualifiers<'db>,
|
||||
_count: u32,
|
||||
_self: Type<'db>,
|
||||
_name: Name,
|
||||
_policy: MemberLookupPolicy,
|
||||
) -> salsa::CycleRecoveryAction<SymbolAndQualifiers<'db>> {
|
||||
salsa::CycleRecoveryAction::Iterate
|
||||
}
|
||||
|
||||
fn member_lookup_cycle_initial<'db>(
|
||||
_db: &'db dyn Db,
|
||||
_self: Type<'db>,
|
||||
_name: Name,
|
||||
_policy: MemberLookupPolicy,
|
||||
) -> SymbolAndQualifiers<'db> {
|
||||
Symbol::bound(Type::Never).into()
|
||||
}
|
||||
|
||||
/// Meta data for `Type::Todo`, which represents a known limitation in red-knot.
|
||||
|
@ -2631,7 +2651,7 @@ impl<'db> Type<'db> {
|
|||
|
||||
/// Similar to [`Type::member`], but allows the caller to specify what policy should be used
|
||||
/// when looking up attributes. See [`MemberLookupPolicy`] for more information.
|
||||
#[salsa::tracked]
|
||||
#[salsa::tracked(cycle_fn=member_lookup_cycle_recover, cycle_initial=member_lookup_cycle_initial)]
|
||||
fn member_lookup_with_policy(
|
||||
self,
|
||||
db: &'db dyn Db,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue