[ty] Improve disjointness inference for NominalInstanceTypes and SubclassOfTypes (#18864)

Co-authored-by: Carl Meyer <carl@astral.sh>
This commit is contained in:
Alex Waygood 2025-06-24 21:27:37 +01:00 committed by GitHub
parent d89f75f9cc
commit 9d8cba4e8b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 1255 additions and 442 deletions

262
crates/ty/docs/rules.md generated
View file

@ -52,7 +52,7 @@ Calling a non-callable object will raise a `TypeError` at runtime.
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20call-non-callable)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L96)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L97)
</details>
## `conflicting-argument-forms`
@ -83,7 +83,7 @@ f(int) # error
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20conflicting-argument-forms)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L140)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L141)
</details>
## `conflicting-declarations`
@ -113,7 +113,7 @@ a = 1
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20conflicting-declarations)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L166)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L167)
</details>
## `conflicting-metaclass`
@ -144,7 +144,7 @@ class C(A, B): ...
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20conflicting-metaclass)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L191)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L192)
</details>
## `cyclic-class-definition`
@ -175,7 +175,7 @@ class B(A): ...
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20cyclic-class-definition)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L217)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L218)
</details>
## `duplicate-base`
@ -201,7 +201,7 @@ class B(A, A): ...
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20duplicate-base)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L261)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L262)
</details>
## `duplicate-kw-only`
@ -238,7 +238,7 @@ class A: # Crash at runtime
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20duplicate-kw-only)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L282)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L283)
</details>
## `escape-character-in-forward-annotation`
@ -315,69 +315,6 @@ def test(): -> "Literal[5]":
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fstring_annotation.rs#L86)
</details>
## `incompatible-slots`
**Default level**: error
<details>
<summary>detects class definitions whose MRO has conflicting <code>__slots__</code></summary>
### What it does
Checks for classes whose bases define incompatible `__slots__`.
### Why is this bad?
Inheriting from bases with incompatible `__slots__`s
will lead to a `TypeError` at runtime.
Classes with no or empty `__slots__` are always compatible:
```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:
```python
class A:
__slots__ = ("a", "b")
class B:
__slots__ = ("a", "b") # Even if the values are the same
## TypeError: multiple bases have instance lay-out conflict
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.
```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
```
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20incompatible-slots)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L314)
</details>
## `inconsistent-mro`
**Default level**: error
@ -404,7 +341,7 @@ class C(A, B): ...
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20inconsistent-mro)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L400)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L425)
</details>
## `index-out-of-bounds`
@ -429,7 +366,94 @@ t[3] # IndexError: tuple index out of range
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20index-out-of-bounds)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L424)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L449)
</details>
## `instance-layout-conflict`
**Default level**: error
<details>
<summary>detects class definitions that raise <code>TypeError</code> due to instance layout conflict</summary>
### What it does
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 conflicting instance memory layouts
will lead to a `TypeError` at runtime.
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.
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:
__slots__ = ("a", "b")
class B:
__slots__ = ("a", "b") # Even if the values are the same
## TypeError: multiple bases have instance lay-out conflict
class C(A, B): ...
```
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:
```python
class A(int, float): ... # TypeError: multiple bases have instance lay-out conflict
```
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
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20instance-layout-conflict)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L315)
</details>
## `invalid-argument-type`
@ -455,7 +479,7 @@ func("foo") # error: [invalid-argument-type]
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-argument-type)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L444)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L469)
</details>
## `invalid-assignment`
@ -482,7 +506,7 @@ a: int = ''
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-assignment)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L484)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L509)
</details>
## `invalid-attribute-access`
@ -515,7 +539,7 @@ C.instance_var = 3 # error: Cannot assign to instance variable
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-attribute-access)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1488)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1513)
</details>
## `invalid-base`
@ -538,7 +562,7 @@ class A(42): ... # error: [invalid-base]
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-base)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L506)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L531)
</details>
## `invalid-context-manager`
@ -564,7 +588,7 @@ with 1:
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-context-manager)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L557)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L582)
</details>
## `invalid-declaration`
@ -592,7 +616,7 @@ a: str
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-declaration)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L578)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L603)
</details>
## `invalid-exception-caught`
@ -633,7 +657,7 @@ except ZeroDivisionError:
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-exception-caught)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L601)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L626)
</details>
## `invalid-generic-class`
@ -664,7 +688,7 @@ class C[U](Generic[T]): ...
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-generic-class)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L637)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L662)
</details>
## `invalid-legacy-type-variable`
@ -697,7 +721,7 @@ def f(t: TypeVar("U")): ...
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-legacy-type-variable)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L663)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L688)
</details>
## `invalid-metaclass`
@ -729,7 +753,7 @@ class B(metaclass=f): ...
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-metaclass)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L712)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L737)
</details>
## `invalid-overload`
@ -777,7 +801,7 @@ def foo(x: int) -> int: ...
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-overload)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L739)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L764)
</details>
## `invalid-parameter-default`
@ -802,7 +826,7 @@ def f(a: int = ''): ...
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-parameter-default)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L782)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L807)
</details>
## `invalid-protocol`
@ -835,7 +859,7 @@ TypeError: Protocols can only inherit from other protocols, got <class 'int'>
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-protocol)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L372)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L397)
</details>
## `invalid-raise`
@ -883,7 +907,7 @@ def g():
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-raise)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L802)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L827)
</details>
## `invalid-return-type`
@ -907,7 +931,7 @@ def func() -> int:
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-return-type)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L465)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L490)
</details>
## `invalid-super-argument`
@ -951,7 +975,7 @@ super(B, A) # error: `A` does not satisfy `issubclass(A, B)`
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-super-argument)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L845)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L870)
</details>
## `invalid-syntax-in-forward-annotation`
@ -991,7 +1015,7 @@ NewAlias = TypeAliasType(get_name(), int) # error: TypeAliasType name mus
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-alias-type)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L691)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L716)
</details>
## `invalid-type-checking-constant`
@ -1020,7 +1044,7 @@ TYPE_CHECKING = ''
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-checking-constant)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L884)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L909)
</details>
## `invalid-type-form`
@ -1049,7 +1073,7 @@ b: Annotated[int] # `Annotated` expects at least two arguments
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-form)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L908)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L933)
</details>
## `invalid-type-guard-call`
@ -1082,7 +1106,7 @@ f(10) # Error
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-guard-call)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L960)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L985)
</details>
## `invalid-type-guard-definition`
@ -1115,7 +1139,7 @@ class C:
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-guard-definition)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L932)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L957)
</details>
## `invalid-type-variable-constraints`
@ -1149,7 +1173,7 @@ T = TypeVar('T', bound=str) # valid bound TypeVar
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-variable-constraints)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L988)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1013)
</details>
## `missing-argument`
@ -1173,7 +1197,7 @@ func() # TypeError: func() missing 1 required positional argument: 'x'
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20missing-argument)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1017)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1042)
</details>
## `no-matching-overload`
@ -1201,7 +1225,7 @@ func("string") # error: [no-matching-overload]
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20no-matching-overload)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1036)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1061)
</details>
## `non-subscriptable`
@ -1224,7 +1248,7 @@ Subscripting an object that does not support it will raise a `TypeError` at runt
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20non-subscriptable)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1059)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1084)
</details>
## `not-iterable`
@ -1249,7 +1273,7 @@ for i in 34: # TypeError: 'int' object is not iterable
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20not-iterable)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1077)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1102)
</details>
## `parameter-already-assigned`
@ -1275,7 +1299,7 @@ f(1, x=2) # Error raised here
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20parameter-already-assigned)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1128)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1153)
</details>
## `raw-string-type-annotation`
@ -1334,7 +1358,7 @@ static_assert(int(2.0 * 3.0) == 6) # error: does not have a statically known tr
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20static-assert-error)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1464)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1489)
</details>
## `subclass-of-final-class`
@ -1362,7 +1386,7 @@ class B(A): ... # Error raised here
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20subclass-of-final-class)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1219)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1244)
</details>
## `too-many-positional-arguments`
@ -1388,7 +1412,7 @@ f("foo") # Error raised here
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20too-many-positional-arguments)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1264)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1289)
</details>
## `type-assertion-failure`
@ -1415,7 +1439,7 @@ def _(x: int):
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20type-assertion-failure)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1242)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1267)
</details>
## `unavailable-implicit-super-arguments`
@ -1459,7 +1483,7 @@ class A:
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unavailable-implicit-super-arguments)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1285)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1310)
</details>
## `unknown-argument`
@ -1485,7 +1509,7 @@ f(x=1, y=2) # Error raised here
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unknown-argument)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1342)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1367)
</details>
## `unresolved-attribute`
@ -1512,7 +1536,7 @@ A().foo # AttributeError: 'A' object has no attribute 'foo'
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-attribute)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1363)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1388)
</details>
## `unresolved-import`
@ -1536,7 +1560,7 @@ import foo # ModuleNotFoundError: No module named 'foo'
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-import)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1385)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1410)
</details>
## `unresolved-reference`
@ -1560,7 +1584,7 @@ print(x) # NameError: name 'x' is not defined
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-reference)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1404)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1429)
</details>
## `unsupported-bool-conversion`
@ -1596,7 +1620,7 @@ b1 < b2 < b1 # exception raised here
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unsupported-bool-conversion)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1097)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1122)
</details>
## `unsupported-operator`
@ -1623,7 +1647,7 @@ A() + A() # TypeError: unsupported operand type(s) for +: 'A' and 'A'
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unsupported-operator)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1423)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1448)
</details>
## `zero-stepsize-in-slice`
@ -1647,7 +1671,7 @@ l[1:10:0] # ValueError: slice step cannot be zero
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20zero-stepsize-in-slice)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1445)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1470)
</details>
## `invalid-ignore-comment`
@ -1703,7 +1727,7 @@ A.c # AttributeError: type object 'A' has no attribute 'c'
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-unbound-attribute)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1149)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1174)
</details>
## `possibly-unbound-implicit-call`
@ -1734,7 +1758,7 @@ A()[0] # TypeError: 'A' object is not subscriptable
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-unbound-implicit-call)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L114)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L115)
</details>
## `possibly-unbound-import`
@ -1765,7 +1789,7 @@ from module import a # ImportError: cannot import name 'a' from 'module'
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-unbound-import)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1171)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1196)
</details>
## `redundant-cast`
@ -1791,7 +1815,7 @@ cast(int, f()) # Redundant
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20redundant-cast)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1516)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1541)
</details>
## `undefined-reveal`
@ -1814,7 +1838,7 @@ reveal_type(1) # NameError: name 'reveal_type' is not defined
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20undefined-reveal)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1324)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1349)
</details>
## `unknown-rule`
@ -1882,7 +1906,7 @@ class D(C): ... # error: [unsupported-base]
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unsupported-base)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L524)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L549)
</details>
## `division-by-zero`
@ -1905,7 +1929,7 @@ Dividing by zero raises a `ZeroDivisionError` at runtime.
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20division-by-zero)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L243)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L244)
</details>
## `possibly-unresolved-reference`
@ -1932,7 +1956,7 @@ print(x) # NameError: name 'x' is not defined
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-unresolved-reference)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1197)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1222)
</details>
## `unused-ignore-comment`

View file

@ -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

View file

@ -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)
```

