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": [
{