[ty] Strict validation of protocol members (#17750)

This commit is contained in:
Alex Waygood 2025-08-19 23:45:41 +01:00 committed by GitHub
parent e0f4cec7a1
commit 656fc335f2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 619 additions and 78 deletions

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

@ -36,7 +36,7 @@ def test(): -> "int":
<small> <small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of '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%20call-non-callable) · [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)
</small> </small>
**What it does** **What it does**
@ -58,7 +58,7 @@ Calling a non-callable object will raise a `TypeError` at runtime.
<small> <small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of '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-argument-forms) · [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)
</small> </small>
**What it does** **What it does**
@ -88,7 +88,7 @@ f(int) # error
<small> <small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of '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) · [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)
</small> </small>
**What it does** **What it does**
@ -117,7 +117,7 @@ a = 1
<small> <small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of '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-metaclass) · [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)
</small> </small>
**What it does** **What it does**
@ -147,7 +147,7 @@ class C(A, B): ...
<small> <small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of '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%20cyclic-class-definition) · [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)
</small> </small>
**What it does** **What it does**
@ -177,7 +177,7 @@ class B(A): ...
<small> <small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of '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%20duplicate-base) · [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)
</small> </small>
**What it does** **What it does**
@ -202,7 +202,7 @@ class B(A, A): ...
<small> <small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of '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%20duplicate-kw-only) · [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)
</small> </small>
**What it does** **What it does**
@ -306,7 +306,7 @@ def test(): -> "Literal[5]":
<small> <small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of '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%20inconsistent-mro) · [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)
</small> </small>
**What it does** **What it does**
@ -334,7 +334,7 @@ class C(A, B): ...
<small> <small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of '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%20index-out-of-bounds) · [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)
</small> </small>
**What it does** **What it does**
@ -358,7 +358,7 @@ t[3] # IndexError: tuple index out of range
<small> <small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of '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%20instance-layout-conflict) · [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)
</small> </small>
**What it does** **What it does**
@ -445,7 +445,7 @@ an atypical memory layout.
<small> <small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of '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-argument-type) · [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)
</small> </small>
**What it does** **What it does**
@ -470,7 +470,7 @@ func("foo") # error: [invalid-argument-type]
<small> <small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of '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-assignment) · [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)
</small> </small>
**What it does** **What it does**
@ -496,7 +496,7 @@ a: int = ''
<small> <small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of '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-attribute-access) · [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)
</small> </small>
**What it does** **What it does**
@ -528,7 +528,7 @@ C.instance_var = 3 # error: Cannot assign to instance variable
<small> <small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of '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-await) · [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)
</small> </small>
**What it does** **What it does**
@ -562,7 +562,7 @@ asyncio.run(main())
<small> <small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of '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-base) · [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)
</small> </small>
**What it does** **What it does**
@ -584,7 +584,7 @@ class A(42): ... # error: [invalid-base]
<small> <small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of '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-context-manager) · [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)
</small> </small>
**What it does** **What it does**
@ -609,7 +609,7 @@ with 1:
<small> <small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of '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-declaration) · [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)
</small> </small>
**What it does** **What it does**
@ -636,7 +636,7 @@ a: str
<small> <small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of '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-exception-caught) · [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)
</small> </small>
**What it does** **What it does**
@ -678,7 +678,7 @@ except ZeroDivisionError:
<small> <small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of '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-generic-class) · [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)
</small> </small>
**What it does** **What it does**
@ -709,7 +709,7 @@ class C[U](Generic[T]): ...
<small> <small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of '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-key) · [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)
</small> </small>
**What it does** **What it does**
@ -738,7 +738,7 @@ alice["height"] # KeyError: 'height'
<small> <small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of '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-legacy-type-variable) · [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)
</small> </small>
**What it does** **What it does**
@ -771,7 +771,7 @@ def f(t: TypeVar("U")): ...
<small> <small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of '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-metaclass) · [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)
</small> </small>
**What it does** **What it does**
@ -803,7 +803,7 @@ class B(metaclass=f): ...
<small> <small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of '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-named-tuple) · [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)
</small> </small>
**What it does** **What it does**
@ -833,7 +833,7 @@ TypeError: can only inherit from a NamedTuple type and Generic
<small> <small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of '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-overload) · [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)
</small> </small>
**What it does** **What it does**
@ -881,7 +881,7 @@ def foo(x: int) -> int: ...
<small> <small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of '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-parameter-default) · [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)
</small> </small>
**What it does** **What it does**
@ -905,12 +905,12 @@ def f(a: int = ''): ...
<small> <small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of '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-protocol) · [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)
</small> </small>
**What it does** **What it does**
Checks for invalidly defined protocol classes. Checks for protocol classes that will raise `TypeError` at runtime.
**Why is this bad?** **Why is this bad?**
@ -937,7 +937,7 @@ TypeError: Protocols can only inherit from other protocols, got <class 'int'>
<small> <small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of '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-raise) · [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)
</small> </small>
Checks for `raise` statements that raise non-exceptions or use invalid Checks for `raise` statements that raise non-exceptions or use invalid
@ -984,7 +984,7 @@ def g():
<small> <small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of '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-return-type) · [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)
</small> </small>
**What it does** **What it does**
@ -1007,7 +1007,7 @@ def func() -> int:
<small> <small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of '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-super-argument) · [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)
</small> </small>
**What it does** **What it does**
@ -1061,7 +1061,7 @@ TODO #14889
<small> <small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of '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-alias-type) · [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)
</small> </small>
**What it does** **What it does**
@ -1086,7 +1086,7 @@ NewAlias = TypeAliasType(get_name(), int) # error: TypeAliasType name mus
<small> <small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of '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-checking-constant) · [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)
</small> </small>
**What it does** **What it does**
@ -1114,7 +1114,7 @@ TYPE_CHECKING = ''
<small> <small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of '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-form) · [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)
</small> </small>
**What it does** **What it does**
@ -1142,7 +1142,7 @@ b: Annotated[int] # `Annotated` expects at least two arguments
<small> <small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of '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-call) · [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)
</small> </small>
**What it does** **What it does**
@ -1174,7 +1174,7 @@ f(10) # Error
<small> <small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of '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) · [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)
</small> </small>
**What it does** **What it does**
@ -1206,7 +1206,7 @@ class C:
<small> <small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of '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-variable-constraints) · [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)
</small> </small>
**What it does** **What it does**
@ -1239,7 +1239,7 @@ T = TypeVar('T', bound=str) # valid bound TypeVar
<small> <small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of '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%20missing-argument) · [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)
</small> </small>
**What it does** **What it does**
@ -1262,7 +1262,7 @@ func() # TypeError: func() missing 1 required positional argument: 'x'
<small> <small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of '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%20no-matching-overload) · [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)
</small> </small>
**What it does** **What it does**
@ -1289,7 +1289,7 @@ func("string") # error: [no-matching-overload]
<small> <small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of '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%20non-subscriptable) · [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)
</small> </small>
**What it does** **What it does**
@ -1311,7 +1311,7 @@ Subscripting an object that does not support it will raise a `TypeError` at runt
<small> <small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of '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%20not-iterable) · [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)
</small> </small>
**What it does** **What it does**
@ -1335,7 +1335,7 @@ for i in 34: # TypeError: 'int' object is not iterable
<small> <small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of '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%20parameter-already-assigned) · [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)
</small> </small>
**What it does** **What it does**
@ -1389,7 +1389,7 @@ def test(): -> "int":
<small> <small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of '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%20static-assert-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)
</small> </small>
**What it does** **What it does**
@ -1417,7 +1417,7 @@ static_assert(int(2.0 * 3.0) == 6) # error: does not have a statically known tr
<small> <small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of '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%20subclass-of-final-class) · [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)
</small> </small>
**What it does** **What it does**
@ -1444,7 +1444,7 @@ class B(A): ... # Error raised here
<small> <small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of '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%20too-many-positional-arguments) · [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)
</small> </small>
**What it does** **What it does**
@ -1469,7 +1469,7 @@ f("foo") # Error raised here
<small> <small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of '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%20type-assertion-failure) · [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)
</small> </small>
**What it does** **What it does**
@ -1495,7 +1495,7 @@ def _(x: int):
<small> <small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of '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%20unavailable-implicit-super-arguments) · [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)
</small> </small>
**What it does** **What it does**
@ -1539,7 +1539,7 @@ class A:
<small> <small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of '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%20unknown-argument) · [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)
</small> </small>
**What it does** **What it does**
@ -1564,7 +1564,7 @@ f(x=1, y=2) # Error raised here
<small> <small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of '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%20unresolved-attribute) · [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)
</small> </small>
**What it does** **What it does**
@ -1590,7 +1590,7 @@ A().foo # AttributeError: 'A' object has no attribute 'foo'
<small> <small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of '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%20unresolved-import) · [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)
</small> </small>
**What it does** **What it does**
@ -1613,7 +1613,7 @@ import foo # ModuleNotFoundError: No module named 'foo'
<small> <small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of '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%20unresolved-reference) · [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)
</small> </small>
**What it does** **What it does**
@ -1636,7 +1636,7 @@ print(x) # NameError: name 'x' is not defined
<small> <small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of '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%20unsupported-bool-conversion) · [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)
</small> </small>
**What it does** **What it does**
@ -1671,7 +1671,7 @@ b1 < b2 < b1 # exception raised here
<small> <small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of '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%20unsupported-operator) · [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)
</small> </small>
**What it does** **What it does**
@ -1697,7 +1697,7 @@ A() + A() # TypeError: unsupported operand type(s) for +: 'A' and 'A'
<small> <small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of '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%20zero-stepsize-in-slice) · [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)
</small> </small>
**What it does** **What it does**
@ -1715,12 +1715,51 @@ l = list(range(10))
l[1:10:0] # ValueError: slice step cannot be zero l[1:10:0] # ValueError: slice step cannot be zero
``` ```
## `ambiguous-protocol-member`
<small>
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)
</small>
**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` ## `deprecated`
<small> <small>
Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") · 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) · [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)
</small> </small>
**What it does** **What it does**
@ -1773,7 +1812,7 @@ a = 20 / 0 # type: ignore
<small> <small>
Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") · 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) · [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)
</small> </small>
**What it does** **What it does**
@ -1799,7 +1838,7 @@ A.c # AttributeError: type object 'A' has no attribute 'c'
<small> <small>
Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") · 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) · [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)
</small> </small>
**What it does** **What it does**
@ -1829,7 +1868,7 @@ A()[0] # TypeError: 'A' object is not subscriptable
<small> <small>
Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") · 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) · [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)
</small> </small>
**What it does** **What it does**
@ -1859,7 +1898,7 @@ from module import a # ImportError: cannot import name 'a' from 'module'
<small> <small>
Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") · 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) · [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)
</small> </small>
**What it does** **What it does**
@ -1884,7 +1923,7 @@ cast(int, f()) # Redundant
<small> <small>
Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") · 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) · [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)
</small> </small>
**What it does** **What it does**
@ -1935,7 +1974,7 @@ a = 20 / 0 # ty: ignore[division-by-zero]
<small> <small>
Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") · 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) · [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)
</small> </small>
**What it does** **What it does**
@ -1989,7 +2028,7 @@ def g():
<small> <small>
Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") · 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) · [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)
</small> </small>
**What it does** **What it does**
@ -2026,7 +2065,7 @@ class D(C): ... # error: [unsupported-base]
<small> <small>
Default level: [`ignore`](../rules.md#rule-levels "This lint has a default level of 'ignore'.") · 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) · [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)
</small> </small>
**What it does** **What it does**
@ -2048,7 +2087,7 @@ Dividing by zero raises a `ZeroDivisionError` at runtime.
<small> <small>
Default level: [`ignore`](../rules.md#rule-levels "This lint has a default level of 'ignore'.") · 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) · [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)
</small> </small>
**What it does** **What it does**

View file

@ -482,6 +482,8 @@ reveal_type(get_protocol_members(Baz2))
## Protocol members in statically known branches ## Protocol members in statically known branches
<!-- snapshot-diagnostics -->
The list of protocol members does not include any members declared in branches that are statically The list of protocol members does not include any members declared in branches that are statically
known to be unreachable: known to be unreachable:
@ -492,7 +494,7 @@ python-version = "3.9"
```py ```py
import sys import sys
from typing_extensions import Protocol, get_protocol_members from typing_extensions import Protocol, get_protocol_members, reveal_type
class Foo(Protocol): class Foo(Protocol):
if sys.version_info >= (3, 10): if sys.version_info >= (3, 10):
@ -501,7 +503,7 @@ class Foo(Protocol):
def c(self) -> None: ... def c(self) -> None: ...
else: else:
d: int d: int
e = 56 e = 56 # error: [ambiguous-protocol-member]
def f(self) -> None: ... def f(self) -> None: ...
reveal_type(get_protocol_members(Foo)) # revealed: frozenset[Literal["d", "e", "f"]] 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 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 symbol being assigned to is also explicitly declared in the body of the protocol class or one of its
stricter validation of protocol members than many other type checkers currently apply (as of superclasses. Note that this is stricter validation of protocol members than many other type
2025/04/21). 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 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. 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 Nested: ... # also weird, but we should also probably allow it
class NestedProtocol(Protocol): ... # same here... 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 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 pass
match object(): 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"]] # revealed: frozenset[Literal["Nested", "NestedProtocol", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l"]]
reveal_type(get_protocol_members(LotsOfBindings)) 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 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)) 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).
<!-- snapshot-diagnostics -->
```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 ## Equivalence of protocols
Two protocols are considered equivalent types if they specify the same interface, even if they have Two protocols are considered equivalent types if they specify the same interface, even if they have

