[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:
InSync 2025-06-14 05:27:45 +07:00 committed by GitHub
parent 89d915a1e3
commit 6d56ee803e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 841 additions and 97 deletions

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

@ -52,7 +52,7 @@ Calling a non-callable object will raise a `TypeError` at runtime.
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20call-non-callable)
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#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`

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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("]")
}
}
}
}

View file

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

View file

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

View file

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

View file

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