View file

@ -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:

View file

@ -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]

View file

@ -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

View file

@ -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
```

View file

@ -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
```

View file

@ -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
```

View file

@ -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
```

View file

@ -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
```

View file

@ -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]))
```

View file

@ -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]

View file

@ -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 {

View file

@ -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)
})
}

View file

@ -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::*;

View file

@ -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(

View file

@ -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.

View file

@ -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 {

View file

@ -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);
}
}
}

View file

@ -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),

View file

@ -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>) {

20
ty.schema.json generated
View file

@ -391,16 +391,6 @@
}
]
},
"incompatible-slots": {
"title": "detects class definitions whose MRO has conflicting `__slots__`",
"description": "## What it does\nChecks for classes whose bases define incompatible `__slots__`.\n\n## Why is this bad?\nInheriting from bases with incompatible `__slots__`s\nwill lead to a `TypeError` at runtime.\n\nClasses with no or empty `__slots__` are always compatible:\n\n```python\nclass A: ...\nclass B:\n __slots__ = ()\nclass C:\n __slots__ = (\"a\", \"b\")\n\n# fine\nclass D(A, B, C): ...\n```\n\nMultiple inheritance from more than one different class\ndefining non-empty `__slots__` is not allowed:\n\n```python\nclass A:\n __slots__ = (\"a\", \"b\")\n\nclass B:\n __slots__ = (\"a\", \"b\") # Even if the values are the same\n\n# TypeError: multiple bases have instance lay-out conflict\nclass C(A, B): ...\n```\n\n## Known problems\nDynamic (not tuple or string literal) `__slots__` are not checked.\nAdditionally, classes inheriting from built-in classes with implicit layouts\nlike `str` or `int` are also not checked.\n\n```pycon\n>>> hasattr(int, \"__slots__\")\nFalse\n>>> hasattr(str, \"__slots__\")\nFalse\n>>> class A(int, str): ...\nTraceback (most recent call last):\n File \"<python-input-0>\", line 1, in <module>\n class A(int, str): ...\nTypeError: multiple bases have instance lay-out conflict\n```",
"default": "error",
"oneOf": [
{
"$ref": "#/definitions/Level"
}
]
},
"inconsistent-mro": {
"title": "detects class definitions with an inconsistent MRO",
"description": "## What it does\nChecks for classes with an inconsistent [method resolution order] (MRO).\n\n## Why is this bad?\nClasses with an inconsistent MRO will raise a `TypeError` at runtime.\n\n## Examples\n```python\nclass A: ...\nclass B(A): ...\n\n# TypeError: Cannot create a consistent method resolution order\nclass C(A, B): ...\n```\n\n[method resolution order]: https://docs.python.org/3/glossary.html#term-method-resolution-order",
@ -421,6 +411,16 @@
}
]
},
"instance-layout-conflict": {
"title": "detects class definitions that raise `TypeError` due to instance layout conflict",
"description": "## What it does\nChecks for classes definitions which will fail at runtime due to\n\"instance memory layout conflicts\".\n\nThis error is usually caused by attempting to combine multiple classes\nthat define non-empty `__slots__` in a class's [Method Resolution Order]\n(MRO), or by attempting to combine multiple builtin classes in a class's\nMRO.\n\n## Why is this bad?\nInheriting from bases with conflicting instance memory layouts\nwill lead to a `TypeError` at runtime.\n\nAn instance memory layout conflict occurs when CPython cannot determine\nthe memory layout instances of a class should have, because the instance\nmemory layout of one of its bases conflicts with the instance memory layout\nof one or more of its other bases.\n\nFor example, if a Python class defines non-empty `__slots__`, this will\nimpact the memory layout of instances of that class. Multiple inheritance\nfrom more than one different class defining non-empty `__slots__` is not\nallowed:\n\n```python\nclass A:\n __slots__ = (\"a\", \"b\")\n\nclass B:\n __slots__ = (\"a\", \"b\") # Even if the values are the same\n\n# TypeError: multiple bases have instance lay-out conflict\nclass C(A, B): ...\n```\n\nAn instance layout conflict can also be caused by attempting to use\nmultiple inheritance with two builtin classes, due to the way that these\nclasses are implemented in a CPython C extension:\n\n```python\nclass A(int, float): ... # TypeError: multiple bases have instance lay-out conflict\n```\n\nNote that pure-Python classes with no `__slots__`, or pure-Python classes\nwith empty `__slots__`, are always compatible:\n\n```python\nclass A: ...\nclass B:\n __slots__ = ()\nclass C:\n __slots__ = (\"a\", \"b\")\n\n# fine\nclass D(A, B, C): ...\n```\n\n## Known problems\nClasses that have \"dynamic\" definitions of `__slots__` (definitions do not consist\nof string literals, or tuples of string literals) are not currently considered solid\nbases by ty.\n\nAdditionally, this check is not exhaustive: many C extensions (including several in\nthe standard library) define classes that use extended memory layouts and thus cannot\ncoexist in a single MRO. Since it is currently not possible to represent this fact in\nstub files, having a full knowledge of these classes is also impossible. When it comes\nto classes that do not define `__slots__` at the Python level, therefore, ty, currently\nonly hard-codes a number of cases where it knows that a class will produce instances with\nan atypical memory layout.\n\n## Further reading\n- [CPython documentation: `__slots__`](https://docs.python.org/3/reference/datamodel.html#slots)\n- [CPython documentation: Method Resolution Order](https://docs.python.org/3/glossary.html#term-method-resolution-order)\n\n[Method Resolution Order]: https://docs.python.org/3/glossary.html#term-method-resolution-order",
"default": "error",
"oneOf": [
{
"$ref": "#/definitions/Level"
}
]
},
"invalid-argument-type": {
"title": "detects call arguments whose type is not assignable to the corresponding typed parameter",
"description": "## What it does\nDetects call arguments whose type is not assignable to the corresponding typed parameter.\n\n## Why is this bad?\nPassing an argument of a type the function (or callable object) does not accept violates\nthe expectations of the function author and may cause unexpected runtime errors within the\nbody of the function.\n\n## Examples\n```python\ndef func(x: int): ...\nfunc(\"foo\") # error: [invalid-argument-type]\n```",