mirror of
https://github.com/astral-sh/ruff.git
synced 2025-08-04 02:38:25 +00:00
[ty] Add partial support for TypeIs
(#18589)
## Summary Part of [#117](https://github.com/astral-sh/ty/issues/117). `TypeIs[]` is a special form that allows users to define their own narrowing functions. Despite the syntax, `TypeIs` is not a generic and, on its own, it is meaningless as a type. [Officially](https://typing.python.org/en/latest/spec/narrowing.html#typeis), a function annotated as returning a `TypeIs[T]` is a <i>type narrowing function</i>, where `T` is called the <i>`TypeIs` return type</i>. A `TypeIs[T]` may or may not be bound to a symbol. Only bound types have narrowing effect: ```python def f(v: object = object()) -> TypeIs[int]: ... a: str = returns_str() if reveal_type(f()): # Unbound: TypeIs[int] reveal_type(a) # str if reveal_type(f(a)): # Bound: TypeIs[a, int] reveal_type(a) # str & int ``` Delayed usages of a bound type has no effect, however: ```python b = f(a) if b: reveal_type(a) # str ``` A `TypeIs[T]` type: * Is fully static when `T` is fully static. * Is a singleton/single-valued when it is bound. * Has exactly two runtime inhabitants when it is unbound: `True` and `False`. In other words, an unbound type have ambiguous truthiness. It is possible to infer more precise truthiness for bound types; however, that is not part of this change. `TypeIs[T]` is a subtype of or otherwise assignable to `bool`. `TypeIs` is invariant with respect to the `TypeIs` return type: `TypeIs[int]` is neither a subtype nor a supertype of `TypeIs[bool]`. When ty sees a function marked as returning `TypeIs[T]`, its `return`s will be checked against `bool` instead. ty will also report such functions if they don't accept a positional argument. Addtionally, a type narrowing function call with no positional arguments (e.g., `f()` in the example above) will be considered invalid. ## Test Plan Markdown tests. --------- Co-authored-by: Carl Meyer <carl@astral.sh>
This commit is contained in:
parent
89d915a1e3
commit
6d56ee803e
15 changed files with 841 additions and 97 deletions
174
crates/ty/docs/rules.md
generated
174
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#L92)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L94)
|
||||
</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#L136)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L138)
|
||||
</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#L162)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L164)
|
||||
</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#L187)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L189)
|
||||
</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#L213)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L215)
|
||||
</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#L257)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L259)
|
||||
</details>
|
||||
|
||||
## `escape-character-in-forward-annotation`
|
||||
|
@ -338,7 +338,7 @@ 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#L278)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L280)
|
||||
</details>
|
||||
|
||||
## `inconsistent-mro`
|
||||
|
@ -367,7 +367,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#L364)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L366)
|
||||
</details>
|
||||
|
||||
## `index-out-of-bounds`
|
||||
|
@ -392,7 +392,7 @@ 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#L388)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L390)
|
||||
</details>
|
||||
|
||||
## `invalid-argument-type`
|
||||
|
@ -418,7 +418,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#L408)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L410)
|
||||
</details>
|
||||
|
||||
## `invalid-assignment`
|
||||
|
@ -445,7 +445,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#L448)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L450)
|
||||
</details>
|
||||
|
||||
## `invalid-attribute-access`
|
||||
|
@ -478,7 +478,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#L1396)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1454)
|
||||
</details>
|
||||
|
||||
## `invalid-base`
|
||||
|
@ -501,7 +501,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#L470)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L472)
|
||||
</details>
|
||||
|
||||
## `invalid-context-manager`
|
||||
|
@ -527,7 +527,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#L521)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L523)
|
||||
</details>
|
||||
|
||||
## `invalid-declaration`
|
||||
|
@ -555,7 +555,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#L542)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L544)
|
||||
</details>
|
||||
|
||||
## `invalid-exception-caught`
|
||||
|
@ -596,7 +596,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#L565)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L567)
|
||||
</details>
|
||||
|
||||
## `invalid-generic-class`
|
||||
|
@ -627,7 +627,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#L601)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L603)
|
||||
</details>
|
||||
|
||||
## `invalid-legacy-type-variable`
|
||||
|
@ -660,7 +660,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#L627)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L629)
|
||||
</details>
|
||||
|
||||
## `invalid-metaclass`
|
||||
|
@ -692,7 +692,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#L676)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L678)
|
||||
</details>
|
||||
|
||||
## `invalid-overload`
|
||||
|
@ -740,7 +740,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#L703)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L705)
|
||||
</details>
|
||||
|
||||
## `invalid-parameter-default`
|
||||
|
@ -765,7 +765,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#L746)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L748)
|
||||
</details>
|
||||
|
||||
## `invalid-protocol`
|
||||
|
@ -798,7 +798,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#L336)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L338)
|
||||
</details>
|
||||
|
||||
## `invalid-raise`
|
||||
|
@ -846,7 +846,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#L766)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L768)
|
||||
</details>
|
||||
|
||||
## `invalid-return-type`
|
||||
|
@ -870,7 +870,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#L429)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L431)
|
||||
</details>
|
||||
|
||||
## `invalid-super-argument`
|
||||
|
@ -914,7 +914,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#L809)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L811)
|
||||
</details>
|
||||
|
||||
## `invalid-syntax-in-forward-annotation`
|
||||
|
@ -954,7 +954,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#L655)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L657)
|
||||
</details>
|
||||
|
||||
## `invalid-type-checking-constant`
|
||||
|
@ -983,7 +983,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#L848)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L850)
|
||||
</details>
|
||||
|
||||
## `invalid-type-form`
|
||||
|
@ -1012,7 +1012,73 @@ 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#L872)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L874)
|
||||
</details>
|
||||
|
||||
## `invalid-type-guard-call`
|
||||
|
||||
**Default level**: error
|
||||
|
||||
<details>
|
||||
<summary>detects type guard function calls that has no narrowing effect</summary>
|
||||
|
||||
### What it does
|
||||
Checks for type guard function calls without a valid target.
|
||||
|
||||
### Why is this bad?
|
||||
The first non-keyword non-variadic argument to a type guard function
|
||||
is its target and must map to a symbol.
|
||||
|
||||
Starred (`is_str(*a)`), literal (`is_str(42)`) and other non-symbol-like
|
||||
expressions are invalid as narrowing targets.
|
||||
|
||||
### Examples
|
||||
```python
|
||||
from typing import TypeIs
|
||||
|
||||
def f(v: object) -> TypeIs[int]: ...
|
||||
|
||||
f() # Error
|
||||
f(*a) # Error
|
||||
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#L926)
|
||||
</details>
|
||||
|
||||
## `invalid-type-guard-definition`
|
||||
|
||||
**Default level**: error
|
||||
|
||||
<details>
|
||||
<summary>detects malformed type guard functions</summary>
|
||||
|
||||
### What it does
|
||||
Checks for type guard functions without
|
||||
a first non-self-like non-keyword-only non-variadic parameter.
|
||||
|
||||
### Why is this bad?
|
||||
Type narrowing functions must accept at least one positional argument
|
||||
(non-static methods must accept another in addition to `self`/`cls`).
|
||||
|
||||
Extra parameters/arguments are allowed but do not affect narrowing.
|
||||
|
||||
### Examples
|
||||
```python
|
||||
from typing import TypeIs
|
||||
|
||||
def f() -> TypeIs[int]: ... # Error, no parameter
|
||||
def f(*, v: object) -> TypeIs[int]: ... # Error, no positional arguments allowed
|
||||
def f(*args: object) -> TypeIs[int]: ... # Error, expect variadic arguments
|
||||
class C:
|
||||
def f(self) -> TypeIs[int]: ... # Error, only positional argument expected is `self`
|
||||
```
|
||||
|
||||
### 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#L898)
|
||||
</details>
|
||||
|
||||
## `invalid-type-variable-constraints`
|
||||
|
@ -1046,7 +1112,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#L896)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L954)
|
||||
</details>
|
||||
|
||||
## `missing-argument`
|
||||
|
@ -1070,7 +1136,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#L925)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L983)
|
||||
</details>
|
||||
|
||||
## `no-matching-overload`
|
||||
|
@ -1098,7 +1164,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#L944)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1002)
|
||||
</details>
|
||||
|
||||
## `non-subscriptable`
|
||||
|
@ -1121,7 +1187,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#L967)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1025)
|
||||
</details>
|
||||
|
||||
## `not-iterable`
|
||||
|
@ -1146,7 +1212,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#L985)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1043)
|
||||
</details>
|
||||
|
||||
## `parameter-already-assigned`
|
||||
|
@ -1172,7 +1238,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#L1036)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1094)
|
||||
</details>
|
||||
|
||||
## `raw-string-type-annotation`
|
||||
|
@ -1231,7 +1297,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#L1372)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1430)
|
||||
</details>
|
||||
|
||||
## `subclass-of-final-class`
|
||||
|
@ -1259,7 +1325,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#L1127)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1185)
|
||||
</details>
|
||||
|
||||
## `too-many-positional-arguments`
|
||||
|
@ -1285,7 +1351,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#L1172)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1230)
|
||||
</details>
|
||||
|
||||
## `type-assertion-failure`
|
||||
|
@ -1312,7 +1378,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#L1150)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1208)
|
||||
</details>
|
||||
|
||||
## `unavailable-implicit-super-arguments`
|
||||
|
@ -1356,7 +1422,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#L1193)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1251)
|
||||
</details>
|
||||
|
||||
## `unknown-argument`
|
||||
|
@ -1382,7 +1448,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#L1250)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1308)
|
||||
</details>
|
||||
|
||||
## `unresolved-attribute`
|
||||
|
@ -1409,7 +1475,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#L1271)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1329)
|
||||
</details>
|
||||
|
||||
## `unresolved-import`
|
||||
|
@ -1433,7 +1499,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#L1293)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1351)
|
||||
</details>
|
||||
|
||||
## `unresolved-reference`
|
||||
|
@ -1457,7 +1523,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#L1312)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1370)
|
||||
</details>
|
||||
|
||||
## `unsupported-bool-conversion`
|
||||
|
@ -1493,7 +1559,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#L1005)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1063)
|
||||
</details>
|
||||
|
||||
## `unsupported-operator`
|
||||
|
@ -1520,7 +1586,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#L1331)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1389)
|
||||
</details>
|
||||
|
||||
## `zero-stepsize-in-slice`
|
||||
|
@ -1544,7 +1610,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#L1353)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1411)
|
||||
</details>
|
||||
|
||||
## `invalid-ignore-comment`
|
||||
|
@ -1600,7 +1666,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#L1057)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1115)
|
||||
</details>
|
||||
|
||||
## `possibly-unbound-implicit-call`
|
||||
|
@ -1631,7 +1697,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#L110)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L112)
|
||||
</details>
|
||||
|
||||
## `possibly-unbound-import`
|
||||
|
@ -1662,7 +1728,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#L1079)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1137)
|
||||
</details>
|
||||
|
||||
## `redundant-cast`
|
||||
|
@ -1688,7 +1754,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#L1424)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1482)
|
||||
</details>
|
||||
|
||||
## `undefined-reveal`
|
||||
|
@ -1711,7 +1777,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#L1232)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1290)
|
||||
</details>
|
||||
|
||||
## `unknown-rule`
|
||||
|
@ -1779,7 +1845,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#L488)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L490)
|
||||
</details>
|
||||
|
||||
## `division-by-zero`
|
||||
|
@ -1802,7 +1868,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#L239)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L241)
|
||||
</details>
|
||||
|
||||
## `possibly-unresolved-reference`
|
||||
|
@ -1829,7 +1895,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#L1105)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1163)
|
||||
</details>
|
||||
|
||||
## `unused-ignore-comment`
|
||||
|
|
|
@ -19,7 +19,6 @@ def f(*args: Unpack[Ts]) -> tuple[Unpack[Ts]]:
|
|||
reveal_type(Alias) # revealed: @Todo(Support for `typing.TypeAlias`)
|
||||
|
||||
def g() -> TypeGuard[int]: ...
|
||||
def h() -> TypeIs[int]: ...
|
||||
def i(callback: Callable[Concatenate[int, P], R_co], *args: P.args, **kwargs: P.kwargs) -> R_co:
|
||||
reveal_type(args) # revealed: tuple[@Todo(Support for `typing.ParamSpec`), ...]
|
||||
reveal_type(kwargs) # revealed: dict[str, @Todo(Support for `typing.ParamSpec`)]
|
||||
|
|
330
crates/ty_python_semantic/resources/mdtest/narrow/type_guards.md
Normal file
330
crates/ty_python_semantic/resources/mdtest/narrow/type_guards.md
Normal file
|
@ -0,0 +1,330 @@
|
|||
# User-defined type guards
|
||||
|
||||
User-defined type guards are functions of which the return type is either `TypeGuard[...]` or
|
||||
`TypeIs[...]`.
|
||||
|
||||
## Display
|
||||
|
||||
```py
|
||||
from ty_extensions import Intersection, Not, TypeOf
|
||||
from typing_extensions import TypeGuard, TypeIs
|
||||
|
||||
def _(
|
||||
a: TypeGuard[str],
|
||||
b: TypeIs[str | int],
|
||||
c: TypeGuard[Intersection[complex, Not[int], Not[float]]],
|
||||
d: TypeIs[tuple[TypeOf[bytes]]],
|
||||
e: TypeGuard, # error: [invalid-type-form]
|
||||
f: TypeIs, # error: [invalid-type-form]
|
||||
):
|
||||
# TODO: Should be `TypeGuard[str]`
|
||||
reveal_type(a) # revealed: @Todo(`TypeGuard[]` special form)
|
||||
reveal_type(b) # revealed: TypeIs[str | int]
|
||||
# TODO: Should be `TypeGuard[complex & ~int & ~float]`
|
||||
reveal_type(c) # revealed: @Todo(`TypeGuard[]` special form)
|
||||
reveal_type(d) # revealed: TypeIs[tuple[<class 'bytes'>]]
|
||||
reveal_type(e) # revealed: Unknown
|
||||
reveal_type(f) # revealed: Unknown
|
||||
|
||||
# TODO: error: [invalid-return-type] "Function always implicitly returns `None`, which is not assignable to return type `TypeGuard[str]`"
|
||||
def _(a) -> TypeGuard[str]: ...
|
||||
|
||||
# error: [invalid-return-type] "Function always implicitly returns `None`, which is not assignable to return type `TypeIs[str]`"
|
||||
def _(a) -> TypeIs[str]: ...
|
||||
def f(a) -> TypeGuard[str]:
|
||||
return True
|
||||
|
||||
def g(a) -> TypeIs[str]:
|
||||
return True
|
||||
|
||||
def _(a: object):
|
||||
# TODO: Should be `TypeGuard[str @ a]`
|
||||
reveal_type(f(a)) # revealed: @Todo(`TypeGuard[]` special form)
|
||||
reveal_type(g(a)) # revealed: TypeIs[str @ a]
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
||||
A user-defined type guard must accept at least one positional argument (in addition to `self`/`cls`
|
||||
for non-static methods).
|
||||
|
||||
```pyi
|
||||
from typing_extensions import TypeGuard, TypeIs
|
||||
|
||||
# TODO: error: [invalid-type-guard-definition]
|
||||
def _() -> TypeGuard[str]: ...
|
||||
|
||||
# TODO: error: [invalid-type-guard-definition]
|
||||
def _(**kwargs) -> TypeIs[str]: ...
|
||||
|
||||
class _:
|
||||
# fine
|
||||
def _(self, /, a) -> TypeGuard[str]: ...
|
||||
@classmethod
|
||||
def _(cls, a) -> TypeGuard[str]: ...
|
||||
@staticmethod
|
||||
def _(a) -> TypeIs[str]: ...
|
||||
|
||||
# errors
|
||||
def _(self) -> TypeGuard[str]: ... # TODO: error: [invalid-type-guard-definition]
|
||||
def _(self, /, *, a) -> TypeGuard[str]: ... # TODO: error: [invalid-type-guard-definition]
|
||||
@classmethod
|
||||
def _(cls) -> TypeIs[str]: ... # TODO: error: [invalid-type-guard-definition]
|
||||
@classmethod
|
||||
def _() -> TypeIs[str]: ... # TODO: error: [invalid-type-guard-definition]
|
||||
@staticmethod
|
||||
def _(*, a) -> TypeGuard[str]: ... # TODO: error: [invalid-type-guard-definition]
|
||||
```
|
||||
|
||||
For `TypeIs` functions, the narrowed type must be assignable to the declared type of that parameter,
|
||||
if any.
|
||||
|
||||
```pyi
|
||||
from typing import Any
|
||||
from typing_extensions import TypeIs
|
||||
|
||||
def _(a: object) -> TypeIs[str]: ...
|
||||
def _(a: Any) -> TypeIs[str]: ...
|
||||
def _(a: tuple[object]) -> TypeIs[tuple[str]]: ...
|
||||
def _(a: str | Any) -> TypeIs[str]: ...
|
||||
def _(a) -> TypeIs[str]: ...
|
||||
|
||||
# TODO: error: [invalid-type-guard-definition]
|
||||
def _(a: int) -> TypeIs[str]: ...
|
||||
|
||||
# TODO: error: [invalid-type-guard-definition]
|
||||
def _(a: bool | str) -> TypeIs[int]: ...
|
||||
```
|
||||
|
||||
## Arguments to special forms
|
||||
|
||||
`TypeGuard` and `TypeIs` accept exactly one type argument.
|
||||
|
||||
```py
|
||||
from typing_extensions import TypeGuard, TypeIs
|
||||
|
||||
a = 123
|
||||
|
||||
# TODO: error: [invalid-type-form]
|
||||
def f(_) -> TypeGuard[int, str]: ...
|
||||
|
||||
# error: [invalid-type-form] "Special form `typing.TypeIs` expected exactly one type parameter"
|
||||
# error: [invalid-type-form] "Variable of type `Literal[123]` is not allowed in a type expression"
|
||||
def g(_) -> TypeIs[a, str]: ...
|
||||
|
||||
# TODO: Should be `Unknown`
|
||||
reveal_type(f(0)) # revealed: @Todo(`TypeGuard[]` special form)
|
||||
reveal_type(g(0)) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Return types
|
||||
|
||||
All code paths in a type guard function must return booleans.
|
||||
|
||||
```py
|
||||
from typing_extensions import Literal, TypeGuard, TypeIs, assert_never
|
||||
|
||||
def _(a: object, flag: bool) -> TypeGuard[str]:
|
||||
if flag:
|
||||
return 0
|
||||
|
||||
# TODO: error: [invalid-return-type] "Return type does not match returned value: expected `TypeIs[str]`, found `Literal["foo"]`"
|
||||
return "foo"
|
||||
|
||||
# error: [invalid-return-type] "Function can implicitly return `None`, which is not assignable to return type `TypeIs[str]`"
|
||||
def f(a: object, flag: bool) -> TypeIs[str]:
|
||||
if flag:
|
||||
# error: [invalid-return-type] "Return type does not match returned value: expected `TypeIs[str]`, found `float`"
|
||||
return 1.2
|
||||
|
||||
def g(a: Literal["foo", "bar"]) -> TypeIs[Literal["foo"]]:
|
||||
if a == "foo":
|
||||
# Logically wrong, but allowed regardless
|
||||
return False
|
||||
|
||||
return False
|
||||
```
|
||||
|
||||
## Invalid calls
|
||||
|
||||
```py
|
||||
from typing import Any
|
||||
from typing_extensions import TypeGuard, TypeIs
|
||||
|
||||
def f(a: object) -> TypeGuard[str]:
|
||||
return True
|
||||
|
||||
def g(a: object) -> TypeIs[int]:
|
||||
return True
|
||||
|
||||
def _(d: Any):
|
||||
if f(): # error: [missing-argument]
|
||||
...
|
||||
|
||||
# TODO: no error, once we support splatted call args
|
||||
if g(*d): # error: [missing-argument]
|
||||
...
|
||||
|
||||
if f("foo"): # TODO: error: [invalid-type-guard-call]
|
||||
...
|
||||
|
||||
if g(a=d): # error: [invalid-type-guard-call]
|
||||
...
|
||||
```
|
||||
|
||||
## Narrowing
|
||||
|
||||
```py
|
||||
from typing import Any
|
||||
from typing_extensions import TypeGuard, TypeIs
|
||||
|
||||
def guard_str(a: object) -> TypeGuard[str]:
|
||||
return True
|
||||
|
||||
def is_int(a: object) -> TypeIs[int]:
|
||||
return True
|
||||
```
|
||||
|
||||
```py
|
||||
def _(a: str | int):
|
||||
if guard_str(a):
|
||||
# TODO: Should be `str`
|
||||
reveal_type(a) # revealed: str | int
|
||||
else:
|
||||
reveal_type(a) # revealed: str | int
|
||||
|
||||
if is_int(a):
|
||||
reveal_type(a) # revealed: int
|
||||
else:
|
||||
reveal_type(a) # revealed: str & ~int
|
||||
```
|
||||
|
||||
Attribute and subscript narrowing is supported:
|
||||
|
||||
```py
|
||||
from typing_extensions import Any, Generic, Protocol, TypeVar
|
||||
|
||||
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: Unknown
|
||||
|
||||
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]
|
||||
# TODO: Should be `int`
|
||||
reveal_type(a[0]) # revealed: Unknown
|
||||
|
||||
# TODO: Should be `TypeGuard[str @ c.v]`
|
||||
if reveal_type(guard_str(c.v)): # revealed: @Todo(`TypeGuard[]` special form)
|
||||
reveal_type(c) # revealed: C[Any]
|
||||
# TODO: Should be `str`
|
||||
reveal_type(c.v) # revealed: Any
|
||||
|
||||
if reveal_type(is_int(c.v)): # revealed: TypeIs[int @ c.v]
|
||||
reveal_type(c) # revealed: C[Any]
|
||||
# TODO: Should be `int`
|
||||
reveal_type(c.v) # revealed: Any
|
||||
```
|
||||
|
||||
Indirect usage is supported within the same scope:
|
||||
|
||||
```py
|
||||
def _(a: str | int):
|
||||
b = guard_str(a)
|
||||
c = is_int(a)
|
||||
|
||||
reveal_type(a) # revealed: str | int
|
||||
# TODO: Should be `TypeGuard[str @ a]`
|
||||
reveal_type(b) # revealed: @Todo(`TypeGuard[]` special form)
|
||||
reveal_type(c) # revealed: TypeIs[int @ a]
|
||||
|
||||
if b:
|
||||
# TODO should be `str`
|
||||
reveal_type(a) # revealed: str | int
|
||||
else:
|
||||
reveal_type(a) # revealed: str | int
|
||||
|
||||
if c:
|
||||
# TODO should be `int`
|
||||
reveal_type(a) # revealed: str | int
|
||||
else:
|
||||
# TODO should be `str & ~int`
|
||||
reveal_type(a) # revealed: str | int
|
||||
```
|
||||
|
||||
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]
|
||||
|
||||
if flag:
|
||||
x = ""
|
||||
|
||||
if b:
|
||||
reveal_type(x) # revealed: str | int
|
||||
```
|
||||
|
||||
The `TypeIs` type remains effective across generic boundaries:
|
||||
|
||||
```py
|
||||
from typing_extensions import TypeVar, reveal_type
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
def f(v: object) -> TypeIs[int]:
|
||||
return True
|
||||
|
||||
def g(v: T) -> T:
|
||||
return v
|
||||
|
||||
def _(a: str):
|
||||
# `reveal_type()` has the type `[T]() -> T`
|
||||
if reveal_type(f(a)): # revealed: TypeIs[int @ a]
|
||||
reveal_type(a) # revealed: str & int
|
||||
|
||||
if g(f(a)):
|
||||
reveal_type(a) # revealed: str & int
|
||||
```
|
||||
|
||||
## `TypeGuard` special cases
|
||||
|
||||
```py
|
||||
from typing import Any
|
||||
from typing_extensions import TypeGuard, TypeIs
|
||||
|
||||
def guard_int(a: object) -> TypeGuard[int]:
|
||||
return True
|
||||
|
||||
def is_int(a: object) -> TypeIs[int]:
|
||||
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
|
||||
else:
|
||||
reveal_type(a) # revealed: str | int
|
||||
|
||||
def narrowed_type_must_be_exact(a: object, b: bool):
|
||||
if guard_int(b):
|
||||
# TODO: Should be `int`
|
||||
reveal_type(b) # revealed: bool
|
||||
|
||||
if isinstance(a, bool) and is_int(a):
|
||||
reveal_type(a) # revealed: bool
|
||||
|
||||
if isinstance(a, bool) and guard_int(a):
|
||||
# TODO: Should be `int`
|
||||
reveal_type(a) # revealed: bool
|
||||
```
|
|
@ -871,4 +871,20 @@ def g3(obj: Foo[tuple[A]]):
|
|||
f3(obj)
|
||||
```
|
||||
|
||||
## `TypeGuard` and `TypeIs`
|
||||
|
||||
`TypeGuard[...]` and `TypeIs[...]` are always assignable to `bool`.
|
||||
|
||||
```py
|
||||
from ty_extensions import Unknown, is_assignable_to, static_assert
|
||||
from typing_extensions import Any, TypeGuard, TypeIs
|
||||
|
||||
static_assert(is_assignable_to(TypeGuard[Unknown], bool))
|
||||
static_assert(is_assignable_to(TypeIs[Any], bool))
|
||||
|
||||
# TODO no error
|
||||
static_assert(not is_assignable_to(TypeGuard[Unknown], str)) # error: [static-assert-error]
|
||||
static_assert(not is_assignable_to(TypeIs[Any], str))
|
||||
```
|
||||
|
||||
[typing documentation]: https://typing.python.org/en/latest/spec/concepts.html#the-assignable-to-or-consistent-subtyping-relation
|
||||
|
|
|
@ -402,6 +402,20 @@ static_assert(is_disjoint_from(TypeOf[C.prop], D))
|
|||
static_assert(is_disjoint_from(D, TypeOf[C.prop]))
|
||||
```
|
||||
|
||||
### `TypeGuard` and `TypeIs`
|
||||
|
||||
```py
|
||||
from ty_extensions import static_assert, is_disjoint_from
|
||||
from typing_extensions import TypeGuard, TypeIs
|
||||
|
||||
static_assert(not is_disjoint_from(bool, TypeGuard[str]))
|
||||
static_assert(not is_disjoint_from(bool, TypeIs[str]))
|
||||
|
||||
# TODO no error
|
||||
static_assert(is_disjoint_from(str, TypeGuard[str])) # error: [static-assert-error]
|
||||
static_assert(is_disjoint_from(str, TypeIs[str]))
|
||||
```
|
||||
|
||||
## Callables
|
||||
|
||||
No two callable types are disjoint because there exists a non-empty callable type
|
||||
|
|
|
@ -342,6 +342,38 @@ static_assert(is_subtype_of(Intersection[LiteralString, Not[Literal[""]]], Not[A
|
|||
static_assert(is_subtype_of(Intersection[LiteralString, Not[Literal["", "a"]]], Not[AlwaysFalsy]))
|
||||
```
|
||||
|
||||
### `TypeGuard` and `TypeIs`
|
||||
|
||||
Fully-static `TypeGuard[...]` and `TypeIs[...]` are subtypes of `bool`.
|
||||
|
||||
```py
|
||||
from ty_extensions import is_subtype_of, static_assert
|
||||
from typing_extensions import TypeGuard, TypeIs
|
||||
|
||||
# TODO: TypeGuard
|
||||
# static_assert(is_subtype_of(TypeGuard[int], bool))
|
||||
# static_assert(is_subtype_of(TypeGuard[int], int))
|
||||
static_assert(is_subtype_of(TypeIs[str], bool))
|
||||
static_assert(is_subtype_of(TypeIs[str], int))
|
||||
```
|
||||
|
||||
`TypeIs` is invariant. `TypeGuard` is covariant.
|
||||
|
||||
```py
|
||||
from ty_extensions import is_equivalent_to, is_subtype_of, static_assert
|
||||
from typing_extensions import TypeGuard, TypeIs
|
||||
|
||||
# TODO: TypeGuard
|
||||
# static_assert(is_subtype_of(TypeGuard[int], TypeGuard[int]))
|
||||
# static_assert(is_subtype_of(TypeGuard[bool], TypeGuard[int]))
|
||||
static_assert(is_subtype_of(TypeIs[int], TypeIs[int]))
|
||||
static_assert(is_subtype_of(TypeIs[int], TypeIs[int]))
|
||||
|
||||
static_assert(not is_subtype_of(TypeGuard[int], TypeGuard[bool]))
|
||||
static_assert(not is_subtype_of(TypeIs[bool], TypeIs[int]))
|
||||
static_assert(not is_subtype_of(TypeIs[int], TypeIs[bool]))
|
||||
```
|
||||
|
||||
### Module literals
|
||||
|
||||
```py
|
||||
|
|
|
@ -35,8 +35,8 @@ use crate::module_resolver::{KnownModule, resolve_module};
|
|||
use crate::place::{Boundness, Place, PlaceAndQualifiers, imported_symbol};
|
||||
use crate::semantic_index::ast_ids::HasScopedExpressionId;
|
||||
use crate::semantic_index::definition::Definition;
|
||||
use crate::semantic_index::place::ScopeId;
|
||||
use crate::semantic_index::{imported_modules, semantic_index};
|
||||
use crate::semantic_index::place::{ScopeId, ScopedPlaceId};
|
||||
use crate::semantic_index::{imported_modules, place_table, semantic_index};
|
||||
use crate::suppression::check_suppressions;
|
||||
use crate::types::call::{Binding, Bindings, CallArgumentTypes, CallableBinding};
|
||||
pub(crate) use crate::types::class_base::ClassBase;
|
||||
|
@ -553,6 +553,8 @@ pub enum Type<'db> {
|
|||
// This type doesn't handle an unbound super object like `super(A)`; for that we just use
|
||||
// a `Type::NominalInstance` of `builtins.super`.
|
||||
BoundSuper(BoundSuperType<'db>),
|
||||
/// A subtype of `bool` that allows narrowing in both positive and negative cases.
|
||||
TypeIs(TypeIsType<'db>),
|
||||
// TODO protocols, overloads, generics
|
||||
}
|
||||
|
||||
|
@ -726,6 +728,9 @@ impl<'db> Type<'db> {
|
|||
.map(|ty| ty.materialize(db, variance)),
|
||||
),
|
||||
Type::TypeVar(type_var) => Type::TypeVar(type_var.materialize(db, variance)),
|
||||
Type::TypeIs(type_is) => {
|
||||
type_is.with_type(db, type_is.return_type(db).materialize(db, variance))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -777,6 +782,11 @@ impl<'db> Type<'db> {
|
|||
*self
|
||||
}
|
||||
|
||||
Self::TypeIs(type_is) => type_is.with_type(
|
||||
db,
|
||||
type_is.return_type(db).replace_self_reference(db, class),
|
||||
),
|
||||
|
||||
Self::Dynamic(_)
|
||||
| Self::AlwaysFalsy
|
||||
| Self::AlwaysTruthy
|
||||
|
@ -910,6 +920,8 @@ impl<'db> Type<'db> {
|
|||
.iter()
|
||||
.any(|ty| ty.any_over_type(db, type_fn)),
|
||||
},
|
||||
|
||||
Self::TypeIs(type_is) => type_is.return_type(db).any_over_type(db, type_fn),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1145,6 +1157,7 @@ impl<'db> Type<'db> {
|
|||
Type::KnownInstance(known_instance) => {
|
||||
Type::KnownInstance(known_instance.normalized(db))
|
||||
}
|
||||
Type::TypeIs(type_is) => type_is.with_type(db, type_is.return_type(db).normalized(db)),
|
||||
Type::LiteralString
|
||||
| Type::AlwaysFalsy
|
||||
| Type::AlwaysTruthy
|
||||
|
@ -1404,6 +1417,11 @@ impl<'db> Type<'db> {
|
|||
false
|
||||
}
|
||||
|
||||
// `TypeIs[T]` is a subtype of `bool`.
|
||||
(Type::TypeIs(_), _) => KnownClass::Bool
|
||||
.to_instance(db)
|
||||
.has_relation_to(db, target, relation),
|
||||
|
||||
// Function-like callables are subtypes of `FunctionType`
|
||||
(Type::Callable(callable), _)
|
||||
if callable.is_function_like(db)
|
||||
|
@ -1949,14 +1967,15 @@ impl<'db> Type<'db> {
|
|||
known_instance_ty @ (Type::SpecialForm(_) | Type::KnownInstance(_)),
|
||||
) => known_instance_ty.is_disjoint_from(db, tuple.homogeneous_supertype(db)),
|
||||
|
||||
(Type::BooleanLiteral(..), Type::NominalInstance(instance))
|
||||
| (Type::NominalInstance(instance), Type::BooleanLiteral(..)) => {
|
||||
(Type::BooleanLiteral(..) | Type::TypeIs(_), Type::NominalInstance(instance))
|
||||
| (Type::NominalInstance(instance), Type::BooleanLiteral(..) | Type::TypeIs(_)) => {
|
||||
// A `Type::BooleanLiteral()` must be an instance of exactly `bool`
|
||||
// (it cannot be an instance of a `bool` subclass)
|
||||
!KnownClass::Bool.is_subclass_of(db, instance.class)
|
||||
}
|
||||
|
||||
(Type::BooleanLiteral(..), _) | (_, Type::BooleanLiteral(..)) => true,
|
||||
(Type::BooleanLiteral(..) | Type::TypeIs(_), _)
|
||||
| (_, Type::BooleanLiteral(..) | Type::TypeIs(_)) => true,
|
||||
|
||||
(Type::IntLiteral(..), Type::NominalInstance(instance))
|
||||
| (Type::NominalInstance(instance), Type::IntLiteral(..)) => {
|
||||
|
@ -2186,6 +2205,7 @@ impl<'db> Type<'db> {
|
|||
.iter()
|
||||
.all(|elem| elem.is_fully_static(db)),
|
||||
Type::Callable(callable) => callable.is_fully_static(db),
|
||||
Type::TypeIs(type_is) => type_is.return_type(db).is_fully_static(db),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2310,6 +2330,7 @@ impl<'db> Type<'db> {
|
|||
false
|
||||
}
|
||||
Type::AlwaysTruthy | Type::AlwaysFalsy => false,
|
||||
Type::TypeIs(type_is) => type_is.is_bound(db),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2367,6 +2388,8 @@ impl<'db> Type<'db> {
|
|||
false
|
||||
}
|
||||
|
||||
Type::TypeIs(type_is) => type_is.is_bound(db),
|
||||
|
||||
Type::Dynamic(_)
|
||||
| Type::Never
|
||||
| Type::Union(..)
|
||||
|
@ -2495,7 +2518,8 @@ impl<'db> Type<'db> {
|
|||
| Type::TypeVar(_)
|
||||
| Type::NominalInstance(_)
|
||||
| Type::ProtocolInstance(_)
|
||||
| Type::PropertyInstance(_) => None,
|
||||
| Type::PropertyInstance(_)
|
||||
| Type::TypeIs(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2595,7 +2619,9 @@ impl<'db> Type<'db> {
|
|||
},
|
||||
|
||||
Type::IntLiteral(_) => KnownClass::Int.to_instance(db).instance_member(db, name),
|
||||
Type::BooleanLiteral(_) => KnownClass::Bool.to_instance(db).instance_member(db, name),
|
||||
Type::BooleanLiteral(_) | Type::TypeIs(_) => {
|
||||
KnownClass::Bool.to_instance(db).instance_member(db, name)
|
||||
}
|
||||
Type::StringLiteral(_) | Type::LiteralString => {
|
||||
KnownClass::Str.to_instance(db).instance_member(db, name)
|
||||
}
|
||||
|
@ -3116,7 +3142,8 @@ impl<'db> Type<'db> {
|
|||
| Type::SpecialForm(..)
|
||||
| Type::KnownInstance(..)
|
||||
| Type::PropertyInstance(..)
|
||||
| Type::FunctionLiteral(..) => {
|
||||
| Type::FunctionLiteral(..)
|
||||
| Type::TypeIs(..) => {
|
||||
let fallback = self.instance_member(db, name_str);
|
||||
|
||||
let result = self.invoke_descriptor_protocol(
|
||||
|
@ -3381,9 +3408,11 @@ impl<'db> Type<'db> {
|
|||
};
|
||||
|
||||
let truthiness = match self {
|
||||
Type::Dynamic(_) | Type::Never | Type::Callable(_) | Type::LiteralString => {
|
||||
Truthiness::Ambiguous
|
||||
}
|
||||
Type::Dynamic(_)
|
||||
| Type::Never
|
||||
| Type::Callable(_)
|
||||
| Type::LiteralString
|
||||
| Type::TypeIs(_) => Truthiness::Ambiguous,
|
||||
|
||||
Type::FunctionLiteral(_)
|
||||
| Type::BoundMethod(_)
|
||||
|
@ -4348,7 +4377,8 @@ impl<'db> Type<'db> {
|
|||
| Type::LiteralString
|
||||
| Type::Tuple(_)
|
||||
| Type::BoundSuper(_)
|
||||
| Type::ModuleLiteral(_) => CallableBinding::not_callable(self).into(),
|
||||
| Type::ModuleLiteral(_)
|
||||
| Type::TypeIs(_) => CallableBinding::not_callable(self).into(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -4836,7 +4866,8 @@ impl<'db> Type<'db> {
|
|||
| Type::LiteralString
|
||||
| Type::BoundSuper(_)
|
||||
| Type::AlwaysTruthy
|
||||
| Type::AlwaysFalsy => None,
|
||||
| Type::AlwaysFalsy
|
||||
| Type::TypeIs(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -4902,7 +4933,8 @@ impl<'db> Type<'db> {
|
|||
| Type::FunctionLiteral(_)
|
||||
| Type::BoundSuper(_)
|
||||
| Type::ProtocolInstance(_)
|
||||
| Type::PropertyInstance(_) => Err(InvalidTypeExpressionError {
|
||||
| Type::PropertyInstance(_)
|
||||
| Type::TypeIs(_) => Err(InvalidTypeExpressionError {
|
||||
invalid_expressions: smallvec::smallvec![InvalidTypeExpression::InvalidType(
|
||||
*self, scope_id
|
||||
)],
|
||||
|
@ -5141,7 +5173,7 @@ impl<'db> Type<'db> {
|
|||
Type::SpecialForm(special_form) => special_form.to_meta_type(db),
|
||||
Type::PropertyInstance(_) => KnownClass::Property.to_class_literal(db),
|
||||
Type::Union(union) => union.map(db, |ty| ty.to_meta_type(db)),
|
||||
Type::BooleanLiteral(_) => KnownClass::Bool.to_class_literal(db),
|
||||
Type::BooleanLiteral(_) | Type::TypeIs(_) => KnownClass::Bool.to_class_literal(db),
|
||||
Type::BytesLiteral(_) => KnownClass::Bytes.to_class_literal(db),
|
||||
Type::IntLiteral(_) => KnownClass::Int.to_class_literal(db),
|
||||
Type::FunctionLiteral(_) => KnownClass::FunctionType.to_class_literal(db),
|
||||
|
@ -5315,6 +5347,8 @@ impl<'db> Type<'db> {
|
|||
.map(|ty| ty.apply_type_mapping(db, type_mapping)),
|
||||
),
|
||||
|
||||
Type::TypeIs(type_is) => type_is.with_type(db, type_is.return_type(db).apply_type_mapping(db, type_mapping)),
|
||||
|
||||
Type::ModuleLiteral(_)
|
||||
| Type::IntLiteral(_)
|
||||
| Type::BooleanLiteral(_)
|
||||
|
@ -5424,6 +5458,10 @@ impl<'db> Type<'db> {
|
|||
subclass_of.find_legacy_typevars(db, typevars);
|
||||
}
|
||||
|
||||
Type::TypeIs(type_is) => {
|
||||
type_is.return_type(db).find_legacy_typevars(db, typevars);
|
||||
}
|
||||
|
||||
Type::Dynamic(_)
|
||||
| Type::Never
|
||||
| Type::AlwaysTruthy
|
||||
|
@ -5553,8 +5591,9 @@ impl<'db> Type<'db> {
|
|||
| Self::Never
|
||||
| Self::Callable(_)
|
||||
| Self::AlwaysTruthy
|
||||
| Self::AlwaysFalsy
|
||||
| Self::SpecialForm(_)
|
||||
| Self::AlwaysFalsy => None,
|
||||
| Self::TypeIs(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -8476,6 +8515,54 @@ impl<'db> BoundSuperType<'db> {
|
|||
}
|
||||
}
|
||||
|
||||
#[salsa::interned(debug)]
|
||||
pub struct TypeIsType<'db> {
|
||||
return_type: Type<'db>,
|
||||
/// The ID of the scope to which the place belongs
|
||||
/// and the ID of the place itself within that scope.
|
||||
place_info: Option<(ScopeId<'db>, ScopedPlaceId)>,
|
||||
}
|
||||
|
||||
impl<'db> TypeIsType<'db> {
|
||||
pub fn place_name(self, db: &'db dyn Db) -> Option<String> {
|
||||
let (scope, place) = self.place_info(db)?;
|
||||
let table = place_table(db, scope);
|
||||
|
||||
Some(format!("{}", table.place_expr(place)))
|
||||
}
|
||||
|
||||
pub fn unbound(db: &'db dyn Db, ty: Type<'db>) -> Type<'db> {
|
||||
Type::TypeIs(Self::new(db, ty, None))
|
||||
}
|
||||
|
||||
pub fn bound(
|
||||
db: &'db dyn Db,
|
||||
return_type: Type<'db>,
|
||||
scope: ScopeId<'db>,
|
||||
place: ScopedPlaceId,
|
||||
) -> Type<'db> {
|
||||
Type::TypeIs(Self::new(db, return_type, Some((scope, place))))
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn bind(self, db: &'db dyn Db, scope: ScopeId<'db>, place: ScopedPlaceId) -> Type<'db> {
|
||||
Self::bound(db, self.return_type(db), scope, place)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_type(self, db: &'db dyn Db, ty: Type<'db>) -> Type<'db> {
|
||||
Type::TypeIs(Self::new(db, ty, self.place_info(db)))
|
||||
}
|
||||
|
||||
pub fn is_bound(&self, db: &'db dyn Db) -> bool {
|
||||
self.place_info(db).is_some()
|
||||
}
|
||||
|
||||
pub fn is_unbound(&self, db: &'db dyn Db) -> bool {
|
||||
self.place_info(db).is_none()
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure that the `Type` enum does not grow unexpectedly.
|
||||
#[cfg(not(debug_assertions))]
|
||||
#[cfg(target_pointer_width = "64")]
|
||||
|
|
|
@ -146,7 +146,8 @@ impl<'db> ClassBase<'db> {
|
|||
| Type::BoundSuper(_)
|
||||
| Type::ProtocolInstance(_)
|
||||
| Type::AlwaysFalsy
|
||||
| Type::AlwaysTruthy => None,
|
||||
| Type::AlwaysTruthy
|
||||
| Type::TypeIs(_) => None,
|
||||
|
||||
Type::KnownInstance(known_instance) => match known_instance {
|
||||
KnownInstanceType::SubscriptedGeneric(_) => Some(Self::Generic),
|
||||
|
|
|
@ -54,6 +54,8 @@ pub(crate) fn register_lints(registry: &mut LintRegistryBuilder) {
|
|||
registry.register_lint(&INVALID_SUPER_ARGUMENT);
|
||||
registry.register_lint(&INVALID_TYPE_CHECKING_CONSTANT);
|
||||
registry.register_lint(&INVALID_TYPE_FORM);
|
||||
registry.register_lint(&INVALID_TYPE_GUARD_DEFINITION);
|
||||
registry.register_lint(&INVALID_TYPE_GUARD_CALL);
|
||||
registry.register_lint(&INVALID_TYPE_VARIABLE_CONSTRAINTS);
|
||||
registry.register_lint(&MISSING_ARGUMENT);
|
||||
registry.register_lint(&NO_MATCHING_OVERLOAD);
|
||||
|
@ -893,6 +895,62 @@ declare_lint! {
|
|||
}
|
||||
}
|
||||
|
||||
declare_lint! {
|
||||
/// ## What it does
|
||||
/// Checks for type guard functions without
|
||||
/// a first non-self-like non-keyword-only non-variadic parameter.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Type narrowing functions must accept at least one positional argument
|
||||
/// (non-static methods must accept another in addition to `self`/`cls`).
|
||||
///
|
||||
/// Extra parameters/arguments are allowed but do not affect narrowing.
|
||||
///
|
||||
/// ## Examples
|
||||
/// ```python
|
||||
/// from typing import TypeIs
|
||||
///
|
||||
/// def f() -> TypeIs[int]: ... # Error, no parameter
|
||||
/// def f(*, v: object) -> TypeIs[int]: ... # Error, no positional arguments allowed
|
||||
/// def f(*args: object) -> TypeIs[int]: ... # Error, expect variadic arguments
|
||||
/// class C:
|
||||
/// def f(self) -> TypeIs[int]: ... # Error, only positional argument expected is `self`
|
||||
/// ```
|
||||
pub(crate) static INVALID_TYPE_GUARD_DEFINITION = {
|
||||
summary: "detects malformed type guard functions",
|
||||
status: LintStatus::preview("1.0.0"),
|
||||
default_level: Level::Error,
|
||||
}
|
||||
}
|
||||
|
||||
declare_lint! {
|
||||
/// ## What it does
|
||||
/// Checks for type guard function calls without a valid target.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// The first non-keyword non-variadic argument to a type guard function
|
||||
/// is its target and must map to a symbol.
|
||||
///
|
||||
/// Starred (`is_str(*a)`), literal (`is_str(42)`) and other non-symbol-like
|
||||
/// expressions are invalid as narrowing targets.
|
||||
///
|
||||
/// ## Examples
|
||||
/// ```python
|
||||
/// from typing import TypeIs
|
||||
///
|
||||
/// def f(v: object) -> TypeIs[int]: ...
|
||||
///
|
||||
/// f() # Error
|
||||
/// f(*a) # Error
|
||||
/// f(10) # Error
|
||||
/// ```
|
||||
pub(crate) static INVALID_TYPE_GUARD_CALL = {
|
||||
summary: "detects type guard function calls that has no narrowing effect",
|
||||
status: LintStatus::preview("1.0.0"),
|
||||
default_level: Level::Error,
|
||||
}
|
||||
}
|
||||
|
||||
declare_lint! {
|
||||
/// ## What it does
|
||||
/// Checks for constrained [type variables] with only one constraint.
|
||||
|
|
|
@ -211,6 +211,15 @@ impl Display for DisplayRepresentation<'_> {
|
|||
owner = bound_super.owner(self.db).into_type().display(self.db)
|
||||
)
|
||||
}
|
||||
Type::TypeIs(type_is) => {
|
||||
f.write_str("TypeIs[")?;
|
||||
type_is.return_type(self.db).display(self.db).fmt(f)?;
|
||||
if let Some(name) = type_is.place_name(self.db) {
|
||||
f.write_str(" @ ")?;
|
||||
f.write_str(&name)?;
|
||||
}
|
||||
f.write_str("]")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -116,7 +116,8 @@ impl AllMembers {
|
|||
| Type::SpecialForm(_)
|
||||
| Type::KnownInstance(_)
|
||||
| Type::TypeVar(_)
|
||||
| Type::BoundSuper(_) => {
|
||||
| Type::BoundSuper(_)
|
||||
| Type::TypeIs(_) => {
|
||||
if let Type::ClassLiteral(class_literal) = ty.to_meta_type(db) {
|
||||
self.extend_with_class_members(db, class_literal);
|
||||
}
|
||||
|
|
|
@ -68,7 +68,7 @@ use crate::semantic_index::narrowing_constraints::ConstraintKey;
|
|||
use crate::semantic_index::place::{
|
||||
FileScopeId, NodeWithScopeKind, NodeWithScopeRef, PlaceExpr, ScopeId, ScopeKind, ScopedPlaceId,
|
||||
};
|
||||
use crate::semantic_index::{EagerSnapshotResult, SemanticIndex, semantic_index};
|
||||
use crate::semantic_index::{EagerSnapshotResult, SemanticIndex, place_table, semantic_index};
|
||||
use crate::types::call::{
|
||||
Argument, Binding, Bindings, CallArgumentTypes, CallArguments, CallError,
|
||||
};
|
||||
|
@ -78,13 +78,14 @@ use crate::types::diagnostic::{
|
|||
CYCLIC_CLASS_DEFINITION, DIVISION_BY_ZERO, INCONSISTENT_MRO, 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_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_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_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_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,
|
||||
|
@ -99,8 +100,8 @@ use crate::types::{
|
|||
KnownInstanceType, LintDiagnosticGuard, MemberLookupPolicy, MetaclassCandidate,
|
||||
PEP695TypeAliasType, Parameter, ParameterForm, Parameters, SpecialFormType, StringLiteralType,
|
||||
SubclassOfType, Truthiness, TupleType, Type, TypeAliasType, TypeAndQualifiers,
|
||||
TypeArrayDisplay, TypeQualifiers, TypeVarBoundOrConstraints, TypeVarInstance, TypeVarKind,
|
||||
TypeVarVariance, UnionBuilder, UnionType, binding_type, todo_type,
|
||||
TypeArrayDisplay, TypeIsType, TypeQualifiers, TypeVarBoundOrConstraints, TypeVarInstance,
|
||||
TypeVarKind, TypeVarVariance, UnionBuilder, UnionType, binding_type, todo_type,
|
||||
};
|
||||
use crate::unpack::{Unpack, UnpackPosition};
|
||||
use crate::util::subscript::{PyIndex, PySlice};
|
||||
|
@ -672,6 +673,10 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
|||
self.types.expressions.extend(inference.expressions.iter());
|
||||
self.types.deferred.extend(inference.deferred.iter());
|
||||
self.context.extend(inference.diagnostics());
|
||||
self.types.cycle_fallback_type = self
|
||||
.types
|
||||
.cycle_fallback_type
|
||||
.or(inference.cycle_fallback_type);
|
||||
}
|
||||
|
||||
fn file(&self) -> File {
|
||||
|
@ -1904,6 +1909,10 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
|||
}
|
||||
|
||||
let declared_ty = self.file_expression_type(returns);
|
||||
let expected_ty = match declared_ty {
|
||||
Type::TypeIs(_) => KnownClass::Bool.to_instance(self.db()),
|
||||
ty => ty,
|
||||
};
|
||||
|
||||
let scope_id = self.index.node_scope(NodeWithScopeRef::Function(function));
|
||||
if scope_id.is_generator_function(self.index) {
|
||||
|
@ -1921,7 +1930,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
|||
|
||||
if !inferred_return
|
||||
.to_instance(self.db())
|
||||
.is_assignable_to(self.db(), declared_ty)
|
||||
.is_assignable_to(self.db(), expected_ty)
|
||||
{
|
||||
report_invalid_generator_function_return_type(
|
||||
&self.context,
|
||||
|
@ -1947,7 +1956,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
|||
ty if ty.is_notimplemented(self.db()) => None,
|
||||
_ => Some(ty_range),
|
||||
})
|
||||
.filter(|ty_range| !ty_range.ty.is_assignable_to(self.db(), declared_ty))
|
||||
.filter(|ty_range| !ty_range.ty.is_assignable_to(self.db(), expected_ty))
|
||||
{
|
||||
report_invalid_return_type(
|
||||
&self.context,
|
||||
|
@ -1959,7 +1968,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
|||
}
|
||||
let use_def = self.index.use_def_map(scope_id);
|
||||
if use_def.can_implicit_return(self.db())
|
||||
&& !Type::none(self.db()).is_assignable_to(self.db(), declared_ty)
|
||||
&& !Type::none(self.db()).is_assignable_to(self.db(), expected_ty)
|
||||
{
|
||||
let no_return = self.return_types_and_ranges.is_empty();
|
||||
report_implicit_return_type(
|
||||
|
@ -3213,7 +3222,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
|||
| Type::DataclassTransformer(_)
|
||||
| Type::TypeVar(..)
|
||||
| Type::AlwaysTruthy
|
||||
| Type::AlwaysFalsy => {
|
||||
| Type::AlwaysFalsy
|
||||
| Type::TypeIs(_) => {
|
||||
let is_read_only = || {
|
||||
let dataclass_params = match object_ty {
|
||||
Type::NominalInstance(instance) => match instance.class {
|
||||
|
@ -5800,7 +5810,45 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
|||
}
|
||||
}
|
||||
}
|
||||
bindings.return_type(self.db())
|
||||
|
||||
let db = self.db();
|
||||
let scope = self.scope();
|
||||
let return_ty = bindings.return_type(db);
|
||||
|
||||
let find_narrowed_place = || match arguments.args.first() {
|
||||
None => {
|
||||
// This branch looks extraneous, especially in the face of `missing-arguments`.
|
||||
// However, that lint won't be able to catch this:
|
||||
//
|
||||
// ```python
|
||||
// def f(v: object = object()) -> TypeIs[int]: ...
|
||||
//
|
||||
// if f(): ...
|
||||
// ```
|
||||
//
|
||||
// TODO: Will this report things that is actually fine?
|
||||
if let Some(builder) = self
|
||||
.context
|
||||
.report_lint(&INVALID_TYPE_GUARD_CALL, arguments)
|
||||
{
|
||||
builder.into_diagnostic("Type guard call does not have a target");
|
||||
}
|
||||
None
|
||||
}
|
||||
Some(expr) => match PlaceExpr::try_from(expr) {
|
||||
Ok(place_expr) => place_table(db, scope).place_id_by_expr(&place_expr),
|
||||
Err(()) => None,
|
||||
},
|
||||
};
|
||||
|
||||
match return_ty {
|
||||
// TODO: TypeGuard
|
||||
Type::TypeIs(type_is) => match find_narrowed_place() {
|
||||
Some(place) => type_is.bind(db, scope, place),
|
||||
None => return_ty,
|
||||
},
|
||||
_ => return_ty,
|
||||
}
|
||||
}
|
||||
|
||||
Err(CallError(_, bindings)) => {
|
||||
|
@ -6428,7 +6476,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
|||
| Type::BytesLiteral(_)
|
||||
| Type::Tuple(_)
|
||||
| Type::BoundSuper(_)
|
||||
| Type::TypeVar(_),
|
||||
| Type::TypeVar(_)
|
||||
| Type::TypeIs(_),
|
||||
) => {
|
||||
let unary_dunder_method = match op {
|
||||
ast::UnaryOp::Invert => "__invert__",
|
||||
|
@ -6759,7 +6808,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
|||
| Type::BytesLiteral(_)
|
||||
| Type::Tuple(_)
|
||||
| Type::BoundSuper(_)
|
||||
| Type::TypeVar(_),
|
||||
| Type::TypeVar(_)
|
||||
| Type::TypeIs(_),
|
||||
Type::FunctionLiteral(_)
|
||||
| Type::Callable(..)
|
||||
| Type::BoundMethod(_)
|
||||
|
@ -6785,7 +6835,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
|||
| Type::BytesLiteral(_)
|
||||
| Type::Tuple(_)
|
||||
| Type::BoundSuper(_)
|
||||
| Type::TypeVar(_),
|
||||
| Type::TypeVar(_)
|
||||
| Type::TypeIs(_),
|
||||
op,
|
||||
) => {
|
||||
// We either want to call lhs.__op__ or rhs.__rop__. The full decision tree from
|
||||
|
@ -9552,10 +9603,22 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
|
|||
self.infer_type_expression(arguments_slice);
|
||||
todo_type!("`Required[]` type qualifier")
|
||||
}
|
||||
SpecialFormType::TypeIs => {
|
||||
self.infer_type_expression(arguments_slice);
|
||||
todo_type!("`TypeIs[]` special form")
|
||||
}
|
||||
SpecialFormType::TypeIs => match arguments_slice {
|
||||
ast::Expr::Tuple(_) => {
|
||||
self.infer_type_expression(arguments_slice);
|
||||
|
||||
if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) {
|
||||
let diag = builder.into_diagnostic(format_args!(
|
||||
"Special form `{}` expected exactly one type parameter",
|
||||
special_form.repr()
|
||||
));
|
||||
diagnostic::add_type_expression_reference_link(diag);
|
||||
}
|
||||
|
||||
Type::unknown()
|
||||
}
|
||||
_ => TypeIsType::unbound(self.db(), self.infer_type_expression(arguments_slice)),
|
||||
},
|
||||
SpecialFormType::TypeGuard => {
|
||||
self.infer_type_expression(arguments_slice);
|
||||
todo_type!("`TypeGuard[]` special form")
|
||||
|
|
|
@ -388,7 +388,6 @@ impl<'db, 'ast> NarrowingConstraintsBuilder<'db, 'ast> {
|
|||
let ast::ExprName { id, .. } = expr_name;
|
||||
|
||||
let symbol = self.expect_expr_name_symbol(id);
|
||||
|
||||
let ty = if is_positive {
|
||||
Type::AlwaysFalsy.negate(self.db)
|
||||
} else {
|
||||
|
@ -728,6 +727,29 @@ impl<'db, 'ast> NarrowingConstraintsBuilder<'db, 'ast> {
|
|||
// TODO: add support for PEP 604 union types on the right hand side of `isinstance`
|
||||
// and `issubclass`, for example `isinstance(x, str | (int | float))`.
|
||||
match callable_ty {
|
||||
Type::FunctionLiteral(function_type)
|
||||
if matches!(
|
||||
function_type.known(self.db),
|
||||
None | Some(KnownFunction::RevealType)
|
||||
) =>
|
||||
{
|
||||
let return_ty =
|
||||
inference.expression_type(expr_call.scoped_expression_id(self.db, scope));
|
||||
|
||||
let (guarded_ty, place) = match return_ty {
|
||||
// TODO: TypeGuard
|
||||
Type::TypeIs(type_is) => {
|
||||
let (_, place) = type_is.place_info(self.db)?;
|
||||
(type_is.return_type(self.db), place)
|
||||
}
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
Some(NarrowingConstraints::from_iter([(
|
||||
place,
|
||||
guarded_ty.negate_if(self.db, !is_positive),
|
||||
)]))
|
||||
}
|
||||
Type::FunctionLiteral(function_type) if expr_call.arguments.keywords.is_empty() => {
|
||||
let [first_arg, second_arg] = &*expr_call.arguments.args else {
|
||||
return None;
|
||||
|
|
|
@ -3,7 +3,7 @@ use std::cmp::Ordering;
|
|||
use crate::db::Db;
|
||||
|
||||
use super::{
|
||||
DynamicType, SuperOwnerKind, TodoType, Type, class_base::ClassBase,
|
||||
DynamicType, SuperOwnerKind, TodoType, Type, TypeIsType, class_base::ClassBase,
|
||||
subclass_of::SubclassOfInner,
|
||||
};
|
||||
|
||||
|
@ -126,6 +126,10 @@ pub(super) fn union_or_intersection_elements_ordering<'db>(
|
|||
(Type::SubclassOf(_), _) => Ordering::Less,
|
||||
(_, Type::SubclassOf(_)) => Ordering::Greater,
|
||||
|
||||
(Type::TypeIs(left), Type::TypeIs(right)) => typeis_ordering(db, *left, *right),
|
||||
(Type::TypeIs(_), _) => Ordering::Less,
|
||||
(_, Type::TypeIs(_)) => Ordering::Greater,
|
||||
|
||||
(Type::NominalInstance(left), Type::NominalInstance(right)) => left.class.cmp(&right.class),
|
||||
(Type::NominalInstance(_), _) => Ordering::Less,
|
||||
(_, Type::NominalInstance(_)) => Ordering::Greater,
|
||||
|
@ -248,3 +252,25 @@ fn dynamic_elements_ordering(left: DynamicType, right: DynamicType) -> Ordering
|
|||
(_, DynamicType::TodoPEP695ParamSpec) => Ordering::Greater,
|
||||
}
|
||||
}
|
||||
|
||||
/// Determine a canonical order for two instances of [`TypeIsType`].
|
||||
///
|
||||
/// The following criteria are considered, in order:
|
||||
/// * Boundness: Unbound precedes bound
|
||||
/// * Symbol name: String comparison
|
||||
/// * Guarded type: [`union_or_intersection_elements_ordering`]
|
||||
fn typeis_ordering(db: &dyn Db, left: TypeIsType, right: TypeIsType) -> Ordering {
|
||||
let (left_ty, right_ty) = (left.return_type(db), right.return_type(db));
|
||||
|
||||
match (left.place_info(db), right.place_info(db)) {
|
||||
(None, Some(_)) => Ordering::Less,
|
||||
(Some(_), None) => Ordering::Greater,
|
||||
|
||||
(None, None) => union_or_intersection_elements_ordering(db, &left_ty, &right_ty),
|
||||
|
||||
(Some(_), Some(_)) => match left.place_name(db).cmp(&right.place_name(db)) {
|
||||
Ordering::Equal => union_or_intersection_elements_ordering(db, &left_ty, &right_ty),
|
||||
ordering => ordering,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue