diff --git a/crates/ty/docs/rules.md b/crates/ty/docs/rules.md index 1e190e7dd6..cd207b21f3 100644 --- a/crates/ty/docs/rules.md +++ b/crates/ty/docs/rules.md @@ -36,7 +36,7 @@ def test(): -> "int": Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [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#L104) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L109) **What it does** @@ -58,7 +58,7 @@ Calling a non-callable object will raise a `TypeError` at runtime. Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [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#L148) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L153) **What it does** @@ -88,7 +88,7 @@ f(int) # error Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [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#L174) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L179) **What it does** @@ -117,7 +117,7 @@ a = 1 Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [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#L199) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L204) **What it does** @@ -147,7 +147,7 @@ class C(A, B): ... Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [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#L225) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L230) **What it does** @@ -177,7 +177,7 @@ class B(A): ... Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [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#L290) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L295) **What it does** @@ -202,7 +202,7 @@ class B(A, A): ... Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20duplicate-kw-only) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L311) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L316) **What it does** @@ -306,7 +306,7 @@ def test(): -> "Literal[5]": Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [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#L479) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L519) **What it does** @@ -334,7 +334,7 @@ class C(A, B): ... Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [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#L503) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L543) **What it does** @@ -358,7 +358,7 @@ t[3] # IndexError: tuple index out of range Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20instance-layout-conflict) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L343) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L348) **What it does** @@ -445,7 +445,7 @@ an atypical memory layout. Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [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#L548) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L588) **What it does** @@ -470,7 +470,7 @@ func("foo") # error: [invalid-argument-type] Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [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#L588) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L628) **What it does** @@ -496,7 +496,7 @@ a: int = '' Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [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#L1622) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1662) **What it does** @@ -528,7 +528,7 @@ C.instance_var = 3 # error: Cannot assign to instance variable Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-await) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L610) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L650) **What it does** @@ -562,7 +562,7 @@ asyncio.run(main()) Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [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#L640) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L680) **What it does** @@ -584,7 +584,7 @@ class A(42): ... # error: [invalid-base] Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [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#L691) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L731) **What it does** @@ -609,7 +609,7 @@ with 1: Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [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#L712) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L752) **What it does** @@ -636,7 +636,7 @@ a: str Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [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#L735) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L775) **What it does** @@ -678,7 +678,7 @@ except ZeroDivisionError: Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [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#L771) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L811) **What it does** @@ -709,7 +709,7 @@ class C[U](Generic[T]): ... Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-key) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L523) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L563) **What it does** @@ -738,7 +738,7 @@ alice["height"] # KeyError: 'height' Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [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#L797) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L837) **What it does** @@ -771,7 +771,7 @@ def f(t: TypeVar("U")): ... Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [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#L846) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L886) **What it does** @@ -803,7 +803,7 @@ class B(metaclass=f): ... Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-named-tuple) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L453) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L493) **What it does** @@ -833,7 +833,7 @@ TypeError: can only inherit from a NamedTuple type and Generic Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [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#L873) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L913) **What it does** @@ -881,7 +881,7 @@ def foo(x: int) -> int: ... Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [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#L916) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L956) **What it does** @@ -905,12 +905,12 @@ def f(a: int = ''): ... Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [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#L425) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L430) **What it does** -Checks for invalidly defined protocol classes. +Checks for protocol classes that will raise `TypeError` at runtime. **Why is this bad?** @@ -937,7 +937,7 @@ TypeError: Protocols can only inherit from other protocols, got Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [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#L936) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L976) Checks for `raise` statements that raise non-exceptions or use invalid @@ -984,7 +984,7 @@ def g(): Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [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#L569) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L609) **What it does** @@ -1007,7 +1007,7 @@ def func() -> int: Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [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#L979) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1019) **What it does** @@ -1061,7 +1061,7 @@ TODO #14889 Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [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#L825) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L865) **What it does** @@ -1086,7 +1086,7 @@ NewAlias = TypeAliasType(get_name(), int) # error: TypeAliasType name mus Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [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#L1018) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1058) **What it does** @@ -1114,7 +1114,7 @@ TYPE_CHECKING = '' Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [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#L1042) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1082) **What it does** @@ -1142,7 +1142,7 @@ b: Annotated[int] # `Annotated` expects at least two arguments Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [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#L1094) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1134) **What it does** @@ -1174,7 +1174,7 @@ f(10) # Error Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [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#L1066) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1106) **What it does** @@ -1206,7 +1206,7 @@ class C: Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [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#L1122) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1162) **What it does** @@ -1239,7 +1239,7 @@ T = TypeVar('T', bound=str) # valid bound TypeVar Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [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#L1151) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1191) **What it does** @@ -1262,7 +1262,7 @@ func() # TypeError: func() missing 1 required positional argument: 'x' Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [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#L1170) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1210) **What it does** @@ -1289,7 +1289,7 @@ func("string") # error: [no-matching-overload] Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [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#L1193) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1233) **What it does** @@ -1311,7 +1311,7 @@ Subscripting an object that does not support it will raise a `TypeError` at runt Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [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#L1211) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1251) **What it does** @@ -1335,7 +1335,7 @@ for i in 34: # TypeError: 'int' object is not iterable Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [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#L1262) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1302) **What it does** @@ -1389,7 +1389,7 @@ def test(): -> "int": Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [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#L1598) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1638) **What it does** @@ -1417,7 +1417,7 @@ static_assert(int(2.0 * 3.0) == 6) # error: does not have a statically known tr Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [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#L1353) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1393) **What it does** @@ -1444,7 +1444,7 @@ class B(A): ... # Error raised here Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [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#L1398) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1438) **What it does** @@ -1469,7 +1469,7 @@ f("foo") # Error raised here Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [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#L1376) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1416) **What it does** @@ -1495,7 +1495,7 @@ def _(x: int): Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [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#L1419) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1459) **What it does** @@ -1539,7 +1539,7 @@ class A: Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [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#L1476) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1516) **What it does** @@ -1564,7 +1564,7 @@ f(x=1, y=2) # Error raised here Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [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#L1497) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1537) **What it does** @@ -1590,7 +1590,7 @@ A().foo # AttributeError: 'A' object has no attribute 'foo' Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [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#L1519) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1559) **What it does** @@ -1613,7 +1613,7 @@ import foo # ModuleNotFoundError: No module named 'foo' Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [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#L1538) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1578) **What it does** @@ -1636,7 +1636,7 @@ print(x) # NameError: name 'x' is not defined Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [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#L1231) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1271) **What it does** @@ -1671,7 +1671,7 @@ b1 < b2 < b1 # exception raised here Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [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#L1557) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1597) **What it does** @@ -1697,7 +1697,7 @@ A() + A() # TypeError: unsupported operand type(s) for +: 'A' and 'A' Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [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#L1579) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1619) **What it does** @@ -1715,12 +1715,51 @@ l = list(range(10)) l[1:10:0] # ValueError: slice step cannot be zero ``` +## `ambiguous-protocol-member` + + +Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") · +[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20ambiguous-protocol-member) · +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L458) + + +**What it does** + +Checks for protocol classes with members that will lead to ambiguous interfaces. + +**Why is this bad?** + +Assigning to an undeclared variable in a protocol class leads to an ambiguous +interface which may lead to the type checker inferring unexpected things. It's +recommended to ensure that all members of a protocol class are explicitly declared. + +**Examples** + + +```py +from typing import Protocol + +class BaseProto(Protocol): + a: int # fine (explicitly declared as `int`) + def method_member(self) -> int: ... # fine: a method definition using `def` is considered a declaration + c = "some variable" # error: no explicit declaration, leading to ambiguity + b = method_member # error: no explicit declaration, leading to ambiguity + + # error: this creates implicit assignments of `d` and `e` in the protocol class body. + # Were they really meant to be considered protocol members? + for d, e in enumerate(range(42)): + pass + +class SubProto(BaseProto, Protocol): + a = 42 # fine (declared in superclass) +``` + ## `deprecated` Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20deprecated) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L269) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L274) **What it does** @@ -1773,7 +1812,7 @@ a = 20 / 0 # type: ignore Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") · [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#L1283) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1323) **What it does** @@ -1799,7 +1838,7 @@ A.c # AttributeError: type object 'A' has no attribute 'c' Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") · [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#L122) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L127) **What it does** @@ -1829,7 +1868,7 @@ A()[0] # TypeError: 'A' object is not subscriptable Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") · [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#L1305) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1345) **What it does** @@ -1859,7 +1898,7 @@ from module import a # ImportError: cannot import name 'a' from 'module' Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") · [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#L1650) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1690) **What it does** @@ -1884,7 +1923,7 @@ cast(int, f()) # Redundant Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") · [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#L1458) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1498) **What it does** @@ -1935,7 +1974,7 @@ a = 20 / 0 # ty: ignore[division-by-zero] Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-global) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1671) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1711) **What it does** @@ -1989,7 +2028,7 @@ def g(): Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") · [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#L658) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L698) **What it does** @@ -2026,7 +2065,7 @@ class D(C): ... # error: [unsupported-base] Default level: [`ignore`](../rules.md#rule-levels "This lint has a default level of 'ignore'.") · [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#L251) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L256) **What it does** @@ -2048,7 +2087,7 @@ Dividing by zero raises a `ZeroDivisionError` at runtime. Default level: [`ignore`](../rules.md#rule-levels "This lint has a default level of 'ignore'.") · [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#L1331) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1371) **What it does** diff --git a/crates/ty_python_semantic/resources/mdtest/protocols.md b/crates/ty_python_semantic/resources/mdtest/protocols.md index ae8dc65d9d..5ad6f576d5 100644 --- a/crates/ty_python_semantic/resources/mdtest/protocols.md +++ b/crates/ty_python_semantic/resources/mdtest/protocols.md @@ -482,6 +482,8 @@ reveal_type(get_protocol_members(Baz2)) ## Protocol members in statically known branches + + The list of protocol members does not include any members declared in branches that are statically known to be unreachable: @@ -492,7 +494,7 @@ python-version = "3.9" ```py import sys -from typing_extensions import Protocol, get_protocol_members +from typing_extensions import Protocol, get_protocol_members, reveal_type class Foo(Protocol): if sys.version_info >= (3, 10): @@ -501,7 +503,7 @@ class Foo(Protocol): def c(self) -> None: ... else: d: int - e = 56 + e = 56 # error: [ambiguous-protocol-member] def f(self) -> None: ... reveal_type(get_protocol_members(Foo)) # revealed: frozenset[Literal["d", "e", "f"]] @@ -797,9 +799,9 @@ def f(arg: HasXWithDefault): ``` Assignments in a class body of a protocol -- of any kind -- are not permitted by ty unless the -symbol being assigned to is also explicitly declared in the protocol's class body. Note that this is -stricter validation of protocol members than many other type checkers currently apply (as of -2025/04/21). +symbol being assigned to is also explicitly declared in the body of the protocol class or one of its +superclasses. Note that this is stricter validation of protocol members than many other type +checkers currently apply (as of 2025/04/21). The reason for this strict validation is that undeclared variables in the class body would lead to an ambiguous interface being declared by the protocol. @@ -823,24 +825,75 @@ class LotsOfBindings(Protocol): class Nested: ... # also weird, but we should also probably allow it class NestedProtocol(Protocol): ... # same here... - e = 72 # TODO: this should error with `[invalid-protocol]` (`e` is not declared) + e = 72 # error: [ambiguous-protocol-member] - f, g = (1, 2) # TODO: this should error with `[invalid-protocol]` (`f` and `g` are not declared) + # error: [ambiguous-protocol-member] "Consider adding an annotation, e.g. `f: int = ...`" + # error: [ambiguous-protocol-member] "Consider adding an annotation, e.g. `g: int = ...`" + f, g = (1, 2) - h: int = (i := 3) # TODO: this should error with `[invalid-protocol]` (`i` is not declared) + h: int = (i := 3) # error: [ambiguous-protocol-member] - for j in range(42): # TODO: this should error with `[invalid-protocol]` (`j` is not declared) + for j in range(42): # error: [ambiguous-protocol-member] pass - with MyContext() as k: # TODO: this should error with `[invalid-protocol]` (`k` is not declared) + with MyContext() as k: # error: [ambiguous-protocol-member] pass match object(): - case l: # TODO: this should error with `[invalid-protocol]` (`l` is not declared) + case l: # error: [ambiguous-protocol-member] ... # revealed: frozenset[Literal["Nested", "NestedProtocol", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l"]] reveal_type(get_protocol_members(LotsOfBindings)) + +class Foo(Protocol): + a: int + +class Bar(Foo, Protocol): + a = 42 # fine, because it's declared in the superclass + +reveal_type(get_protocol_members(Bar)) # revealed: frozenset[Literal["a"]] +``` + +A binding-without-declaration will not be reported if it occurs in a branch that we can statically +determine to be unreachable. The reason is that we don't consider it to be a protocol member at all +if all definitions for the variable are in unreachable blocks: + +```py +import sys + +class Protocol694(Protocol): + if sys.version_info > (3, 694): + x = 42 # no error! +``` + +If there are multiple bindings of the variable in the class body, however, and at least one of the +bindings occurs in a block of code that is understood to be (possibly) reachable, a diagnostic will +be reported. The diagnostic will be attached to the first binding that occurs in the class body, +even if that first definition occurs in an unreachable block: + +```py +class Protocol695(Protocol): + if sys.version_info > (3, 695): + x = 42 + else: + x = 42 + + x = 56 # error: [ambiguous-protocol-member] +``` + +In order for the variable to be considered declared, the declaration of the variable must also take +place in a block of code that is understood to be (possibly) reachable: + +```py +class Protocol696(Protocol): + if sys.version_info > (3, 696): + x: int + else: + x = 42 # error: [ambiguous-protocol-member] + y: int + + y = 56 # no error ``` Attribute members are allowed to have assignments in methods on the protocol class, just like @@ -943,6 +996,40 @@ static_assert(not is_assignable_to(HasX, Foo)) static_assert(not is_subtype_of(HasX, Foo)) ``` +## Diagnostics for protocols with invalid attribute members + +This is a short appendix to the previous section with the `snapshot-diagnostics` directive enabled +(enabling snapshots for the previous section in its entirety would lead to a huge snapshot, since +it's a large section). + + + +```py +from typing import Protocol + +def coinflip() -> bool: + return True + +class A(Protocol): + # The `x` and `y` members attempt to use Python-2-style type comments + # to indicate that the type should be `int | None` and `str` respectively, + # but we don't support those + + # error: [ambiguous-protocol-member] + a = None # type: int + # error: [ambiguous-protocol-member] + b = ... # type: str + + if coinflip(): + c = 1 # error: [ambiguous-protocol-member] + else: + c = 2 + + # error: [ambiguous-protocol-member] + for d in range(42): + pass +``` + ## Equivalence of protocols Two protocols are considered equivalent types if they specify the same interface, even if they have diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/protocols.md_-_Protocols_-_Diagnostics_for_prot…_(585a3e9545d41b64).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/protocols.md_-_Protocols_-_Diagnostics_for_prot…_(585a3e9545d41b64).snap new file mode 100644 index 0000000000..425665b486 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/protocols.md_-_Protocols_-_Diagnostics_for_prot…_(585a3e9545d41b64).snap @@ -0,0 +1,140 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: protocols.md - Protocols - Diagnostics for protocols with invalid attribute members +mdtest path: crates/ty_python_semantic/resources/mdtest/protocols.md +--- + +# Python source files + +## mdtest_snippet.py + +``` + 1 | from typing import Protocol + 2 | + 3 | def coinflip() -> bool: + 4 | return True + 5 | + 6 | class A(Protocol): + 7 | # The `x` and `y` members attempt to use Python-2-style type comments + 8 | # to indicate that the type should be `int | None` and `str` respectively, + 9 | # but we don't support those +10 | +11 | # error: [ambiguous-protocol-member] +12 | a = None # type: int +13 | # error: [ambiguous-protocol-member] +14 | b = ... # type: str +15 | +16 | if coinflip(): +17 | c = 1 # error: [ambiguous-protocol-member] +18 | else: +19 | c = 2 +20 | +21 | # error: [ambiguous-protocol-member] +22 | for d in range(42): +23 | pass +``` + +# Diagnostics + +``` +warning[ambiguous-protocol-member]: Cannot assign to undeclared variable in the body of a protocol class + --> src/mdtest_snippet.py:12:5 + | +11 | # error: [ambiguous-protocol-member] +12 | a = None # type: int + | ^^^^^^^^ Consider adding an annotation for `a` +13 | # error: [ambiguous-protocol-member] +14 | b = ... # type: str + | +info: Assigning to an undeclared variable in a protocol class leads to an ambiguous interface + --> src/mdtest_snippet.py:6:7 + | +4 | return True +5 | +6 | class A(Protocol): + | ^^^^^^^^^^^ `A` declared as a protocol here +7 | # The `x` and `y` members attempt to use Python-2-style type comments +8 | # to indicate that the type should be `int | None` and `str` respectively, + | +info: No declarations found for `a` in the body of `A` or any of its superclasses +info: rule `ambiguous-protocol-member` is enabled by default + +``` + +``` +warning[ambiguous-protocol-member]: Cannot assign to undeclared variable in the body of a protocol class + --> src/mdtest_snippet.py:14:5 + | +12 | a = None # type: int +13 | # error: [ambiguous-protocol-member] +14 | b = ... # type: str + | ^^^^^^^ Consider adding an annotation for `b` +15 | +16 | if coinflip(): + | +info: Assigning to an undeclared variable in a protocol class leads to an ambiguous interface + --> src/mdtest_snippet.py:6:7 + | +4 | return True +5 | +6 | class A(Protocol): + | ^^^^^^^^^^^ `A` declared as a protocol here +7 | # The `x` and `y` members attempt to use Python-2-style type comments +8 | # to indicate that the type should be `int | None` and `str` respectively, + | +info: No declarations found for `b` in the body of `A` or any of its superclasses +info: rule `ambiguous-protocol-member` is enabled by default + +``` + +``` +warning[ambiguous-protocol-member]: Cannot assign to undeclared variable in the body of a protocol class + --> src/mdtest_snippet.py:17:9 + | +16 | if coinflip(): +17 | c = 1 # error: [ambiguous-protocol-member] + | ^^^^^ Consider adding an annotation, e.g. `c: int = ...` +18 | else: +19 | c = 2 + | +info: Assigning to an undeclared variable in a protocol class leads to an ambiguous interface + --> src/mdtest_snippet.py:6:7 + | +4 | return True +5 | +6 | class A(Protocol): + | ^^^^^^^^^^^ `A` declared as a protocol here +7 | # The `x` and `y` members attempt to use Python-2-style type comments +8 | # to indicate that the type should be `int | None` and `str` respectively, + | +info: No declarations found for `c` in the body of `A` or any of its superclasses +info: rule `ambiguous-protocol-member` is enabled by default + +``` + +``` +warning[ambiguous-protocol-member]: Cannot assign to undeclared variable in the body of a protocol class + --> src/mdtest_snippet.py:22:9 + | +21 | # error: [ambiguous-protocol-member] +22 | for d in range(42): + | ^ `d` is not declared as a protocol member +23 | pass + | +info: Assigning to an undeclared variable in a protocol class leads to an ambiguous interface + --> src/mdtest_snippet.py:6:7 + | +4 | return True +5 | +6 | class A(Protocol): + | ^^^^^^^^^^^ `A` declared as a protocol here +7 | # The `x` and `y` members attempt to use Python-2-style type comments +8 | # to indicate that the type should be `int | None` and `str` respectively, + | +info: No declarations found for `d` in the body of `A` or any of its superclasses +info: rule `ambiguous-protocol-member` is enabled by default + +``` diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/protocols.md_-_Protocols_-_Protocol_members_in_…_(21be5d9bdab1c844).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/protocols.md_-_Protocols_-_Protocol_members_in_…_(21be5d9bdab1c844).snap new file mode 100644 index 0000000000..d7436cbec7 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/protocols.md_-_Protocols_-_Protocol_members_in_…_(21be5d9bdab1c844).snap @@ -0,0 +1,68 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: protocols.md - Protocols - Protocol members in statically known branches +mdtest path: crates/ty_python_semantic/resources/mdtest/protocols.md +--- + +# Python source files + +## mdtest_snippet.py + +``` + 1 | import sys + 2 | from typing_extensions import Protocol, get_protocol_members, reveal_type + 3 | + 4 | class Foo(Protocol): + 5 | if sys.version_info >= (3, 10): + 6 | a: int + 7 | b = 42 + 8 | def c(self) -> None: ... + 9 | else: +10 | d: int +11 | e = 56 # error: [ambiguous-protocol-member] +12 | def f(self) -> None: ... +13 | +14 | reveal_type(get_protocol_members(Foo)) # revealed: frozenset[Literal["d", "e", "f"]] +``` + +# Diagnostics + +``` +warning[ambiguous-protocol-member]: Cannot assign to undeclared variable in the body of a protocol class + --> src/mdtest_snippet.py:11:9 + | + 9 | else: +10 | d: int +11 | e = 56 # error: [ambiguous-protocol-member] + | ^^^^^^ Consider adding an annotation, e.g. `e: int = ...` +12 | def f(self) -> None: ... + | +info: Assigning to an undeclared variable in a protocol class leads to an ambiguous interface + --> src/mdtest_snippet.py:4:7 + | +2 | from typing_extensions import Protocol, get_protocol_members, reveal_type +3 | +4 | class Foo(Protocol): + | ^^^^^^^^^^^^^ `Foo` declared as a protocol here +5 | if sys.version_info >= (3, 10): +6 | a: int + | +info: No declarations found for `e` in the body of `Foo` or any of its superclasses +info: rule `ambiguous-protocol-member` is enabled by default + +``` + +``` +info[revealed-type]: Revealed type + --> src/mdtest_snippet.py:14:13 + | +12 | def f(self) -> None: ... +13 | +14 | reveal_type(get_protocol_members(Foo)) # revealed: frozenset[Literal["d", "e", "f"]] + | ^^^^^^^^^^^^^^^^^^^^^^^^^ `frozenset[Literal["d", "e", "f"]]` + | + +``` diff --git a/crates/ty_python_semantic/src/semantic_index/definition.rs b/crates/ty_python_semantic/src/semantic_index/definition.rs index 0e44e1a9c4..2e2ff104de 100644 --- a/crates/ty_python_semantic/src/semantic_index/definition.rs +++ b/crates/ty_python_semantic/src/semantic_index/definition.rs @@ -702,6 +702,10 @@ impl DefinitionKind<'_> { ) } + pub(crate) const fn is_unannotated_assignment(&self) -> bool { + matches!(self, DefinitionKind::Assignment(_)) + } + pub(crate) fn as_typevar(&self) -> Option<&AstNodeRef> { match self { DefinitionKind::TypeVar(type_var) => Some(type_var), diff --git a/crates/ty_python_semantic/src/types/diagnostic.rs b/crates/ty_python_semantic/src/types/diagnostic.rs index bbbeff5044..4930dd4f9e 100644 --- a/crates/ty_python_semantic/src/types/diagnostic.rs +++ b/crates/ty_python_semantic/src/types/diagnostic.rs @@ -7,8 +7,9 @@ use super::{ }; use crate::lint::{Level, LintRegistryBuilder, LintStatus}; use crate::semantic_index::SemanticIndex; +use crate::semantic_index::definition::Definition; +use crate::semantic_index::place::{PlaceTable, ScopedPlaceId}; use crate::suppression::FileSuppressionId; -use crate::types::LintDiagnosticGuard; use crate::types::class::{Field, SolidBase, SolidBaseKind}; use crate::types::function::KnownFunction; use crate::types::string_annotation::{ @@ -16,6 +17,9 @@ use crate::types::string_annotation::{ IMPLICIT_CONCATENATED_STRING_TYPE_ANNOTATION, INVALID_SYNTAX_IN_FORWARD_ANNOTATION, RAW_STRING_TYPE_ANNOTATION, }; +use crate::types::{ + DynamicType, LintDiagnosticGuard, Protocol, ProtocolInstanceType, SubclassOfInner, binding_type, +}; use crate::types::{SpecialFormType, Type, protocol_class::ProtocolClassLiteral}; use crate::util::diagnostics::format_enumeration; use crate::{Db, FxIndexMap, FxOrderMap, Module, ModuleName, Program, declare_lint}; @@ -29,6 +33,7 @@ use std::fmt::Formatter; /// Registers all known type check lints. pub(crate) fn register_lints(registry: &mut LintRegistryBuilder) { + registry.register_lint(&AMBIGUOUS_PROTOCOL_MEMBER); registry.register_lint(&CALL_NON_CALLABLE); registry.register_lint(&POSSIBLY_UNBOUND_IMPLICIT_CALL); registry.register_lint(&CONFLICTING_ARGUMENT_FORMS); @@ -424,7 +429,7 @@ declare_lint! { declare_lint! { /// ## What it does - /// Checks for invalidly defined protocol classes. + /// Checks for protocol classes that will raise `TypeError` at runtime. /// /// ## Why is this bad? /// An invalidly defined protocol class may lead to the type checker inferring @@ -450,6 +455,41 @@ declare_lint! { } } +declare_lint! { + /// ## What it does + /// Checks for protocol classes with members that will lead to ambiguous interfaces. + /// + /// ## Why is this bad? + /// Assigning to an undeclared variable in a protocol class leads to an ambiguous + /// interface which may lead to the type checker inferring unexpected things. It's + /// recommended to ensure that all members of a protocol class are explicitly declared. + /// + /// ## Examples + /// + /// ```py + /// from typing import Protocol + /// + /// class BaseProto(Protocol): + /// a: int # fine (explicitly declared as `int`) + /// def method_member(self) -> int: ... # fine: a method definition using `def` is considered a declaration + /// c = "some variable" # error: no explicit declaration, leading to ambiguity + /// b = method_member # error: no explicit declaration, leading to ambiguity + /// + /// # error: this creates implicit assignments of `d` and `e` in the protocol class body. + /// # Were they really meant to be considered protocol members? + /// for d, e in enumerate(range(42)): + /// pass + /// + /// class SubProto(BaseProto, Protocol): + /// a = 42 # fine (declared in superclass) + /// ``` + pub(crate) static AMBIGUOUS_PROTOCOL_MEMBER = { + summary: "detects protocol classes with ambiguous interfaces", + status: LintStatus::preview("1.0.0"), + default_level: Level::Warn, + } +} + declare_lint! { /// ## What it does /// Checks for invalidly defined `NamedTuple` classes. @@ -2456,6 +2496,95 @@ pub(crate) fn report_attempted_protocol_instantiation( diagnostic.sub(class_def_diagnostic); } +pub(crate) fn report_undeclared_protocol_member( + context: &InferContext, + definition: Definition, + protocol_class: ProtocolClassLiteral, + class_symbol_table: &PlaceTable, +) { + /// We want to avoid suggesting an annotation for e.g. `x = None`, + /// because the user almost certainly doesn't want to write `x: None = None`. + /// We also want to avoid suggesting invalid syntax such as `x: = int`. + fn should_give_hint<'db>(db: &'db dyn Db, ty: Type<'db>) -> bool { + let class = match ty { + Type::ProtocolInstance(ProtocolInstanceType { + inner: Protocol::FromClass(_), + .. + }) => return true, + Type::SubclassOf(subclass_of) => match subclass_of.subclass_of() { + SubclassOfInner::Class(class) => class, + SubclassOfInner::Dynamic(DynamicType::Any) => return true, + SubclassOfInner::Dynamic(_) => return false, + }, + Type::NominalInstance(instance) => instance.class(db), + _ => return false, + }; + + !matches!( + class.known(db), + Some(KnownClass::NoneType | KnownClass::EllipsisType) + ) + } + + let db = context.db(); + + let Some(builder) = context.report_lint( + &AMBIGUOUS_PROTOCOL_MEMBER, + definition.full_range(db, context.module()), + ) else { + return; + }; + + let ScopedPlaceId::Symbol(symbol_id) = definition.place(db) else { + return; + }; + + let symbol_name = class_symbol_table.symbol(symbol_id).name(); + let class_name = protocol_class.name(db); + + let mut diagnostic = builder + .into_diagnostic("Cannot assign to undeclared variable in the body of a protocol class"); + + if definition.kind(db).is_unannotated_assignment() { + let binding_type = binding_type(db, definition); + + let suggestion = binding_type + .literal_fallback_instance(db) + .unwrap_or(binding_type); + + if should_give_hint(db, suggestion) { + diagnostic.set_primary_message(format_args!( + "Consider adding an annotation, e.g. `{symbol_name}: {} = ...`", + suggestion.display(db) + )); + } else { + diagnostic.set_primary_message(format_args!( + "Consider adding an annotation for `{symbol_name}`" + )); + } + } else { + diagnostic.set_primary_message(format_args!( + "`{symbol_name}` is not declared as a protocol member" + )); + } + + let mut class_def_diagnostic = SubDiagnostic::new( + SubDiagnosticSeverity::Info, + "Assigning to an undeclared variable in a protocol class \ + leads to an ambiguous interface", + ); + class_def_diagnostic.annotate( + Annotation::primary(protocol_class.header_span(db)) + .message(format_args!("`{class_name}` declared as a protocol here",)), + ); + diagnostic.sub(class_def_diagnostic); + + diagnostic.info(format_args!( + "No declarations found for `{symbol_name}` \ + in the body of `{class_name}` or any of its superclasses" + )); +} + pub(crate) fn report_duplicate_bases( context: &InferContext, class: ClassLiteral, diff --git a/crates/ty_python_semantic/src/types/infer.rs b/crates/ty_python_semantic/src/types/infer.rs index 86a289d300..5502715e38 100644 --- a/crates/ty_python_semantic/src/types/infer.rs +++ b/crates/ty_python_semantic/src/types/infer.rs @@ -1434,6 +1434,10 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } } } + + if let Some(protocol) = class.into_protocol_class(self.db()) { + protocol.validate_members(&self.context, self.index); + } } } diff --git a/crates/ty_python_semantic/src/types/protocol_class.rs b/crates/ty_python_semantic/src/types/protocol_class.rs index 2d5c996a8f..12fb5fb988 100644 --- a/crates/ty_python_semantic/src/types/protocol_class.rs +++ b/crates/ty_python_semantic/src/types/protocol_class.rs @@ -7,7 +7,10 @@ use ruff_python_ast::name::Name; use rustc_hash::FxHashMap; use super::TypeVarVariance; -use crate::semantic_index::place_table; +use crate::semantic_index::place::ScopedPlaceId; +use crate::semantic_index::{SemanticIndex, place_table}; +use crate::types::context::InferContext; +use crate::types::diagnostic::report_undeclared_protocol_member; use crate::{ Db, FxOrderSet, place::{Boundness, Place, PlaceAndQualifiers, place_from_bindings, place_from_declarations}, @@ -55,6 +58,59 @@ impl<'db> ProtocolClassLiteral<'db> { self.known_function_decorators(db) .contains(&KnownFunction::RuntimeCheckable) } + + /// Iterate through the body of the protocol class. Check that all definitions + /// in the protocol class body are either explicitly declared directly in the + /// class body, or are declared in a superclass of the protocol class. + pub(super) fn validate_members(self, context: &InferContext, index: &SemanticIndex<'db>) { + let db = context.db(); + let interface = self.interface(db); + let class_place_table = index.place_table(self.body_scope(db).file_scope_id(db)); + + for (symbol_id, mut bindings_iterator) in + use_def_map(db, self.body_scope(db)).all_end_of_scope_symbol_bindings() + { + let symbol_name = class_place_table.symbol(symbol_id).name(); + + if !interface.includes_member(db, symbol_name) { + continue; + } + + let has_declaration = self + .iter_mro(db, None) + .filter_map(ClassBase::into_class) + .any(|superclass| { + let superclass_scope = superclass.class_literal(db).0.body_scope(db); + let Some(scoped_symbol_id) = + place_table(db, superclass_scope).symbol_id(symbol_name) + else { + return false; + }; + !place_from_declarations( + db, + index + .use_def_map(superclass_scope.file_scope_id(db)) + .end_of_scope_declarations(ScopedPlaceId::Symbol(scoped_symbol_id)), + ) + .into_place_and_conflicting_declarations() + .0 + .place + .is_unbound() + }); + + if has_declaration { + continue; + } + + let Some(first_definition) = + bindings_iterator.find_map(|binding| binding.binding.definition()) + else { + continue; + }; + + report_undeclared_protocol_member(context, first_definition, self, class_place_table); + } + } } impl<'db> Deref for ProtocolClassLiteral<'db> { @@ -147,6 +203,10 @@ impl<'db> ProtocolInterface<'db> { }) } + pub(super) fn includes_member(self, db: &'db dyn Db, name: &str) -> bool { + self.inner(db).contains_key(name) + } + pub(super) fn instance_member(self, db: &'db dyn Db, name: &str) -> PlaceAndQualifiers<'db> { self.member_by_name(db, name) .map(|member| PlaceAndQualifiers { diff --git a/ty.schema.json b/ty.schema.json index f6043e2d8f..c8b9668506 100644 --- a/ty.schema.json +++ b/ty.schema.json @@ -271,6 +271,16 @@ "Rules": { "type": "object", "properties": { + "ambiguous-protocol-member": { + "title": "detects protocol classes with ambiguous interfaces", + "description": "## What it does\nChecks for protocol classes with members that will lead to ambiguous interfaces.\n\n## Why is this bad?\nAssigning to an undeclared variable in a protocol class leads to an ambiguous\ninterface which may lead to the type checker inferring unexpected things. It's\nrecommended to ensure that all members of a protocol class are explicitly declared.\n\n## Examples\n\n```py\nfrom typing import Protocol\n\nclass BaseProto(Protocol):\n a: int # fine (explicitly declared as `int`)\n def method_member(self) -> int: ... # fine: a method definition using `def` is considered a declaration\n c = \"some variable\" # error: no explicit declaration, leading to ambiguity\n b = method_member # error: no explicit declaration, leading to ambiguity\n\n # error: this creates implicit assignments of `d` and `e` in the protocol class body.\n # Were they really meant to be considered protocol members?\n for d, e in enumerate(range(42)):\n pass\n\nclass SubProto(BaseProto, Protocol):\n a = 42 # fine (declared in superclass)\n```", + "default": "warn", + "oneOf": [ + { + "$ref": "#/definitions/Level" + } + ] + }, "byte-string-type-annotation": { "title": "detects byte strings in type annotation positions", "description": "## What it does\nChecks for byte-strings in type annotation positions.\n\n## Why is this bad?\nStatic analysis tools like ty can't analyze type annotations that use byte-string notation.\n\n## Examples\n```python\ndef test(): -> b\"int\":\n ...\n```\n\nUse instead:\n```python\ndef test(): -> \"int\":\n ...\n```", @@ -593,7 +603,7 @@ }, "invalid-protocol": { "title": "detects invalid protocol class definitions", - "description": "## What it does\nChecks for invalidly defined protocol classes.\n\n## Why is this bad?\nAn invalidly defined protocol class may lead to the type checker inferring\nunexpected things. It may also lead to `TypeError`s at runtime.\n\n## Examples\nA `Protocol` class cannot inherit from a non-`Protocol` class;\nthis raises a `TypeError` at runtime:\n\n```pycon\n>>> from typing import Protocol\n>>> class Foo(int, Protocol): ...\n...\nTraceback (most recent call last):\n File \"\", line 1, in \n class Foo(int, Protocol): ...\nTypeError: Protocols can only inherit from other protocols, got \n```", + "description": "## What it does\nChecks for protocol classes that will raise `TypeError` at runtime.\n\n## Why is this bad?\nAn invalidly defined protocol class may lead to the type checker inferring\nunexpected things. It may also lead to `TypeError`s at runtime.\n\n## Examples\nA `Protocol` class cannot inherit from a non-`Protocol` class;\nthis raises a `TypeError` at runtime:\n\n```pycon\n>>> from typing import Protocol\n>>> class Foo(int, Protocol): ...\n...\nTraceback (most recent call last):\n File \"\", line 1, in \n class Foo(int, Protocol): ...\nTypeError: Protocols can only inherit from other protocols, got \n```", "default": "error", "oneOf": [ {