diff --git a/crates/ty/docs/rules.md b/crates/ty/docs/rules.md index 7cc1eb9ef0..ca0fdf429f 100644 --- a/crates/ty/docs/rules.md +++ b/crates/ty/docs/rules.md @@ -50,7 +50,7 @@ Calling a non-callable object will raise a `TypeError` at runtime. ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20call-non-callable) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L88) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L90) ## `conflicting-argument-forms` @@ -81,7 +81,7 @@ f(int) # error ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20conflicting-argument-forms) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L119) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L121) ## `conflicting-declarations` @@ -111,7 +111,7 @@ a = 1 ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20conflicting-declarations) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L145) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L147) ## `conflicting-metaclass` @@ -142,7 +142,7 @@ class C(A, B): ... ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20conflicting-metaclass) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L170) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L172) ## `cyclic-class-definition` @@ -173,7 +173,7 @@ class B(A): ... ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20cyclic-class-definition) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L196) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L198) ## `duplicate-base` @@ -199,7 +199,7 @@ class B(A, A): ... ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20duplicate-base) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L240) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L242) ## `escape-character-in-forward-annotation` @@ -336,7 +336,7 @@ TypeError: multiple bases have instance lay-out conflict ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20incompatible-slots) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L261) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L263) ## `inconsistent-mro` @@ -365,7 +365,7 @@ class C(A, B): ... ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20inconsistent-mro) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L347) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L349) ## `index-out-of-bounds` @@ -390,7 +390,7 @@ t[3] # IndexError: tuple index out of range ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20index-out-of-bounds) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L371) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L373) ## `invalid-argument-type` @@ -416,7 +416,7 @@ func("foo") # error: [invalid-argument-type] ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-argument-type) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L391) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L393) ## `invalid-assignment` @@ -443,7 +443,7 @@ a: int = '' ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-assignment) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L431) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L433) ## `invalid-attribute-access` @@ -476,7 +476,7 @@ C.instance_var = 3 # error: Cannot assign to instance variable ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-attribute-access) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1337) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1381) ## `invalid-base` @@ -484,13 +484,22 @@ C.instance_var = 3 # error: Cannot assign to instance variable **Default level**: error
-detects invalid bases in class definitions +detects class bases that will cause the class definition to raise an exception at runtime -TODO #14889 +### What it does +Checks for class definitions that have bases which are not instances of `type`. + +### Why is this bad? +Class definitions with bases like this will lead to `TypeError` being raised at runtime. + +### Examples +```python +class A(42): ... # error: [invalid-base] +``` ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-base) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L453) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L455)
## `invalid-context-manager` @@ -516,7 +525,7 @@ with 1: ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-context-manager) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L462) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L506) ## `invalid-declaration` @@ -544,7 +553,7 @@ a: str ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-declaration) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L483) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L527) ## `invalid-exception-caught` @@ -585,7 +594,7 @@ except ZeroDivisionError: ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-exception-caught) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L506) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L550) ## `invalid-generic-class` @@ -616,7 +625,7 @@ class C[U](Generic[T]): ... ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-generic-class) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L542) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L586) ## `invalid-legacy-type-variable` @@ -649,7 +658,7 @@ def f(t: TypeVar("U")): ... ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-legacy-type-variable) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L568) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L612) ## `invalid-metaclass` @@ -681,7 +690,7 @@ class B(metaclass=f): ... ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-metaclass) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L617) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L661) ## `invalid-overload` @@ -729,7 +738,7 @@ def foo(x: int) -> int: ... ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-overload) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L644) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L688) ## `invalid-parameter-default` @@ -754,7 +763,7 @@ def f(a: int = ''): ... ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-parameter-default) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L687) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L731) ## `invalid-protocol` @@ -787,7 +796,7 @@ TypeError: Protocols can only inherit from other protocols, got ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-protocol) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L319) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L321) ## `invalid-raise` @@ -835,7 +844,7 @@ def g(): ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-raise) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L707) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L751) ## `invalid-return-type` @@ -859,7 +868,7 @@ def func() -> int: ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-return-type) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L412) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L414) ## `invalid-super-argument` @@ -903,7 +912,7 @@ super(B, A) # error: `A` does not satisfy `issubclass(A, B)` ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-super-argument) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L750) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L794) ## `invalid-syntax-in-forward-annotation` @@ -943,7 +952,7 @@ NewAlias = TypeAliasType(get_name(), int) # error: TypeAliasType name mus ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-alias-type) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L596) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L640) ## `invalid-type-checking-constant` @@ -972,7 +981,7 @@ TYPE_CHECKING = '' ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-checking-constant) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L789) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L833) ## `invalid-type-form` @@ -1001,7 +1010,7 @@ b: Annotated[int] # `Annotated` expects at least two arguments ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-form) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L813) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L857) ## `invalid-type-variable-constraints` @@ -1035,7 +1044,7 @@ T = TypeVar('T', bound=str) # valid bound TypeVar ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-variable-constraints) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L837) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L881) ## `missing-argument` @@ -1059,7 +1068,7 @@ func() # TypeError: func() missing 1 required positional argument: 'x' ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20missing-argument) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L866) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L910) ## `no-matching-overload` @@ -1087,7 +1096,7 @@ func("string") # error: [no-matching-overload] ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20no-matching-overload) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L885) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L929) ## `non-subscriptable` @@ -1110,7 +1119,7 @@ Subscripting an object that does not support it will raise a `TypeError` at runt ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20non-subscriptable) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L908) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L952) ## `not-iterable` @@ -1135,7 +1144,7 @@ for i in 34: # TypeError: 'int' object is not iterable ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20not-iterable) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L926) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L970) ## `parameter-already-assigned` @@ -1161,7 +1170,7 @@ f(1, x=2) # Error raised here ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20parameter-already-assigned) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L977) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1021) ## `raw-string-type-annotation` @@ -1220,7 +1229,7 @@ static_assert(int(2.0 * 3.0) == 6) # error: does not have a statically known tr ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20static-assert-error) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1313) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1357) ## `subclass-of-final-class` @@ -1248,7 +1257,7 @@ class B(A): ... # Error raised here ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20subclass-of-final-class) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1068) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1112) ## `too-many-positional-arguments` @@ -1274,7 +1283,7 @@ f("foo") # Error raised here ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20too-many-positional-arguments) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1113) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1157) ## `type-assertion-failure` @@ -1301,7 +1310,7 @@ def _(x: int): ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20type-assertion-failure) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1091) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1135) ## `unavailable-implicit-super-arguments` @@ -1345,7 +1354,7 @@ class A: ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unavailable-implicit-super-arguments) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1134) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1178) ## `unknown-argument` @@ -1371,7 +1380,7 @@ f(x=1, y=2) # Error raised here ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unknown-argument) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1191) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1235) ## `unresolved-attribute` @@ -1398,7 +1407,7 @@ A().foo # AttributeError: 'A' object has no attribute 'foo' ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-attribute) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1212) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1256) ## `unresolved-import` @@ -1422,7 +1431,7 @@ import foo # ModuleNotFoundError: No module named 'foo' ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-import) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1234) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1278) ## `unresolved-reference` @@ -1446,7 +1455,7 @@ print(x) # NameError: name 'x' is not defined ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-reference) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1253) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1297) ## `unsupported-bool-conversion` @@ -1482,7 +1491,7 @@ b1 < b2 < b1 # exception raised here ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unsupported-bool-conversion) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L946) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L990) ## `unsupported-operator` @@ -1509,7 +1518,7 @@ A() + A() # TypeError: unsupported operand type(s) for +: 'A' and 'A' ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unsupported-operator) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1272) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1316) ## `zero-stepsize-in-slice` @@ -1533,7 +1542,7 @@ l[1:10:0] # ValueError: slice step cannot be zero ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20zero-stepsize-in-slice) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1294) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1338) ## `call-possibly-unbound-method` @@ -1551,7 +1560,7 @@ Calling an unbound method will raise an `AttributeError` at runtime. ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20call-possibly-unbound-method) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L106) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L108) ## `invalid-ignore-comment` @@ -1607,7 +1616,7 @@ A.c # AttributeError: type object 'A' has no attribute 'c' ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-unbound-attribute) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L998) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1042) ## `possibly-unbound-import` @@ -1638,7 +1647,7 @@ from module import a # ImportError: cannot import name 'a' from 'module' ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-unbound-import) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1020) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1064) ## `redundant-cast` @@ -1664,7 +1673,7 @@ cast(int, f()) # Redundant ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20redundant-cast) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1365) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1409) ## `undefined-reveal` @@ -1687,7 +1696,7 @@ reveal_type(1) # NameError: name 'reveal_type' is not defined ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20undefined-reveal) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1173) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1217) ## `unknown-rule` @@ -1720,6 +1729,44 @@ a = 20 / 0 # ty: ignore[division-by-zero] * [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Fsuppression.rs#L40) +## `unsupported-base` + +**Default level**: warn + +
+detects class bases that are unsupported as ty could not feasibly calculate the class's MRO + +### What it does +Checks for class definitions that have bases which are unsupported by ty. + +### Why is this bad? +If a class has a base that is an instance of a complex type such as a union type, +ty will not be able to resolve the [method resolution order] (MRO) for the class. +This will lead to an inferior understanding of your codebase and unpredictable +type-checking behavior. + +### Examples +```python +import datetime + +class A: ... +class B: ... + +if datetime.date.today().weekday() != 6: + C = A +else: + C = B + +class D(C): ... # error: [unsupported-base] +``` + +[method resolution order]: https://docs.python.org/3/glossary.html#term-method-resolution-order + +### Links +* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unsupported-base) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L473) +
+ ## `division-by-zero` **Default level**: ignore @@ -1740,7 +1787,7 @@ Dividing by zero raises a `ZeroDivisionError` at runtime. ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20division-by-zero) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L222) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L224) ## `possibly-unresolved-reference` @@ -1767,7 +1814,7 @@ print(x) # NameError: name 'x' is not defined ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-unresolved-reference) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1046) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1090) ## `unused-ignore-comment` diff --git a/crates/ty_python_semantic/resources/mdtest/mro.md b/crates/ty_python_semantic/resources/mdtest/mro.md index 4f6e1c9c72..810e716688 100644 --- a/crates/ty_python_semantic/resources/mdtest/mro.md +++ b/crates/ty_python_semantic/resources/mdtest/mro.md @@ -173,7 +173,7 @@ if hasattr(DoesNotExist, "__mro__"): if not isinstance(DoesNotExist, type): reveal_type(DoesNotExist) # revealed: Unknown & ~type - class Foo(DoesNotExist): ... # error: [invalid-base] + class Foo(DoesNotExist): ... # error: [unsupported-base] reveal_type(Foo.__mro__) # revealed: tuple[, Unknown, ] ``` @@ -232,11 +232,15 @@ reveal_type(AA.__mro__) # revealed: tuple[, , Unknown, < ## `__bases__` includes a `Union` + + We don't support union types in a class's bases; a base must resolve to a single `ClassType`. If we find a union type in a class's bases, we infer the class's `__mro__` as being `[, Unknown, object]`, the same as for MROs that cause errors at runtime. ```py +from typing_extensions import reveal_type + def returns_bool() -> bool: return True @@ -250,7 +254,7 @@ else: reveal_type(x) # revealed: | -# error: 11 [invalid-base] "Invalid class base with type ` | ` (all bases must be a class, `Any`, `Unknown` or `Todo`)" +# error: 11 [unsupported-base] "Unsupported class base with type ` | `" class Foo(x): ... reveal_type(Foo.__mro__) # revealed: tuple[, Unknown, ] @@ -259,8 +263,8 @@ reveal_type(Foo.__mro__) # revealed: tuple[, Unknown, | reveal_type(y) # revealed: | -# error: 11 [invalid-base] "Invalid class base with type ` | ` (all bases must be a class, `Any`, `Unknown` or `Todo`)" -# error: 14 [invalid-base] "Invalid class base with type ` | ` (all bases must be a class, `Any`, `Unknown` or `Todo`)" +# error: 11 [unsupported-base] "Unsupported class base with type ` | `" +# error: 14 [unsupported-base] "Unsupported class base with type ` | `" class Foo(x, y): ... reveal_type(Foo.__mro__) # revealed: tuple[, Unknown, ] @@ -321,7 +325,7 @@ if returns_bool(): else: foo = object -# error: 21 [invalid-base] "Invalid class base with type ` | ` (all bases must be a class, `Any`, `Unknown` or `Todo`)" +# error: 21 [unsupported-base] "Unsupported class base with type ` | `" class PossibleError(foo, X): ... reveal_type(PossibleError.__mro__) # revealed: tuple[, Unknown, ] @@ -339,12 +343,47 @@ else: # revealed: tuple[, , , , ] | tuple[, , , , ] reveal_type(B.__mro__) -# error: 12 [invalid-base] "Invalid class base with type ` | ` (all bases must be a class, `Any`, `Unknown` or `Todo`)" +# error: 12 [unsupported-base] "Unsupported class base with type ` | `" class Z(A, B): ... reveal_type(Z.__mro__) # revealed: tuple[, Unknown, ] ``` +## `__bases__` lists that include objects that are not instances of `type` + + + +```py +class Foo(2): ... # error: [invalid-base] +``` + +A base that is not an instance of `type` but does have an `__mro_entries__` method will not raise an +exception at runtime, so we issue `unsupported-base` rather than `invalid-base`: + +```py +class Foo: + def __mro_entries__(self, bases: tuple[type, ...]) -> tuple[type, ...]: + return () + +class Bar(Foo()): ... # error: [unsupported-base] +``` + +But for objects that have badly defined `__mro_entries__`, `invalid-base` is emitted rather than +`unsupported-base`: + +```py +class Bad1: + def __mro_entries__(self, bases, extra_arg): + return () + +class Bad2: + def __mro_entries__(self, bases) -> int: + return 42 + +class BadSub1(Bad1()): ... # error: [invalid-base] +class BadSub2(Bad2()): ... # error: [invalid-base] +``` + ## `__bases__` lists with duplicate bases diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/mro.md_-_Method_Resolution_Or…_-_`__bases__`_includes…_(d2532518c44112c8).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/mro.md_-_Method_Resolution_Or…_-_`__bases__`_includes…_(d2532518c44112c8).snap new file mode 100644 index 0000000000..1b60443d4d --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/mro.md_-_Method_Resolution_Or…_-_`__bases__`_includes…_(d2532518c44112c8).snap @@ -0,0 +1,78 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: mro.md - Method Resolution Order tests - `__bases__` includes a `Union` +mdtest path: crates/ty_python_semantic/resources/mdtest/mro.md +--- + +# Python source files + +## mdtest_snippet.py + +``` + 1 | from typing_extensions import reveal_type + 2 | + 3 | def returns_bool() -> bool: + 4 | return True + 5 | + 6 | class A: ... + 7 | class B: ... + 8 | + 9 | if returns_bool(): +10 | x = A +11 | else: +12 | x = B +13 | +14 | reveal_type(x) # revealed: | +15 | +16 | # error: 11 [unsupported-base] "Unsupported class base with type ` | `" +17 | class Foo(x): ... +18 | +19 | reveal_type(Foo.__mro__) # revealed: tuple[, Unknown, ] +``` + +# Diagnostics + +``` +info[revealed-type]: Revealed type + --> src/mdtest_snippet.py:14:13 + | +12 | x = B +13 | +14 | reveal_type(x) # revealed: | + | ^ ` | ` +15 | +16 | # error: 11 [unsupported-base] "Unsupported class base with type ` | `" + | + +``` + +``` +warning[unsupported-base]: Unsupported class base with type ` | ` + --> src/mdtest_snippet.py:17:11 + | +16 | # error: 11 [unsupported-base] "Unsupported class base with type ` | `" +17 | class Foo(x): ... + | ^ +18 | +19 | reveal_type(Foo.__mro__) # revealed: tuple[, Unknown, ] + | +info: ty cannot resolve a consistent MRO for class `Foo` due to this base +info: Only class objects or `Any` are supported as class bases +info: rule `unsupported-base` is enabled by default + +``` + +``` +info[revealed-type]: Revealed type + --> src/mdtest_snippet.py:19:13 + | +17 | class Foo(x): ... +18 | +19 | reveal_type(Foo.__mro__) # revealed: tuple[, Unknown, ] + | ^^^^^^^^^^^ `tuple[, Unknown, ]` + | + +``` diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/mro.md_-_Method_Resolution_Or…_-_`__bases__`_lists_th…_(6f8d0bf648c4b305).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/mro.md_-_Method_Resolution_Or…_-_`__bases__`_lists_th…_(6f8d0bf648c4b305).snap new file mode 100644 index 0000000000..3a7a401bb7 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/mro.md_-_Method_Resolution_Or…_-_`__bases__`_lists_th…_(6f8d0bf648c4b305).snap @@ -0,0 +1,97 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: mro.md - Method Resolution Order tests - `__bases__` lists that include objects that are not instances of `type` +mdtest path: crates/ty_python_semantic/resources/mdtest/mro.md +--- + +# Python source files + +## mdtest_snippet.py + +``` + 1 | class Foo(2): ... # error: [invalid-base] + 2 | class Foo: + 3 | def __mro_entries__(self, bases: tuple[type, ...]) -> tuple[type, ...]: + 4 | return () + 5 | + 6 | class Bar(Foo()): ... # error: [unsupported-base] + 7 | class Bad1: + 8 | def __mro_entries__(self, bases, extra_arg): + 9 | return () +10 | +11 | class Bad2: +12 | def __mro_entries__(self, bases) -> int: +13 | return 42 +14 | +15 | class BadSub1(Bad1()): ... # error: [invalid-base] +16 | class BadSub2(Bad2()): ... # error: [invalid-base] +``` + +# Diagnostics + +``` +error[invalid-base]: Invalid class base with type `Literal[2]` + --> src/mdtest_snippet.py:1:11 + | +1 | class Foo(2): ... # error: [invalid-base] + | ^ +2 | class Foo: +3 | def __mro_entries__(self, bases: tuple[type, ...]) -> tuple[type, ...]: + | +info: Definition of class `Foo` will raise `TypeError` at runtime +info: rule `invalid-base` is enabled by default + +``` + +``` +warning[unsupported-base]: Unsupported class base with type `Foo` + --> src/mdtest_snippet.py:6:11 + | +4 | return () +5 | +6 | class Bar(Foo()): ... # error: [unsupported-base] + | ^^^^^ +7 | class Bad1: +8 | def __mro_entries__(self, bases, extra_arg): + | +info: ty cannot resolve a consistent MRO for class `Bar` due to this base +info: Only class objects or `Any` are supported as class bases +info: rule `unsupported-base` is enabled by default + +``` + +``` +error[invalid-base]: Invalid class base with type `Bad1` + --> src/mdtest_snippet.py:15:15 + | +13 | return 42 +14 | +15 | class BadSub1(Bad1()): ... # error: [invalid-base] + | ^^^^^^ +16 | class BadSub2(Bad2()): ... # error: [invalid-base] + | +info: Definition of class `BadSub1` will raise `TypeError` at runtime +info: An instance type is only a valid class base if it has a valid `__mro_entries__` method +info: Type `Bad1` has an `__mro_entries__` method, but it cannot be called with the expected arguments +info: Expected a signature at least as permissive as `def __mro_entries__(self, bases: tuple[type, ...], /) -> tuple[type, ...]` +info: rule `invalid-base` is enabled by default + +``` + +``` +error[invalid-base]: Invalid class base with type `Bad2` + --> src/mdtest_snippet.py:16:15 + | +15 | class BadSub1(Bad1()): ... # error: [invalid-base] +16 | class BadSub2(Bad2()): ... # error: [invalid-base] + | ^^^^^^ + | +info: Definition of class `BadSub2` will raise `TypeError` at runtime +info: An instance type is only a valid class base if it has a valid `__mro_entries__` method +info: Type `Bad2` has an `__mro_entries__` method, but it does not return a tuple of types +info: rule `invalid-base` is enabled by default + +``` diff --git a/crates/ty_python_semantic/src/types/class_base.rs b/crates/ty_python_semantic/src/types/class_base.rs index edb304fec1..9ff7fcad60 100644 --- a/crates/ty_python_semantic/src/types/class_base.rs +++ b/crates/ty_python_semantic/src/types/class_base.rs @@ -164,8 +164,12 @@ impl<'db> ClassBase<'db> { } } Type::NominalInstance(_) => None, // TODO -- handle `__mro_entries__`? - Type::PropertyInstance(_) => None, - Type::Never + + // This likely means that we're in unreachable code, + // in which case we want to treat `Never` in a forgiving way and silence diagnostics + Type::Never => Some(ClassBase::unknown()), + + Type::PropertyInstance(_) | Type::BooleanLiteral(_) | Type::FunctionLiteral(_) | Type::Callable(..) diff --git a/crates/ty_python_semantic/src/types/diagnostic.rs b/crates/ty_python_semantic/src/types/diagnostic.rs index c33590aab7..af7c678947 100644 --- a/crates/ty_python_semantic/src/types/diagnostic.rs +++ b/crates/ty_python_semantic/src/types/diagnostic.rs @@ -1,6 +1,7 @@ +use super::call::CallErrorKind; use super::context::InferContext; use super::mro::DuplicateBaseError; -use super::{ClassBase, ClassLiteral, KnownClass}; +use super::{CallArgumentTypes, CallDunderError, ClassBase, ClassLiteral, KnownClass}; use crate::db::Db; use crate::lint::{Level, LintRegistryBuilder, LintStatus}; use crate::suppression::FileSuppressionId; @@ -70,6 +71,7 @@ pub(crate) fn register_lints(registry: &mut LintRegistryBuilder) { registry.register_lint(&UNRESOLVED_ATTRIBUTE); registry.register_lint(&UNRESOLVED_IMPORT); registry.register_lint(&UNRESOLVED_REFERENCE); + registry.register_lint(&UNSUPPORTED_BASE); registry.register_lint(&UNSUPPORTED_OPERATOR); registry.register_lint(&ZERO_STEPSIZE_IN_SLICE); registry.register_lint(&STATIC_ASSERT_ERROR); @@ -451,14 +453,56 @@ declare_lint! { } declare_lint! { - /// TODO #14889 + /// ## What it does + /// Checks for class definitions that have bases which are not instances of `type`. + /// + /// ## Why is this bad? + /// Class definitions with bases like this will lead to `TypeError` being raised at runtime. + /// + /// ## Examples + /// ```python + /// class A(42): ... # error: [invalid-base] + /// ``` pub(crate) static INVALID_BASE = { - summary: "detects invalid bases in class definitions", + summary: "detects class bases that will cause the class definition to raise an exception at runtime", status: LintStatus::preview("1.0.0"), default_level: Level::Error, } } +declare_lint! { + /// ## What it does + /// Checks for class definitions that have bases which are unsupported by ty. + /// + /// ## Why is this bad? + /// If a class has a base that is an instance of a complex type such as a union type, + /// ty will not be able to resolve the [method resolution order] (MRO) for the class. + /// This will lead to an inferior understanding of your codebase and unpredictable + /// type-checking behavior. + /// + /// ## Examples + /// ```python + /// import datetime + /// + /// class A: ... + /// class B: ... + /// + /// if datetime.date.today().weekday() != 6: + /// C = A + /// else: + /// C = B + /// + /// class D(C): ... # error: [unsupported-base] + /// ``` + /// + /// [method resolution order]: https://docs.python.org/3/glossary.html#term-method-resolution-order + pub(crate) static UNSUPPORTED_BASE = { + summary: "detects class bases that are unsupported as ty could not feasibly calculate the class's MRO", + status: LintStatus::preview("1.0.0"), + default_level: Level::Warn, + } +} + declare_lint! { /// ## What it does /// Checks for expressions used in `with` statements @@ -1976,3 +2020,132 @@ pub(crate) fn report_duplicate_bases( diagnostic.sub(sub_diagnostic); } + +pub(crate) fn report_invalid_or_unsupported_base( + context: &InferContext, + base_node: &ast::Expr, + base_type: Type, + class: ClassLiteral, +) { + let db = context.db(); + let instance_of_type = KnownClass::Type.to_instance(db); + + if base_type.is_assignable_to(db, instance_of_type) { + report_unsupported_base(context, base_node, base_type, class); + return; + } + + let tuple_of_types = KnownClass::Tuple.to_specialized_instance(db, [instance_of_type]); + + let explain_mro_entries = |diagnostic: &mut LintDiagnosticGuard| { + diagnostic.info( + "An instance type is only a valid class base \ + if it has a valid `__mro_entries__` method", + ); + }; + + match base_type.try_call_dunder( + db, + "__mro_entries__", + CallArgumentTypes::positional([tuple_of_types]), + ) { + Ok(ret) => { + if ret.return_type(db).is_assignable_to(db, tuple_of_types) { + report_unsupported_base(context, base_node, base_type, class); + } else { + let Some(mut diagnostic) = + report_invalid_base(context, base_node, base_type, class) + else { + return; + }; + explain_mro_entries(&mut diagnostic); + diagnostic.info(format_args!( + "Type `{}` has an `__mro_entries__` method, but it does not return a tuple of types", + base_type.display(db) + )); + } + } + Err(mro_entries_call_error) => { + let Some(mut diagnostic) = report_invalid_base(context, base_node, base_type, class) + else { + return; + }; + + match mro_entries_call_error { + CallDunderError::MethodNotAvailable => {} + CallDunderError::PossiblyUnbound(_) => { + explain_mro_entries(&mut diagnostic); + diagnostic.info(format_args!( + "Type `{}` has an `__mro_entries__` attribute, but it is possibly unbound", + base_type.display(db) + )); + } + CallDunderError::CallError(CallErrorKind::NotCallable, _) => { + explain_mro_entries(&mut diagnostic); + diagnostic.info(format_args!( + "Type `{}` has an `__mro_entries__` attribute, but it is not callable", + base_type.display(db) + )); + } + CallDunderError::CallError(CallErrorKind::BindingError, _) => { + explain_mro_entries(&mut diagnostic); + diagnostic.info(format_args!( + "Type `{}` has an `__mro_entries__` method, \ + but it cannot be called with the expected arguments", + base_type.display(db) + )); + diagnostic.info( + "Expected a signature at least as permissive as \ + `def __mro_entries__(self, bases: tuple[type, ...], /) -> tuple[type, ...]`" + ); + } + CallDunderError::CallError(CallErrorKind::PossiblyNotCallable, _) => { + explain_mro_entries(&mut diagnostic); + diagnostic.info(format_args!( + "Type `{}` has an `__mro_entries__` method, \ + but it may not be callable", + base_type.display(db) + )); + } + } + } + } +} + +fn report_unsupported_base( + context: &InferContext, + base_node: &ast::Expr, + base_type: Type, + class: ClassLiteral, +) { + let Some(builder) = context.report_lint(&UNSUPPORTED_BASE, base_node) else { + return; + }; + let mut diagnostic = builder.into_diagnostic(format_args!( + "Unsupported class base with type `{}`", + base_type.display(context.db()) + )); + diagnostic.info(format_args!( + "ty cannot resolve a consistent MRO for class `{}` due to this base", + class.name(context.db()) + )); + diagnostic.info("Only class objects or `Any` are supported as class bases"); +} + +fn report_invalid_base<'ctx, 'db>( + context: &'ctx InferContext<'db>, + base_node: &ast::Expr, + base_type: Type<'db>, + class: ClassLiteral<'db>, +) -> Option> { + let builder = context.report_lint(&INVALID_BASE, base_node)?; + let mut diagnostic = builder.into_diagnostic(format_args!( + "Invalid class base with type `{}`", + base_type.display(context.db()) + )); + diagnostic.info(format_args!( + "Definition of class `{}` will raise `TypeError` at runtime", + class.name(context.db()) + )); + Some(diagnostic) +} diff --git a/crates/ty_python_semantic/src/types/infer.rs b/crates/ty_python_semantic/src/types/infer.rs index 403ce508ab..9ba6220198 100644 --- a/crates/ty_python_semantic/src/types/infer.rs +++ b/crates/ty_python_semantic/src/types/infer.rs @@ -102,8 +102,9 @@ use super::diagnostic::{ SUBCLASS_OF_FINAL_CLASS, TYPE_ASSERTION_FAILURE, report_attempted_protocol_instantiation, report_bad_argument_to_get_protocol_members, report_duplicate_bases, report_index_out_of_bounds, report_invalid_exception_caught, report_invalid_exception_cause, - report_invalid_exception_raised, report_invalid_type_checking_constant, - report_non_subscriptable, report_possibly_unresolved_reference, + report_invalid_exception_raised, report_invalid_or_unsupported_base, + report_invalid_type_checking_constant, report_non_subscriptable, + report_possibly_unresolved_reference, report_runtime_check_against_non_runtime_checkable_protocol, report_slice_step_size_zero, report_unresolved_reference, }; @@ -892,63 +893,51 @@ impl<'db> TypeInferenceBuilder<'db> { // (3) Check that the class's MRO is resolvable match class.try_mro(self.db(), None) { - Err(mro_error) => { - match mro_error.reason() { - MroErrorKind::DuplicateBases(duplicates) => { - let base_nodes = class_node.bases(); - for duplicate in duplicates { - report_duplicate_bases(&self.context, class, duplicate, base_nodes); - } - } - MroErrorKind::InvalidBases(bases) => { - let base_nodes = class_node.bases(); - for (index, base_ty) in bases { - if base_ty.is_never() { - // A class base of type `Never` can appear in unreachable code. It - // does not indicate a problem, since the actual construction of the - // class will never happen. - continue; - } - let Some(builder) = - self.context.report_lint(&INVALID_BASE, &base_nodes[*index]) - else { - continue; - }; - builder.into_diagnostic(format_args!( - "Invalid class base with type `{}` \ - (all bases must be a class, `Any`, `Unknown` or `Todo`)", - base_ty.display(self.db()) - )); - } - } - MroErrorKind::UnresolvableMro { bases_list } => { - if let Some(builder) = - self.context.report_lint(&INCONSISTENT_MRO, class_node) - { - builder.into_diagnostic(format_args!( - "Cannot create a consistent method resolution order (MRO) \ - for class `{}` with bases list `[{}]`", - class.name(self.db()), - bases_list - .iter() - .map(|base| base.display(self.db())) - .join(", ") - )); - } - } - MroErrorKind::InheritanceCycle => { - if let Some(builder) = self - .context - .report_lint(&CYCLIC_CLASS_DEFINITION, class_node) - { - builder.into_diagnostic(format_args!( - "Cyclic definition of `{}` (class cannot inherit from itself)", - class.name(self.db()) - )); - } + Err(mro_error) => match mro_error.reason() { + MroErrorKind::DuplicateBases(duplicates) => { + let base_nodes = class_node.bases(); + for duplicate in duplicates { + report_duplicate_bases(&self.context, class, duplicate, base_nodes); } } - } + MroErrorKind::InvalidBases(bases) => { + let base_nodes = class_node.bases(); + for (index, base_ty) in bases { + report_invalid_or_unsupported_base( + &self.context, + &base_nodes[*index], + *base_ty, + class, + ); + } + } + MroErrorKind::UnresolvableMro { bases_list } => { + if let Some(builder) = + self.context.report_lint(&INCONSISTENT_MRO, class_node) + { + builder.into_diagnostic(format_args!( + "Cannot create a consistent method resolution order (MRO) \ + for class `{}` with bases list `[{}]`", + class.name(self.db()), + bases_list + .iter() + .map(|base| base.display(self.db())) + .join(", ") + )); + } + } + MroErrorKind::InheritanceCycle => { + if let Some(builder) = self + .context + .report_lint(&CYCLIC_CLASS_DEFINITION, class_node) + { + builder.into_diagnostic(format_args!( + "Cyclic definition of `{}` (class cannot inherit from itself)", + class.name(self.db()) + )); + } + } + }, Ok(_) => check_class_slots(&self.context, class, class_node), } diff --git a/crates/ty_python_semantic/src/types/mro.rs b/crates/ty_python_semantic/src/types/mro.rs index c1120a2f76..151fbf6966 100644 --- a/crates/ty_python_semantic/src/types/mro.rs +++ b/crates/ty_python_semantic/src/types/mro.rs @@ -1,7 +1,8 @@ use std::collections::VecDeque; use std::ops::Deref; -use rustc_hash::FxHashMap; +use indexmap::IndexMap; +use rustc_hash::FxBuildHasher; use crate::Db; use crate::types::class_base::ClassBase; @@ -157,8 +158,8 @@ impl<'db> Mro<'db> { let mut duplicate_dynamic_bases = false; let duplicate_bases: Vec> = { - let mut base_to_indices: FxHashMap, Vec> = - FxHashMap::default(); + let mut base_to_indices: IndexMap, Vec, FxBuildHasher> = + IndexMap::default(); for (index, base) in valid_bases.iter().enumerate() { base_to_indices.entry(*base).or_default().push(index); diff --git a/ty.schema.json b/ty.schema.json index 636912e0bb..48f089f198 100644 --- a/ty.schema.json +++ b/ty.schema.json @@ -401,8 +401,8 @@ ] }, "invalid-base": { - "title": "detects invalid bases in class definitions", - "description": "TODO #14889", + "title": "detects class bases that will cause the class definition to raise an exception at runtime", + "description": "## What it does\nChecks for class definitions that have bases which are not instances of `type`.\n\n## Why is this bad?\nClass definitions with bases like this will lead to `TypeError` being raised at runtime.\n\n## Examples\n```python\nclass A(42): ... # error: [invalid-base]\n```", "default": "error", "oneOf": [ { @@ -800,6 +800,16 @@ } ] }, + "unsupported-base": { + "title": "detects class bases that are unsupported as ty could not feasibly calculate the class's MRO", + "description": "## What it does\nChecks for class definitions that have bases which are unsupported by ty.\n\n## Why is this bad?\nIf a class has a base that is an instance of a complex type such as a union type,\nty will not be able to resolve the [method resolution order] (MRO) for the class.\nThis will lead to an inferior understanding of your codebase and unpredictable\ntype-checking behavior.\n\n## Examples\n```python\nimport datetime\n\nclass A: ...\nclass B: ...\n\nif datetime.date.today().weekday() != 6:\n C = A\nelse:\n C = B\n\nclass D(C): ... # error: [unsupported-base]\n```\n\n[method resolution order]: https://docs.python.org/3/glossary.html#term-method-resolution-order", + "default": "warn", + "oneOf": [ + { + "$ref": "#/definitions/Level" + } + ] + }, "unsupported-bool-conversion": { "title": "detects boolean conversion where the object incorrectly implements `__bool__`", "description": "## What it does\nChecks for bool conversions where the object doesn't correctly implement `__bool__`.\n\n## Why is this bad?\nIf an exception is raised when you attempt to evaluate the truthiness of an object,\nusing the object in a boolean context will fail at runtime.\n\n## Examples\n\n```python\nclass NotBoolable:\n __bool__ = None\n\nb1 = NotBoolable()\nb2 = NotBoolable()\n\nif b1: # exception raised here\n pass\n\nb1 and b2 # exception raised here\nnot b1 # exception raised here\nb1 < b2 < b1 # exception raised here\n```",