View file

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

View file

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

View file

@ -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<ast::TypeParamTypeVar>> { pub(crate) fn as_typevar(&self) -> Option<&AstNodeRef<ast::TypeParamTypeVar>> {
match self { match self {
DefinitionKind::TypeVar(type_var) => Some(type_var), DefinitionKind::TypeVar(type_var) => Some(type_var),

View file

@ -7,8 +7,9 @@ use super::{
}; };
use crate::lint::{Level, LintRegistryBuilder, LintStatus}; use crate::lint::{Level, LintRegistryBuilder, LintStatus};
use crate::semantic_index::SemanticIndex; use crate::semantic_index::SemanticIndex;
use crate::semantic_index::definition::Definition;
use crate::semantic_index::place::{PlaceTable, ScopedPlaceId};
use crate::suppression::FileSuppressionId; use crate::suppression::FileSuppressionId;
use crate::types::LintDiagnosticGuard;
use crate::types::class::{Field, SolidBase, SolidBaseKind}; use crate::types::class::{Field, SolidBase, SolidBaseKind};
use crate::types::function::KnownFunction; use crate::types::function::KnownFunction;
use crate::types::string_annotation::{ use crate::types::string_annotation::{
@ -16,6 +17,9 @@ use crate::types::string_annotation::{
IMPLICIT_CONCATENATED_STRING_TYPE_ANNOTATION, INVALID_SYNTAX_IN_FORWARD_ANNOTATION, IMPLICIT_CONCATENATED_STRING_TYPE_ANNOTATION, INVALID_SYNTAX_IN_FORWARD_ANNOTATION,
RAW_STRING_TYPE_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::types::{SpecialFormType, Type, protocol_class::ProtocolClassLiteral};
use crate::util::diagnostics::format_enumeration; use crate::util::diagnostics::format_enumeration;
use crate::{Db, FxIndexMap, FxOrderMap, Module, ModuleName, Program, declare_lint}; use crate::{Db, FxIndexMap, FxOrderMap, Module, ModuleName, Program, declare_lint};
@ -29,6 +33,7 @@ use std::fmt::Formatter;
/// Registers all known type check lints. /// Registers all known type check lints.
pub(crate) fn register_lints(registry: &mut LintRegistryBuilder) { pub(crate) fn register_lints(registry: &mut LintRegistryBuilder) {
registry.register_lint(&AMBIGUOUS_PROTOCOL_MEMBER);
registry.register_lint(&CALL_NON_CALLABLE); registry.register_lint(&CALL_NON_CALLABLE);
registry.register_lint(&POSSIBLY_UNBOUND_IMPLICIT_CALL); registry.register_lint(&POSSIBLY_UNBOUND_IMPLICIT_CALL);
registry.register_lint(&CONFLICTING_ARGUMENT_FORMS); registry.register_lint(&CONFLICTING_ARGUMENT_FORMS);
@ -424,7 +429,7 @@ declare_lint! {
declare_lint! { declare_lint! {
/// ## What it does /// ## What it does
/// Checks for invalidly defined protocol classes. /// Checks for protocol classes that will raise `TypeError` at runtime.
/// ///
/// ## Why is this bad? /// ## Why is this bad?
/// An invalidly defined protocol class may lead to the type checker inferring /// 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! { declare_lint! {
/// ## What it does /// ## What it does
/// Checks for invalidly defined `NamedTuple` classes. /// Checks for invalidly defined `NamedTuple` classes.
@ -2456,6 +2496,95 @@ pub(crate) fn report_attempted_protocol_instantiation(
diagnostic.sub(class_def_diagnostic); 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: <class 'int'> = 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( pub(crate) fn report_duplicate_bases(
context: &InferContext, context: &InferContext,
class: ClassLiteral, class: ClassLiteral,

View file

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

View file

@ -7,7 +7,10 @@ use ruff_python_ast::name::Name;
use rustc_hash::FxHashMap; use rustc_hash::FxHashMap;
use super::TypeVarVariance; 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::{ use crate::{
Db, FxOrderSet, Db, FxOrderSet,
place::{Boundness, Place, PlaceAndQualifiers, place_from_bindings, place_from_declarations}, place::{Boundness, Place, PlaceAndQualifiers, place_from_bindings, place_from_declarations},
@ -55,6 +58,59 @@ impl<'db> ProtocolClassLiteral<'db> {
self.known_function_decorators(db) self.known_function_decorators(db)
.contains(&KnownFunction::RuntimeCheckable) .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> { 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> { pub(super) fn instance_member(self, db: &'db dyn Db, name: &str) -> PlaceAndQualifiers<'db> {
self.member_by_name(db, name) self.member_by_name(db, name)
.map(|member| PlaceAndQualifiers { .map(|member| PlaceAndQualifiers {

12
ty.schema.json generated
View file

@ -271,6 +271,16 @@
"Rules": { "Rules": {
"type": "object", "type": "object",
"properties": { "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": { "byte-string-type-annotation": {
"title": "detects byte strings in type annotation positions", "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```", "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": { "invalid-protocol": {
"title": "detects invalid protocol class definitions", "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 \"<python-input-1>\", line 1, in <module>\n class Foo(int, Protocol): ...\nTypeError: Protocols can only inherit from other protocols, got <class 'int'>\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 \"<python-input-1>\", line 1, in <module>\n class Foo(int, Protocol): ...\nTypeError: Protocols can only inherit from other protocols, got <class 'int'>\n```",
"default": "error", "default": "error",
"oneOf": [ "oneOf": [
{ {