mirror of
https://github.com/astral-sh/ruff.git
synced 2025-08-04 18:58:04 +00:00
[ty] Improve disjointness inference for NominalInstanceType
s and SubclassOfType
s (#18864)
Co-authored-by: Carl Meyer <carl@astral.sh>
This commit is contained in:
parent
d89f75f9cc
commit
9d8cba4e8b
23 changed files with 1255 additions and 442 deletions
262
crates/ty/docs/rules.md
generated
262
crates/ty/docs/rules.md
generated
|
@ -52,7 +52,7 @@ Calling a non-callable object will raise a `TypeError` at runtime.
|
|||
|
||||
### Links
|
||||
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20call-non-callable)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L96)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L97)
|
||||
</details>
|
||||
|
||||
## `conflicting-argument-forms`
|
||||
|
@ -83,7 +83,7 @@ f(int) # error
|
|||
|
||||
### Links
|
||||
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20conflicting-argument-forms)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L140)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L141)
|
||||
</details>
|
||||
|
||||
## `conflicting-declarations`
|
||||
|
@ -113,7 +113,7 @@ a = 1
|
|||
|
||||
### Links
|
||||
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20conflicting-declarations)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L166)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L167)
|
||||
</details>
|
||||
|
||||
## `conflicting-metaclass`
|
||||
|
@ -144,7 +144,7 @@ class C(A, B): ...
|
|||
|
||||
### Links
|
||||
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20conflicting-metaclass)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L191)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L192)
|
||||
</details>
|
||||
|
||||
## `cyclic-class-definition`
|
||||
|
@ -175,7 +175,7 @@ class B(A): ...
|
|||
|
||||
### Links
|
||||
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20cyclic-class-definition)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L217)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L218)
|
||||
</details>
|
||||
|
||||
## `duplicate-base`
|
||||
|
@ -201,7 +201,7 @@ class B(A, A): ...
|
|||
|
||||
### Links
|
||||
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20duplicate-base)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L261)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L262)
|
||||
</details>
|
||||
|
||||
## `duplicate-kw-only`
|
||||
|
@ -238,7 +238,7 @@ class A: # Crash at runtime
|
|||
|
||||
### Links
|
||||
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20duplicate-kw-only)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L282)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L283)
|
||||
</details>
|
||||
|
||||
## `escape-character-in-forward-annotation`
|
||||
|
@ -315,69 +315,6 @@ def test(): -> "Literal[5]":
|
|||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fstring_annotation.rs#L86)
|
||||
</details>
|
||||
|
||||
## `incompatible-slots`
|
||||
|
||||
**Default level**: error
|
||||
|
||||
<details>
|
||||
<summary>detects class definitions whose MRO has conflicting <code>__slots__</code></summary>
|
||||
|
||||
### What it does
|
||||
Checks for classes whose bases define incompatible `__slots__`.
|
||||
|
||||
### Why is this bad?
|
||||
Inheriting from bases with incompatible `__slots__`s
|
||||
will lead to a `TypeError` at runtime.
|
||||
|
||||
Classes with no or empty `__slots__` are always compatible:
|
||||
|
||||
```python
|
||||
class A: ...
|
||||
class B:
|
||||
__slots__ = ()
|
||||
class C:
|
||||
__slots__ = ("a", "b")
|
||||
|
||||
## fine
|
||||
class D(A, B, C): ...
|
||||
```
|
||||
|
||||
Multiple inheritance from more than one different class
|
||||
defining non-empty `__slots__` is not allowed:
|
||||
|
||||
```python
|
||||
class A:
|
||||
__slots__ = ("a", "b")
|
||||
|
||||
class B:
|
||||
__slots__ = ("a", "b") # Even if the values are the same
|
||||
|
||||
## TypeError: multiple bases have instance lay-out conflict
|
||||
class C(A, B): ...
|
||||
```
|
||||
|
||||
### Known problems
|
||||
Dynamic (not tuple or string literal) `__slots__` are not checked.
|
||||
Additionally, classes inheriting from built-in classes with implicit layouts
|
||||
like `str` or `int` are also not checked.
|
||||
|
||||
```pycon
|
||||
>>> hasattr(int, "__slots__")
|
||||
False
|
||||
>>> hasattr(str, "__slots__")
|
||||
False
|
||||
>>> class A(int, str): ...
|
||||
Traceback (most recent call last):
|
||||
File "<python-input-0>", line 1, in <module>
|
||||
class A(int, str): ...
|
||||
TypeError: multiple bases have instance lay-out conflict
|
||||
```
|
||||
|
||||
### Links
|
||||
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20incompatible-slots)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L314)
|
||||
</details>
|
||||
|
||||
## `inconsistent-mro`
|
||||
|
||||
**Default level**: error
|
||||
|
@ -404,7 +341,7 @@ class C(A, B): ...
|
|||
|
||||
### Links
|
||||
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20inconsistent-mro)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L400)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L425)
|
||||
</details>
|
||||
|
||||
## `index-out-of-bounds`
|
||||
|
@ -429,7 +366,94 @@ t[3] # IndexError: tuple index out of range
|
|||
|
||||
### Links
|
||||
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20index-out-of-bounds)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L424)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L449)
|
||||
</details>
|
||||
|
||||
## `instance-layout-conflict`
|
||||
|
||||
**Default level**: error
|
||||
|
||||
<details>
|
||||
<summary>detects class definitions that raise <code>TypeError</code> due to instance layout conflict</summary>
|
||||
|
||||
### What it does
|
||||
Checks for classes definitions which will fail at runtime due to
|
||||
"instance memory layout conflicts".
|
||||
|
||||
This error is usually caused by attempting to combine multiple classes
|
||||
that define non-empty `__slots__` in a class's [Method Resolution Order]
|
||||
(MRO), or by attempting to combine multiple builtin classes in a class's
|
||||
MRO.
|
||||
|
||||
### Why is this bad?
|
||||
Inheriting from bases with conflicting instance memory layouts
|
||||
will lead to a `TypeError` at runtime.
|
||||
|
||||
An instance memory layout conflict occurs when CPython cannot determine
|
||||
the memory layout instances of a class should have, because the instance
|
||||
memory layout of one of its bases conflicts with the instance memory layout
|
||||
of one or more of its other bases.
|
||||
|
||||
For example, if a Python class defines non-empty `__slots__`, this will
|
||||
impact the memory layout of instances of that class. Multiple inheritance
|
||||
from more than one different class defining non-empty `__slots__` is not
|
||||
allowed:
|
||||
|
||||
```python
|
||||
class A:
|
||||
__slots__ = ("a", "b")
|
||||
|
||||
class B:
|
||||
__slots__ = ("a", "b") # Even if the values are the same
|
||||
|
||||
## TypeError: multiple bases have instance lay-out conflict
|
||||
class C(A, B): ...
|
||||
```
|
||||
|
||||
An instance layout conflict can also be caused by attempting to use
|
||||
multiple inheritance with two builtin classes, due to the way that these
|
||||
classes are implemented in a CPython C extension:
|
||||
|
||||
```python
|
||||
class A(int, float): ... # TypeError: multiple bases have instance lay-out conflict
|
||||
```
|
||||
|
||||
Note that pure-Python classes with no `__slots__`, or pure-Python classes
|
||||
with empty `__slots__`, are always compatible:
|
||||
|
||||
```python
|
||||
class A: ...
|
||||
class B:
|
||||
__slots__ = ()
|
||||
class C:
|
||||
__slots__ = ("a", "b")
|
||||
|
||||
## fine
|
||||
class D(A, B, C): ...
|
||||
```
|
||||
|
||||
### Known problems
|
||||
Classes that have "dynamic" definitions of `__slots__` (definitions do not consist
|
||||
of string literals, or tuples of string literals) are not currently considered solid
|
||||
bases by ty.
|
||||
|
||||
Additionally, this check is not exhaustive: many C extensions (including several in
|
||||
the standard library) define classes that use extended memory layouts and thus cannot
|
||||
coexist in a single MRO. Since it is currently not possible to represent this fact in
|
||||
stub files, having a full knowledge of these classes is also impossible. When it comes
|
||||
to classes that do not define `__slots__` at the Python level, therefore, ty, currently
|
||||
only hard-codes a number of cases where it knows that a class will produce instances with
|
||||
an atypical memory layout.
|
||||
|
||||
### Further reading
|
||||
- [CPython documentation: `__slots__`](https://docs.python.org/3/reference/datamodel.html#slots)
|
||||
- [CPython documentation: Method Resolution Order](https://docs.python.org/3/glossary.html#term-method-resolution-order)
|
||||
|
||||
[Method Resolution Order]: https://docs.python.org/3/glossary.html#term-method-resolution-order
|
||||
|
||||
### Links
|
||||
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20instance-layout-conflict)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L315)
|
||||
</details>
|
||||
|
||||
## `invalid-argument-type`
|
||||
|
@ -455,7 +479,7 @@ func("foo") # error: [invalid-argument-type]
|
|||
|
||||
### Links
|
||||
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-argument-type)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L444)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L469)
|
||||
</details>
|
||||
|
||||
## `invalid-assignment`
|
||||
|
@ -482,7 +506,7 @@ a: int = ''
|
|||
|
||||
### Links
|
||||
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-assignment)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L484)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L509)
|
||||
</details>
|
||||
|
||||
## `invalid-attribute-access`
|
||||
|
@ -515,7 +539,7 @@ C.instance_var = 3 # error: Cannot assign to instance variable
|
|||
|
||||
### Links
|
||||
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-attribute-access)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1488)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1513)
|
||||
</details>
|
||||
|
||||
## `invalid-base`
|
||||
|
@ -538,7 +562,7 @@ class A(42): ... # error: [invalid-base]
|
|||
|
||||
### Links
|
||||
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-base)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L506)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L531)
|
||||
</details>
|
||||
|
||||
## `invalid-context-manager`
|
||||
|
@ -564,7 +588,7 @@ with 1:
|
|||
|
||||
### Links
|
||||
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-context-manager)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L557)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L582)
|
||||
</details>
|
||||
|
||||
## `invalid-declaration`
|
||||
|
@ -592,7 +616,7 @@ a: str
|
|||
|
||||
### Links
|
||||
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-declaration)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L578)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L603)
|
||||
</details>
|
||||
|
||||
## `invalid-exception-caught`
|
||||
|
@ -633,7 +657,7 @@ except ZeroDivisionError:
|
|||
|
||||
### Links
|
||||
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-exception-caught)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L601)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L626)
|
||||
</details>
|
||||
|
||||
## `invalid-generic-class`
|
||||
|
@ -664,7 +688,7 @@ class C[U](Generic[T]): ...
|
|||
|
||||
### Links
|
||||
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-generic-class)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L637)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L662)
|
||||
</details>
|
||||
|
||||
## `invalid-legacy-type-variable`
|
||||
|
@ -697,7 +721,7 @@ def f(t: TypeVar("U")): ...
|
|||
|
||||
### Links
|
||||
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-legacy-type-variable)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L663)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L688)
|
||||
</details>
|
||||
|
||||
## `invalid-metaclass`
|
||||
|
@ -729,7 +753,7 @@ class B(metaclass=f): ...
|
|||
|
||||
### Links
|
||||
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-metaclass)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L712)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L737)
|
||||
</details>
|
||||
|
||||
## `invalid-overload`
|
||||
|
@ -777,7 +801,7 @@ def foo(x: int) -> int: ...
|
|||
|
||||
### Links
|
||||
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-overload)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L739)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L764)
|
||||
</details>
|
||||
|
||||
## `invalid-parameter-default`
|
||||
|
@ -802,7 +826,7 @@ def f(a: int = ''): ...
|
|||
|
||||
### Links
|
||||
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-parameter-default)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L782)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L807)
|
||||
</details>
|
||||
|
||||
## `invalid-protocol`
|
||||
|
@ -835,7 +859,7 @@ TypeError: Protocols can only inherit from other protocols, got <class 'int'>
|
|||
|
||||
### Links
|
||||
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-protocol)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L372)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L397)
|
||||
</details>
|
||||
|
||||
## `invalid-raise`
|
||||
|
@ -883,7 +907,7 @@ def g():
|
|||
|
||||
### Links
|
||||
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-raise)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L802)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L827)
|
||||
</details>
|
||||
|
||||
## `invalid-return-type`
|
||||
|
@ -907,7 +931,7 @@ def func() -> int:
|
|||
|
||||
### Links
|
||||
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-return-type)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L465)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L490)
|
||||
</details>
|
||||
|
||||
## `invalid-super-argument`
|
||||
|
@ -951,7 +975,7 @@ super(B, A) # error: `A` does not satisfy `issubclass(A, B)`
|
|||
|
||||
### Links
|
||||
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-super-argument)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L845)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L870)
|
||||
</details>
|
||||
|
||||
## `invalid-syntax-in-forward-annotation`
|
||||
|
@ -991,7 +1015,7 @@ NewAlias = TypeAliasType(get_name(), int) # error: TypeAliasType name mus
|
|||
|
||||
### Links
|
||||
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-alias-type)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L691)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L716)
|
||||
</details>
|
||||
|
||||
## `invalid-type-checking-constant`
|
||||
|
@ -1020,7 +1044,7 @@ TYPE_CHECKING = ''
|
|||
|
||||
### Links
|
||||
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-checking-constant)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L884)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L909)
|
||||
</details>
|
||||
|
||||
## `invalid-type-form`
|
||||
|
@ -1049,7 +1073,7 @@ b: Annotated[int] # `Annotated` expects at least two arguments
|
|||
|
||||
### Links
|
||||
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-form)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L908)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L933)
|
||||
</details>
|
||||
|
||||
## `invalid-type-guard-call`
|
||||
|
@ -1082,7 +1106,7 @@ f(10) # Error
|
|||
|
||||
### Links
|
||||
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-guard-call)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L960)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L985)
|
||||
</details>
|
||||
|
||||
## `invalid-type-guard-definition`
|
||||
|
@ -1115,7 +1139,7 @@ class C:
|
|||
|
||||
### Links
|
||||
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-guard-definition)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L932)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L957)
|
||||
</details>
|
||||
|
||||
## `invalid-type-variable-constraints`
|
||||
|
@ -1149,7 +1173,7 @@ T = TypeVar('T', bound=str) # valid bound TypeVar
|
|||
|
||||
### Links
|
||||
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-variable-constraints)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L988)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1013)
|
||||
</details>
|
||||
|
||||
## `missing-argument`
|
||||
|
@ -1173,7 +1197,7 @@ func() # TypeError: func() missing 1 required positional argument: 'x'
|
|||
|
||||
### Links
|
||||
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20missing-argument)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1017)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1042)
|
||||
</details>
|
||||
|
||||
## `no-matching-overload`
|
||||
|
@ -1201,7 +1225,7 @@ func("string") # error: [no-matching-overload]
|
|||
|
||||
### Links
|
||||
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20no-matching-overload)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1036)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1061)
|
||||
</details>
|
||||
|
||||
## `non-subscriptable`
|
||||
|
@ -1224,7 +1248,7 @@ Subscripting an object that does not support it will raise a `TypeError` at runt
|
|||
|
||||
### Links
|
||||
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20non-subscriptable)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1059)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1084)
|
||||
</details>
|
||||
|
||||
## `not-iterable`
|
||||
|
@ -1249,7 +1273,7 @@ for i in 34: # TypeError: 'int' object is not iterable
|
|||
|
||||
### Links
|
||||
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20not-iterable)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1077)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1102)
|
||||
</details>
|
||||
|
||||
## `parameter-already-assigned`
|
||||
|
@ -1275,7 +1299,7 @@ f(1, x=2) # Error raised here
|
|||
|
||||
### Links
|
||||
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20parameter-already-assigned)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1128)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1153)
|
||||
</details>
|
||||
|
||||
## `raw-string-type-annotation`
|
||||
|
@ -1334,7 +1358,7 @@ static_assert(int(2.0 * 3.0) == 6) # error: does not have a statically known tr
|
|||
|
||||
### Links
|
||||
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20static-assert-error)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1464)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1489)
|
||||
</details>
|
||||
|
||||
## `subclass-of-final-class`
|
||||
|
@ -1362,7 +1386,7 @@ class B(A): ... # Error raised here
|
|||
|
||||
### Links
|
||||
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20subclass-of-final-class)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1219)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1244)
|
||||
</details>
|
||||
|
||||
## `too-many-positional-arguments`
|
||||
|
@ -1388,7 +1412,7 @@ f("foo") # Error raised here
|
|||
|
||||
### Links
|
||||
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20too-many-positional-arguments)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1264)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1289)
|
||||
</details>
|
||||
|
||||
## `type-assertion-failure`
|
||||
|
@ -1415,7 +1439,7 @@ def _(x: int):
|
|||
|
||||
### Links
|
||||
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20type-assertion-failure)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1242)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1267)
|
||||
</details>
|
||||
|
||||
## `unavailable-implicit-super-arguments`
|
||||
|
@ -1459,7 +1483,7 @@ class A:
|
|||
|
||||
### Links
|
||||
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unavailable-implicit-super-arguments)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1285)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1310)
|
||||
</details>
|
||||
|
||||
## `unknown-argument`
|
||||
|
@ -1485,7 +1509,7 @@ f(x=1, y=2) # Error raised here
|
|||
|
||||
### Links
|
||||
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unknown-argument)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1342)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1367)
|
||||
</details>
|
||||
|
||||
## `unresolved-attribute`
|
||||
|
@ -1512,7 +1536,7 @@ A().foo # AttributeError: 'A' object has no attribute 'foo'
|
|||
|
||||
### Links
|
||||
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-attribute)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1363)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1388)
|
||||
</details>
|
||||
|
||||
## `unresolved-import`
|
||||
|
@ -1536,7 +1560,7 @@ import foo # ModuleNotFoundError: No module named 'foo'
|
|||
|
||||
### Links
|
||||
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-import)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1385)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1410)
|
||||
</details>
|
||||
|
||||
## `unresolved-reference`
|
||||
|
@ -1560,7 +1584,7 @@ print(x) # NameError: name 'x' is not defined
|
|||
|
||||
### Links
|
||||
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-reference)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1404)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1429)
|
||||
</details>
|
||||
|
||||
## `unsupported-bool-conversion`
|
||||
|
@ -1596,7 +1620,7 @@ b1 < b2 < b1 # exception raised here
|
|||
|
||||
### Links
|
||||
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unsupported-bool-conversion)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1097)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1122)
|
||||
</details>
|
||||
|
||||
## `unsupported-operator`
|
||||
|
@ -1623,7 +1647,7 @@ A() + A() # TypeError: unsupported operand type(s) for +: 'A' and 'A'
|
|||
|
||||
### Links
|
||||
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unsupported-operator)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1423)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1448)
|
||||
</details>
|
||||
|
||||
## `zero-stepsize-in-slice`
|
||||
|
@ -1647,7 +1671,7 @@ l[1:10:0] # ValueError: slice step cannot be zero
|
|||
|
||||
### Links
|
||||
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20zero-stepsize-in-slice)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1445)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1470)
|
||||
</details>
|
||||
|
||||
## `invalid-ignore-comment`
|
||||
|
@ -1703,7 +1727,7 @@ A.c # AttributeError: type object 'A' has no attribute 'c'
|
|||
|
||||
### Links
|
||||
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-unbound-attribute)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1149)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1174)
|
||||
</details>
|
||||
|
||||
## `possibly-unbound-implicit-call`
|
||||
|
@ -1734,7 +1758,7 @@ A()[0] # TypeError: 'A' object is not subscriptable
|
|||
|
||||
### Links
|
||||
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-unbound-implicit-call)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L114)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L115)
|
||||
</details>
|
||||
|
||||
## `possibly-unbound-import`
|
||||
|
@ -1765,7 +1789,7 @@ from module import a # ImportError: cannot import name 'a' from 'module'
|
|||
|
||||
### Links
|
||||
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-unbound-import)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1171)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1196)
|
||||
</details>
|
||||
|
||||
## `redundant-cast`
|
||||
|
@ -1791,7 +1815,7 @@ cast(int, f()) # Redundant
|
|||
|
||||
### Links
|
||||
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20redundant-cast)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1516)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1541)
|
||||
</details>
|
||||
|
||||
## `undefined-reveal`
|
||||
|
@ -1814,7 +1838,7 @@ reveal_type(1) # NameError: name 'reveal_type' is not defined
|
|||
|
||||
### Links
|
||||
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20undefined-reveal)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1324)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1349)
|
||||
</details>
|
||||
|
||||
## `unknown-rule`
|
||||
|
@ -1882,7 +1906,7 @@ class D(C): ... # error: [unsupported-base]
|
|||
|
||||
### Links
|
||||
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unsupported-base)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L524)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L549)
|
||||
</details>
|
||||
|
||||
## `division-by-zero`
|
||||
|
@ -1905,7 +1929,7 @@ Dividing by zero raises a `ZeroDivisionError` at runtime.
|
|||
|
||||
### Links
|
||||
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20division-by-zero)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L243)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L244)
|
||||
</details>
|
||||
|
||||
## `possibly-unresolved-reference`
|
||||
|
@ -1932,7 +1956,7 @@ print(x) # NameError: name 'x' is not defined
|
|||
|
||||
### Links
|
||||
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-unresolved-reference)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1197)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1222)
|
||||
</details>
|
||||
|
||||
## `unused-ignore-comment`
|
||||
|
|
|
@ -192,16 +192,18 @@ def _(
|
|||
from typing import Callable, Union
|
||||
from ty_extensions import Intersection, Not
|
||||
|
||||
class Foo: ...
|
||||
|
||||
def _(
|
||||
c: Intersection[Callable[[Union[int, str]], int], int],
|
||||
d: Intersection[int, Callable[[Union[int, str]], int]],
|
||||
e: Intersection[int, Callable[[Union[int, str]], int], str],
|
||||
f: Intersection[Not[Callable[[int, str], Intersection[int, str]]]],
|
||||
e: Intersection[int, Callable[[Union[int, str]], int], Foo],
|
||||
f: Intersection[Not[Callable[[int, str], Intersection[int, Foo]]]],
|
||||
):
|
||||
reveal_type(c) # revealed: ((int | str, /) -> int) & int
|
||||
reveal_type(d) # revealed: int & ((int | str, /) -> int)
|
||||
reveal_type(e) # revealed: int & ((int | str, /) -> int) & str
|
||||
reveal_type(f) # revealed: ~((int, str, /) -> int & str)
|
||||
reveal_type(e) # revealed: int & ((int | str, /) -> int) & Foo
|
||||
reveal_type(f) # revealed: ~((int, str, /) -> int & Foo)
|
||||
```
|
||||
|
||||
## Nested
|
||||
|
|
|
@ -88,3 +88,26 @@ def assigns_complex(x: complex):
|
|||
def f(x: complex):
|
||||
reveal_type(x) # revealed: int | float | complex
|
||||
```
|
||||
|
||||
## Narrowing
|
||||
|
||||
`int`, `float` and `complex` are all disjoint, which means that the union `int | float` can easily
|
||||
be narrowed to `int` or `float`:
|
||||
|
||||
```py
|
||||
from typing_extensions import assert_type
|
||||
from ty_extensions import JustFloat
|
||||
|
||||
def f(x: complex):
|
||||
reveal_type(x) # revealed: int | float | complex
|
||||
|
||||
if isinstance(x, int):
|
||||
reveal_type(x) # revealed: int
|
||||
elif isinstance(x, float):
|
||||
reveal_type(x) # revealed: float
|
||||
else:
|
||||
reveal_type(x) # revealed: complex
|
||||
|
||||
assert isinstance(x, float)
|
||||
assert_type(x, JustFloat)
|
||||
```
|
||||
|
|
|
@ -271,7 +271,9 @@ def _(target: int | None | float):
|
|||
|
||||
reveal_type(y) # revealed: Literal[1, 2]
|
||||
|
||||
def _(target: None | str):
|
||||
class Foo: ...
|
||||
|
||||
def _(target: None | Foo):
|
||||
y = 1
|
||||
|
||||
match target:
|
||||
|
|
|
@ -653,7 +653,7 @@ from ty_extensions import Not
|
|||
|
||||
def remove_constraint[T: (int, str, bool)](t: T) -> None:
|
||||
def _(x: Intersection[T, Not[int]]) -> None:
|
||||
reveal_type(x) # revealed: str & ~int
|
||||
reveal_type(x) # revealed: str
|
||||
|
||||
def _(x: Intersection[T, Not[str]]) -> None:
|
||||
# With OneOf this would be OneOf[int, bool]
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# `__slots__`
|
||||
# Tests for ty's `instance-layout-conflict` error code
|
||||
|
||||
## Not specified and empty
|
||||
## `__slots__`: not specified or empty
|
||||
|
||||
```py
|
||||
class A: ...
|
||||
|
@ -17,7 +17,9 @@ class BC(B, C): ... # fine
|
|||
class ABC(A, B, C): ... # fine
|
||||
```
|
||||
|
||||
## Incompatible tuples
|
||||
## `__slots__`: incompatible tuples
|
||||
|
||||
<!-- snapshot-diagnostics -->
|
||||
|
||||
```py
|
||||
class A:
|
||||
|
@ -26,13 +28,13 @@ class A:
|
|||
class B:
|
||||
__slots__ = ("c", "d")
|
||||
|
||||
class C(
|
||||
A, # error: [incompatible-slots]
|
||||
B, # error: [incompatible-slots]
|
||||
class C( # error: [instance-layout-conflict]
|
||||
A,
|
||||
B,
|
||||
): ...
|
||||
```
|
||||
|
||||
## Same value
|
||||
## `__slots__` are the same value
|
||||
|
||||
```py
|
||||
class A:
|
||||
|
@ -41,13 +43,13 @@ class A:
|
|||
class B:
|
||||
__slots__ = ("a", "b")
|
||||
|
||||
class C(
|
||||
A, # error: [incompatible-slots]
|
||||
B, # error: [incompatible-slots]
|
||||
class C( # error: [instance-layout-conflict]
|
||||
A,
|
||||
B,
|
||||
): ...
|
||||
```
|
||||
|
||||
## Strings
|
||||
## `__slots__` is a string
|
||||
|
||||
```py
|
||||
class A:
|
||||
|
@ -56,13 +58,13 @@ class A:
|
|||
class B:
|
||||
__slots__ = ("abc",)
|
||||
|
||||
class AB(
|
||||
A, # error: [incompatible-slots]
|
||||
B, # error: [incompatible-slots]
|
||||
class AB( # error: [instance-layout-conflict]
|
||||
A,
|
||||
B,
|
||||
): ...
|
||||
```
|
||||
|
||||
## Invalid
|
||||
## Invalid `__slots__` definitions
|
||||
|
||||
TODO: Emit diagnostics
|
||||
|
||||
|
@ -83,7 +85,7 @@ class NonIdentifier3:
|
|||
__slots__ = (e for e in ("lorem", "42"))
|
||||
```
|
||||
|
||||
## Inheritance
|
||||
## Inherited `__slots__`
|
||||
|
||||
```py
|
||||
class A:
|
||||
|
@ -95,13 +97,13 @@ class C:
|
|||
__slots__ = ("c", "d")
|
||||
|
||||
class D(C): ...
|
||||
class E(
|
||||
B, # error: [incompatible-slots]
|
||||
D, # error: [incompatible-slots]
|
||||
class E( # error: [instance-layout-conflict]
|
||||
B,
|
||||
D,
|
||||
): ...
|
||||
```
|
||||
|
||||
## Single solid base
|
||||
## A single "solid base"
|
||||
|
||||
```py
|
||||
class A:
|
||||
|
@ -113,7 +115,7 @@ class D(B, A): ... # fine
|
|||
class E(B, C, A): ... # fine
|
||||
```
|
||||
|
||||
## Post-hoc modifications
|
||||
## Post-hoc modifications to `__slots__`
|
||||
|
||||
```py
|
||||
class A:
|
||||
|
@ -125,15 +127,105 @@ reveal_type(A.__slots__) # revealed: tuple[Literal["a"], Literal["b"]]
|
|||
class B:
|
||||
__slots__ = ("c", "d")
|
||||
|
||||
class C(
|
||||
A, # error: [incompatible-slots]
|
||||
B, # error: [incompatible-slots]
|
||||
class C( # error: [instance-layout-conflict]
|
||||
A,
|
||||
B,
|
||||
): ...
|
||||
```
|
||||
|
||||
## Explicitly annotated `__slots__`
|
||||
|
||||
We do not emit false positives on classes with empty `__slots__` definitions, even if the
|
||||
`__slots__` definitions are annotated with variadic tuples:
|
||||
|
||||
```py
|
||||
class Foo:
|
||||
__slots__: tuple[str, ...] = ()
|
||||
|
||||
class Bar:
|
||||
__slots__: tuple[str, ...] = ()
|
||||
|
||||
class Baz(Foo, Bar): ... # fine
|
||||
```
|
||||
|
||||
## Built-ins with implicit layouts
|
||||
|
||||
<!-- snapshot-diagnostics -->
|
||||
|
||||
Certain classes implemented in C extensions also have an extended instance memory layout, in the
|
||||
same way as classes that define non-empty `__slots__`. (CPython internally calls all such classes
|
||||
with a unique instance memory layout "solid bases", and we also borrow this term.) There is
|
||||
currently no generalized way for ty to detect such a C-extension class, as there is currently no way
|
||||
of expressing the fact that a class is a solid base in a stub file. However, ty special-cases
|
||||
certain builtin classes in order to detect that attempting to combine them in a single MRO would
|
||||
fail:
|
||||
|
||||
```py
|
||||
# fmt: off
|
||||
|
||||
class A( # error: [instance-layout-conflict]
|
||||
int,
|
||||
str
|
||||
): ...
|
||||
|
||||
class B:
|
||||
__slots__ = ("b",)
|
||||
|
||||
class C( # error: [instance-layout-conflict]
|
||||
int,
|
||||
B,
|
||||
): ...
|
||||
class D(int): ...
|
||||
|
||||
class E( # error: [instance-layout-conflict]
|
||||
D,
|
||||
str
|
||||
): ...
|
||||
|
||||
class F(int, str, bytes, bytearray): ... # error: [instance-layout-conflict]
|
||||
|
||||
# fmt: on
|
||||
```
|
||||
|
||||
We avoid emitting an `instance-layout-conflict` diagnostic for this class definition, because
|
||||
`range` is `@final`, so we'll complain about the `class` statement anyway:
|
||||
|
||||
```py
|
||||
class Foo(range, str): ... # error: [subclass-of-final-class]
|
||||
```
|
||||
|
||||
## Multiple "solid bases" where one is a subclass of the other
|
||||
|
||||
A class is permitted to multiple-inherit from multiple solid bases if one is a subclass of the
|
||||
other:
|
||||
|
||||
```py
|
||||
class A:
|
||||
__slots__ = ("a",)
|
||||
|
||||
class B(A):
|
||||
__slots__ = ("b",)
|
||||
|
||||
class C(B, A): ... # fine
|
||||
```
|
||||
|
||||
The same principle, but a more complex example:
|
||||
|
||||
```py
|
||||
class AA:
|
||||
__slots__ = ("a",)
|
||||
|
||||
class BB(AA):
|
||||
__slots__ = ("b",)
|
||||
|
||||
class CC(BB): ...
|
||||
class DD(AA): ...
|
||||
class FF(CC, DD): ... # fine
|
||||
```
|
||||
|
||||
## False negatives
|
||||
|
||||
### Possibly unbound
|
||||
### Possibly unbound `__slots__`
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
|
@ -148,7 +240,7 @@ def _(flag: bool):
|
|||
class C(A, B): ...
|
||||
```
|
||||
|
||||
### Bound but with different types
|
||||
### Bound `__slots__` but with different types
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
|
@ -165,7 +257,7 @@ def _(flag: bool):
|
|||
class C(A, B): ...
|
||||
```
|
||||
|
||||
### Non-tuples
|
||||
### Non-tuple `__slots__` definitions
|
||||
|
||||
```py
|
||||
class A:
|
||||
|
@ -178,13 +270,6 @@ class B:
|
|||
class C(A, B): ...
|
||||
```
|
||||
|
||||
### Built-ins with implicit layouts
|
||||
|
||||
```py
|
||||
# False negative: [incompatible-slots]
|
||||
class A(int, str): ...
|
||||
```
|
||||
|
||||
### Diagnostic if `__slots__` is externally modified
|
||||
|
||||
We special-case type inference for `__slots__` and return the pure inferred type, even if the symbol
|
|
@ -46,12 +46,15 @@ def _(flag1: bool, flag2: bool):
|
|||
## Assignment expressions
|
||||
|
||||
```py
|
||||
def f() -> int | str | None: ...
|
||||
class Foo: ...
|
||||
class Bar: ...
|
||||
|
||||
if isinstance(x := f(), int):
|
||||
reveal_type(x) # revealed: int
|
||||
elif isinstance(x, str):
|
||||
reveal_type(x) # revealed: str & ~int
|
||||
def f() -> Foo | Bar | None: ...
|
||||
|
||||
if isinstance(x := f(), Foo):
|
||||
reveal_type(x) # revealed: Foo
|
||||
elif isinstance(x, Bar):
|
||||
reveal_type(x) # revealed: Bar & ~Foo
|
||||
else:
|
||||
reveal_type(x) # revealed: None
|
||||
```
|
||||
|
|
|
@ -87,7 +87,7 @@ match x:
|
|||
case 6.0:
|
||||
reveal_type(x) # revealed: float
|
||||
case 1j:
|
||||
reveal_type(x) # revealed: complex & ~float
|
||||
reveal_type(x) # revealed: complex
|
||||
case b"foo":
|
||||
reveal_type(x) # revealed: Literal[b"foo"]
|
||||
|
||||
|
@ -137,7 +137,7 @@ match x:
|
|||
case True | False:
|
||||
reveal_type(x) # revealed: bool
|
||||
case 3.14 | 2.718 | 1.414:
|
||||
reveal_type(x) # revealed: float & ~tuple[Unknown, ...]
|
||||
reveal_type(x) # revealed: float
|
||||
|
||||
reveal_type(x) # revealed: object
|
||||
```
|
||||
|
|
|
@ -178,25 +178,26 @@ def _(d: Any):
|
|||
from typing import Any
|
||||
from typing_extensions import TypeGuard, TypeIs
|
||||
|
||||
def guard_str(a: object) -> TypeGuard[str]:
|
||||
class Foo: ...
|
||||
class Bar: ...
|
||||
|
||||
def guard_foo(a: object) -> TypeGuard[Foo]:
|
||||
return True
|
||||
|
||||
def is_int(a: object) -> TypeIs[int]:
|
||||
def is_bar(a: object) -> TypeIs[Bar]:
|
||||
return True
|
||||
```
|
||||
|
||||
```py
|
||||
def _(a: str | int):
|
||||
if guard_str(a):
|
||||
# TODO: Should be `str`
|
||||
reveal_type(a) # revealed: str | int
|
||||
def _(a: Foo | Bar):
|
||||
if guard_foo(a):
|
||||
# TODO: Should be `Foo`
|
||||
reveal_type(a) # revealed: Foo | Bar
|
||||
else:
|
||||
reveal_type(a) # revealed: str | int
|
||||
reveal_type(a) # revealed: Foo | Bar
|
||||
|
||||
if is_int(a):
|
||||
reveal_type(a) # revealed: int
|
||||
if is_bar(a):
|
||||
reveal_type(a) # revealed: Bar
|
||||
else:
|
||||
reveal_type(a) # revealed: str & ~int
|
||||
reveal_type(a) # revealed: Foo & ~Bar
|
||||
```
|
||||
|
||||
Attribute and subscript narrowing is supported:
|
||||
|
@ -209,68 +210,68 @@ T = TypeVar("T")
|
|||
class C(Generic[T]):
|
||||
v: T
|
||||
|
||||
def _(a: tuple[str, int] | tuple[int, str], c: C[Any]):
|
||||
# TODO: Should be `TypeGuard[str @ a[1]]`
|
||||
if reveal_type(guard_str(a[1])): # revealed: @Todo(`TypeGuard[]` special form)
|
||||
# TODO: Should be `tuple[int, str]`
|
||||
reveal_type(a) # revealed: tuple[str, int] | tuple[int, str]
|
||||
# TODO: Should be `str`
|
||||
reveal_type(a[1]) # revealed: int | str
|
||||
def _(a: tuple[Foo, Bar] | tuple[Bar, Foo], c: C[Any]):
|
||||
# TODO: Should be `TypeGuard[Foo @ a[1]]`
|
||||
if reveal_type(guard_foo(a[1])): # revealed: @Todo(`TypeGuard[]` special form)
|
||||
# TODO: Should be `tuple[Bar, Foo]`
|
||||
reveal_type(a) # revealed: tuple[Foo, Bar] | tuple[Bar, Foo]
|
||||
# TODO: Should be `Foo`
|
||||
reveal_type(a[1]) # revealed: Bar | Foo
|
||||
|
||||
if reveal_type(is_int(a[0])): # revealed: TypeIs[int @ a[0]]
|
||||
# TODO: Should be `tuple[int, str]`
|
||||
reveal_type(a) # revealed: tuple[str, int] | tuple[int, str]
|
||||
reveal_type(a[0]) # revealed: int
|
||||
if reveal_type(is_bar(a[0])): # revealed: TypeIs[Bar @ a[0]]
|
||||
# TODO: Should be `tuple[Bar, Bar & Foo]`
|
||||
reveal_type(a) # revealed: tuple[Foo, Bar] | tuple[Bar, Foo]
|
||||
reveal_type(a[0]) # revealed: Bar
|
||||
|
||||
# TODO: Should be `TypeGuard[str @ c.v]`
|
||||
if reveal_type(guard_str(c.v)): # revealed: @Todo(`TypeGuard[]` special form)
|
||||
# TODO: Should be `TypeGuard[Foo @ c.v]`
|
||||
if reveal_type(guard_foo(c.v)): # revealed: @Todo(`TypeGuard[]` special form)
|
||||
reveal_type(c) # revealed: C[Any]
|
||||
# TODO: Should be `str`
|
||||
# TODO: Should be `Foo`
|
||||
reveal_type(c.v) # revealed: Any
|
||||
|
||||
if reveal_type(is_int(c.v)): # revealed: TypeIs[int @ c.v]
|
||||
if reveal_type(is_bar(c.v)): # revealed: TypeIs[Bar @ c.v]
|
||||
reveal_type(c) # revealed: C[Any]
|
||||
reveal_type(c.v) # revealed: Any & int
|
||||
reveal_type(c.v) # revealed: Any & Bar
|
||||
```
|
||||
|
||||
Indirect usage is supported within the same scope:
|
||||
|
||||
```py
|
||||
def _(a: str | int):
|
||||
b = guard_str(a)
|
||||
c = is_int(a)
|
||||
def _(a: Foo | Bar):
|
||||
b = guard_foo(a)
|
||||
c = is_bar(a)
|
||||
|
||||
reveal_type(a) # revealed: str | int
|
||||
# TODO: Should be `TypeGuard[str @ a]`
|
||||
reveal_type(a) # revealed: Foo | Bar
|
||||
# TODO: Should be `TypeGuard[Foo @ a]`
|
||||
reveal_type(b) # revealed: @Todo(`TypeGuard[]` special form)
|
||||
reveal_type(c) # revealed: TypeIs[int @ a]
|
||||
reveal_type(c) # revealed: TypeIs[Bar @ a]
|
||||
|
||||
if b:
|
||||
# TODO should be `str`
|
||||
reveal_type(a) # revealed: str | int
|
||||
# TODO should be `Foo`
|
||||
reveal_type(a) # revealed: Foo | Bar
|
||||
else:
|
||||
reveal_type(a) # revealed: str | int
|
||||
reveal_type(a) # revealed: Foo | Bar
|
||||
|
||||
if c:
|
||||
# TODO should be `int`
|
||||
reveal_type(a) # revealed: str | int
|
||||
# TODO should be `Bar`
|
||||
reveal_type(a) # revealed: Foo | Bar
|
||||
else:
|
||||
# TODO should be `str & ~int`
|
||||
reveal_type(a) # revealed: str | int
|
||||
# TODO should be `Foo & ~Bar`
|
||||
reveal_type(a) # revealed: Foo | Bar
|
||||
```
|
||||
|
||||
Further writes to the narrowed place invalidate the narrowing:
|
||||
|
||||
```py
|
||||
def _(x: str | int, flag: bool) -> None:
|
||||
b = is_int(x)
|
||||
reveal_type(b) # revealed: TypeIs[int @ x]
|
||||
def _(x: Foo | Bar, flag: bool) -> None:
|
||||
b = is_bar(x)
|
||||
reveal_type(b) # revealed: TypeIs[Bar @ x]
|
||||
|
||||
if flag:
|
||||
x = ""
|
||||
x = Foo()
|
||||
|
||||
if b:
|
||||
reveal_type(x) # revealed: str | int
|
||||
reveal_type(x) # revealed: Foo | Bar
|
||||
```
|
||||
|
||||
The `TypeIs` type remains effective across generic boundaries:
|
||||
|
@ -280,19 +281,19 @@ from typing_extensions import TypeVar, reveal_type
|
|||
|
||||
T = TypeVar("T")
|
||||
|
||||
def f(v: object) -> TypeIs[int]:
|
||||
def f(v: object) -> TypeIs[Bar]:
|
||||
return True
|
||||
|
||||
def g(v: T) -> T:
|
||||
return v
|
||||
|
||||
def _(a: str):
|
||||
def _(a: Foo):
|
||||
# `reveal_type()` has the type `[T]() -> T`
|
||||
if reveal_type(f(a)): # revealed: TypeIs[int @ a]
|
||||
reveal_type(a) # revealed: str & int
|
||||
if reveal_type(f(a)): # revealed: TypeIs[Bar @ a]
|
||||
reveal_type(a) # revealed: Foo & Bar
|
||||
|
||||
if g(f(a)):
|
||||
reveal_type(a) # revealed: str & int
|
||||
reveal_type(a) # revealed: Foo & Bar
|
||||
```
|
||||
|
||||
## `TypeGuard` special cases
|
||||
|
@ -301,28 +302,32 @@ def _(a: str):
|
|||
from typing import Any
|
||||
from typing_extensions import TypeGuard, TypeIs
|
||||
|
||||
def guard_int(a: object) -> TypeGuard[int]:
|
||||
class Foo: ...
|
||||
class Bar: ...
|
||||
class Baz(Bar): ...
|
||||
|
||||
def guard_foo(a: object) -> TypeGuard[Foo]:
|
||||
return True
|
||||
|
||||
def is_int(a: object) -> TypeIs[int]:
|
||||
def is_bar(a: object) -> TypeIs[Bar]:
|
||||
return True
|
||||
|
||||
def does_not_narrow_in_negative_case(a: str | int):
|
||||
if not guard_int(a):
|
||||
# TODO: Should be `str`
|
||||
reveal_type(a) # revealed: str | int
|
||||
def does_not_narrow_in_negative_case(a: Foo | Bar):
|
||||
if not guard_foo(a):
|
||||
# TODO: Should be `Bar`
|
||||
reveal_type(a) # revealed: Foo | Bar
|
||||
else:
|
||||
reveal_type(a) # revealed: str | int
|
||||
reveal_type(a) # revealed: Foo | Bar
|
||||
|
||||
def narrowed_type_must_be_exact(a: object, b: bool):
|
||||
if guard_int(b):
|
||||
# TODO: Should be `int`
|
||||
reveal_type(b) # revealed: bool
|
||||
def narrowed_type_must_be_exact(a: object, b: Baz):
|
||||
if guard_foo(b):
|
||||
# TODO: Should be `Foo`
|
||||
reveal_type(b) # revealed: Baz
|
||||
|
||||
if isinstance(a, bool) and is_int(a):
|
||||
reveal_type(a) # revealed: bool
|
||||
if isinstance(a, Baz) and is_bar(a):
|
||||
reveal_type(a) # revealed: Baz
|
||||
|
||||
if isinstance(a, bool) and guard_int(a):
|
||||
# TODO: Should be `int`
|
||||
reveal_type(a) # revealed: bool
|
||||
if isinstance(a, Bar) and guard_foo(a):
|
||||
# TODO: Should be `Foo`
|
||||
reveal_type(a) # revealed: Bar
|
||||
```
|
||||
|
|
|
@ -0,0 +1,173 @@
|
|||
---
|
||||
source: crates/ty_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: instance_layout_conflict.md - Tests for ty's `instance-layout-conflict` error code - Built-ins with implicit layouts
|
||||
mdtest path: crates/ty_python_semantic/resources/mdtest/instance_layout_conflict.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet.py
|
||||
|
||||
```
|
||||
1 | # fmt: off
|
||||
2 |
|
||||
3 | class A( # error: [instance-layout-conflict]
|
||||
4 | int,
|
||||
5 | str
|
||||
6 | ): ...
|
||||
7 |
|
||||
8 | class B:
|
||||
9 | __slots__ = ("b",)
|
||||
10 |
|
||||
11 | class C( # error: [instance-layout-conflict]
|
||||
12 | int,
|
||||
13 | B,
|
||||
14 | ): ...
|
||||
15 | class D(int): ...
|
||||
16 |
|
||||
17 | class E( # error: [instance-layout-conflict]
|
||||
18 | D,
|
||||
19 | str
|
||||
20 | ): ...
|
||||
21 |
|
||||
22 | class F(int, str, bytes, bytearray): ... # error: [instance-layout-conflict]
|
||||
23 |
|
||||
24 | # fmt: on
|
||||
25 | class Foo(range, str): ... # error: [subclass-of-final-class]
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error[instance-layout-conflict]: Class will raise `TypeError` at runtime due to incompatible bases
|
||||
--> src/mdtest_snippet.py:3:7
|
||||
|
|
||||
1 | # fmt: off
|
||||
2 |
|
||||
3 | class A( # error: [instance-layout-conflict]
|
||||
| _______^
|
||||
4 | | int,
|
||||
5 | | str
|
||||
6 | | ): ...
|
||||
| |_^ Bases `int` and `str` cannot be combined in multiple inheritance
|
||||
7 |
|
||||
8 | class B:
|
||||
|
|
||||
info: Two classes cannot coexist in a class's MRO if their instances have incompatible memory layouts
|
||||
--> src/mdtest_snippet.py:4:5
|
||||
|
|
||||
3 | class A( # error: [instance-layout-conflict]
|
||||
4 | int,
|
||||
| --- `int` instances have a distinct memory layout because of the way `int` is implemented in a C extension
|
||||
5 | str
|
||||
| --- `str` instances have a distinct memory layout because of the way `str` is implemented in a C extension
|
||||
6 | ): ...
|
||||
|
|
||||
info: rule `instance-layout-conflict` is enabled by default
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error[instance-layout-conflict]: Class will raise `TypeError` at runtime due to incompatible bases
|
||||
--> src/mdtest_snippet.py:11:7
|
||||
|
|
||||
9 | __slots__ = ("b",)
|
||||
10 |
|
||||
11 | class C( # error: [instance-layout-conflict]
|
||||
| _______^
|
||||
12 | | int,
|
||||
13 | | B,
|
||||
14 | | ): ...
|
||||
| |_^ Bases `int` and `B` cannot be combined in multiple inheritance
|
||||
15 | class D(int): ...
|
||||
|
|
||||
info: Two classes cannot coexist in a class's MRO if their instances have incompatible memory layouts
|
||||
--> src/mdtest_snippet.py:12:5
|
||||
|
|
||||
11 | class C( # error: [instance-layout-conflict]
|
||||
12 | int,
|
||||
| --- `int` instances have a distinct memory layout because of the way `int` is implemented in a C extension
|
||||
13 | B,
|
||||
| - `B` instances have a distinct memory layout because `B` defines non-empty `__slots__`
|
||||
14 | ): ...
|
||||
15 | class D(int): ...
|
||||
|
|
||||
info: rule `instance-layout-conflict` is enabled by default
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error[instance-layout-conflict]: Class will raise `TypeError` at runtime due to incompatible bases
|
||||
--> src/mdtest_snippet.py:17:7
|
||||
|
|
||||
15 | class D(int): ...
|
||||
16 |
|
||||
17 | class E( # error: [instance-layout-conflict]
|
||||
| _______^
|
||||
18 | | D,
|
||||
19 | | str
|
||||
20 | | ): ...
|
||||
| |_^ Bases `D` and `str` cannot be combined in multiple inheritance
|
||||
21 |
|
||||
22 | class F(int, str, bytes, bytearray): ... # error: [instance-layout-conflict]
|
||||
|
|
||||
info: Two classes cannot coexist in a class's MRO if their instances have incompatible memory layouts
|
||||
--> src/mdtest_snippet.py:18:5
|
||||
|
|
||||
17 | class E( # error: [instance-layout-conflict]
|
||||
18 | D,
|
||||
| -
|
||||
| |
|
||||
| `D` instances have a distinct memory layout because `D` inherits from `int`
|
||||
| `int` instances have a distinct memory layout because of the way `int` is implemented in a C extension
|
||||
19 | str
|
||||
| --- `str` instances have a distinct memory layout because of the way `str` is implemented in a C extension
|
||||
20 | ): ...
|
||||
|
|
||||
info: rule `instance-layout-conflict` is enabled by default
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error[instance-layout-conflict]: Class will raise `TypeError` at runtime due to incompatible bases
|
||||
--> src/mdtest_snippet.py:22:7
|
||||
|
|
||||
20 | ): ...
|
||||
21 |
|
||||
22 | class F(int, str, bytes, bytearray): ... # error: [instance-layout-conflict]
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Bases `int`, `str`, `bytes` and `bytearray` cannot be combined in multiple inheritance
|
||||
23 |
|
||||
24 | # fmt: on
|
||||
|
|
||||
info: Two classes cannot coexist in a class's MRO if their instances have incompatible memory layouts
|
||||
--> src/mdtest_snippet.py:22:9
|
||||
|
|
||||
20 | ): ...
|
||||
21 |
|
||||
22 | class F(int, str, bytes, bytearray): ... # error: [instance-layout-conflict]
|
||||
| --- --- ----- --------- `bytearray` instances have a distinct memory layout because of the way `bytearray` is implemented in a C extension
|
||||
| | | |
|
||||
| | | `bytes` instances have a distinct memory layout because of the way `bytes` is implemented in a C extension
|
||||
| | `str` instances have a distinct memory layout because of the way `str` is implemented in a C extension
|
||||
| `int` instances have a distinct memory layout because of the way `int` is implemented in a C extension
|
||||
23 |
|
||||
24 | # fmt: on
|
||||
|
|
||||
info: rule `instance-layout-conflict` is enabled by default
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error[subclass-of-final-class]: Class `Foo` cannot inherit from final class `range`
|
||||
--> src/mdtest_snippet.py:25:11
|
||||
|
|
||||
24 | # fmt: on
|
||||
25 | class Foo(range, str): ... # error: [subclass-of-final-class]
|
||||
| ^^^^^
|
||||
|
|
||||
info: rule `subclass-of-final-class` is enabled by default
|
||||
|
||||
```
|
|
@ -0,0 +1,54 @@
|
|||
---
|
||||
source: crates/ty_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: instance_layout_conflict.md - Tests for ty's `instance-layout-conflict` error code - `__slots__`: incompatible tuples
|
||||
mdtest path: crates/ty_python_semantic/resources/mdtest/instance_layout_conflict.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet.py
|
||||
|
||||
```
|
||||
1 | class A:
|
||||
2 | __slots__ = ("a", "b")
|
||||
3 |
|
||||
4 | class B:
|
||||
5 | __slots__ = ("c", "d")
|
||||
6 |
|
||||
7 | class C( # error: [instance-layout-conflict]
|
||||
8 | A,
|
||||
9 | B,
|
||||
10 | ): ...
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error[instance-layout-conflict]: Class will raise `TypeError` at runtime due to incompatible bases
|
||||
--> src/mdtest_snippet.py:7:7
|
||||
|
|
||||
5 | __slots__ = ("c", "d")
|
||||
6 |
|
||||
7 | class C( # error: [instance-layout-conflict]
|
||||
| _______^
|
||||
8 | | A,
|
||||
9 | | B,
|
||||
10 | | ): ...
|
||||
| |_^ Bases `A` and `B` cannot be combined in multiple inheritance
|
||||
|
|
||||
info: Two classes cannot coexist in a class's MRO if their instances have incompatible memory layouts
|
||||
--> src/mdtest_snippet.py:8:5
|
||||
|
|
||||
7 | class C( # error: [instance-layout-conflict]
|
||||
8 | A,
|
||||
| - `A` instances have a distinct memory layout because `A` defines non-empty `__slots__`
|
||||
9 | B,
|
||||
| - `B` instances have a distinct memory layout because `B` defines non-empty `__slots__`
|
||||
10 | ): ...
|
||||
|
|
||||
info: rule `instance-layout-conflict` is enabled by default
|
||||
|
||||
```
|
|
@ -20,8 +20,6 @@ static_assert(not is_disjoint_from(Any, Not[Any]))
|
|||
|
||||
static_assert(not is_disjoint_from(LiteralString, LiteralString))
|
||||
static_assert(not is_disjoint_from(str, LiteralString))
|
||||
static_assert(not is_disjoint_from(str, type))
|
||||
static_assert(not is_disjoint_from(str, type[Any]))
|
||||
```
|
||||
|
||||
## Class hierarchies
|
||||
|
@ -71,6 +69,88 @@ class UsesMeta2(metaclass=Meta2): ...
|
|||
static_assert(is_disjoint_from(UsesMeta1, UsesMeta2))
|
||||
```
|
||||
|
||||
## `@final` builtin types
|
||||
|
||||
Some builtins types are declared as `@final`:
|
||||
|
||||
```py
|
||||
from ty_extensions import static_assert, is_disjoint_from
|
||||
|
||||
class Foo: ...
|
||||
|
||||
# `range`, `slice` and `memoryview` are all declared as `@final`:
|
||||
static_assert(is_disjoint_from(range, Foo))
|
||||
static_assert(is_disjoint_from(type[range], type[Foo]))
|
||||
static_assert(is_disjoint_from(slice, Foo))
|
||||
static_assert(is_disjoint_from(type[slice], type[Foo]))
|
||||
static_assert(is_disjoint_from(memoryview, Foo))
|
||||
static_assert(is_disjoint_from(type[memoryview], type[Foo]))
|
||||
```
|
||||
|
||||
## "Solid base" builtin types
|
||||
|
||||
Most other builtins can be subclassed and can even be used in multiple inheritance. However, builtin
|
||||
classes *cannot* generally be used in multiple inheritance with other builtin types. This is because
|
||||
the CPython interpreter considers these classes "solid bases": due to the way they are implemented
|
||||
in C, they have atypical instance memory layouts. No class can ever have more than one "solid base"
|
||||
in its MRO.
|
||||
|
||||
It's not currently possible for ty to detect in a generalized way whether a class is a "solid base"
|
||||
or not, but we special-case some commonly used builtin types:
|
||||
|
||||
```py
|
||||
from typing import Any
|
||||
from ty_extensions import static_assert, is_disjoint_from
|
||||
|
||||
class Foo: ...
|
||||
|
||||
static_assert(is_disjoint_from(list, dict))
|
||||
static_assert(is_disjoint_from(list[Foo], dict))
|
||||
static_assert(is_disjoint_from(list[Any], dict))
|
||||
static_assert(is_disjoint_from(list, dict[Foo, Foo]))
|
||||
static_assert(is_disjoint_from(list[Foo], dict[Foo, Foo]))
|
||||
static_assert(is_disjoint_from(list[Any], dict[Foo, Foo]))
|
||||
static_assert(is_disjoint_from(list, dict[Any, Any]))
|
||||
static_assert(is_disjoint_from(list[Foo], dict[Any, Any]))
|
||||
static_assert(is_disjoint_from(list[Any], dict[Any, Any]))
|
||||
static_assert(is_disjoint_from(type[list], type[dict]))
|
||||
```
|
||||
|
||||
## Other solid bases
|
||||
|
||||
As well as certain classes that are implemented in C extensions, any class that declares non-empty
|
||||
`__slots__` is also considered a "solid base"; these types are also considered to be disjoint by ty:
|
||||
|
||||
```py
|
||||
from ty_extensions import static_assert, is_disjoint_from
|
||||
|
||||
class A:
|
||||
__slots__ = ("a",)
|
||||
|
||||
class B:
|
||||
__slots__ = ("a",)
|
||||
|
||||
class C:
|
||||
__slots__ = ()
|
||||
|
||||
static_assert(is_disjoint_from(A, B))
|
||||
static_assert(is_disjoint_from(type[A], type[B]))
|
||||
static_assert(not is_disjoint_from(A, C))
|
||||
static_assert(not is_disjoint_from(type[A], type[C]))
|
||||
static_assert(not is_disjoint_from(B, C))
|
||||
static_assert(not is_disjoint_from(type[B], type[C]))
|
||||
```
|
||||
|
||||
Two solid bases are not disjoint if one inherits from the other, however:
|
||||
|
||||
```py
|
||||
class D(A):
|
||||
__slots__ = ("d",)
|
||||
|
||||
static_assert(is_disjoint_from(D, B))
|
||||
static_assert(not is_disjoint_from(D, A))
|
||||
```
|
||||
|
||||
## Tuple types
|
||||
|
||||
```py
|
||||
|
@ -396,8 +476,10 @@ reveal_type(C.prop) # revealed: property
|
|||
class D:
|
||||
pass
|
||||
|
||||
static_assert(not is_disjoint_from(int, TypeOf[C.prop]))
|
||||
static_assert(not is_disjoint_from(TypeOf[C.prop], int))
|
||||
class Whatever: ...
|
||||
|
||||
static_assert(not is_disjoint_from(Whatever, TypeOf[C.prop]))
|
||||
static_assert(not is_disjoint_from(TypeOf[C.prop], Whatever))
|
||||
static_assert(is_disjoint_from(TypeOf[C.prop], D))
|
||||
static_assert(is_disjoint_from(D, TypeOf[C.prop]))
|
||||
```
|
||||
|
|
|
@ -263,8 +263,10 @@ reveal_type(top_materialization(Intersection[Any | int, tuple[str, Unknown]]))
|
|||
# revealed: Never
|
||||
reveal_type(bottom_materialization(Intersection[Any | int, tuple[str, Unknown]]))
|
||||
|
||||
# revealed: int & tuple[str]
|
||||
reveal_type(bottom_materialization(Intersection[Any | int, tuple[str]]))
|
||||
class Foo: ...
|
||||
|
||||
# revealed: Foo & tuple[str]
|
||||
reveal_type(bottom_materialization(Intersection[Any | Foo, tuple[str]]))
|
||||
|
||||
reveal_type(top_materialization(Intersection[list[Any], list[int]])) # revealed: list[T_all] & list[int]
|
||||
reveal_type(bottom_materialization(Intersection[list[Any], list[int]])) # revealed: list[T_all] & list[int]
|
||||
|
|
|
@ -39,6 +39,7 @@ mod util;
|
|||
pub mod pull_types;
|
||||
|
||||
type FxOrderSet<V> = ordermap::set::OrderSet<V, BuildHasherDefault<FxHasher>>;
|
||||
type FxIndexMap<K, V> = indexmap::IndexMap<K, V, BuildHasherDefault<FxHasher>>;
|
||||
|
||||
/// Returns the default registry with all known semantic lints.
|
||||
pub fn default_lint_registry() -> &'static LintRegistry {
|
||||
|
|
|
@ -75,7 +75,6 @@ mod mro;
|
|||
mod narrow;
|
||||
mod protocol_class;
|
||||
mod signatures;
|
||||
mod slots;
|
||||
mod special_form;
|
||||
mod string_annotation;
|
||||
mod subclass_of;
|
||||
|
@ -1824,6 +1823,8 @@ impl<'db> Type<'db> {
|
|||
}
|
||||
}
|
||||
|
||||
(Type::SubclassOf(left), Type::SubclassOf(right)) => left.is_disjoint_from(db, right),
|
||||
|
||||
(
|
||||
Type::SubclassOf(_),
|
||||
Type::BooleanLiteral(..)
|
||||
|
@ -2107,7 +2108,7 @@ impl<'db> Type<'db> {
|
|||
(Type::Tuple(tuple), Type::NominalInstance(instance))
|
||||
| (Type::NominalInstance(instance), Type::Tuple(tuple)) => {
|
||||
tuple.to_class_type(db).is_some_and(|tuple_class| {
|
||||
instance.is_disjoint_from_nominal_instance_of_class(db, tuple_class)
|
||||
!instance.class.could_coexist_in_mro_with(db, tuple_class)
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -298,6 +298,11 @@ impl<'db> ClassType<'db> {
|
|||
class_literal.definition(db)
|
||||
}
|
||||
|
||||
/// Return `Some` if this class is known to be a [`SolidBase`], or `None` if it is not.
|
||||
pub(super) fn as_solid_base(self, db: &'db dyn Db) -> Option<SolidBase<'db>> {
|
||||
self.class_literal(db).0.as_solid_base(db)
|
||||
}
|
||||
|
||||
/// Return `true` if this class represents `known_class`
|
||||
pub(crate) fn is_known(self, db: &'db dyn Db, known_class: KnownClass) -> bool {
|
||||
self.known(db) == Some(known_class)
|
||||
|
@ -434,6 +439,69 @@ impl<'db> ClassType<'db> {
|
|||
.apply_optional_specialization(db, specialization)
|
||||
}
|
||||
|
||||
/// Return the [`SolidBase`] that appears first in the MRO of this class.
|
||||
///
|
||||
/// Returns `None` if this class does not have any solid bases in its MRO.
|
||||
pub(super) fn nearest_solid_base(self, db: &'db dyn Db) -> Option<SolidBase<'db>> {
|
||||
self.iter_mro(db)
|
||||
.filter_map(ClassBase::into_class)
|
||||
.find_map(|base| base.as_solid_base(db))
|
||||
}
|
||||
|
||||
/// Return `true` if this class could coexist in an MRO with `other`.
|
||||
///
|
||||
/// For two given classes `A` and `B`, it is often possible to say for sure
|
||||
/// that there could never exist any class `C` that inherits from both `A` and `B`.
|
||||
/// In these situations, this method returns `false`; in all others, it returns `true`.
|
||||
pub(super) fn could_coexist_in_mro_with(self, db: &'db dyn Db, other: Self) -> bool {
|
||||
if self == other {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Optimisation: if either class is `@final`, we only need to do one `is_subclass_of` call.
|
||||
if self.is_final(db) {
|
||||
return self.is_subclass_of(db, other);
|
||||
}
|
||||
if other.is_final(db) {
|
||||
return other.is_subclass_of(db, self);
|
||||
}
|
||||
|
||||
// Two solid bases can only coexist in an MRO if one is a subclass of the other.
|
||||
if self.nearest_solid_base(db).is_some_and(|solid_base_1| {
|
||||
other.nearest_solid_base(db).is_some_and(|solid_base_2| {
|
||||
!solid_base_1.could_coexist_in_mro_with(db, &solid_base_2)
|
||||
})
|
||||
}) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check to see whether the metaclasses of `self` and `other` are disjoint.
|
||||
// Avoid this check if the metaclass of either `self` or `other` is `type`,
|
||||
// however, since we end up with infinite recursion in that case due to the fact
|
||||
// that `type` is its own metaclass (and we know that `type` can coexist in an MRO
|
||||
// with any other arbitrary class, anyway).
|
||||
let type_class = KnownClass::Type.to_class_literal(db);
|
||||
let self_metaclass = self.metaclass(db);
|
||||
if self_metaclass == type_class {
|
||||
return true;
|
||||
}
|
||||
let other_metaclass = other.metaclass(db);
|
||||
if other_metaclass == type_class {
|
||||
return true;
|
||||
}
|
||||
let Some(self_metaclass_instance) = self_metaclass.to_instance(db) else {
|
||||
return true;
|
||||
};
|
||||
let Some(other_metaclass_instance) = other_metaclass.to_instance(db) else {
|
||||
return true;
|
||||
};
|
||||
if self_metaclass_instance.is_disjoint_from(db, other_metaclass_instance) {
|
||||
return false;
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
/// Return a type representing "the set of all instances of the metaclass of this class".
|
||||
pub(super) fn metaclass_instance_type(self, db: &'db dyn Db) -> Type<'db> {
|
||||
self
|
||||
|
@ -860,6 +928,19 @@ impl<'db> ClassLiteral<'db> {
|
|||
.collect()
|
||||
}
|
||||
|
||||
/// Return `Some()` if this class is known to be a [`SolidBase`], or `None` if it is not.
|
||||
pub(super) fn as_solid_base(self, db: &'db dyn Db) -> Option<SolidBase<'db>> {
|
||||
if let Some(known_class) = self.known(db) {
|
||||
known_class
|
||||
.is_solid_base()
|
||||
.then_some(SolidBase::hard_coded(self))
|
||||
} else if SlotsKind::from(db, self) == SlotsKind::NotEmpty {
|
||||
Some(SolidBase::due_to_dunder_slots(self))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Iterate over this class's explicit bases, filtering out any bases that are not class
|
||||
/// objects, and applying default specialization to any unspecialized generic class literals.
|
||||
fn fully_static_explicit_bases(self, db: &'db dyn Db) -> impl Iterator<Item = ClassType<'db>> {
|
||||
|
@ -2122,6 +2203,60 @@ impl InheritanceCycle {
|
|||
}
|
||||
}
|
||||
|
||||
/// CPython internally considers a class a "solid base" if it has an atypical instance memory layout,
|
||||
/// with additional memory "slots" for each instance, besides the default object metadata and an
|
||||
/// attribute dictionary. A "solid base" can be a class defined in a C extension which defines C-level
|
||||
/// instance slots, or a Python class that defines non-empty `__slots__`.
|
||||
///
|
||||
/// Two solid bases can only coexist in a class's MRO if one is a subclass of the other. Knowing if
|
||||
/// a class is "solid base" or not is therefore valuable for inferring whether two instance types or
|
||||
/// two subclass-of types are disjoint from each other. It also allows us to detect possible
|
||||
/// `TypeError`s resulting from class definitions.
|
||||
#[derive(Debug, PartialEq, Eq, Hash, Copy, Clone)]
|
||||
pub(super) struct SolidBase<'db> {
|
||||
pub(super) class: ClassLiteral<'db>,
|
||||
pub(super) kind: SolidBaseKind,
|
||||
}
|
||||
|
||||
impl<'db> SolidBase<'db> {
|
||||
/// Creates a [`SolidBase`] instance where we know the class is a solid base
|
||||
/// because it is special-cased by ty.
|
||||
fn hard_coded(class: ClassLiteral<'db>) -> Self {
|
||||
Self {
|
||||
class,
|
||||
kind: SolidBaseKind::HardCoded,
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a [`SolidBase`] instance where we know the class is a solid base
|
||||
/// because of its `__slots__` definition.
|
||||
fn due_to_dunder_slots(class: ClassLiteral<'db>) -> Self {
|
||||
Self {
|
||||
class,
|
||||
kind: SolidBaseKind::DefinesSlots,
|
||||
}
|
||||
}
|
||||
|
||||
/// Two solid bases can only coexist in a class's MRO if one is a subclass of the other
|
||||
fn could_coexist_in_mro_with(&self, db: &'db dyn Db, other: &Self) -> bool {
|
||||
self == other
|
||||
|| self
|
||||
.class
|
||||
.is_subclass_of(db, None, other.class.default_specialization(db))
|
||||
|| other
|
||||
.class
|
||||
.is_subclass_of(db, None, self.class.default_specialization(db))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
|
||||
pub(super) enum SolidBaseKind {
|
||||
/// We know the class is a solid base because of some hardcoded knowledge in ty.
|
||||
HardCoded,
|
||||
/// We know the class is a solid base because it has a non-empty `__slots__` definition.
|
||||
DefinesSlots,
|
||||
}
|
||||
|
||||
/// Non-exhaustive enumeration of known classes (e.g. `builtins.int`, `typing.Any`, ...) to allow
|
||||
/// for easier syntax when interacting with very common classes.
|
||||
///
|
||||
|
@ -2294,6 +2429,83 @@ impl<'db> KnownClass {
|
|||
}
|
||||
}
|
||||
|
||||
/// Return `true` if this class is a [`SolidBase`]
|
||||
const fn is_solid_base(self) -> bool {
|
||||
match self {
|
||||
Self::Object => false,
|
||||
|
||||
// Most non-`@final` builtins (other than `object`) are solid bases.
|
||||
Self::Set
|
||||
| Self::FrozenSet
|
||||
| Self::BaseException
|
||||
| Self::Bytearray
|
||||
| Self::Int
|
||||
| Self::Float
|
||||
| Self::Complex
|
||||
| Self::Str
|
||||
| Self::List
|
||||
| Self::Tuple
|
||||
| Self::Dict
|
||||
| Self::Slice
|
||||
| Self::Property
|
||||
| Self::Staticmethod
|
||||
| Self::Classmethod
|
||||
| Self::Type
|
||||
| Self::ModuleType
|
||||
| Self::Super
|
||||
| Self::GenericAlias
|
||||
| Self::Deque
|
||||
| Self::Bytes => true,
|
||||
|
||||
// It doesn't really make sense to ask the question for `@final` types,
|
||||
// since these are "more than solid bases". But we'll anyway infer a `@final`
|
||||
// class as being disjoint from a class that doesn't appear in its MRO,
|
||||
// and we'll anyway complain if we see a class definition that includes a
|
||||
// `@final` class in its bases. We therefore return `false` here to avoid
|
||||
// unnecessary duplicate diagnostics elsewhere.
|
||||
Self::TypeVarTuple
|
||||
| Self::TypeAliasType
|
||||
| Self::UnionType
|
||||
| Self::NoDefaultType
|
||||
| Self::MethodType
|
||||
| Self::MethodWrapperType
|
||||
| Self::FunctionType
|
||||
| Self::GeneratorType
|
||||
| Self::AsyncGeneratorType
|
||||
| Self::StdlibAlias
|
||||
| Self::SpecialForm
|
||||
| Self::TypeVar
|
||||
| Self::ParamSpec
|
||||
| Self::ParamSpecArgs
|
||||
| Self::ParamSpecKwargs
|
||||
| Self::WrapperDescriptorType
|
||||
| Self::EllipsisType
|
||||
| Self::NotImplementedType
|
||||
| Self::KwOnly
|
||||
| Self::VersionInfo
|
||||
| Self::Bool
|
||||
| Self::NoneType => false,
|
||||
|
||||
// Anything with a *runtime* MRO (N.B. sometimes different from the MRO that typeshed gives!)
|
||||
// with length >2, or anything that is implemented in pure Python, is not a solid base.
|
||||
Self::ABCMeta
|
||||
| Self::Any
|
||||
| Self::Enum
|
||||
| Self::ChainMap
|
||||
| Self::Exception
|
||||
| Self::ExceptionGroup
|
||||
| Self::Field
|
||||
| Self::SupportsIndex
|
||||
| Self::NamedTuple
|
||||
| Self::NamedTupleFallback
|
||||
| Self::Counter
|
||||
| Self::DefaultDict
|
||||
| Self::OrderedDict
|
||||
| Self::NewType
|
||||
| Self::BaseExceptionGroup => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Return `true` if this class is a protocol class.
|
||||
///
|
||||
/// In an ideal world, perhaps we wouldn't hardcode this knowledge here;
|
||||
|
@ -3114,6 +3326,52 @@ pub(super) enum MetaclassErrorKind<'db> {
|
|||
Cycle,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
||||
enum SlotsKind {
|
||||
/// `__slots__` is not found in the class.
|
||||
NotSpecified,
|
||||
/// `__slots__` is defined but empty: `__slots__ = ()`.
|
||||
Empty,
|
||||
/// `__slots__` is defined and is not empty: `__slots__ = ("a", "b")`.
|
||||
NotEmpty,
|
||||
/// `__slots__` is defined but its value is dynamic:
|
||||
/// * `__slots__ = tuple(a for a in b)`
|
||||
/// * `__slots__ = ["a", "b"]`
|
||||
Dynamic,
|
||||
}
|
||||
|
||||
impl SlotsKind {
|
||||
fn from(db: &dyn Db, base: ClassLiteral) -> Self {
|
||||
let Place::Type(slots_ty, bound) = base.own_class_member(db, None, "__slots__").place
|
||||
else {
|
||||
return Self::NotSpecified;
|
||||
};
|
||||
|
||||
if matches!(bound, Boundness::PossiblyUnbound) {
|
||||
return Self::Dynamic;
|
||||
}
|
||||
|
||||
match slots_ty {
|
||||
// __slots__ = ("a", "b")
|
||||
Type::Tuple(tuple) => {
|
||||
let tuple = tuple.tuple(db);
|
||||
if tuple.is_variadic() {
|
||||
Self::Dynamic
|
||||
} else if tuple.is_empty() {
|
||||
Self::Empty
|
||||
} else {
|
||||
Self::NotEmpty
|
||||
}
|
||||
}
|
||||
|
||||
// __slots__ = "abc" # Same as `("abc",)`
|
||||
Type::StringLiteral(_) => Self::NotEmpty,
|
||||
|
||||
_ => Self::Dynamic,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
|
|
@ -8,6 +8,7 @@ use super::{
|
|||
use crate::lint::{Level, LintRegistryBuilder, LintStatus};
|
||||
use crate::suppression::FileSuppressionId;
|
||||
use crate::types::LintDiagnosticGuard;
|
||||
use crate::types::class::{SolidBase, SolidBaseKind};
|
||||
use crate::types::function::KnownFunction;
|
||||
use crate::types::string_annotation::{
|
||||
BYTE_STRING_TYPE_ANNOTATION, ESCAPE_CHARACTER_IN_FORWARD_ANNOTATION, FSTRING_TYPE_ANNOTATION,
|
||||
|
@ -16,7 +17,7 @@ use crate::types::string_annotation::{
|
|||
};
|
||||
use crate::types::tuple::TupleType;
|
||||
use crate::types::{SpecialFormType, Type, protocol_class::ProtocolClassLiteral};
|
||||
use crate::{Db, Module, ModuleName, Program, declare_lint};
|
||||
use crate::{Db, FxIndexMap, Module, ModuleName, Program, declare_lint};
|
||||
use itertools::Itertools;
|
||||
use ruff_db::diagnostic::{Annotation, Diagnostic, Severity, SubDiagnostic};
|
||||
use ruff_python_ast::{self as ast, AnyNodeRef};
|
||||
|
@ -35,7 +36,7 @@ pub(crate) fn register_lints(registry: &mut LintRegistryBuilder) {
|
|||
registry.register_lint(&DIVISION_BY_ZERO);
|
||||
registry.register_lint(&DUPLICATE_BASE);
|
||||
registry.register_lint(&DUPLICATE_KW_ONLY);
|
||||
registry.register_lint(&INCOMPATIBLE_SLOTS);
|
||||
registry.register_lint(&INSTANCE_LAYOUT_CONFLICT);
|
||||
registry.register_lint(&INCONSISTENT_MRO);
|
||||
registry.register_lint(&INDEX_OUT_OF_BOUNDS);
|
||||
registry.register_lint(&INVALID_ARGUMENT_TYPE);
|
||||
|
@ -313,27 +314,27 @@ declare_lint! {
|
|||
|
||||
declare_lint! {
|
||||
/// ## What it does
|
||||
/// Checks for classes whose bases define incompatible `__slots__`.
|
||||
/// Checks for classes definitions which will fail at runtime due to
|
||||
/// "instance memory layout conflicts".
|
||||
///
|
||||
/// This error is usually caused by attempting to combine multiple classes
|
||||
/// that define non-empty `__slots__` in a class's [Method Resolution Order]
|
||||
/// (MRO), or by attempting to combine multiple builtin classes in a class's
|
||||
/// MRO.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Inheriting from bases with incompatible `__slots__`s
|
||||
/// Inheriting from bases with conflicting instance memory layouts
|
||||
/// will lead to a `TypeError` at runtime.
|
||||
///
|
||||
/// Classes with no or empty `__slots__` are always compatible:
|
||||
/// An instance memory layout conflict occurs when CPython cannot determine
|
||||
/// the memory layout instances of a class should have, because the instance
|
||||
/// memory layout of one of its bases conflicts with the instance memory layout
|
||||
/// of one or more of its other bases.
|
||||
///
|
||||
/// ```python
|
||||
/// class A: ...
|
||||
/// class B:
|
||||
/// __slots__ = ()
|
||||
/// class C:
|
||||
/// __slots__ = ("a", "b")
|
||||
///
|
||||
/// # fine
|
||||
/// class D(A, B, C): ...
|
||||
/// ```
|
||||
///
|
||||
/// Multiple inheritance from more than one different class
|
||||
/// defining non-empty `__slots__` is not allowed:
|
||||
/// For example, if a Python class defines non-empty `__slots__`, this will
|
||||
/// impact the memory layout of instances of that class. Multiple inheritance
|
||||
/// from more than one different class defining non-empty `__slots__` is not
|
||||
/// allowed:
|
||||
///
|
||||
/// ```python
|
||||
/// class A:
|
||||
|
@ -346,24 +347,48 @@ declare_lint! {
|
|||
/// class C(A, B): ...
|
||||
/// ```
|
||||
///
|
||||
/// ## Known problems
|
||||
/// Dynamic (not tuple or string literal) `__slots__` are not checked.
|
||||
/// Additionally, classes inheriting from built-in classes with implicit layouts
|
||||
/// like `str` or `int` are also not checked.
|
||||
/// An instance layout conflict can also be caused by attempting to use
|
||||
/// multiple inheritance with two builtin classes, due to the way that these
|
||||
/// classes are implemented in a CPython C extension:
|
||||
///
|
||||
/// ```pycon
|
||||
/// >>> hasattr(int, "__slots__")
|
||||
/// False
|
||||
/// >>> hasattr(str, "__slots__")
|
||||
/// False
|
||||
/// >>> class A(int, str): ...
|
||||
/// Traceback (most recent call last):
|
||||
/// File "<python-input-0>", line 1, in <module>
|
||||
/// class A(int, str): ...
|
||||
/// TypeError: multiple bases have instance lay-out conflict
|
||||
/// ```python
|
||||
/// class A(int, float): ... # TypeError: multiple bases have instance lay-out conflict
|
||||
/// ```
|
||||
pub(crate) static INCOMPATIBLE_SLOTS = {
|
||||
summary: "detects class definitions whose MRO has conflicting `__slots__`",
|
||||
///
|
||||
/// Note that pure-Python classes with no `__slots__`, or pure-Python classes
|
||||
/// with empty `__slots__`, are always compatible:
|
||||
///
|
||||
/// ```python
|
||||
/// class A: ...
|
||||
/// class B:
|
||||
/// __slots__ = ()
|
||||
/// class C:
|
||||
/// __slots__ = ("a", "b")
|
||||
///
|
||||
/// # fine
|
||||
/// class D(A, B, C): ...
|
||||
/// ```
|
||||
///
|
||||
/// ## Known problems
|
||||
/// Classes that have "dynamic" definitions of `__slots__` (definitions do not consist
|
||||
/// of string literals, or tuples of string literals) are not currently considered solid
|
||||
/// bases by ty.
|
||||
///
|
||||
/// Additionally, this check is not exhaustive: many C extensions (including several in
|
||||
/// the standard library) define classes that use extended memory layouts and thus cannot
|
||||
/// coexist in a single MRO. Since it is currently not possible to represent this fact in
|
||||
/// stub files, having a full knowledge of these classes is also impossible. When it comes
|
||||
/// to classes that do not define `__slots__` at the Python level, therefore, ty, currently
|
||||
/// only hard-codes a number of cases where it knows that a class will produce instances with
|
||||
/// an atypical memory layout.
|
||||
///
|
||||
/// ## Further reading
|
||||
/// - [CPython documentation: `__slots__`](https://docs.python.org/3/reference/datamodel.html#slots)
|
||||
/// - [CPython documentation: Method Resolution Order](https://docs.python.org/3/glossary.html#term-method-resolution-order)
|
||||
///
|
||||
/// [Method Resolution Order]: https://docs.python.org/3/glossary.html#term-method-resolution-order
|
||||
pub(crate) static INSTANCE_LAYOUT_CONFLICT = {
|
||||
summary: "detects class definitions that raise `TypeError` due to instance layout conflict",
|
||||
status: LintStatus::preview("1.0.0"),
|
||||
default_level: Level::Error,
|
||||
}
|
||||
|
@ -1901,11 +1926,193 @@ pub(crate) fn report_invalid_exception_cause(context: &InferContext, node: &ast:
|
|||
));
|
||||
}
|
||||
|
||||
pub(crate) fn report_base_with_incompatible_slots(context: &InferContext, node: &ast::Expr) {
|
||||
let Some(builder) = context.report_lint(&INCOMPATIBLE_SLOTS, node) else {
|
||||
pub(crate) fn report_instance_layout_conflict(
|
||||
context: &InferContext,
|
||||
class: ClassLiteral,
|
||||
node: &ast::StmtClassDef,
|
||||
solid_bases: &IncompatibleBases,
|
||||
) {
|
||||
debug_assert!(solid_bases.len() > 1);
|
||||
|
||||
let db = context.db();
|
||||
|
||||
let Some(builder) = context.report_lint(&INSTANCE_LAYOUT_CONFLICT, class.header_range(db))
|
||||
else {
|
||||
return;
|
||||
};
|
||||
builder.into_diagnostic("Class base has incompatible `__slots__`");
|
||||
|
||||
let mut diagnostic = builder
|
||||
.into_diagnostic("Class will raise `TypeError` at runtime due to incompatible bases");
|
||||
|
||||
diagnostic.set_primary_message(format_args!(
|
||||
"Bases {} cannot be combined in multiple inheritance",
|
||||
solid_bases.describe_problematic_class_bases(db)
|
||||
));
|
||||
|
||||
let mut subdiagnostic = SubDiagnostic::new(
|
||||
Severity::Info,
|
||||
"Two classes cannot coexist in a class's MRO if their instances \
|
||||
have incompatible memory layouts",
|
||||
);
|
||||
|
||||
for (solid_base, solid_base_info) in solid_bases {
|
||||
let IncompatibleBaseInfo {
|
||||
node_index,
|
||||
originating_base,
|
||||
} = solid_base_info;
|
||||
|
||||
let span = context.span(&node.bases()[*node_index]);
|
||||
let mut annotation = Annotation::secondary(span.clone());
|
||||
if solid_base.class == *originating_base {
|
||||
match solid_base.kind {
|
||||
SolidBaseKind::DefinesSlots => {
|
||||
annotation = annotation.message(format_args!(
|
||||
"`{base}` instances have a distinct memory layout because `{base}` defines non-empty `__slots__`",
|
||||
base = originating_base.name(db)
|
||||
));
|
||||
}
|
||||
SolidBaseKind::HardCoded => {
|
||||
annotation = annotation.message(format_args!(
|
||||
"`{base}` instances have a distinct memory layout because of the way `{base}` \
|
||||
is implemented in a C extension",
|
||||
base = originating_base.name(db)
|
||||
));
|
||||
}
|
||||
}
|
||||
subdiagnostic.annotate(annotation);
|
||||
} else {
|
||||
annotation = annotation.message(format_args!(
|
||||
"`{base}` instances have a distinct memory layout \
|
||||
because `{base}` inherits from `{solid_base}`",
|
||||
base = originating_base.name(db),
|
||||
solid_base = solid_base.class.name(db)
|
||||
));
|
||||
subdiagnostic.annotate(annotation);
|
||||
|
||||
let mut additional_annotation = Annotation::secondary(span);
|
||||
|
||||
additional_annotation = match solid_base.kind {
|
||||
SolidBaseKind::DefinesSlots => additional_annotation.message(format_args!(
|
||||
"`{solid_base}` instances have a distinct memory layout because `{solid_base}` \
|
||||
defines non-empty `__slots__`",
|
||||
solid_base = solid_base.class.name(db),
|
||||
)),
|
||||
|
||||
SolidBaseKind::HardCoded => additional_annotation.message(format_args!(
|
||||
"`{solid_base}` instances have a distinct memory layout \
|
||||
because of the way `{solid_base}` is implemented in a C extension",
|
||||
solid_base = solid_base.class.name(db),
|
||||
)),
|
||||
};
|
||||
|
||||
subdiagnostic.annotate(additional_annotation);
|
||||
}
|
||||
}
|
||||
|
||||
diagnostic.sub(subdiagnostic);
|
||||
}
|
||||
|
||||
/// Information regarding the conflicting solid bases a class is inferred to have in its MRO.
|
||||
///
|
||||
/// For each solid base, we record information about which element in the class's bases list
|
||||
/// caused the solid base to be included in the class's MRO.
|
||||
///
|
||||
/// The inner data is an `IndexMap` to ensure that diagnostics regarding conflicting solid bases
|
||||
/// are reported in a stable order.
|
||||
#[derive(Debug, Default)]
|
||||
pub(super) struct IncompatibleBases<'db>(FxIndexMap<SolidBase<'db>, IncompatibleBaseInfo<'db>>);
|
||||
|
||||
impl<'db> IncompatibleBases<'db> {
|
||||
pub(super) fn insert(
|
||||
&mut self,
|
||||
base: SolidBase<'db>,
|
||||
node_index: usize,
|
||||
class: ClassLiteral<'db>,
|
||||
) {
|
||||
let info = IncompatibleBaseInfo {
|
||||
node_index,
|
||||
originating_base: class,
|
||||
};
|
||||
self.0.insert(base, info);
|
||||
}
|
||||
|
||||
/// List the problematic class bases in a human-readable format.
|
||||
fn describe_problematic_class_bases(&self, db: &dyn Db) -> String {
|
||||
let num_bases = self.len();
|
||||
debug_assert!(num_bases >= 2);
|
||||
|
||||
let mut bad_base_names = self.0.values().map(|info| info.originating_base.name(db));
|
||||
|
||||
let final_base = bad_base_names.next_back().unwrap();
|
||||
let penultimate_base = bad_base_names.next_back().unwrap();
|
||||
|
||||
let mut buffer = String::new();
|
||||
|
||||
for base_name in bad_base_names {
|
||||
buffer.push('`');
|
||||
buffer.push_str(base_name);
|
||||
buffer.push_str("`, ");
|
||||
}
|
||||
|
||||
buffer.push('`');
|
||||
buffer.push_str(penultimate_base);
|
||||
buffer.push_str("` and `");
|
||||
buffer.push_str(final_base);
|
||||
buffer.push('`');
|
||||
|
||||
buffer
|
||||
}
|
||||
|
||||
pub(super) fn len(&self) -> usize {
|
||||
self.0.len()
|
||||
}
|
||||
|
||||
/// Two solid bases are allowed to coexist in an MRO if one is a subclass of the other.
|
||||
/// This method therefore removes any entry in `self` that is a subclass of one or more
|
||||
/// other entries also contained in `self`.
|
||||
pub(super) fn remove_redundant_entries(&mut self, db: &'db dyn Db) {
|
||||
self.0 = self
|
||||
.0
|
||||
.iter()
|
||||
.filter(|(solid_base, _)| {
|
||||
self.0
|
||||
.keys()
|
||||
.filter(|other_base| other_base != solid_base)
|
||||
.all(|other_base| {
|
||||
!solid_base.class.is_subclass_of(
|
||||
db,
|
||||
None,
|
||||
other_base.class.default_specialization(db),
|
||||
)
|
||||
})
|
||||
})
|
||||
.map(|(base, info)| (*base, *info))
|
||||
.collect();
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'db> IntoIterator for &'a IncompatibleBases<'db> {
|
||||
type Item = (&'a SolidBase<'db>, &'a IncompatibleBaseInfo<'db>);
|
||||
type IntoIter = indexmap::map::Iter<'a, SolidBase<'db>, IncompatibleBaseInfo<'db>>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.0.iter()
|
||||
}
|
||||
}
|
||||
|
||||
/// Information about which class base the "solid base" stems from
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub(super) struct IncompatibleBaseInfo<'db> {
|
||||
/// The index of the problematic base in the [`ast::StmtClassDef`]'s bases list.
|
||||
node_index: usize,
|
||||
|
||||
/// The base class in the [`ast::StmtClassDef`]'s bases list that caused
|
||||
/// the solid base to be included in the class's MRO.
|
||||
///
|
||||
/// This won't necessarily be the same class as the `SolidBase`'s class,
|
||||
/// as the `SolidBase` may have found its way into the class's MRO by dint of it being a
|
||||
/// superclass of one of the classes in the class definition's bases list.
|
||||
originating_base: ClassLiteral<'db>,
|
||||
}
|
||||
|
||||
pub(crate) fn report_invalid_arguments_to_annotated(
|
||||
|
|
|
@ -81,13 +81,14 @@ use crate::types::diagnostic::{
|
|||
INVALID_ARGUMENT_TYPE, INVALID_ASSIGNMENT, INVALID_ATTRIBUTE_ACCESS, INVALID_BASE,
|
||||
INVALID_DECLARATION, INVALID_GENERIC_CLASS, INVALID_LEGACY_TYPE_VARIABLE,
|
||||
INVALID_PARAMETER_DEFAULT, INVALID_TYPE_ALIAS_TYPE, INVALID_TYPE_FORM, INVALID_TYPE_GUARD_CALL,
|
||||
INVALID_TYPE_VARIABLE_CONSTRAINTS, POSSIBLY_UNBOUND_IMPLICIT_CALL, POSSIBLY_UNBOUND_IMPORT,
|
||||
TypeCheckDiagnostics, UNDEFINED_REVEAL, UNRESOLVED_ATTRIBUTE, UNRESOLVED_IMPORT,
|
||||
UNRESOLVED_REFERENCE, UNSUPPORTED_OPERATOR, report_implicit_return_type,
|
||||
report_invalid_argument_number_to_special_form, report_invalid_arguments_to_annotated,
|
||||
report_invalid_arguments_to_callable, report_invalid_assignment,
|
||||
report_invalid_attribute_assignment, report_invalid_generator_function_return_type,
|
||||
report_invalid_return_type, report_possibly_unbound_attribute,
|
||||
INVALID_TYPE_VARIABLE_CONSTRAINTS, IncompatibleBases, POSSIBLY_UNBOUND_IMPLICIT_CALL,
|
||||
POSSIBLY_UNBOUND_IMPORT, TypeCheckDiagnostics, UNDEFINED_REVEAL, UNRESOLVED_ATTRIBUTE,
|
||||
UNRESOLVED_IMPORT, UNRESOLVED_REFERENCE, UNSUPPORTED_OPERATOR, report_implicit_return_type,
|
||||
report_instance_layout_conflict, report_invalid_argument_number_to_special_form,
|
||||
report_invalid_arguments_to_annotated, report_invalid_arguments_to_callable,
|
||||
report_invalid_assignment, report_invalid_attribute_assignment,
|
||||
report_invalid_generator_function_return_type, report_invalid_return_type,
|
||||
report_possibly_unbound_attribute,
|
||||
};
|
||||
use crate::types::function::{
|
||||
FunctionDecorators, FunctionLiteral, FunctionType, KnownFunction, OverloadLiteral,
|
||||
|
@ -123,7 +124,6 @@ use super::diagnostic::{
|
|||
report_runtime_check_against_non_runtime_checkable_protocol, report_slice_step_size_zero,
|
||||
};
|
||||
use super::generics::LegacyGenericBase;
|
||||
use super::slots::check_class_slots;
|
||||
use super::string_annotation::{
|
||||
BYTE_STRING_TYPE_ANNOTATION, FSTRING_TYPE_ANNOTATION, parse_string_annotation,
|
||||
};
|
||||
|
@ -887,12 +887,20 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
|||
}
|
||||
|
||||
let is_protocol = class.is_protocol(self.db());
|
||||
let mut solid_bases = IncompatibleBases::default();
|
||||
|
||||
// (2) Iterate through the class's explicit bases to check for various possible errors:
|
||||
// - Check for inheritance from plain `Generic`,
|
||||
// - Check for inheritance from a `@final` classes
|
||||
// - If the class is a protocol class: check for inheritance from a non-protocol class
|
||||
for (i, base_class) in class.explicit_bases(self.db()).iter().enumerate() {
|
||||
if let Some((class, solid_base)) = base_class
|
||||
.to_class_type(self.db())
|
||||
.and_then(|class| Some((class, class.nearest_solid_base(self.db())?)))
|
||||
{
|
||||
solid_bases.insert(solid_base, i, class.class_literal(self.db()).0);
|
||||
}
|
||||
|
||||
let base_class = match base_class {
|
||||
Type::SpecialForm(SpecialFormType::Generic) => {
|
||||
if let Some(builder) = self
|
||||
|
@ -1016,7 +1024,18 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
|||
}
|
||||
}
|
||||
},
|
||||
Ok(_) => check_class_slots(&self.context, class, class_node),
|
||||
Ok(_) => {
|
||||
solid_bases.remove_redundant_entries(self.db());
|
||||
|
||||
if solid_bases.len() > 1 {
|
||||
report_instance_layout_conflict(
|
||||
&self.context,
|
||||
class,
|
||||
class_node,
|
||||
&solid_bases,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// (4) Check that the class's metaclass can be determined without error.
|
||||
|
|
|
@ -105,42 +105,7 @@ impl<'db> NominalInstanceType<'db> {
|
|||
}
|
||||
|
||||
pub(super) fn is_disjoint_from(self, db: &'db dyn Db, other: Self) -> bool {
|
||||
self.is_disjoint_from_nominal_instance_of_class(db, other.class)
|
||||
}
|
||||
|
||||
// Note that this method only exists so that we can check disjointness between nominal
|
||||
// instances of `tuple` and some other class. Tuples are currently represented by the
|
||||
// `Type::Tuple` variant, not `Type::NominalInstance`. We have a TODO to try to remove the
|
||||
// dedicated `Tuple` variant in favor of `NominalInstance`; if we can do that, then we won't
|
||||
// need this method, and its logic can be subsumed into `is_disjoint_from`.
|
||||
pub(super) fn is_disjoint_from_nominal_instance_of_class(
|
||||
self,
|
||||
db: &'db dyn Db,
|
||||
other_class: ClassType,
|
||||
) -> bool {
|
||||
if self.class.is_final(db) && !self.class.is_subclass_of(db, other_class) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if other_class.is_final(db) && !other_class.is_subclass_of(db, self.class) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check to see whether the metaclasses of `self` and `other` are disjoint.
|
||||
// Avoid this check if the metaclass of either `self` or `other` is `type`,
|
||||
// however, since we end up with infinite recursion in that case due to the fact
|
||||
// that `type` is its own metaclass (and we know that `type` cannot be disjoint
|
||||
// from any metaclass, anyway).
|
||||
let type_type = KnownClass::Type.to_instance(db);
|
||||
let self_metaclass = self.class.metaclass_instance_type(db);
|
||||
if self_metaclass == type_type {
|
||||
return false;
|
||||
}
|
||||
let other_metaclass = other_class.metaclass_instance_type(db);
|
||||
if other_metaclass == type_type {
|
||||
return false;
|
||||
}
|
||||
self_metaclass.is_disjoint_from(db, other_metaclass)
|
||||
!self.class.could_coexist_in_mro_with(db, other.class)
|
||||
}
|
||||
|
||||
pub(super) fn is_gradual_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool {
|
||||
|
|
|
@ -1,109 +0,0 @@
|
|||
use ruff_python_ast as ast;
|
||||
|
||||
use crate::db::Db;
|
||||
use crate::place::{Boundness, Place};
|
||||
use crate::types::class_base::ClassBase;
|
||||
use crate::types::diagnostic::report_base_with_incompatible_slots;
|
||||
use crate::types::{ClassLiteral, Type};
|
||||
|
||||
use super::InferContext;
|
||||
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
||||
enum SlotsKind {
|
||||
/// `__slots__` is not found in the class.
|
||||
NotSpecified,
|
||||
/// `__slots__` is defined but empty: `__slots__ = ()`.
|
||||
Empty,
|
||||
/// `__slots__` is defined and is not empty: `__slots__ = ("a", "b")`.
|
||||
NotEmpty,
|
||||
/// `__slots__` is defined but its value is dynamic:
|
||||
/// * `__slots__ = tuple(a for a in b)`
|
||||
/// * `__slots__ = ["a", "b"]`
|
||||
Dynamic,
|
||||
}
|
||||
|
||||
impl SlotsKind {
|
||||
fn from(db: &dyn Db, base: ClassLiteral) -> Self {
|
||||
let Place::Type(slots_ty, bound) = base.own_class_member(db, None, "__slots__").place
|
||||
else {
|
||||
return Self::NotSpecified;
|
||||
};
|
||||
|
||||
if matches!(bound, Boundness::PossiblyUnbound) {
|
||||
return Self::Dynamic;
|
||||
}
|
||||
|
||||
match slots_ty {
|
||||
// __slots__ = ("a", "b")
|
||||
Type::Tuple(tuple) => {
|
||||
if tuple.tuple(db).is_empty() {
|
||||
Self::Empty
|
||||
} else {
|
||||
Self::NotEmpty
|
||||
}
|
||||
}
|
||||
|
||||
// __slots__ = "abc" # Same as `("abc",)`
|
||||
Type::StringLiteral(_) => Self::NotEmpty,
|
||||
|
||||
_ => Self::Dynamic,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn check_class_slots(
|
||||
context: &InferContext,
|
||||
class: ClassLiteral,
|
||||
node: &ast::StmtClassDef,
|
||||
) {
|
||||
let db = context.db();
|
||||
|
||||
let mut first_with_solid_base = None;
|
||||
let mut common_solid_base = None;
|
||||
let mut found_second = false;
|
||||
|
||||
for (index, base) in class.explicit_bases(db).iter().enumerate() {
|
||||
let Type::ClassLiteral(base) = base else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let solid_base = base.iter_mro(db, None).find_map(|current| {
|
||||
let ClassBase::Class(current) = current else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let (class_literal, _) = current.class_literal(db);
|
||||
match SlotsKind::from(db, class_literal) {
|
||||
SlotsKind::NotEmpty => Some(current),
|
||||
SlotsKind::NotSpecified | SlotsKind::Empty => None,
|
||||
SlotsKind::Dynamic => None,
|
||||
}
|
||||
});
|
||||
|
||||
if solid_base.is_none() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let base_node = &node.bases()[index];
|
||||
|
||||
if first_with_solid_base.is_none() {
|
||||
first_with_solid_base = Some(index);
|
||||
common_solid_base = solid_base;
|
||||
continue;
|
||||
}
|
||||
|
||||
if solid_base == common_solid_base {
|
||||
continue;
|
||||
}
|
||||
|
||||
found_second = true;
|
||||
report_base_with_incompatible_slots(context, base_node);
|
||||
}
|
||||
|
||||
if found_second {
|
||||
if let Some(index) = first_with_solid_base {
|
||||
let base_node = &node.bases()[index];
|
||||
report_base_with_incompatible_slots(context, base_node);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -159,6 +159,18 @@ impl<'db> SubclassOfType<'db> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Return` true` if `self` is a disjoint type from `other`.
|
||||
///
|
||||
/// See [`Type::is_disjoint_from`] for more details.
|
||||
pub(crate) fn is_disjoint_from(self, db: &'db dyn Db, other: Self) -> bool {
|
||||
match (self.subclass_of, other.subclass_of) {
|
||||
(SubclassOfInner::Dynamic(_), _) | (_, SubclassOfInner::Dynamic(_)) => false,
|
||||
(SubclassOfInner::Class(self_class), SubclassOfInner::Class(other_class)) => {
|
||||
!self_class.could_coexist_in_mro_with(db, other_class)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn normalized(self, db: &'db dyn Db) -> Self {
|
||||
Self {
|
||||
subclass_of: self.subclass_of.normalized(db),
|
||||
|
|
|
@ -710,6 +710,10 @@ impl<'db> TupleSpec<'db> {
|
|||
}
|
||||
}
|
||||
|
||||
pub(crate) const fn is_variadic(&self) -> bool {
|
||||
matches!(self, TupleSpec::Variable(_))
|
||||
}
|
||||
|
||||
/// Returns the minimum and maximum length of this tuple. (The maximum length will be `None`
|
||||
/// for a tuple with a variable-length portion.)
|
||||
pub(crate) fn size_hint(&self) -> (usize, Option<usize>) {
|
||||
|
|
20
ty.schema.json
generated
20
ty.schema.json
generated
|
@ -391,16 +391,6 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
"incompatible-slots": {
|
||||
"title": "detects class definitions whose MRO has conflicting `__slots__`",
|
||||
"description": "## What it does\nChecks for classes whose bases define incompatible `__slots__`.\n\n## Why is this bad?\nInheriting from bases with incompatible `__slots__`s\nwill lead to a `TypeError` at runtime.\n\nClasses with no or empty `__slots__` are always compatible:\n\n```python\nclass A: ...\nclass B:\n __slots__ = ()\nclass C:\n __slots__ = (\"a\", \"b\")\n\n# fine\nclass D(A, B, C): ...\n```\n\nMultiple inheritance from more than one different class\ndefining non-empty `__slots__` is not allowed:\n\n```python\nclass A:\n __slots__ = (\"a\", \"b\")\n\nclass B:\n __slots__ = (\"a\", \"b\") # Even if the values are the same\n\n# TypeError: multiple bases have instance lay-out conflict\nclass C(A, B): ...\n```\n\n## Known problems\nDynamic (not tuple or string literal) `__slots__` are not checked.\nAdditionally, classes inheriting from built-in classes with implicit layouts\nlike `str` or `int` are also not checked.\n\n```pycon\n>>> hasattr(int, \"__slots__\")\nFalse\n>>> hasattr(str, \"__slots__\")\nFalse\n>>> class A(int, str): ...\nTraceback (most recent call last):\n File \"<python-input-0>\", line 1, in <module>\n class A(int, str): ...\nTypeError: multiple bases have instance lay-out conflict\n```",
|
||||
"default": "error",
|
||||
"oneOf": [
|
||||
{
|
||||
"$ref": "#/definitions/Level"
|
||||
}
|
||||
]
|
||||
},
|
||||
"inconsistent-mro": {
|
||||
"title": "detects class definitions with an inconsistent MRO",
|
||||
"description": "## What it does\nChecks for classes with an inconsistent [method resolution order] (MRO).\n\n## Why is this bad?\nClasses with an inconsistent MRO will raise a `TypeError` at runtime.\n\n## Examples\n```python\nclass A: ...\nclass B(A): ...\n\n# TypeError: Cannot create a consistent method resolution order\nclass C(A, B): ...\n```\n\n[method resolution order]: https://docs.python.org/3/glossary.html#term-method-resolution-order",
|
||||
|
@ -421,6 +411,16 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
"instance-layout-conflict": {
|
||||
"title": "detects class definitions that raise `TypeError` due to instance layout conflict",
|
||||
"description": "## What it does\nChecks for classes definitions which will fail at runtime due to\n\"instance memory layout conflicts\".\n\nThis error is usually caused by attempting to combine multiple classes\nthat define non-empty `__slots__` in a class's [Method Resolution Order]\n(MRO), or by attempting to combine multiple builtin classes in a class's\nMRO.\n\n## Why is this bad?\nInheriting from bases with conflicting instance memory layouts\nwill lead to a `TypeError` at runtime.\n\nAn instance memory layout conflict occurs when CPython cannot determine\nthe memory layout instances of a class should have, because the instance\nmemory layout of one of its bases conflicts with the instance memory layout\nof one or more of its other bases.\n\nFor example, if a Python class defines non-empty `__slots__`, this will\nimpact the memory layout of instances of that class. Multiple inheritance\nfrom more than one different class defining non-empty `__slots__` is not\nallowed:\n\n```python\nclass A:\n __slots__ = (\"a\", \"b\")\n\nclass B:\n __slots__ = (\"a\", \"b\") # Even if the values are the same\n\n# TypeError: multiple bases have instance lay-out conflict\nclass C(A, B): ...\n```\n\nAn instance layout conflict can also be caused by attempting to use\nmultiple inheritance with two builtin classes, due to the way that these\nclasses are implemented in a CPython C extension:\n\n```python\nclass A(int, float): ... # TypeError: multiple bases have instance lay-out conflict\n```\n\nNote that pure-Python classes with no `__slots__`, or pure-Python classes\nwith empty `__slots__`, are always compatible:\n\n```python\nclass A: ...\nclass B:\n __slots__ = ()\nclass C:\n __slots__ = (\"a\", \"b\")\n\n# fine\nclass D(A, B, C): ...\n```\n\n## Known problems\nClasses that have \"dynamic\" definitions of `__slots__` (definitions do not consist\nof string literals, or tuples of string literals) are not currently considered solid\nbases by ty.\n\nAdditionally, this check is not exhaustive: many C extensions (including several in\nthe standard library) define classes that use extended memory layouts and thus cannot\ncoexist in a single MRO. Since it is currently not possible to represent this fact in\nstub files, having a full knowledge of these classes is also impossible. When it comes\nto classes that do not define `__slots__` at the Python level, therefore, ty, currently\nonly hard-codes a number of cases where it knows that a class will produce instances with\nan atypical memory layout.\n\n## Further reading\n- [CPython documentation: `__slots__`](https://docs.python.org/3/reference/datamodel.html#slots)\n- [CPython documentation: Method Resolution Order](https://docs.python.org/3/glossary.html#term-method-resolution-order)\n\n[Method Resolution Order]: https://docs.python.org/3/glossary.html#term-method-resolution-order",
|
||||
"default": "error",
|
||||
"oneOf": [
|
||||
{
|
||||
"$ref": "#/definitions/Level"
|
||||
}
|
||||
]
|
||||
},
|
||||
"invalid-argument-type": {
|
||||
"title": "detects call arguments whose type is not assignable to the corresponding typed parameter",
|
||||
"description": "## What it does\nDetects call arguments whose type is not assignable to the corresponding typed parameter.\n\n## Why is this bad?\nPassing an argument of a type the function (or callable object) does not accept violates\nthe expectations of the function author and may cause unexpected runtime errors within the\nbody of the function.\n\n## Examples\n```python\ndef func(x: int): ...\nfunc(\"foo\") # error: [invalid-argument-type]\n```",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue