diff --git a/crates/ty/docs/rules.md b/crates/ty/docs/rules.md index 30fe995231..b36ccae657 100644 --- a/crates/ty/docs/rules.md +++ b/crates/ty/docs/rules.md @@ -52,7 +52,7 @@ Calling a non-callable object will raise a `TypeError` at runtime. ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20call-non-callable) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L92) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L94) ## `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) ## `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) ## `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) ## `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) ## `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) ## `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) ## `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) ## `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) ## `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) ## `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) ## `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) ## `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) ## `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) ## `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) ## `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) ## `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) ## `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) ## `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) ## `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) ## `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) ## `invalid-protocol` @@ -798,7 +798,7 @@ TypeError: Protocols can only inherit from other protocols, got ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-protocol) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L336) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L338) ## `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) ## `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) ## `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) ## `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) ## `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) ## `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) + + +## `invalid-type-guard-call` + +**Default level**: error + +
+detects type guard function calls that has no narrowing effect + +### 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) +
+ +## `invalid-type-guard-definition` + +**Default level**: error + +
+detects malformed type guard functions + +### 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)
## `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) ## `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) ## `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) ## `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) ## `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) ## `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) ## `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) ## `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) ## `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) ## `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) ## `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) ## `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) ## `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) ## `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) ## `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) ## `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) ## `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) ## `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) ## `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) ## `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) ## `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) ## `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) ## `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) ## `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) ## `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) ## `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) ## `unused-ignore-comment` diff --git a/crates/ty_python_semantic/resources/mdtest/annotations/unsupported_special_forms.md b/crates/ty_python_semantic/resources/mdtest/annotations/unsupported_special_forms.md index 3887d9bb84..254ed90eea 100644 --- a/crates/ty_python_semantic/resources/mdtest/annotations/unsupported_special_forms.md +++ b/crates/ty_python_semantic/resources/mdtest/annotations/unsupported_special_forms.md @@ -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`)] diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/type_guards.md b/crates/ty_python_semantic/resources/mdtest/narrow/type_guards.md new file mode 100644 index 0000000000..c65a3b22c6 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/narrow/type_guards.md @@ -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[]] + 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 +``` diff --git a/crates/ty_python_semantic/resources/mdtest/type_properties/is_assignable_to.md b/crates/ty_python_semantic/resources/mdtest/type_properties/is_assignable_to.md index 96993f1623..b67c74d693 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_properties/is_assignable_to.md +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/is_assignable_to.md @@ -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 diff --git a/crates/ty_python_semantic/resources/mdtest/type_properties/is_disjoint_from.md b/crates/ty_python_semantic/resources/mdtest/type_properties/is_disjoint_from.md index 20725ebd89..b94093e867 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_properties/is_disjoint_from.md +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/is_disjoint_from.md @@ -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 diff --git a/crates/ty_python_semantic/resources/mdtest/type_properties/is_subtype_of.md b/crates/ty_python_semantic/resources/mdtest/type_properties/is_subtype_of.md index ffa1e0fad0..a77022df7a 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_properties/is_subtype_of.md +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/is_subtype_of.md @@ -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 diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 94dbe1be27..08deb8327c 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -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 { + 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")] diff --git a/crates/ty_python_semantic/src/types/class_base.rs b/crates/ty_python_semantic/src/types/class_base.rs index 8e6e27a7ef..c9c51647c9 100644 --- a/crates/ty_python_semantic/src/types/class_base.rs +++ b/crates/ty_python_semantic/src/types/class_base.rs @@ -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), diff --git a/crates/ty_python_semantic/src/types/diagnostic.rs b/crates/ty_python_semantic/src/types/diagnostic.rs index 8bcf8a8bb0..c613374c47 100644 --- a/crates/ty_python_semantic/src/types/diagnostic.rs +++ b/crates/ty_python_semantic/src/types/diagnostic.rs @@ -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. diff --git a/crates/ty_python_semantic/src/types/display.rs b/crates/ty_python_semantic/src/types/display.rs index 02f38c5701..fd2ad5bea6 100644 --- a/crates/ty_python_semantic/src/types/display.rs +++ b/crates/ty_python_semantic/src/types/display.rs @@ -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("]") + } } } } diff --git a/crates/ty_python_semantic/src/types/ide_support.rs b/crates/ty_python_semantic/src/types/ide_support.rs index b79d232c70..296c9266d5 100644 --- a/crates/ty_python_semantic/src/types/ide_support.rs +++ b/crates/ty_python_semantic/src/types/ide_support.rs @@ -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); } diff --git a/crates/ty_python_semantic/src/types/infer.rs b/crates/ty_python_semantic/src/types/infer.rs index 7b3b79472f..812e379ff9 100644 --- a/crates/ty_python_semantic/src/types/infer.rs +++ b/crates/ty_python_semantic/src/types/infer.rs @@ -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") diff --git a/crates/ty_python_semantic/src/types/narrow.rs b/crates/ty_python_semantic/src/types/narrow.rs index 9af855a100..9fa59dfcac 100644 --- a/crates/ty_python_semantic/src/types/narrow.rs +++ b/crates/ty_python_semantic/src/types/narrow.rs @@ -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; diff --git a/crates/ty_python_semantic/src/types/type_ordering.rs b/crates/ty_python_semantic/src/types/type_ordering.rs index 669d116fd9..d4b1a5bcf4 100644 --- a/crates/ty_python_semantic/src/types/type_ordering.rs +++ b/crates/ty_python_semantic/src/types/type_ordering.rs @@ -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, + }, + } +} diff --git a/ty.schema.json b/ty.schema.json index d3180fb4db..cfef1c1aef 100644 --- a/ty.schema.json +++ b/ty.schema.json @@ -563,6 +563,26 @@ } ] }, + "invalid-type-guard-call": { + "title": "detects type guard function calls that has no narrowing effect", + "description": "## What it does\nChecks for type guard function calls without a valid target.\n\n## Why is this bad?\nThe first non-keyword non-variadic argument to a type guard function\nis its target and must map to a symbol.\n\nStarred (`is_str(*a)`), literal (`is_str(42)`) and other non-symbol-like\nexpressions are invalid as narrowing targets.\n\n## Examples\n```python\nfrom typing import TypeIs\n\ndef f(v: object) -> TypeIs[int]: ...\n\nf() # Error\nf(*a) # Error\nf(10) # Error\n```", + "default": "error", + "oneOf": [ + { + "$ref": "#/definitions/Level" + } + ] + }, + "invalid-type-guard-definition": { + "title": "detects malformed type guard functions", + "description": "## What it does\nChecks for type guard functions without\na first non-self-like non-keyword-only non-variadic parameter.\n\n## Why is this bad?\nType narrowing functions must accept at least one positional argument\n(non-static methods must accept another in addition to `self`/`cls`).\n\nExtra parameters/arguments are allowed but do not affect narrowing.\n\n## Examples\n```python\nfrom typing import TypeIs\n\ndef f() -> TypeIs[int]: ... # Error, no parameter\ndef f(*, v: object) -> TypeIs[int]: ... # Error, no positional arguments allowed\ndef f(*args: object) -> TypeIs[int]: ... # Error, expect variadic arguments\nclass C:\n def f(self) -> TypeIs[int]: ... # Error, only positional argument expected is `self`\n```", + "default": "error", + "oneOf": [ + { + "$ref": "#/definitions/Level" + } + ] + }, "invalid-type-variable-constraints": { "title": "detects invalid type variable constraints", "description": "## What it does\nChecks for constrained [type variables] with only one constraint.\n\n## Why is this bad?\nA constrained type variable must have at least two constraints.\n\n## Examples\n```python\nfrom typing import TypeVar\n\nT = TypeVar('T', str) # invalid constrained TypeVar\n```\n\nUse instead:\n```python\nT = TypeVar('T', str, int) # valid constrained TypeVar\n# or\nT = TypeVar('T', bound=str) # valid bound TypeVar\n```\n\n[type variables]: https://docs.python.org/3/library/typing.html#typing.TypeVar",