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```",