[ty] Improve disjointness inference for NominalInstanceTypes and SubclassOfTypes (#18864)
Co-authored-by: Carl Meyer <carl@astral.sh>
This commit is contained in:
@@ -192,16 +192,18 @@ def _(
|
||||
from typing import Callable, Union
|
||||
from ty_extensions import Intersection, Not
|
||||
|
||||
class Foo: ...
|
||||
|
||||
def _(
|
||||
c: Intersection[Callable[[Union[int, str]], int], int],
|
||||
d: Intersection[int, Callable[[Union[int, str]], int]],
|
||||
e: Intersection[int, Callable[[Union[int, str]], int], str],
|
||||
f: Intersection[Not[Callable[[int, str], Intersection[int, str]]]],
|
||||
e: Intersection[int, Callable[[Union[int, str]], int], Foo],
|
||||
f: Intersection[Not[Callable[[int, str], Intersection[int, Foo]]]],
|
||||
):
|
||||
reveal_type(c) # revealed: ((int | str, /) -> int) & int
|
||||
reveal_type(d) # revealed: int & ((int | str, /) -> int)
|
||||
reveal_type(e) # revealed: int & ((int | str, /) -> int) & str
|
||||
reveal_type(f) # revealed: ~((int, str, /) -> int & str)
|
||||
reveal_type(e) # revealed: int & ((int | str, /) -> int) & Foo
|
||||
reveal_type(f) # revealed: ~((int, str, /) -> int & Foo)
|
||||
```
|
||||
|
||||
## Nested
|
||||
|
||||
@@ -88,3 +88,26 @@ def assigns_complex(x: complex):
|
||||
def f(x: complex):
|
||||
reveal_type(x) # revealed: int | float | complex
|
||||
```
|
||||
|
||||
## Narrowing
|
||||
|
||||
`int`, `float` and `complex` are all disjoint, which means that the union `int | float` can easily
|
||||
be narrowed to `int` or `float`:
|
||||
|
||||
```py
|
||||
from typing_extensions import assert_type
|
||||
from ty_extensions import JustFloat
|
||||
|
||||
def f(x: complex):
|
||||
reveal_type(x) # revealed: int | float | complex
|
||||
|
||||
if isinstance(x, int):
|
||||
reveal_type(x) # revealed: int
|
||||
elif isinstance(x, float):
|
||||
reveal_type(x) # revealed: float
|
||||
else:
|
||||
reveal_type(x) # revealed: complex
|
||||
|
||||
assert isinstance(x, float)
|
||||
assert_type(x, JustFloat)
|
||||
```
|
||||
|
||||
@@ -271,7 +271,9 @@ def _(target: int | None | float):
|
||||
|
||||
reveal_type(y) # revealed: Literal[1, 2]
|
||||
|
||||
def _(target: None | str):
|
||||
class Foo: ...
|
||||
|
||||
def _(target: None | Foo):
|
||||
y = 1
|
||||
|
||||
match target:
|
||||
|
||||
@@ -653,7 +653,7 @@ from ty_extensions import Not
|
||||
|
||||
def remove_constraint[T: (int, str, bool)](t: T) -> None:
|
||||
def _(x: Intersection[T, Not[int]]) -> None:
|
||||
reveal_type(x) # revealed: str & ~int
|
||||
reveal_type(x) # revealed: str
|
||||
|
||||
def _(x: Intersection[T, Not[str]]) -> None:
|
||||
# With OneOf this would be OneOf[int, bool]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# `__slots__`
|
||||
# Tests for ty's `instance-layout-conflict` error code
|
||||
|
||||
## Not specified and empty
|
||||
## `__slots__`: not specified or empty
|
||||
|
||||
```py
|
||||
class A: ...
|
||||
@@ -17,7 +17,9 @@ class BC(B, C): ... # fine
|
||||
class ABC(A, B, C): ... # fine
|
||||
```
|
||||
|
||||
## Incompatible tuples
|
||||
## `__slots__`: incompatible tuples
|
||||
|
||||
<!-- snapshot-diagnostics -->
|
||||
|
||||
```py
|
||||
class A:
|
||||
@@ -26,13 +28,13 @@ class A:
|
||||
class B:
|
||||
__slots__ = ("c", "d")
|
||||
|
||||
class C(
|
||||
A, # error: [incompatible-slots]
|
||||
B, # error: [incompatible-slots]
|
||||
class C( # error: [instance-layout-conflict]
|
||||
A,
|
||||
B,
|
||||
): ...
|
||||
```
|
||||
|
||||
## Same value
|
||||
## `__slots__` are the same value
|
||||
|
||||
```py
|
||||
class A:
|
||||
@@ -41,13 +43,13 @@ class A:
|
||||
class B:
|
||||
__slots__ = ("a", "b")
|
||||
|
||||
class C(
|
||||
A, # error: [incompatible-slots]
|
||||
B, # error: [incompatible-slots]
|
||||
class C( # error: [instance-layout-conflict]
|
||||
A,
|
||||
B,
|
||||
): ...
|
||||
```
|
||||
|
||||
## Strings
|
||||
## `__slots__` is a string
|
||||
|
||||
```py
|
||||
class A:
|
||||
@@ -56,13 +58,13 @@ class A:
|
||||
class B:
|
||||
__slots__ = ("abc",)
|
||||
|
||||
class AB(
|
||||
A, # error: [incompatible-slots]
|
||||
B, # error: [incompatible-slots]
|
||||
class AB( # error: [instance-layout-conflict]
|
||||
A,
|
||||
B,
|
||||
): ...
|
||||
```
|
||||
|
||||
## Invalid
|
||||
## Invalid `__slots__` definitions
|
||||
|
||||
TODO: Emit diagnostics
|
||||
|
||||
@@ -83,7 +85,7 @@ class NonIdentifier3:
|
||||
__slots__ = (e for e in ("lorem", "42"))
|
||||
```
|
||||
|
||||
## Inheritance
|
||||
## Inherited `__slots__`
|
||||
|
||||
```py
|
||||
class A:
|
||||
@@ -95,13 +97,13 @@ class C:
|
||||
__slots__ = ("c", "d")
|
||||
|
||||
class D(C): ...
|
||||
class E(
|
||||
B, # error: [incompatible-slots]
|
||||
D, # error: [incompatible-slots]
|
||||
class E( # error: [instance-layout-conflict]
|
||||
B,
|
||||
D,
|
||||
): ...
|
||||
```
|
||||
|
||||
## Single solid base
|
||||
## A single "solid base"
|
||||
|
||||
```py
|
||||
class A:
|
||||
@@ -113,7 +115,7 @@ class D(B, A): ... # fine
|
||||
class E(B, C, A): ... # fine
|
||||
```
|
||||
|
||||
## Post-hoc modifications
|
||||
## Post-hoc modifications to `__slots__`
|
||||
|
||||
```py
|
||||
class A:
|
||||
@@ -125,15 +127,105 @@ reveal_type(A.__slots__) # revealed: tuple[Literal["a"], Literal["b"]]
|
||||
class B:
|
||||
__slots__ = ("c", "d")
|
||||
|
||||
class C(
|
||||
A, # error: [incompatible-slots]
|
||||
B, # error: [incompatible-slots]
|
||||
class C( # error: [instance-layout-conflict]
|
||||
A,
|
||||
B,
|
||||
): ...
|
||||
```
|
||||
|
||||
## Explicitly annotated `__slots__`
|
||||
|
||||
We do not emit false positives on classes with empty `__slots__` definitions, even if the
|
||||
`__slots__` definitions are annotated with variadic tuples:
|
||||
|
||||
```py
|
||||
class Foo:
|
||||
__slots__: tuple[str, ...] = ()
|
||||
|
||||
class Bar:
|
||||
__slots__: tuple[str, ...] = ()
|
||||
|
||||
class Baz(Foo, Bar): ... # fine
|
||||
```
|
||||
|
||||
## Built-ins with implicit layouts
|
||||
|
||||
<!-- snapshot-diagnostics -->
|
||||
|
||||
Certain classes implemented in C extensions also have an extended instance memory layout, in the
|
||||
same way as classes that define non-empty `__slots__`. (CPython internally calls all such classes
|
||||
with a unique instance memory layout "solid bases", and we also borrow this term.) There is
|
||||
currently no generalized way for ty to detect such a C-extension class, as there is currently no way
|
||||
of expressing the fact that a class is a solid base in a stub file. However, ty special-cases
|
||||
certain builtin classes in order to detect that attempting to combine them in a single MRO would
|
||||
fail:
|
||||
|
||||
```py
|
||||
# fmt: off
|
||||
|
||||
class A( # error: [instance-layout-conflict]
|
||||
int,
|
||||
str
|
||||
): ...
|
||||
|
||||
class B:
|
||||
__slots__ = ("b",)
|
||||
|
||||
class C( # error: [instance-layout-conflict]
|
||||
int,
|
||||
B,
|
||||
): ...
|
||||
class D(int): ...
|
||||
|
||||
class E( # error: [instance-layout-conflict]
|
||||
D,
|
||||
str
|
||||
): ...
|
||||
|
||||
class F(int, str, bytes, bytearray): ... # error: [instance-layout-conflict]
|
||||
|
||||
# fmt: on
|
||||
```
|
||||
|
||||
We avoid emitting an `instance-layout-conflict` diagnostic for this class definition, because
|
||||
`range` is `@final`, so we'll complain about the `class` statement anyway:
|
||||
|
||||
```py
|
||||
class Foo(range, str): ... # error: [subclass-of-final-class]
|
||||
```
|
||||
|
||||
## Multiple "solid bases" where one is a subclass of the other
|
||||
|
||||
A class is permitted to multiple-inherit from multiple solid bases if one is a subclass of the
|
||||
other:
|
||||
|
||||
```py
|
||||
class A:
|
||||
__slots__ = ("a",)
|
||||
|
||||
class B(A):
|
||||
__slots__ = ("b",)
|
||||
|
||||
class C(B, A): ... # fine
|
||||
```
|
||||
|
||||
The same principle, but a more complex example:
|
||||
|
||||
```py
|
||||
class AA:
|
||||
__slots__ = ("a",)
|
||||
|
||||
class BB(AA):
|
||||
__slots__ = ("b",)
|
||||
|
||||
class CC(BB): ...
|
||||
class DD(AA): ...
|
||||
class FF(CC, DD): ... # fine
|
||||
```
|
||||
|
||||
## False negatives
|
||||
|
||||
### Possibly unbound
|
||||
### Possibly unbound `__slots__`
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
@@ -148,7 +240,7 @@ def _(flag: bool):
|
||||
class C(A, B): ...
|
||||
```
|
||||
|
||||
### Bound but with different types
|
||||
### Bound `__slots__` but with different types
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
@@ -165,7 +257,7 @@ def _(flag: bool):
|
||||
class C(A, B): ...
|
||||
```
|
||||
|
||||
### Non-tuples
|
||||
### Non-tuple `__slots__` definitions
|
||||
|
||||
```py
|
||||
class A:
|
||||
@@ -178,13 +270,6 @@ class B:
|
||||
class C(A, B): ...
|
||||
```
|
||||
|
||||
### Built-ins with implicit layouts
|
||||
|
||||
```py
|
||||
# False negative: [incompatible-slots]
|
||||
class A(int, str): ...
|
||||
```
|
||||
|
||||
### Diagnostic if `__slots__` is externally modified
|
||||
|
||||
We special-case type inference for `__slots__` and return the pure inferred type, even if the symbol
|
||||
@@ -46,12 +46,15 @@ def _(flag1: bool, flag2: bool):
|
||||
## Assignment expressions
|
||||
|
||||
```py
|
||||
def f() -> int | str | None: ...
|
||||
class Foo: ...
|
||||
class Bar: ...
|
||||
|
||||
if isinstance(x := f(), int):
|
||||
reveal_type(x) # revealed: int
|
||||
elif isinstance(x, str):
|
||||
reveal_type(x) # revealed: str & ~int
|
||||
def f() -> Foo | Bar | None: ...
|
||||
|
||||
if isinstance(x := f(), Foo):
|
||||
reveal_type(x) # revealed: Foo
|
||||
elif isinstance(x, Bar):
|
||||
reveal_type(x) # revealed: Bar & ~Foo
|
||||
else:
|
||||
reveal_type(x) # revealed: None
|
||||
```
|
||||
|
||||
@@ -87,7 +87,7 @@ match x:
|
||||
case 6.0:
|
||||
reveal_type(x) # revealed: float
|
||||
case 1j:
|
||||
reveal_type(x) # revealed: complex & ~float
|
||||
reveal_type(x) # revealed: complex
|
||||
case b"foo":
|
||||
reveal_type(x) # revealed: Literal[b"foo"]
|
||||
|
||||
@@ -137,7 +137,7 @@ match x:
|
||||
case True | False:
|
||||
reveal_type(x) # revealed: bool
|
||||
case 3.14 | 2.718 | 1.414:
|
||||
reveal_type(x) # revealed: float & ~tuple[Unknown, ...]
|
||||
reveal_type(x) # revealed: float
|
||||
|
||||
reveal_type(x) # revealed: object
|
||||
```
|
||||
|
||||
@@ -178,25 +178,26 @@ def _(d: Any):
|
||||
from typing import Any
|
||||
from typing_extensions import TypeGuard, TypeIs
|
||||
|
||||
def guard_str(a: object) -> TypeGuard[str]:
|
||||
class Foo: ...
|
||||
class Bar: ...
|
||||
|
||||
def guard_foo(a: object) -> TypeGuard[Foo]:
|
||||
return True
|
||||
|
||||
def is_int(a: object) -> TypeIs[int]:
|
||||
def is_bar(a: object) -> TypeIs[Bar]:
|
||||
return True
|
||||
```
|
||||
|
||||
```py
|
||||
def _(a: str | int):
|
||||
if guard_str(a):
|
||||
# TODO: Should be `str`
|
||||
reveal_type(a) # revealed: str | int
|
||||
def _(a: Foo | Bar):
|
||||
if guard_foo(a):
|
||||
# TODO: Should be `Foo`
|
||||
reveal_type(a) # revealed: Foo | Bar
|
||||
else:
|
||||
reveal_type(a) # revealed: str | int
|
||||
reveal_type(a) # revealed: Foo | Bar
|
||||
|
||||
if is_int(a):
|
||||
reveal_type(a) # revealed: int
|
||||
if is_bar(a):
|
||||
reveal_type(a) # revealed: Bar
|
||||
else:
|
||||
reveal_type(a) # revealed: str & ~int
|
||||
reveal_type(a) # revealed: Foo & ~Bar
|
||||
```
|
||||
|
||||
Attribute and subscript narrowing is supported:
|
||||
@@ -209,68 +210,68 @@ T = TypeVar("T")
|
||||
class C(Generic[T]):
|
||||
v: T
|
||||
|
||||
def _(a: tuple[str, int] | tuple[int, str], c: C[Any]):
|
||||
# TODO: Should be `TypeGuard[str @ a[1]]`
|
||||
if reveal_type(guard_str(a[1])): # revealed: @Todo(`TypeGuard[]` special form)
|
||||
# TODO: Should be `tuple[int, str]`
|
||||
reveal_type(a) # revealed: tuple[str, int] | tuple[int, str]
|
||||
# TODO: Should be `str`
|
||||
reveal_type(a[1]) # revealed: int | str
|
||||
def _(a: tuple[Foo, Bar] | tuple[Bar, Foo], c: C[Any]):
|
||||
# TODO: Should be `TypeGuard[Foo @ a[1]]`
|
||||
if reveal_type(guard_foo(a[1])): # revealed: @Todo(`TypeGuard[]` special form)
|
||||
# TODO: Should be `tuple[Bar, Foo]`
|
||||
reveal_type(a) # revealed: tuple[Foo, Bar] | tuple[Bar, Foo]
|
||||
# TODO: Should be `Foo`
|
||||
reveal_type(a[1]) # revealed: Bar | Foo
|
||||
|
||||
if reveal_type(is_int(a[0])): # revealed: TypeIs[int @ a[0]]
|
||||
# TODO: Should be `tuple[int, str]`
|
||||
reveal_type(a) # revealed: tuple[str, int] | tuple[int, str]
|
||||
reveal_type(a[0]) # revealed: int
|
||||
if reveal_type(is_bar(a[0])): # revealed: TypeIs[Bar @ a[0]]
|
||||
# TODO: Should be `tuple[Bar, Bar & Foo]`
|
||||
reveal_type(a) # revealed: tuple[Foo, Bar] | tuple[Bar, Foo]
|
||||
reveal_type(a[0]) # revealed: Bar
|
||||
|
||||
# TODO: Should be `TypeGuard[str @ c.v]`
|
||||
if reveal_type(guard_str(c.v)): # revealed: @Todo(`TypeGuard[]` special form)
|
||||
# TODO: Should be `TypeGuard[Foo @ c.v]`
|
||||
if reveal_type(guard_foo(c.v)): # revealed: @Todo(`TypeGuard[]` special form)
|
||||
reveal_type(c) # revealed: C[Any]
|
||||
# TODO: Should be `str`
|
||||
# TODO: Should be `Foo`
|
||||
reveal_type(c.v) # revealed: Any
|
||||
|
||||
if reveal_type(is_int(c.v)): # revealed: TypeIs[int @ c.v]
|
||||
if reveal_type(is_bar(c.v)): # revealed: TypeIs[Bar @ c.v]
|
||||
reveal_type(c) # revealed: C[Any]
|
||||
reveal_type(c.v) # revealed: Any & int
|
||||
reveal_type(c.v) # revealed: Any & Bar
|
||||
```
|
||||
|
||||
Indirect usage is supported within the same scope:
|
||||
|
||||
```py
|
||||
def _(a: str | int):
|
||||
b = guard_str(a)
|
||||
c = is_int(a)
|
||||
def _(a: Foo | Bar):
|
||||
b = guard_foo(a)
|
||||
c = is_bar(a)
|
||||
|
||||
reveal_type(a) # revealed: str | int
|
||||
# TODO: Should be `TypeGuard[str @ a]`
|
||||
reveal_type(a) # revealed: Foo | Bar
|
||||
# TODO: Should be `TypeGuard[Foo @ a]`
|
||||
reveal_type(b) # revealed: @Todo(`TypeGuard[]` special form)
|
||||
reveal_type(c) # revealed: TypeIs[int @ a]
|
||||
reveal_type(c) # revealed: TypeIs[Bar @ a]
|
||||
|
||||
if b:
|
||||
# TODO should be `str`
|
||||
reveal_type(a) # revealed: str | int
|
||||
# TODO should be `Foo`
|
||||
reveal_type(a) # revealed: Foo | Bar
|
||||
else:
|
||||
reveal_type(a) # revealed: str | int
|
||||
reveal_type(a) # revealed: Foo | Bar
|
||||
|
||||
if c:
|
||||
# TODO should be `int`
|
||||
reveal_type(a) # revealed: str | int
|
||||
# TODO should be `Bar`
|
||||
reveal_type(a) # revealed: Foo | Bar
|
||||
else:
|
||||
# TODO should be `str & ~int`
|
||||
reveal_type(a) # revealed: str | int
|
||||
# TODO should be `Foo & ~Bar`
|
||||
reveal_type(a) # revealed: Foo | Bar
|
||||
```
|
||||
|
||||
Further writes to the narrowed place invalidate the narrowing:
|
||||
|
||||
```py
|
||||
def _(x: str | int, flag: bool) -> None:
|
||||
b = is_int(x)
|
||||
reveal_type(b) # revealed: TypeIs[int @ x]
|
||||
def _(x: Foo | Bar, flag: bool) -> None:
|
||||
b = is_bar(x)
|
||||
reveal_type(b) # revealed: TypeIs[Bar @ x]
|
||||
|
||||
if flag:
|
||||
x = ""
|
||||
x = Foo()
|
||||
|
||||
if b:
|
||||
reveal_type(x) # revealed: str | int
|
||||
reveal_type(x) # revealed: Foo | Bar
|
||||
```
|
||||
|
||||
The `TypeIs` type remains effective across generic boundaries:
|
||||
@@ -280,19 +281,19 @@ from typing_extensions import TypeVar, reveal_type
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
def f(v: object) -> TypeIs[int]:
|
||||
def f(v: object) -> TypeIs[Bar]:
|
||||
return True
|
||||
|
||||
def g(v: T) -> T:
|
||||
return v
|
||||
|
||||
def _(a: str):
|
||||
def _(a: Foo):
|
||||
# `reveal_type()` has the type `[T]() -> T`
|
||||
if reveal_type(f(a)): # revealed: TypeIs[int @ a]
|
||||
reveal_type(a) # revealed: str & int
|
||||
if reveal_type(f(a)): # revealed: TypeIs[Bar @ a]
|
||||
reveal_type(a) # revealed: Foo & Bar
|
||||
|
||||
if g(f(a)):
|
||||
reveal_type(a) # revealed: str & int
|
||||
reveal_type(a) # revealed: Foo & Bar
|
||||
```
|
||||
|
||||
## `TypeGuard` special cases
|
||||
@@ -301,28 +302,32 @@ def _(a: str):
|
||||
from typing import Any
|
||||
from typing_extensions import TypeGuard, TypeIs
|
||||
|
||||
def guard_int(a: object) -> TypeGuard[int]:
|
||||
class Foo: ...
|
||||
class Bar: ...
|
||||
class Baz(Bar): ...
|
||||
|
||||
def guard_foo(a: object) -> TypeGuard[Foo]:
|
||||
return True
|
||||
|
||||
def is_int(a: object) -> TypeIs[int]:
|
||||
def is_bar(a: object) -> TypeIs[Bar]:
|
||||
return True
|
||||
|
||||
def does_not_narrow_in_negative_case(a: str | int):
|
||||
if not guard_int(a):
|
||||
# TODO: Should be `str`
|
||||
reveal_type(a) # revealed: str | int
|
||||
def does_not_narrow_in_negative_case(a: Foo | Bar):
|
||||
if not guard_foo(a):
|
||||
# TODO: Should be `Bar`
|
||||
reveal_type(a) # revealed: Foo | Bar
|
||||
else:
|
||||
reveal_type(a) # revealed: str | int
|
||||
reveal_type(a) # revealed: Foo | Bar
|
||||
|
||||
def narrowed_type_must_be_exact(a: object, b: bool):
|
||||
if guard_int(b):
|
||||
# TODO: Should be `int`
|
||||
reveal_type(b) # revealed: bool
|
||||
def narrowed_type_must_be_exact(a: object, b: Baz):
|
||||
if guard_foo(b):
|
||||
# TODO: Should be `Foo`
|
||||
reveal_type(b) # revealed: Baz
|
||||
|
||||
if isinstance(a, bool) and is_int(a):
|
||||
reveal_type(a) # revealed: bool
|
||||
if isinstance(a, Baz) and is_bar(a):
|
||||
reveal_type(a) # revealed: Baz
|
||||
|
||||
if isinstance(a, bool) and guard_int(a):
|
||||
# TODO: Should be `int`
|
||||
reveal_type(a) # revealed: bool
|
||||
if isinstance(a, Bar) and guard_foo(a):
|
||||
# TODO: Should be `Foo`
|
||||
reveal_type(a) # revealed: Bar
|
||||
```
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
---
|
||||
source: crates/ty_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: instance_layout_conflict.md - Tests for ty's `instance-layout-conflict` error code - Built-ins with implicit layouts
|
||||
mdtest path: crates/ty_python_semantic/resources/mdtest/instance_layout_conflict.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet.py
|
||||
|
||||
```
|
||||
1 | # fmt: off
|
||||
2 |
|
||||
3 | class A( # error: [instance-layout-conflict]
|
||||
4 | int,
|
||||
5 | str
|
||||
6 | ): ...
|
||||
7 |
|
||||
8 | class B:
|
||||
9 | __slots__ = ("b",)
|
||||
10 |
|
||||
11 | class C( # error: [instance-layout-conflict]
|
||||
12 | int,
|
||||
13 | B,
|
||||
14 | ): ...
|
||||
15 | class D(int): ...
|
||||
16 |
|
||||
17 | class E( # error: [instance-layout-conflict]
|
||||
18 | D,
|
||||
19 | str
|
||||
20 | ): ...
|
||||
21 |
|
||||
22 | class F(int, str, bytes, bytearray): ... # error: [instance-layout-conflict]
|
||||
23 |
|
||||
24 | # fmt: on
|
||||
25 | class Foo(range, str): ... # error: [subclass-of-final-class]
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error[instance-layout-conflict]: Class will raise `TypeError` at runtime due to incompatible bases
|
||||
--> src/mdtest_snippet.py:3:7
|
||||
|
|
||||
1 | # fmt: off
|
||||
2 |
|
||||
3 | class A( # error: [instance-layout-conflict]
|
||||
| _______^
|
||||
4 | | int,
|
||||
5 | | str
|
||||
6 | | ): ...
|
||||
| |_^ Bases `int` and `str` cannot be combined in multiple inheritance
|
||||
7 |
|
||||
8 | class B:
|
||||
|
|
||||
info: Two classes cannot coexist in a class's MRO if their instances have incompatible memory layouts
|
||||
--> src/mdtest_snippet.py:4:5
|
||||
|
|
||||
3 | class A( # error: [instance-layout-conflict]
|
||||
4 | int,
|
||||
| --- `int` instances have a distinct memory layout because of the way `int` is implemented in a C extension
|
||||
5 | str
|
||||
| --- `str` instances have a distinct memory layout because of the way `str` is implemented in a C extension
|
||||
6 | ): ...
|
||||
|
|
||||
info: rule `instance-layout-conflict` is enabled by default
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error[instance-layout-conflict]: Class will raise `TypeError` at runtime due to incompatible bases
|
||||
--> src/mdtest_snippet.py:11:7
|
||||
|
|
||||
9 | __slots__ = ("b",)
|
||||
10 |
|
||||
11 | class C( # error: [instance-layout-conflict]
|
||||
| _______^
|
||||
12 | | int,
|
||||
13 | | B,
|
||||
14 | | ): ...
|
||||
| |_^ Bases `int` and `B` cannot be combined in multiple inheritance
|
||||
15 | class D(int): ...
|
||||
|
|
||||
info: Two classes cannot coexist in a class's MRO if their instances have incompatible memory layouts
|
||||
--> src/mdtest_snippet.py:12:5
|
||||
|
|
||||
11 | class C( # error: [instance-layout-conflict]
|
||||
12 | int,
|
||||
| --- `int` instances have a distinct memory layout because of the way `int` is implemented in a C extension
|
||||
13 | B,
|
||||
| - `B` instances have a distinct memory layout because `B` defines non-empty `__slots__`
|
||||
14 | ): ...
|
||||
15 | class D(int): ...
|
||||
|
|
||||
info: rule `instance-layout-conflict` is enabled by default
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error[instance-layout-conflict]: Class will raise `TypeError` at runtime due to incompatible bases
|
||||
--> src/mdtest_snippet.py:17:7
|
||||
|
|
||||
15 | class D(int): ...
|
||||
16 |
|
||||
17 | class E( # error: [instance-layout-conflict]
|
||||
| _______^
|
||||
18 | | D,
|
||||
19 | | str
|
||||
20 | | ): ...
|
||||
| |_^ Bases `D` and `str` cannot be combined in multiple inheritance
|
||||
21 |
|
||||
22 | class F(int, str, bytes, bytearray): ... # error: [instance-layout-conflict]
|
||||
|
|
||||
info: Two classes cannot coexist in a class's MRO if their instances have incompatible memory layouts
|
||||
--> src/mdtest_snippet.py:18:5
|
||||
|
|
||||
17 | class E( # error: [instance-layout-conflict]
|
||||
18 | D,
|
||||
| -
|
||||
| |
|
||||
| `D` instances have a distinct memory layout because `D` inherits from `int`
|
||||
| `int` instances have a distinct memory layout because of the way `int` is implemented in a C extension
|
||||
19 | str
|
||||
| --- `str` instances have a distinct memory layout because of the way `str` is implemented in a C extension
|
||||
20 | ): ...
|
||||
|
|
||||
info: rule `instance-layout-conflict` is enabled by default
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error[instance-layout-conflict]: Class will raise `TypeError` at runtime due to incompatible bases
|
||||
--> src/mdtest_snippet.py:22:7
|
||||
|
|
||||
20 | ): ...
|
||||
21 |
|
||||
22 | class F(int, str, bytes, bytearray): ... # error: [instance-layout-conflict]
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Bases `int`, `str`, `bytes` and `bytearray` cannot be combined in multiple inheritance
|
||||
23 |
|
||||
24 | # fmt: on
|
||||
|
|
||||
info: Two classes cannot coexist in a class's MRO if their instances have incompatible memory layouts
|
||||
--> src/mdtest_snippet.py:22:9
|
||||
|
|
||||
20 | ): ...
|
||||
21 |
|
||||
22 | class F(int, str, bytes, bytearray): ... # error: [instance-layout-conflict]
|
||||
| --- --- ----- --------- `bytearray` instances have a distinct memory layout because of the way `bytearray` is implemented in a C extension
|
||||
| | | |
|
||||
| | | `bytes` instances have a distinct memory layout because of the way `bytes` is implemented in a C extension
|
||||
| | `str` instances have a distinct memory layout because of the way `str` is implemented in a C extension
|
||||
| `int` instances have a distinct memory layout because of the way `int` is implemented in a C extension
|
||||
23 |
|
||||
24 | # fmt: on
|
||||
|
|
||||
info: rule `instance-layout-conflict` is enabled by default
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error[subclass-of-final-class]: Class `Foo` cannot inherit from final class `range`
|
||||
--> src/mdtest_snippet.py:25:11
|
||||
|
|
||||
24 | # fmt: on
|
||||
25 | class Foo(range, str): ... # error: [subclass-of-final-class]
|
||||
| ^^^^^
|
||||
|
|
||||
info: rule `subclass-of-final-class` is enabled by default
|
||||
|
||||
```
|
||||
@@ -0,0 +1,54 @@
|
||||
---
|
||||
source: crates/ty_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: instance_layout_conflict.md - Tests for ty's `instance-layout-conflict` error code - `__slots__`: incompatible tuples
|
||||
mdtest path: crates/ty_python_semantic/resources/mdtest/instance_layout_conflict.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet.py
|
||||
|
||||
```
|
||||
1 | class A:
|
||||
2 | __slots__ = ("a", "b")
|
||||
3 |
|
||||
4 | class B:
|
||||
5 | __slots__ = ("c", "d")
|
||||
6 |
|
||||
7 | class C( # error: [instance-layout-conflict]
|
||||
8 | A,
|
||||
9 | B,
|
||||
10 | ): ...
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error[instance-layout-conflict]: Class will raise `TypeError` at runtime due to incompatible bases
|
||||
--> src/mdtest_snippet.py:7:7
|
||||
|
|
||||
5 | __slots__ = ("c", "d")
|
||||
6 |
|
||||
7 | class C( # error: [instance-layout-conflict]
|
||||
| _______^
|
||||
8 | | A,
|
||||
9 | | B,
|
||||
10 | | ): ...
|
||||
| |_^ Bases `A` and `B` cannot be combined in multiple inheritance
|
||||
|
|
||||
info: Two classes cannot coexist in a class's MRO if their instances have incompatible memory layouts
|
||||
--> src/mdtest_snippet.py:8:5
|
||||
|
|
||||
7 | class C( # error: [instance-layout-conflict]
|
||||
8 | A,
|
||||
| - `A` instances have a distinct memory layout because `A` defines non-empty `__slots__`
|
||||
9 | B,
|
||||
| - `B` instances have a distinct memory layout because `B` defines non-empty `__slots__`
|
||||
10 | ): ...
|
||||
|
|
||||
info: rule `instance-layout-conflict` is enabled by default
|
||||
|
||||
```
|
||||
@@ -20,8 +20,6 @@ static_assert(not is_disjoint_from(Any, Not[Any]))
|
||||
|
||||
static_assert(not is_disjoint_from(LiteralString, LiteralString))
|
||||
static_assert(not is_disjoint_from(str, LiteralString))
|
||||
static_assert(not is_disjoint_from(str, type))
|
||||
static_assert(not is_disjoint_from(str, type[Any]))
|
||||
```
|
||||
|
||||
## Class hierarchies
|
||||
@@ -71,6 +69,88 @@ class UsesMeta2(metaclass=Meta2): ...
|
||||
static_assert(is_disjoint_from(UsesMeta1, UsesMeta2))
|
||||
```
|
||||
|
||||
## `@final` builtin types
|
||||
|
||||
Some builtins types are declared as `@final`:
|
||||
|
||||
```py
|
||||
from ty_extensions import static_assert, is_disjoint_from
|
||||
|
||||
class Foo: ...
|
||||
|
||||
# `range`, `slice` and `memoryview` are all declared as `@final`:
|
||||
static_assert(is_disjoint_from(range, Foo))
|
||||
static_assert(is_disjoint_from(type[range], type[Foo]))
|
||||
static_assert(is_disjoint_from(slice, Foo))
|
||||
static_assert(is_disjoint_from(type[slice], type[Foo]))
|
||||
static_assert(is_disjoint_from(memoryview, Foo))
|
||||
static_assert(is_disjoint_from(type[memoryview], type[Foo]))
|
||||
```
|
||||
|
||||
## "Solid base" builtin types
|
||||
|
||||
Most other builtins can be subclassed and can even be used in multiple inheritance. However, builtin
|
||||
classes *cannot* generally be used in multiple inheritance with other builtin types. This is because
|
||||
the CPython interpreter considers these classes "solid bases": due to the way they are implemented
|
||||
in C, they have atypical instance memory layouts. No class can ever have more than one "solid base"
|
||||
in its MRO.
|
||||
|
||||
It's not currently possible for ty to detect in a generalized way whether a class is a "solid base"
|
||||
or not, but we special-case some commonly used builtin types:
|
||||
|
||||
```py
|
||||
from typing import Any
|
||||
from ty_extensions import static_assert, is_disjoint_from
|
||||
|
||||
class Foo: ...
|
||||
|
||||
static_assert(is_disjoint_from(list, dict))
|
||||
static_assert(is_disjoint_from(list[Foo], dict))
|
||||
static_assert(is_disjoint_from(list[Any], dict))
|
||||
static_assert(is_disjoint_from(list, dict[Foo, Foo]))
|
||||
static_assert(is_disjoint_from(list[Foo], dict[Foo, Foo]))
|
||||
static_assert(is_disjoint_from(list[Any], dict[Foo, Foo]))
|
||||
static_assert(is_disjoint_from(list, dict[Any, Any]))
|
||||
static_assert(is_disjoint_from(list[Foo], dict[Any, Any]))
|
||||
static_assert(is_disjoint_from(list[Any], dict[Any, Any]))
|
||||
static_assert(is_disjoint_from(type[list], type[dict]))
|
||||
```
|
||||
|
||||
## Other solid bases
|
||||
|
||||
As well as certain classes that are implemented in C extensions, any class that declares non-empty
|
||||
`__slots__` is also considered a "solid base"; these types are also considered to be disjoint by ty:
|
||||
|
||||
```py
|
||||
from ty_extensions import static_assert, is_disjoint_from
|
||||
|
||||
class A:
|
||||
__slots__ = ("a",)
|
||||
|
||||
class B:
|
||||
__slots__ = ("a",)
|
||||
|
||||
class C:
|
||||
__slots__ = ()
|
||||
|
||||
static_assert(is_disjoint_from(A, B))
|
||||
static_assert(is_disjoint_from(type[A], type[B]))
|
||||
static_assert(not is_disjoint_from(A, C))
|
||||
static_assert(not is_disjoint_from(type[A], type[C]))
|
||||
static_assert(not is_disjoint_from(B, C))
|
||||
static_assert(not is_disjoint_from(type[B], type[C]))
|
||||
```
|
||||
|
||||
Two solid bases are not disjoint if one inherits from the other, however:
|
||||
|
||||
```py
|
||||
class D(A):
|
||||
__slots__ = ("d",)
|
||||
|
||||
static_assert(is_disjoint_from(D, B))
|
||||
static_assert(not is_disjoint_from(D, A))
|
||||
```
|
||||
|
||||
## Tuple types
|
||||
|
||||
```py
|
||||
@@ -396,8 +476,10 @@ reveal_type(C.prop) # revealed: property
|
||||
class D:
|
||||
pass
|
||||
|
||||
static_assert(not is_disjoint_from(int, TypeOf[C.prop]))
|
||||
static_assert(not is_disjoint_from(TypeOf[C.prop], int))
|
||||
class Whatever: ...
|
||||
|
||||
static_assert(not is_disjoint_from(Whatever, TypeOf[C.prop]))
|
||||
static_assert(not is_disjoint_from(TypeOf[C.prop], Whatever))
|
||||
static_assert(is_disjoint_from(TypeOf[C.prop], D))
|
||||
static_assert(is_disjoint_from(D, TypeOf[C.prop]))
|
||||
```
|
||||
|
||||
@@ -263,8 +263,10 @@ reveal_type(top_materialization(Intersection[Any | int, tuple[str, Unknown]]))
|
||||
# revealed: Never
|
||||
reveal_type(bottom_materialization(Intersection[Any | int, tuple[str, Unknown]]))
|
||||
|
||||
# revealed: int & tuple[str]
|
||||
reveal_type(bottom_materialization(Intersection[Any | int, tuple[str]]))
|
||||
class Foo: ...
|
||||
|
||||
# revealed: Foo & tuple[str]
|
||||
reveal_type(bottom_materialization(Intersection[Any | Foo, tuple[str]]))
|
||||
|
||||
reveal_type(top_materialization(Intersection[list[Any], list[int]])) # revealed: list[T_all] & list[int]
|
||||
reveal_type(bottom_materialization(Intersection[list[Any], list[int]])) # revealed: list[T_all] & list[int]
|
||||
|
||||
@@ -39,6 +39,7 @@ mod util;
|
||||
pub mod pull_types;
|
||||
|
||||
type FxOrderSet<V> = ordermap::set::OrderSet<V, BuildHasherDefault<FxHasher>>;
|
||||
type FxIndexMap<K, V> = indexmap::IndexMap<K, V, BuildHasherDefault<FxHasher>>;
|
||||
|
||||
/// Returns the default registry with all known semantic lints.
|
||||
pub fn default_lint_registry() -> &'static LintRegistry {
|
||||
|
||||
@@ -75,7 +75,6 @@ mod mro;
|
||||
mod narrow;
|
||||
mod protocol_class;
|
||||
mod signatures;
|
||||
mod slots;
|
||||
mod special_form;
|
||||
mod string_annotation;
|
||||
mod subclass_of;
|
||||
@@ -1824,6 +1823,8 @@ impl<'db> Type<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
(Type::SubclassOf(left), Type::SubclassOf(right)) => left.is_disjoint_from(db, right),
|
||||
|
||||
(
|
||||
Type::SubclassOf(_),
|
||||
Type::BooleanLiteral(..)
|
||||
@@ -2107,7 +2108,7 @@ impl<'db> Type<'db> {
|
||||
(Type::Tuple(tuple), Type::NominalInstance(instance))
|
||||
| (Type::NominalInstance(instance), Type::Tuple(tuple)) => {
|
||||
tuple.to_class_type(db).is_some_and(|tuple_class| {
|
||||
instance.is_disjoint_from_nominal_instance_of_class(db, tuple_class)
|
||||
!instance.class.could_coexist_in_mro_with(db, tuple_class)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -298,6 +298,11 @@ impl<'db> ClassType<'db> {
|
||||
class_literal.definition(db)
|
||||
}
|
||||
|
||||
/// Return `Some` if this class is known to be a [`SolidBase`], or `None` if it is not.
|
||||
pub(super) fn as_solid_base(self, db: &'db dyn Db) -> Option<SolidBase<'db>> {
|
||||
self.class_literal(db).0.as_solid_base(db)
|
||||
}
|
||||
|
||||
/// Return `true` if this class represents `known_class`
|
||||
pub(crate) fn is_known(self, db: &'db dyn Db, known_class: KnownClass) -> bool {
|
||||
self.known(db) == Some(known_class)
|
||||
@@ -434,6 +439,69 @@ impl<'db> ClassType<'db> {
|
||||
.apply_optional_specialization(db, specialization)
|
||||
}
|
||||
|
||||
/// Return the [`SolidBase`] that appears first in the MRO of this class.
|
||||
///
|
||||
/// Returns `None` if this class does not have any solid bases in its MRO.
|
||||
pub(super) fn nearest_solid_base(self, db: &'db dyn Db) -> Option<SolidBase<'db>> {
|
||||
self.iter_mro(db)
|
||||
.filter_map(ClassBase::into_class)
|
||||
.find_map(|base| base.as_solid_base(db))
|
||||
}
|
||||
|
||||
/// Return `true` if this class could coexist in an MRO with `other`.
|
||||
///
|
||||
/// For two given classes `A` and `B`, it is often possible to say for sure
|
||||
/// that there could never exist any class `C` that inherits from both `A` and `B`.
|
||||
/// In these situations, this method returns `false`; in all others, it returns `true`.
|
||||
pub(super) fn could_coexist_in_mro_with(self, db: &'db dyn Db, other: Self) -> bool {
|
||||
if self == other {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Optimisation: if either class is `@final`, we only need to do one `is_subclass_of` call.
|
||||
if self.is_final(db) {
|
||||
return self.is_subclass_of(db, other);
|
||||
}
|
||||
if other.is_final(db) {
|
||||
return other.is_subclass_of(db, self);
|
||||
}
|
||||
|
||||
// Two solid bases can only coexist in an MRO if one is a subclass of the other.
|
||||
if self.nearest_solid_base(db).is_some_and(|solid_base_1| {
|
||||
other.nearest_solid_base(db).is_some_and(|solid_base_2| {
|
||||
!solid_base_1.could_coexist_in_mro_with(db, &solid_base_2)
|
||||
})
|
||||
}) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check to see whether the metaclasses of `self` and `other` are disjoint.
|
||||
// Avoid this check if the metaclass of either `self` or `other` is `type`,
|
||||
// however, since we end up with infinite recursion in that case due to the fact
|
||||
// that `type` is its own metaclass (and we know that `type` can coexist in an MRO
|
||||
// with any other arbitrary class, anyway).
|
||||
let type_class = KnownClass::Type.to_class_literal(db);
|
||||
let self_metaclass = self.metaclass(db);
|
||||
if self_metaclass == type_class {
|
||||
return true;
|
||||
}
|
||||
let other_metaclass = other.metaclass(db);
|
||||
if other_metaclass == type_class {
|
||||
return true;
|
||||
}
|
||||
let Some(self_metaclass_instance) = self_metaclass.to_instance(db) else {
|
||||
return true;
|
||||
};
|
||||
let Some(other_metaclass_instance) = other_metaclass.to_instance(db) else {
|
||||
return true;
|
||||
};
|
||||
if self_metaclass_instance.is_disjoint_from(db, other_metaclass_instance) {
|
||||
return false;
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
/// Return a type representing "the set of all instances of the metaclass of this class".
|
||||
pub(super) fn metaclass_instance_type(self, db: &'db dyn Db) -> Type<'db> {
|
||||
self
|
||||
@@ -860,6 +928,19 @@ impl<'db> ClassLiteral<'db> {
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Return `Some()` if this class is known to be a [`SolidBase`], or `None` if it is not.
|
||||
pub(super) fn as_solid_base(self, db: &'db dyn Db) -> Option<SolidBase<'db>> {
|
||||
if let Some(known_class) = self.known(db) {
|
||||
known_class
|
||||
.is_solid_base()
|
||||
.then_some(SolidBase::hard_coded(self))
|
||||
} else if SlotsKind::from(db, self) == SlotsKind::NotEmpty {
|
||||
Some(SolidBase::due_to_dunder_slots(self))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Iterate over this class's explicit bases, filtering out any bases that are not class
|
||||
/// objects, and applying default specialization to any unspecialized generic class literals.
|
||||
fn fully_static_explicit_bases(self, db: &'db dyn Db) -> impl Iterator<Item = ClassType<'db>> {
|
||||
@@ -2122,6 +2203,60 @@ impl InheritanceCycle {
|
||||
}
|
||||
}
|
||||
|
||||
/// CPython internally considers a class a "solid base" if it has an atypical instance memory layout,
|
||||
/// with additional memory "slots" for each instance, besides the default object metadata and an
|
||||
/// attribute dictionary. A "solid base" can be a class defined in a C extension which defines C-level
|
||||
/// instance slots, or a Python class that defines non-empty `__slots__`.
|
||||
///
|
||||
/// Two solid bases can only coexist in a class's MRO if one is a subclass of the other. Knowing if
|
||||
/// a class is "solid base" or not is therefore valuable for inferring whether two instance types or
|
||||
/// two subclass-of types are disjoint from each other. It also allows us to detect possible
|
||||
/// `TypeError`s resulting from class definitions.
|
||||
#[derive(Debug, PartialEq, Eq, Hash, Copy, Clone)]
|
||||
pub(super) struct SolidBase<'db> {
|
||||
pub(super) class: ClassLiteral<'db>,
|
||||
pub(super) kind: SolidBaseKind,
|
||||
}
|
||||
|
||||
impl<'db> SolidBase<'db> {
|
||||
/// Creates a [`SolidBase`] instance where we know the class is a solid base
|
||||
/// because it is special-cased by ty.
|
||||
fn hard_coded(class: ClassLiteral<'db>) -> Self {
|
||||
Self {
|
||||
class,
|
||||
kind: SolidBaseKind::HardCoded,
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a [`SolidBase`] instance where we know the class is a solid base
|
||||
/// because of its `__slots__` definition.
|
||||
fn due_to_dunder_slots(class: ClassLiteral<'db>) -> Self {
|
||||
Self {
|
||||
class,
|
||||
kind: SolidBaseKind::DefinesSlots,
|
||||
}
|
||||
}
|
||||
|
||||
/// Two solid bases can only coexist in a class's MRO if one is a subclass of the other
|
||||
fn could_coexist_in_mro_with(&self, db: &'db dyn Db, other: &Self) -> bool {
|
||||
self == other
|
||||
|| self
|
||||
.class
|
||||
.is_subclass_of(db, None, other.class.default_specialization(db))
|
||||
|| other
|
||||
.class
|
||||
.is_subclass_of(db, None, self.class.default_specialization(db))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
|
||||
pub(super) enum SolidBaseKind {
|
||||
/// We know the class is a solid base because of some hardcoded knowledge in ty.
|
||||
HardCoded,
|
||||
/// We know the class is a solid base because it has a non-empty `__slots__` definition.
|
||||
DefinesSlots,
|
||||
}
|
||||
|
||||
/// Non-exhaustive enumeration of known classes (e.g. `builtins.int`, `typing.Any`, ...) to allow
|
||||
/// for easier syntax when interacting with very common classes.
|
||||
///
|
||||
@@ -2294,6 +2429,83 @@ impl<'db> KnownClass {
|
||||
}
|
||||
}
|
||||
|
||||
/// Return `true` if this class is a [`SolidBase`]
|
||||
const fn is_solid_base(self) -> bool {
|
||||
match self {
|
||||
Self::Object => false,
|
||||
|
||||
// Most non-`@final` builtins (other than `object`) are solid bases.
|
||||
Self::Set
|
||||
| Self::FrozenSet
|
||||
| Self::BaseException
|
||||
| Self::Bytearray
|
||||
| Self::Int
|
||||
| Self::Float
|
||||
| Self::Complex
|
||||
| Self::Str
|
||||
| Self::List
|
||||
| Self::Tuple
|
||||
| Self::Dict
|
||||
| Self::Slice
|
||||
| Self::Property
|
||||
| Self::Staticmethod
|
||||
| Self::Classmethod
|
||||
| Self::Type
|
||||
| Self::ModuleType
|
||||
| Self::Super
|
||||
| Self::GenericAlias
|
||||
| Self::Deque
|
||||
| Self::Bytes => true,
|
||||
|
||||
// It doesn't really make sense to ask the question for `@final` types,
|
||||
// since these are "more than solid bases". But we'll anyway infer a `@final`
|
||||
// class as being disjoint from a class that doesn't appear in its MRO,
|
||||
// and we'll anyway complain if we see a class definition that includes a
|
||||
// `@final` class in its bases. We therefore return `false` here to avoid
|
||||
// unnecessary duplicate diagnostics elsewhere.
|
||||
Self::TypeVarTuple
|
||||
| Self::TypeAliasType
|
||||
| Self::UnionType
|
||||
| Self::NoDefaultType
|
||||
| Self::MethodType
|
||||
| Self::MethodWrapperType
|
||||
| Self::FunctionType
|
||||
| Self::GeneratorType
|
||||
| Self::AsyncGeneratorType
|
||||
| Self::StdlibAlias
|
||||
| Self::SpecialForm
|
||||
| Self::TypeVar
|
||||
| Self::ParamSpec
|
||||
| Self::ParamSpecArgs
|
||||
| Self::ParamSpecKwargs
|
||||
| Self::WrapperDescriptorType
|
||||
| Self::EllipsisType
|
||||
| Self::NotImplementedType
|
||||
| Self::KwOnly
|
||||
| Self::VersionInfo
|
||||
| Self::Bool
|
||||
| Self::NoneType => false,
|
||||
|
||||
// Anything with a *runtime* MRO (N.B. sometimes different from the MRO that typeshed gives!)
|
||||
// with length >2, or anything that is implemented in pure Python, is not a solid base.
|
||||
Self::ABCMeta
|
||||
| Self::Any
|
||||
| Self::Enum
|
||||
| Self::ChainMap
|
||||
| Self::Exception
|
||||
| Self::ExceptionGroup
|
||||
| Self::Field
|
||||
| Self::SupportsIndex
|
||||
| Self::NamedTuple
|
||||
| Self::NamedTupleFallback
|
||||
| Self::Counter
|
||||
| Self::DefaultDict
|
||||
| Self::OrderedDict
|
||||
| Self::NewType
|
||||
| Self::BaseExceptionGroup => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Return `true` if this class is a protocol class.
|
||||
///
|
||||
/// In an ideal world, perhaps we wouldn't hardcode this knowledge here;
|
||||
@@ -3114,6 +3326,52 @@ pub(super) enum MetaclassErrorKind<'db> {
|
||||
Cycle,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
||||
enum SlotsKind {
|
||||
/// `__slots__` is not found in the class.
|
||||
NotSpecified,
|
||||
/// `__slots__` is defined but empty: `__slots__ = ()`.
|
||||
Empty,
|
||||
/// `__slots__` is defined and is not empty: `__slots__ = ("a", "b")`.
|
||||
NotEmpty,
|
||||
/// `__slots__` is defined but its value is dynamic:
|
||||
/// * `__slots__ = tuple(a for a in b)`
|
||||
/// * `__slots__ = ["a", "b"]`
|
||||
Dynamic,
|
||||
}
|
||||
|
||||
impl SlotsKind {
|
||||
fn from(db: &dyn Db, base: ClassLiteral) -> Self {
|
||||
let Place::Type(slots_ty, bound) = base.own_class_member(db, None, "__slots__").place
|
||||
else {
|
||||
return Self::NotSpecified;
|
||||
};
|
||||
|
||||
if matches!(bound, Boundness::PossiblyUnbound) {
|
||||
return Self::Dynamic;
|
||||
}
|
||||
|
||||
match slots_ty {
|
||||
// __slots__ = ("a", "b")
|
||||
Type::Tuple(tuple) => {
|
||||
let tuple = tuple.tuple(db);
|
||||
if tuple.is_variadic() {
|
||||
Self::Dynamic
|
||||
} else if tuple.is_empty() {
|
||||
Self::Empty
|
||||
} else {
|
||||
Self::NotEmpty
|
||||
}
|
||||
}
|
||||
|
||||
// __slots__ = "abc" # Same as `("abc",)`
|
||||
Type::StringLiteral(_) => Self::NotEmpty,
|
||||
|
||||
_ => Self::Dynamic,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -8,6 +8,7 @@ use super::{
|
||||
use crate::lint::{Level, LintRegistryBuilder, LintStatus};
|
||||
use crate::suppression::FileSuppressionId;
|
||||
use crate::types::LintDiagnosticGuard;
|
||||
use crate::types::class::{SolidBase, SolidBaseKind};
|
||||
use crate::types::function::KnownFunction;
|
||||
use crate::types::string_annotation::{
|
||||
BYTE_STRING_TYPE_ANNOTATION, ESCAPE_CHARACTER_IN_FORWARD_ANNOTATION, FSTRING_TYPE_ANNOTATION,
|
||||
@@ -16,7 +17,7 @@ use crate::types::string_annotation::{
|
||||
};
|
||||
use crate::types::tuple::TupleType;
|
||||
use crate::types::{SpecialFormType, Type, protocol_class::ProtocolClassLiteral};
|
||||
use crate::{Db, Module, ModuleName, Program, declare_lint};
|
||||
use crate::{Db, FxIndexMap, Module, ModuleName, Program, declare_lint};
|
||||
use itertools::Itertools;
|
||||
use ruff_db::diagnostic::{Annotation, Diagnostic, Severity, SubDiagnostic};
|
||||
use ruff_python_ast::{self as ast, AnyNodeRef};
|
||||
@@ -35,7 +36,7 @@ pub(crate) fn register_lints(registry: &mut LintRegistryBuilder) {
|
||||
registry.register_lint(&DIVISION_BY_ZERO);
|
||||
registry.register_lint(&DUPLICATE_BASE);
|
||||
registry.register_lint(&DUPLICATE_KW_ONLY);
|
||||
registry.register_lint(&INCOMPATIBLE_SLOTS);
|
||||
registry.register_lint(&INSTANCE_LAYOUT_CONFLICT);
|
||||
registry.register_lint(&INCONSISTENT_MRO);
|
||||
registry.register_lint(&INDEX_OUT_OF_BOUNDS);
|
||||
registry.register_lint(&INVALID_ARGUMENT_TYPE);
|
||||
@@ -313,27 +314,27 @@ declare_lint! {
|
||||
|
||||
declare_lint! {
|
||||
/// ## What it does
|
||||
/// Checks for classes whose bases define incompatible `__slots__`.
|
||||
/// Checks for classes definitions which will fail at runtime due to
|
||||
/// "instance memory layout conflicts".
|
||||
///
|
||||
/// This error is usually caused by attempting to combine multiple classes
|
||||
/// that define non-empty `__slots__` in a class's [Method Resolution Order]
|
||||
/// (MRO), or by attempting to combine multiple builtin classes in a class's
|
||||
/// MRO.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Inheriting from bases with incompatible `__slots__`s
|
||||
/// Inheriting from bases with conflicting instance memory layouts
|
||||
/// will lead to a `TypeError` at runtime.
|
||||
///
|
||||
/// Classes with no or empty `__slots__` are always compatible:
|
||||
/// An instance memory layout conflict occurs when CPython cannot determine
|
||||
/// the memory layout instances of a class should have, because the instance
|
||||
/// memory layout of one of its bases conflicts with the instance memory layout
|
||||
/// of one or more of its other bases.
|
||||
///
|
||||
/// ```python
|
||||
/// class A: ...
|
||||
/// class B:
|
||||
/// __slots__ = ()
|
||||
/// class C:
|
||||
/// __slots__ = ("a", "b")
|
||||
///
|
||||
/// # fine
|
||||
/// class D(A, B, C): ...
|
||||
/// ```
|
||||
///
|
||||
/// Multiple inheritance from more than one different class
|
||||
/// defining non-empty `__slots__` is not allowed:
|
||||
/// For example, if a Python class defines non-empty `__slots__`, this will
|
||||
/// impact the memory layout of instances of that class. Multiple inheritance
|
||||
/// from more than one different class defining non-empty `__slots__` is not
|
||||
/// allowed:
|
||||
///
|
||||
/// ```python
|
||||
/// class A:
|
||||
@@ -346,24 +347,48 @@ declare_lint! {
|
||||
/// class C(A, B): ...
|
||||
/// ```
|
||||
///
|
||||
/// ## Known problems
|
||||
/// Dynamic (not tuple or string literal) `__slots__` are not checked.
|
||||
/// Additionally, classes inheriting from built-in classes with implicit layouts
|
||||
/// like `str` or `int` are also not checked.
|
||||
/// An instance layout conflict can also be caused by attempting to use
|
||||
/// multiple inheritance with two builtin classes, due to the way that these
|
||||
/// classes are implemented in a CPython C extension:
|
||||
///
|
||||
/// ```pycon
|
||||
/// >>> hasattr(int, "__slots__")
|
||||
/// False
|
||||
/// >>> hasattr(str, "__slots__")
|
||||
/// False
|
||||
/// >>> class A(int, str): ...
|
||||
/// Traceback (most recent call last):
|
||||
/// File "<python-input-0>", line 1, in <module>
|
||||
/// class A(int, str): ...
|
||||
/// TypeError: multiple bases have instance lay-out conflict
|
||||
/// ```python
|
||||
/// class A(int, float): ... # TypeError: multiple bases have instance lay-out conflict
|
||||
/// ```
|
||||
pub(crate) static INCOMPATIBLE_SLOTS = {
|
||||
summary: "detects class definitions whose MRO has conflicting `__slots__`",
|
||||
///
|
||||
/// Note that pure-Python classes with no `__slots__`, or pure-Python classes
|
||||
/// with empty `__slots__`, are always compatible:
|
||||
///
|
||||
/// ```python
|
||||
/// class A: ...
|
||||
/// class B:
|
||||
/// __slots__ = ()
|
||||
/// class C:
|
||||
/// __slots__ = ("a", "b")
|
||||
///
|
||||
/// # fine
|
||||
/// class D(A, B, C): ...
|
||||
/// ```
|
||||
///
|
||||
/// ## Known problems
|
||||
/// Classes that have "dynamic" definitions of `__slots__` (definitions do not consist
|
||||
/// of string literals, or tuples of string literals) are not currently considered solid
|
||||
/// bases by ty.
|
||||
///
|
||||
/// Additionally, this check is not exhaustive: many C extensions (including several in
|
||||
/// the standard library) define classes that use extended memory layouts and thus cannot
|
||||
/// coexist in a single MRO. Since it is currently not possible to represent this fact in
|
||||
/// stub files, having a full knowledge of these classes is also impossible. When it comes
|
||||
/// to classes that do not define `__slots__` at the Python level, therefore, ty, currently
|
||||
/// only hard-codes a number of cases where it knows that a class will produce instances with
|
||||
/// an atypical memory layout.
|
||||
///
|
||||
/// ## Further reading
|
||||
/// - [CPython documentation: `__slots__`](https://docs.python.org/3/reference/datamodel.html#slots)
|
||||
/// - [CPython documentation: Method Resolution Order](https://docs.python.org/3/glossary.html#term-method-resolution-order)
|
||||
///
|
||||
/// [Method Resolution Order]: https://docs.python.org/3/glossary.html#term-method-resolution-order
|
||||
pub(crate) static INSTANCE_LAYOUT_CONFLICT = {
|
||||
summary: "detects class definitions that raise `TypeError` due to instance layout conflict",
|
||||
status: LintStatus::preview("1.0.0"),
|
||||
default_level: Level::Error,
|
||||
}
|
||||
@@ -1901,11 +1926,193 @@ pub(crate) fn report_invalid_exception_cause(context: &InferContext, node: &ast:
|
||||
));
|
||||
}
|
||||
|
||||
pub(crate) fn report_base_with_incompatible_slots(context: &InferContext, node: &ast::Expr) {
|
||||
let Some(builder) = context.report_lint(&INCOMPATIBLE_SLOTS, node) else {
|
||||
pub(crate) fn report_instance_layout_conflict(
|
||||
context: &InferContext,
|
||||
class: ClassLiteral,
|
||||
node: &ast::StmtClassDef,
|
||||
solid_bases: &IncompatibleBases,
|
||||
) {
|
||||
debug_assert!(solid_bases.len() > 1);
|
||||
|
||||
let db = context.db();
|
||||
|
||||
let Some(builder) = context.report_lint(&INSTANCE_LAYOUT_CONFLICT, class.header_range(db))
|
||||
else {
|
||||
return;
|
||||
};
|
||||
builder.into_diagnostic("Class base has incompatible `__slots__`");
|
||||
|
||||
let mut diagnostic = builder
|
||||
.into_diagnostic("Class will raise `TypeError` at runtime due to incompatible bases");
|
||||
|
||||
diagnostic.set_primary_message(format_args!(
|
||||
"Bases {} cannot be combined in multiple inheritance",
|
||||
solid_bases.describe_problematic_class_bases(db)
|
||||
));
|
||||
|
||||
let mut subdiagnostic = SubDiagnostic::new(
|
||||
Severity::Info,
|
||||
"Two classes cannot coexist in a class's MRO if their instances \
|
||||
have incompatible memory layouts",
|
||||
);
|
||||
|
||||
for (solid_base, solid_base_info) in solid_bases {
|
||||
let IncompatibleBaseInfo {
|
||||
node_index,
|
||||
originating_base,
|
||||
} = solid_base_info;
|
||||
|
||||
let span = context.span(&node.bases()[*node_index]);
|
||||
let mut annotation = Annotation::secondary(span.clone());
|
||||
if solid_base.class == *originating_base {
|
||||
match solid_base.kind {
|
||||
SolidBaseKind::DefinesSlots => {
|
||||
annotation = annotation.message(format_args!(
|
||||
"`{base}` instances have a distinct memory layout because `{base}` defines non-empty `__slots__`",
|
||||
base = originating_base.name(db)
|
||||
));
|
||||
}
|
||||
SolidBaseKind::HardCoded => {
|
||||
annotation = annotation.message(format_args!(
|
||||
"`{base}` instances have a distinct memory layout because of the way `{base}` \
|
||||
is implemented in a C extension",
|
||||
base = originating_base.name(db)
|
||||
));
|
||||
}
|
||||
}
|
||||
subdiagnostic.annotate(annotation);
|
||||
} else {
|
||||
annotation = annotation.message(format_args!(
|
||||
"`{base}` instances have a distinct memory layout \
|
||||
because `{base}` inherits from `{solid_base}`",
|
||||
base = originating_base.name(db),
|
||||
solid_base = solid_base.class.name(db)
|
||||
));
|
||||
subdiagnostic.annotate(annotation);
|
||||
|
||||
let mut additional_annotation = Annotation::secondary(span);
|
||||
|
||||
additional_annotation = match solid_base.kind {
|
||||
SolidBaseKind::DefinesSlots => additional_annotation.message(format_args!(
|
||||
"`{solid_base}` instances have a distinct memory layout because `{solid_base}` \
|
||||
defines non-empty `__slots__`",
|
||||
solid_base = solid_base.class.name(db),
|
||||
)),
|
||||
|
||||
SolidBaseKind::HardCoded => additional_annotation.message(format_args!(
|
||||
"`{solid_base}` instances have a distinct memory layout \
|
||||
because of the way `{solid_base}` is implemented in a C extension",
|
||||
solid_base = solid_base.class.name(db),
|
||||
)),
|
||||
};
|
||||
|
||||
subdiagnostic.annotate(additional_annotation);
|
||||
}
|
||||
}
|
||||
|
||||
diagnostic.sub(subdiagnostic);
|
||||
}
|
||||
|
||||
/// Information regarding the conflicting solid bases a class is inferred to have in its MRO.
|
||||
///
|
||||
/// For each solid base, we record information about which element in the class's bases list
|
||||
/// caused the solid base to be included in the class's MRO.
|
||||
///
|
||||
/// The inner data is an `IndexMap` to ensure that diagnostics regarding conflicting solid bases
|
||||
/// are reported in a stable order.
|
||||
#[derive(Debug, Default)]
|
||||
pub(super) struct IncompatibleBases<'db>(FxIndexMap<SolidBase<'db>, IncompatibleBaseInfo<'db>>);
|
||||
|
||||
impl<'db> IncompatibleBases<'db> {
|
||||
pub(super) fn insert(
|
||||
&mut self,
|
||||
base: SolidBase<'db>,
|
||||
node_index: usize,
|
||||
class: ClassLiteral<'db>,
|
||||
) {
|
||||
let info = IncompatibleBaseInfo {
|
||||
node_index,
|
||||
originating_base: class,
|
||||
};
|
||||
self.0.insert(base, info);
|
||||
}
|
||||
|
||||
/// List the problematic class bases in a human-readable format.
|
||||
fn describe_problematic_class_bases(&self, db: &dyn Db) -> String {
|
||||
let num_bases = self.len();
|
||||
debug_assert!(num_bases >= 2);
|
||||
|
||||
let mut bad_base_names = self.0.values().map(|info| info.originating_base.name(db));
|
||||
|
||||
let final_base = bad_base_names.next_back().unwrap();
|
||||
let penultimate_base = bad_base_names.next_back().unwrap();
|
||||
|
||||
let mut buffer = String::new();
|
||||
|
||||
for base_name in bad_base_names {
|
||||
buffer.push('`');
|
||||
buffer.push_str(base_name);
|
||||
buffer.push_str("`, ");
|
||||
}
|
||||
|
||||
buffer.push('`');
|
||||
buffer.push_str(penultimate_base);
|
||||
buffer.push_str("` and `");
|
||||
buffer.push_str(final_base);
|
||||
buffer.push('`');
|
||||
|
||||
buffer
|
||||
}
|
||||
|
||||
pub(super) fn len(&self) -> usize {
|
||||
self.0.len()
|
||||
}
|
||||
|
||||
/// Two solid bases are allowed to coexist in an MRO if one is a subclass of the other.
|
||||
/// This method therefore removes any entry in `self` that is a subclass of one or more
|
||||
/// other entries also contained in `self`.
|
||||
pub(super) fn remove_redundant_entries(&mut self, db: &'db dyn Db) {
|
||||
self.0 = self
|
||||
.0
|
||||
.iter()
|
||||
.filter(|(solid_base, _)| {
|
||||
self.0
|
||||
.keys()
|
||||
.filter(|other_base| other_base != solid_base)
|
||||
.all(|other_base| {
|
||||
!solid_base.class.is_subclass_of(
|
||||
db,
|
||||
None,
|
||||
other_base.class.default_specialization(db),
|
||||
)
|
||||
})
|
||||
})
|
||||
.map(|(base, info)| (*base, *info))
|
||||
.collect();
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'db> IntoIterator for &'a IncompatibleBases<'db> {
|
||||
type Item = (&'a SolidBase<'db>, &'a IncompatibleBaseInfo<'db>);
|
||||
type IntoIter = indexmap::map::Iter<'a, SolidBase<'db>, IncompatibleBaseInfo<'db>>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.0.iter()
|
||||
}
|
||||
}
|
||||
|
||||
/// Information about which class base the "solid base" stems from
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub(super) struct IncompatibleBaseInfo<'db> {
|
||||
/// The index of the problematic base in the [`ast::StmtClassDef`]'s bases list.
|
||||
node_index: usize,
|
||||
|
||||
/// The base class in the [`ast::StmtClassDef`]'s bases list that caused
|
||||
/// the solid base to be included in the class's MRO.
|
||||
///
|
||||
/// This won't necessarily be the same class as the `SolidBase`'s class,
|
||||
/// as the `SolidBase` may have found its way into the class's MRO by dint of it being a
|
||||
/// superclass of one of the classes in the class definition's bases list.
|
||||
originating_base: ClassLiteral<'db>,
|
||||
}
|
||||
|
||||
pub(crate) fn report_invalid_arguments_to_annotated(
|
||||
|
||||
@@ -81,13 +81,14 @@ use crate::types::diagnostic::{
|
||||
INVALID_ARGUMENT_TYPE, INVALID_ASSIGNMENT, INVALID_ATTRIBUTE_ACCESS, INVALID_BASE,
|
||||
INVALID_DECLARATION, INVALID_GENERIC_CLASS, INVALID_LEGACY_TYPE_VARIABLE,
|
||||
INVALID_PARAMETER_DEFAULT, INVALID_TYPE_ALIAS_TYPE, INVALID_TYPE_FORM, INVALID_TYPE_GUARD_CALL,
|
||||
INVALID_TYPE_VARIABLE_CONSTRAINTS, POSSIBLY_UNBOUND_IMPLICIT_CALL, POSSIBLY_UNBOUND_IMPORT,
|
||||
TypeCheckDiagnostics, UNDEFINED_REVEAL, UNRESOLVED_ATTRIBUTE, UNRESOLVED_IMPORT,
|
||||
UNRESOLVED_REFERENCE, UNSUPPORTED_OPERATOR, report_implicit_return_type,
|
||||
report_invalid_argument_number_to_special_form, report_invalid_arguments_to_annotated,
|
||||
report_invalid_arguments_to_callable, report_invalid_assignment,
|
||||
report_invalid_attribute_assignment, report_invalid_generator_function_return_type,
|
||||
report_invalid_return_type, report_possibly_unbound_attribute,
|
||||
INVALID_TYPE_VARIABLE_CONSTRAINTS, IncompatibleBases, POSSIBLY_UNBOUND_IMPLICIT_CALL,
|
||||
POSSIBLY_UNBOUND_IMPORT, TypeCheckDiagnostics, UNDEFINED_REVEAL, UNRESOLVED_ATTRIBUTE,
|
||||
UNRESOLVED_IMPORT, UNRESOLVED_REFERENCE, UNSUPPORTED_OPERATOR, report_implicit_return_type,
|
||||
report_instance_layout_conflict, report_invalid_argument_number_to_special_form,
|
||||
report_invalid_arguments_to_annotated, report_invalid_arguments_to_callable,
|
||||
report_invalid_assignment, report_invalid_attribute_assignment,
|
||||
report_invalid_generator_function_return_type, report_invalid_return_type,
|
||||
report_possibly_unbound_attribute,
|
||||
};
|
||||
use crate::types::function::{
|
||||
FunctionDecorators, FunctionLiteral, FunctionType, KnownFunction, OverloadLiteral,
|
||||
@@ -123,7 +124,6 @@ use super::diagnostic::{
|
||||
report_runtime_check_against_non_runtime_checkable_protocol, report_slice_step_size_zero,
|
||||
};
|
||||
use super::generics::LegacyGenericBase;
|
||||
use super::slots::check_class_slots;
|
||||
use super::string_annotation::{
|
||||
BYTE_STRING_TYPE_ANNOTATION, FSTRING_TYPE_ANNOTATION, parse_string_annotation,
|
||||
};
|
||||
@@ -887,12 +887,20 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
}
|
||||
|
||||
let is_protocol = class.is_protocol(self.db());
|
||||
let mut solid_bases = IncompatibleBases::default();
|
||||
|
||||
// (2) Iterate through the class's explicit bases to check for various possible errors:
|
||||
// - Check for inheritance from plain `Generic`,
|
||||
// - Check for inheritance from a `@final` classes
|
||||
// - If the class is a protocol class: check for inheritance from a non-protocol class
|
||||
for (i, base_class) in class.explicit_bases(self.db()).iter().enumerate() {
|
||||
if let Some((class, solid_base)) = base_class
|
||||
.to_class_type(self.db())
|
||||
.and_then(|class| Some((class, class.nearest_solid_base(self.db())?)))
|
||||
{
|
||||
solid_bases.insert(solid_base, i, class.class_literal(self.db()).0);
|
||||
}
|
||||
|
||||
let base_class = match base_class {
|
||||
Type::SpecialForm(SpecialFormType::Generic) => {
|
||||
if let Some(builder) = self
|
||||
@@ -1016,7 +1024,18 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
}
|
||||
}
|
||||
},
|
||||
Ok(_) => check_class_slots(&self.context, class, class_node),
|
||||
Ok(_) => {
|
||||
solid_bases.remove_redundant_entries(self.db());
|
||||
|
||||
if solid_bases.len() > 1 {
|
||||
report_instance_layout_conflict(
|
||||
&self.context,
|
||||
class,
|
||||
class_node,
|
||||
&solid_bases,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// (4) Check that the class's metaclass can be determined without error.
|
||||
|
||||
@@ -105,42 +105,7 @@ impl<'db> NominalInstanceType<'db> {
|
||||
}
|
||||
|
||||
pub(super) fn is_disjoint_from(self, db: &'db dyn Db, other: Self) -> bool {
|
||||
self.is_disjoint_from_nominal_instance_of_class(db, other.class)
|
||||
}
|
||||
|
||||
// Note that this method only exists so that we can check disjointness between nominal
|
||||
// instances of `tuple` and some other class. Tuples are currently represented by the
|
||||
// `Type::Tuple` variant, not `Type::NominalInstance`. We have a TODO to try to remove the
|
||||
// dedicated `Tuple` variant in favor of `NominalInstance`; if we can do that, then we won't
|
||||
// need this method, and its logic can be subsumed into `is_disjoint_from`.
|
||||
pub(super) fn is_disjoint_from_nominal_instance_of_class(
|
||||
self,
|
||||
db: &'db dyn Db,
|
||||
other_class: ClassType,
|
||||
) -> bool {
|
||||
if self.class.is_final(db) && !self.class.is_subclass_of(db, other_class) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if other_class.is_final(db) && !other_class.is_subclass_of(db, self.class) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check to see whether the metaclasses of `self` and `other` are disjoint.
|
||||
// Avoid this check if the metaclass of either `self` or `other` is `type`,
|
||||
// however, since we end up with infinite recursion in that case due to the fact
|
||||
// that `type` is its own metaclass (and we know that `type` cannot be disjoint
|
||||
// from any metaclass, anyway).
|
||||
let type_type = KnownClass::Type.to_instance(db);
|
||||
let self_metaclass = self.class.metaclass_instance_type(db);
|
||||
if self_metaclass == type_type {
|
||||
return false;
|
||||
}
|
||||
let other_metaclass = other_class.metaclass_instance_type(db);
|
||||
if other_metaclass == type_type {
|
||||
return false;
|
||||
}
|
||||
self_metaclass.is_disjoint_from(db, other_metaclass)
|
||||
!self.class.could_coexist_in_mro_with(db, other.class)
|
||||
}
|
||||
|
||||
pub(super) fn is_gradual_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool {
|
||||
|
||||
@@ -1,109 +0,0 @@
|
||||
use ruff_python_ast as ast;
|
||||
|
||||
use crate::db::Db;
|
||||
use crate::place::{Boundness, Place};
|
||||
use crate::types::class_base::ClassBase;
|
||||
use crate::types::diagnostic::report_base_with_incompatible_slots;
|
||||
use crate::types::{ClassLiteral, Type};
|
||||
|
||||
use super::InferContext;
|
||||
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
||||
enum SlotsKind {
|
||||
/// `__slots__` is not found in the class.
|
||||
NotSpecified,
|
||||
/// `__slots__` is defined but empty: `__slots__ = ()`.
|
||||
Empty,
|
||||
/// `__slots__` is defined and is not empty: `__slots__ = ("a", "b")`.
|
||||
NotEmpty,
|
||||
/// `__slots__` is defined but its value is dynamic:
|
||||
/// * `__slots__ = tuple(a for a in b)`
|
||||
/// * `__slots__ = ["a", "b"]`
|
||||
Dynamic,
|
||||
}
|
||||
|
||||
impl SlotsKind {
|
||||
fn from(db: &dyn Db, base: ClassLiteral) -> Self {
|
||||
let Place::Type(slots_ty, bound) = base.own_class_member(db, None, "__slots__").place
|
||||
else {
|
||||
return Self::NotSpecified;
|
||||
};
|
||||
|
||||
if matches!(bound, Boundness::PossiblyUnbound) {
|
||||
return Self::Dynamic;
|
||||
}
|
||||
|
||||
match slots_ty {
|
||||
// __slots__ = ("a", "b")
|
||||
Type::Tuple(tuple) => {
|
||||
if tuple.tuple(db).is_empty() {
|
||||
Self::Empty
|
||||
} else {
|
||||
Self::NotEmpty
|
||||
}
|
||||
}
|
||||
|
||||
// __slots__ = "abc" # Same as `("abc",)`
|
||||
Type::StringLiteral(_) => Self::NotEmpty,
|
||||
|
||||
_ => Self::Dynamic,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn check_class_slots(
|
||||
context: &InferContext,
|
||||
class: ClassLiteral,
|
||||
node: &ast::StmtClassDef,
|
||||
) {
|
||||
let db = context.db();
|
||||
|
||||
let mut first_with_solid_base = None;
|
||||
let mut common_solid_base = None;
|
||||
let mut found_second = false;
|
||||
|
||||
for (index, base) in class.explicit_bases(db).iter().enumerate() {
|
||||
let Type::ClassLiteral(base) = base else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let solid_base = base.iter_mro(db, None).find_map(|current| {
|
||||
let ClassBase::Class(current) = current else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let (class_literal, _) = current.class_literal(db);
|
||||
match SlotsKind::from(db, class_literal) {
|
||||
SlotsKind::NotEmpty => Some(current),
|
||||
SlotsKind::NotSpecified | SlotsKind::Empty => None,
|
||||
SlotsKind::Dynamic => None,
|
||||
}
|
||||
});
|
||||
|
||||
if solid_base.is_none() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let base_node = &node.bases()[index];
|
||||
|
||||
if first_with_solid_base.is_none() {
|
||||
first_with_solid_base = Some(index);
|
||||
common_solid_base = solid_base;
|
||||
continue;
|
||||
}
|
||||
|
||||
if solid_base == common_solid_base {
|
||||
continue;
|
||||
}
|
||||
|
||||
found_second = true;
|
||||
report_base_with_incompatible_slots(context, base_node);
|
||||
}
|
||||
|
||||
if found_second {
|
||||
if let Some(index) = first_with_solid_base {
|
||||
let base_node = &node.bases()[index];
|
||||
report_base_with_incompatible_slots(context, base_node);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -159,6 +159,18 @@ impl<'db> SubclassOfType<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Return` true` if `self` is a disjoint type from `other`.
|
||||
///
|
||||
/// See [`Type::is_disjoint_from`] for more details.
|
||||
pub(crate) fn is_disjoint_from(self, db: &'db dyn Db, other: Self) -> bool {
|
||||
match (self.subclass_of, other.subclass_of) {
|
||||
(SubclassOfInner::Dynamic(_), _) | (_, SubclassOfInner::Dynamic(_)) => false,
|
||||
(SubclassOfInner::Class(self_class), SubclassOfInner::Class(other_class)) => {
|
||||
!self_class.could_coexist_in_mro_with(db, other_class)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn normalized(self, db: &'db dyn Db) -> Self {
|
||||
Self {
|
||||
subclass_of: self.subclass_of.normalized(db),
|
||||
|
||||
@@ -710,6 +710,10 @@ impl<'db> TupleSpec<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) const fn is_variadic(&self) -> bool {
|
||||
matches!(self, TupleSpec::Variable(_))
|
||||
}
|
||||
|
||||
/// Returns the minimum and maximum length of this tuple. (The maximum length will be `None`
|
||||
/// for a tuple with a variable-length portion.)
|
||||
pub(crate) fn size_hint(&self) -> (usize, Option<usize>) {
|
||||
|
||||
Reference in New Issue
Block a user