diff --git a/crates/ty/docs/rules.md b/crates/ty/docs/rules.md
index 38d54a65f9..d036fb64bc 100644
--- a/crates/ty/docs/rules.md
+++ b/crates/ty/docs/rules.md
@@ -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)
## `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)
## `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)
## `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)
## `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)
## `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)
## `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)
## `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)
-## `incompatible-slots`
-
-**Default level**: error
-
-
-detects class definitions whose MRO has conflicting __slots__
-
-### 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 "", line 1, in
- 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)
-
-
## `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)
## `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)
+
+
+## `instance-layout-conflict`
+
+**Default level**: error
+
+
+detects class definitions that raise TypeError due to instance layout conflict
+
+### 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)
## `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)
## `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)
## `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)
## `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)
## `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)
## `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)
## `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)
## `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)
## `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)
## `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)
## `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)
## `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)
## `invalid-protocol`
@@ -835,7 +859,7 @@ TypeError: Protocols can only inherit from other protocols, got
### 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)
## `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)
## `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)
## `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)
## `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)
## `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)
## `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)
## `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)
## `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)
## `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)
## `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)
## `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)
## `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)
## `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)
## `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)
## `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)
## `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)
## `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)
## `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)
## `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)
## `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)
## `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)
## `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)
## `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)
## `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)
## `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)
## `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)
## `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)
## `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)
## `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)
## `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)
## `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)
## `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)
## `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)
## `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)
## `unused-ignore-comment`
diff --git a/crates/ty_python_semantic/resources/mdtest/annotations/callable.md b/crates/ty_python_semantic/resources/mdtest/annotations/callable.md
index b5420c2873..133a055ee9 100644
--- a/crates/ty_python_semantic/resources/mdtest/annotations/callable.md
+++ b/crates/ty_python_semantic/resources/mdtest/annotations/callable.md
@@ -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
diff --git a/crates/ty_python_semantic/resources/mdtest/annotations/int_float_complex.md b/crates/ty_python_semantic/resources/mdtest/annotations/int_float_complex.md
index e32bb5d108..fada88bd9b 100644
--- a/crates/ty_python_semantic/resources/mdtest/annotations/int_float_complex.md
+++ b/crates/ty_python_semantic/resources/mdtest/annotations/int_float_complex.md
@@ -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)
+```
diff --git a/crates/ty_python_semantic/resources/mdtest/conditional/match.md b/crates/ty_python_semantic/resources/mdtest/conditional/match.md
index 4947de76b9..532c8c46cf 100644
--- a/crates/ty_python_semantic/resources/mdtest/conditional/match.md
+++ b/crates/ty_python_semantic/resources/mdtest/conditional/match.md
@@ -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:
diff --git a/crates/ty_python_semantic/resources/mdtest/generics/pep695/variables.md b/crates/ty_python_semantic/resources/mdtest/generics/pep695/variables.md
index 8d64ed34a7..d8149b808f 100644
--- a/crates/ty_python_semantic/resources/mdtest/generics/pep695/variables.md
+++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/variables.md
@@ -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]
diff --git a/crates/ty_python_semantic/resources/mdtest/slots.md b/crates/ty_python_semantic/resources/mdtest/instance_layout_conflict.md
similarity index 51%
rename from crates/ty_python_semantic/resources/mdtest/slots.md
rename to crates/ty_python_semantic/resources/mdtest/instance_layout_conflict.md
index 05e44eb4a3..4f5bc1dd2b 100644
--- a/crates/ty_python_semantic/resources/mdtest/slots.md
+++ b/crates/ty_python_semantic/resources/mdtest/instance_layout_conflict.md
@@ -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
+
+
```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
+
+
+
+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
diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/elif_else.md b/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/elif_else.md
index bfa741428b..96b2153b90 100644
--- a/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/elif_else.md
+++ b/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/elif_else.md
@@ -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
```
diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/match.md b/crates/ty_python_semantic/resources/mdtest/narrow/match.md
index a1cd8842f3..b6b0ec90e8 100644
--- a/crates/ty_python_semantic/resources/mdtest/narrow/match.md
+++ b/crates/ty_python_semantic/resources/mdtest/narrow/match.md
@@ -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
```
diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/type_guards.md b/crates/ty_python_semantic/resources/mdtest/narrow/type_guards.md
index eaefacacdb..69586641c9 100644
--- a/crates/ty_python_semantic/resources/mdtest/narrow/type_guards.md
+++ b/crates/ty_python_semantic/resources/mdtest/narrow/type_guards.md
@@ -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
```
diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/instance_layout_conf…_-_Tests_for_ty's_`inst…_-_Built-ins_with_impli…_(f5857d64ce69ca1d).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/instance_layout_conf…_-_Tests_for_ty's_`inst…_-_Built-ins_with_impli…_(f5857d64ce69ca1d).snap
new file mode 100644
index 0000000000..8c9a693f17
--- /dev/null
+++ b/crates/ty_python_semantic/resources/mdtest/snapshots/instance_layout_conf…_-_Tests_for_ty's_`inst…_-_Built-ins_with_impli…_(f5857d64ce69ca1d).snap
@@ -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
+
+```
diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/instance_layout_conf…_-_Tests_for_ty's_`inst…_-_`__slots__`___incompa…_(98b54233987eb654).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/instance_layout_conf…_-_Tests_for_ty's_`inst…_-_`__slots__`___incompa…_(98b54233987eb654).snap
new file mode 100644
index 0000000000..be6151d4fb
--- /dev/null
+++ b/crates/ty_python_semantic/resources/mdtest/snapshots/instance_layout_conf…_-_Tests_for_ty's_`inst…_-_`__slots__`___incompa…_(98b54233987eb654).snap
@@ -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
+
+```
diff --git a/crates/ty_python_semantic/resources/mdtest/type_properties/is_disjoint_from.md b/crates/ty_python_semantic/resources/mdtest/type_properties/is_disjoint_from.md
index b94093e867..d8094d2e78 100644
--- a/crates/ty_python_semantic/resources/mdtest/type_properties/is_disjoint_from.md
+++ b/crates/ty_python_semantic/resources/mdtest/type_properties/is_disjoint_from.md
@@ -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]))
```
diff --git a/crates/ty_python_semantic/resources/mdtest/type_properties/materialization.md b/crates/ty_python_semantic/resources/mdtest/type_properties/materialization.md
index cf03b08d21..03adadce98 100644
--- a/crates/ty_python_semantic/resources/mdtest/type_properties/materialization.md
+++ b/crates/ty_python_semantic/resources/mdtest/type_properties/materialization.md
@@ -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]
diff --git a/crates/ty_python_semantic/src/lib.rs b/crates/ty_python_semantic/src/lib.rs
index 9728cf3731..ad2562a9df 100644
--- a/crates/ty_python_semantic/src/lib.rs
+++ b/crates/ty_python_semantic/src/lib.rs
@@ -39,6 +39,7 @@ mod util;
pub mod pull_types;
type FxOrderSet = ordermap::set::OrderSet>;
+type FxIndexMap = indexmap::IndexMap>;
/// Returns the default registry with all known semantic lints.
pub fn default_lint_registry() -> &'static LintRegistry {
diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs
index eafe6d4f67..091e3b2b84 100644
--- a/crates/ty_python_semantic/src/types.rs
+++ b/crates/ty_python_semantic/src/types.rs
@@ -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)
})
}
diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs
index 3716078370..b28d57cba2 100644
--- a/crates/ty_python_semantic/src/types/class.rs
+++ b/crates/ty_python_semantic/src/types/class.rs
@@ -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> {
+ 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> {
+ 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> {
+ 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- > {
@@ -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::*;
diff --git a/crates/ty_python_semantic/src/types/diagnostic.rs b/crates/ty_python_semantic/src/types/diagnostic.rs
index fa71871e8b..9cd5f861b4 100644
--- a/crates/ty_python_semantic/src/types/diagnostic.rs
+++ b/crates/ty_python_semantic/src/types/diagnostic.rs
@@ -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 "", line 1, in
- /// 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, 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(
diff --git a/crates/ty_python_semantic/src/types/infer.rs b/crates/ty_python_semantic/src/types/infer.rs
index ed9f534b41..ca321f308a 100644
--- a/crates/ty_python_semantic/src/types/infer.rs
+++ b/crates/ty_python_semantic/src/types/infer.rs
@@ -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.
diff --git a/crates/ty_python_semantic/src/types/instance.rs b/crates/ty_python_semantic/src/types/instance.rs
index f8c0db572d..73e05af5c0 100644
--- a/crates/ty_python_semantic/src/types/instance.rs
+++ b/crates/ty_python_semantic/src/types/instance.rs
@@ -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 {
diff --git a/crates/ty_python_semantic/src/types/slots.rs b/crates/ty_python_semantic/src/types/slots.rs
deleted file mode 100644
index 2872406385..0000000000
--- a/crates/ty_python_semantic/src/types/slots.rs
+++ /dev/null
@@ -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);
- }
- }
-}
diff --git a/crates/ty_python_semantic/src/types/subclass_of.rs b/crates/ty_python_semantic/src/types/subclass_of.rs
index 244945b5ca..70f4d5f4e7 100644
--- a/crates/ty_python_semantic/src/types/subclass_of.rs
+++ b/crates/ty_python_semantic/src/types/subclass_of.rs
@@ -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),
diff --git a/crates/ty_python_semantic/src/types/tuple.rs b/crates/ty_python_semantic/src/types/tuple.rs
index d189b05072..0e95610025 100644
--- a/crates/ty_python_semantic/src/types/tuple.rs
+++ b/crates/ty_python_semantic/src/types/tuple.rs
@@ -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) {
diff --git a/ty.schema.json b/ty.schema.json
index e6d3b64f6f..ff6cd7e943 100644
--- a/ty.schema.json
+++ b/ty.schema.json
@@ -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 \"\", line 1, in \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```",