diff --git a/crates/ty/docs/rules.md b/crates/ty/docs/rules.md index d1c4e2e9a4..4fd931497f 100644 --- a/crates/ty/docs/rules.md +++ b/crates/ty/docs/rules.md @@ -36,7 +36,7 @@ def test(): -> "int": Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20call-non-callable) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L112) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L113) **What it does** @@ -58,7 +58,7 @@ Calling a non-callable object will raise a `TypeError` at runtime. Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20conflicting-argument-forms) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L156) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L157) **What it does** @@ -88,7 +88,7 @@ f(int) # error Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20conflicting-declarations) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L182) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L183) **What it does** @@ -117,7 +117,7 @@ a = 1 Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20conflicting-metaclass) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L207) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L208) **What it does** @@ -147,7 +147,7 @@ class C(A, B): ... Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20cyclic-class-definition) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L233) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L234) **What it does** @@ -177,7 +177,7 @@ class B(A): ... Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20duplicate-base) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L298) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L299) **What it does** @@ -202,7 +202,7 @@ class B(A, A): ... Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20duplicate-kw-only) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L319) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L320) **What it does** @@ -306,7 +306,7 @@ def test(): -> "Literal[5]": Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20inconsistent-mro) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L522) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L523) **What it does** @@ -334,7 +334,7 @@ class C(A, B): ... Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20index-out-of-bounds) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L546) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L547) **What it does** @@ -358,7 +358,7 @@ t[3] # IndexError: tuple index out of range Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20instance-layout-conflict) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L351) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L352) **What it does** @@ -445,7 +445,7 @@ an atypical memory layout. Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-argument-type) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L591) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L592) **What it does** @@ -470,7 +470,7 @@ func("foo") # error: [invalid-argument-type] Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-assignment) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L631) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L632) **What it does** @@ -496,7 +496,7 @@ a: int = '' Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-attribute-access) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1665) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1687) **What it does** @@ -528,7 +528,7 @@ C.instance_var = 3 # error: Cannot assign to instance variable Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-await) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L653) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L654) **What it does** @@ -562,7 +562,7 @@ asyncio.run(main()) Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-base) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L683) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L684) **What it does** @@ -584,7 +584,7 @@ class A(42): ... # error: [invalid-base] Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-context-manager) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L734) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L735) **What it does** @@ -609,7 +609,7 @@ with 1: Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-declaration) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L755) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L756) **What it does** @@ -636,7 +636,7 @@ a: str Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-exception-caught) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L778) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L779) **What it does** @@ -678,7 +678,7 @@ except ZeroDivisionError: Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-generic-class) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L814) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L815) **What it does** @@ -709,7 +709,7 @@ class C[U](Generic[T]): ... Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-key) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L566) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L567) **What it does** @@ -738,7 +738,7 @@ alice["height"] # KeyError: 'height' Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-legacy-type-variable) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L840) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L841) **What it does** @@ -771,7 +771,7 @@ def f(t: TypeVar("U")): ... Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-metaclass) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L889) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L911) **What it does** @@ -803,7 +803,7 @@ class B(metaclass=f): ... Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-named-tuple) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L496) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L497) **What it does** @@ -828,12 +828,37 @@ in a class's bases list. TypeError: can only inherit from a NamedTuple type and Generic ``` +## `invalid-newtype` + + +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-newtype) · +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L890) + + +**What it does** + +Checks for the creation of invalid `NewType`s + +**Why is this bad?** + +There are several requirements that you must follow when creating a `NewType`. + +**Examples** + +```python +from typing import NewType + +Foo = NewType("Foo", int) # okay +Bar = NewType(get_name(), int) # error: NewType name must be a string literal +``` + ## `invalid-overload` Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-overload) · -[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#L938) **What it does** @@ -881,7 +906,7 @@ def foo(x: int) -> int: ... Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-parameter-default) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L959) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L981) **What it does** @@ -905,7 +930,7 @@ def f(a: int = ''): ... Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-protocol) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L433) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L434) **What it does** @@ -937,7 +962,7 @@ TypeError: Protocols can only inherit from other protocols, got Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-raise) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L979) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1001) Checks for `raise` statements that raise non-exceptions or use invalid @@ -984,7 +1009,7 @@ def g(): Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-return-type) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L612) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L613) **What it does** @@ -1007,7 +1032,7 @@ def func() -> int: Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-super-argument) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1022) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1044) **What it does** @@ -1061,7 +1086,7 @@ TODO #14889 Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-alias-type) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L868) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L869) **What it does** @@ -1086,7 +1111,7 @@ NewAlias = TypeAliasType(get_name(), int) # error: TypeAliasType name mus Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-checking-constant) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1061) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1083) **What it does** @@ -1114,7 +1139,7 @@ TYPE_CHECKING = '' Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-form) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1085) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1107) **What it does** @@ -1142,7 +1167,7 @@ b: Annotated[int] # `Annotated` expects at least two arguments Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-guard-call) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1137) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1159) **What it does** @@ -1174,7 +1199,7 @@ f(10) # Error Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-guard-definition) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1109) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1131) **What it does** @@ -1206,7 +1231,7 @@ class C: Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-variable-constraints) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1165) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1187) **What it does** @@ -1239,7 +1264,7 @@ T = TypeVar('T', bound=str) # valid bound TypeVar Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20missing-argument) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1194) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1216) **What it does** @@ -1262,7 +1287,7 @@ func() # TypeError: func() missing 1 required positional argument: 'x' Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20missing-typed-dict-key) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1764) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1786) **What it does** @@ -1293,7 +1318,7 @@ alice["age"] # KeyError Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20no-matching-overload) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1213) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1235) **What it does** @@ -1320,7 +1345,7 @@ func("string") # error: [no-matching-overload] Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20non-subscriptable) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1236) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1258) **What it does** @@ -1342,7 +1367,7 @@ Subscripting an object that does not support it will raise a `TypeError` at runt Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20not-iterable) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1254) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1276) **What it does** @@ -1366,7 +1391,7 @@ for i in 34: # TypeError: 'int' object is not iterable Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20parameter-already-assigned) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1305) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1327) **What it does** @@ -1420,7 +1445,7 @@ def test(): -> "int": Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20static-assert-error) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1641) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1663) **What it does** @@ -1448,7 +1473,7 @@ static_assert(int(2.0 * 3.0) == 6) # error: does not have a statically known tr Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20subclass-of-final-class) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1396) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1418) **What it does** @@ -1475,7 +1500,7 @@ class B(A): ... # Error raised here Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20too-many-positional-arguments) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1441) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1463) **What it does** @@ -1500,7 +1525,7 @@ f("foo") # Error raised here Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20type-assertion-failure) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1419) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1441) **What it does** @@ -1526,7 +1551,7 @@ def _(x: int): Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unavailable-implicit-super-arguments) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1462) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1484) **What it does** @@ -1570,7 +1595,7 @@ class A: Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unknown-argument) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1519) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1541) **What it does** @@ -1595,7 +1620,7 @@ f(x=1, y=2) # Error raised here Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-attribute) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1540) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1562) **What it does** @@ -1621,7 +1646,7 @@ A().foo # AttributeError: 'A' object has no attribute 'foo' Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-import) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1562) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1584) **What it does** @@ -1644,7 +1669,7 @@ import foo # ModuleNotFoundError: No module named 'foo' Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-reference) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1581) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1603) **What it does** @@ -1667,7 +1692,7 @@ print(x) # NameError: name 'x' is not defined Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unsupported-bool-conversion) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1274) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1296) **What it does** @@ -1702,7 +1727,7 @@ b1 < b2 < b1 # exception raised here Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unsupported-operator) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1600) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1622) **What it does** @@ -1728,7 +1753,7 @@ A() + A() # TypeError: unsupported operand type(s) for +: 'A' and 'A' Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20zero-stepsize-in-slice) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1622) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1644) **What it does** @@ -1751,7 +1776,7 @@ l[1:10:0] # ValueError: slice step cannot be zero Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20ambiguous-protocol-member) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L461) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L462) **What it does** @@ -1790,7 +1815,7 @@ class SubProto(BaseProto, Protocol): Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20deprecated) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L277) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L278) **What it does** @@ -1843,7 +1868,7 @@ a = 20 / 0 # type: ignore Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-unbound-attribute) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1326) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1348) **What it does** @@ -1869,7 +1894,7 @@ A.c # AttributeError: type object 'A' has no attribute 'c' Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-unbound-implicit-call) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L130) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L131) **What it does** @@ -1899,7 +1924,7 @@ A()[0] # TypeError: 'A' object is not subscriptable Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-unbound-import) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1348) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1370) **What it does** @@ -1929,7 +1954,7 @@ from module import a # ImportError: cannot import name 'a' from 'module' Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20redundant-cast) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1693) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1715) **What it does** @@ -1954,7 +1979,7 @@ cast(int, f()) # Redundant Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20undefined-reveal) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1501) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1523) **What it does** @@ -2005,7 +2030,7 @@ a = 20 / 0 # ty: ignore[division-by-zero] Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-global) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1714) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1736) **What it does** @@ -2059,7 +2084,7 @@ def g(): Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unsupported-base) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L701) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L702) **What it does** @@ -2096,7 +2121,7 @@ class D(C): ... # error: [unsupported-base] Default level: [`ignore`](../rules.md#rule-levels "This lint has a default level of 'ignore'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20division-by-zero) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L259) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L260) **What it does** @@ -2118,7 +2143,7 @@ Dividing by zero raises a `ZeroDivisionError` at runtime. Default level: [`ignore`](../rules.md#rule-levels "This lint has a default level of 'ignore'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-unresolved-reference) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1374) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1396) **What it does** diff --git a/crates/ty_ide/src/goto.rs b/crates/ty_ide/src/goto.rs index 52af38896b..b767aa9b70 100644 --- a/crates/ty_ide/src/goto.rs +++ b/crates/ty_ide/src/goto.rs @@ -165,16 +165,11 @@ impl<'db> DefinitionsOrTargets<'db> { ty_python_semantic::types::TypeDefinition::Module(module) => { ResolvedDefinition::Module(module.file(db)?) } - ty_python_semantic::types::TypeDefinition::Class(definition) => { - ResolvedDefinition::Definition(definition) - } - ty_python_semantic::types::TypeDefinition::Function(definition) => { - ResolvedDefinition::Definition(definition) - } - ty_python_semantic::types::TypeDefinition::TypeVar(definition) => { - ResolvedDefinition::Definition(definition) - } - ty_python_semantic::types::TypeDefinition::TypeAlias(definition) => { + ty_python_semantic::types::TypeDefinition::Class(definition) + | ty_python_semantic::types::TypeDefinition::Function(definition) + | ty_python_semantic::types::TypeDefinition::TypeVar(definition) + | ty_python_semantic::types::TypeDefinition::TypeAlias(definition) + | ty_python_semantic::types::TypeDefinition::NewType(definition) => { ResolvedDefinition::Definition(definition) } }; diff --git a/crates/ty_python_semantic/resources/mdtest/annotations/new_types.md b/crates/ty_python_semantic/resources/mdtest/annotations/new_types.md index 5dc14964cc..07ed3efae4 100644 --- a/crates/ty_python_semantic/resources/mdtest/annotations/new_types.md +++ b/crates/ty_python_semantic/resources/mdtest/annotations/new_types.md @@ -1,7 +1,5 @@ # NewType -Currently, ty doesn't support `typing.NewType` in type annotations. - ## Valid forms ```py @@ -12,13 +10,44 @@ X = GenericAlias(type, ()) A = NewType("A", int) # TODO: typeshed for `typing.GenericAlias` uses `type` for the first argument. `NewType` should be special-cased # to be compatible with `type` -# error: [invalid-argument-type] "Argument to function `__new__` is incorrect: Expected `type`, found `NewType`" +# error: [invalid-argument-type] "Argument to function `__new__` is incorrect: Expected `type`, found `A`" B = GenericAlias(A, ()) def _( a: A, b: B, ): - reveal_type(a) # revealed: @Todo(Support for `typing.NewType` instances in type expressions) + reveal_type(a) # revealed: A reveal_type(b) # revealed: @Todo(Support for `typing.GenericAlias` instances in type expressions) ``` + +## Subtyping + +```py +from typing_extensions import NewType + +Foo = NewType("Foo", int) +Bar = NewType("Bar", Foo) + +Foo(42) +Foo(Foo(42)) # allowed: `Foo` is a subtype of `int`. +Foo(Bar(Foo(42))) # allowed: `Bar` is a subtype of `int`. +Foo(True) # allowed: `bool` is a subtype of `int`. +Foo("fourty-two") # error: [invalid-argument-type] + +def f(_: int): ... +def g(_: Foo): ... +def h(_: Bar): ... + +f(42) +f(Foo(42)) +f(Bar(Foo(42))) + +g(42) # error: [invalid-argument-type] +g(Foo(42)) +g(Bar(Foo(42))) + +h(42) # error: [invalid-argument-type] +h(Foo(42)) # error: [invalid-argument-type] +h(Bar(Foo(42))) +``` diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index ec64c817dd..49587ecbfe 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -731,20 +731,20 @@ impl<'db> Type<'db> { fn is_none(&self, db: &'db dyn Db) -> bool { self.into_nominal_instance() - .is_some_and(|instance| instance.class(db).is_known(db, KnownClass::NoneType)) + .and_then(|instance| instance.class_if_not_newtype(db)) + .is_some_and(|class| class.is_known(db, KnownClass::NoneType)) } fn is_bool(&self, db: &'db dyn Db) -> bool { self.into_nominal_instance() - .is_some_and(|instance| instance.class(db).is_known(db, KnownClass::Bool)) + .and_then(|instance| instance.class_if_not_newtype(db)) + .is_some_and(|class| class.is_known(db, KnownClass::Bool)) } pub(crate) fn is_notimplemented(&self, db: &'db dyn Db) -> bool { - self.into_nominal_instance().is_some_and(|instance| { - instance - .class(db) - .is_known(db, KnownClass::NotImplementedType) - }) + self.into_nominal_instance() + .and_then(|instance| instance.class_if_not_newtype(db)) + .is_some_and(|class| class.is_known(db, KnownClass::NotImplementedType)) } pub(crate) fn is_object(&self, db: &'db dyn Db) -> bool { @@ -1932,8 +1932,13 @@ impl<'db> Type<'db> { return C::unsatisfiable(db); } - let class_literal = instance.class(db).class_literal(db).0; - C::from_bool(db, is_single_member_enum(db, class_literal)) + C::from_bool( + db, + instance.class_if_not_newtype(db).is_some_and(|class| { + let class_literal = class.class_literal(db).0; + is_single_member_enum(db, class_literal) + }), + ) } _ => C::unsatisfiable(db), } @@ -2232,7 +2237,7 @@ impl<'db> Type<'db> { // () (Type::ProtocolInstance(protocol), nominal @ Type::NominalInstance(n)) | (nominal @ Type::NominalInstance(n), Type::ProtocolInstance(protocol)) - if n.class(db).is_final(db) => + if n.class_ignoring_newtype(db).is_final(db) => { visitor.visit((self, other), || { any_protocol_members_absent_or_disjoint(db, protocol, nominal, visitor) @@ -2288,22 +2293,33 @@ impl<'db> Type<'db> { }, (Type::SpecialForm(special_form), Type::NominalInstance(instance)) - | (Type::NominalInstance(instance), Type::SpecialForm(special_form)) => { - C::from_bool(db, !special_form.is_instance_of(db, instance.class(db))) - } + | (Type::NominalInstance(instance), Type::SpecialForm(special_form)) => C::from_bool( + db, + !instance + .class_if_not_newtype(db) + .is_some_and(|class| special_form.is_instance_of(db, class)), + ), (Type::KnownInstance(known_instance), Type::NominalInstance(instance)) | (Type::NominalInstance(instance), Type::KnownInstance(known_instance)) => { - C::from_bool(db, !known_instance.is_instance_of(db, instance.class(db))) + C::from_bool( + db, + !instance + .class_if_not_newtype(db) + .is_some_and(|class| known_instance.is_instance_of(db, class)), + ) } (Type::BooleanLiteral(..) | Type::TypeIs(_), Type::NominalInstance(instance)) | (Type::NominalInstance(instance), Type::BooleanLiteral(..) | Type::TypeIs(_)) => { // A `Type::BooleanLiteral()` must be an instance of exactly `bool` // (it cannot be an instance of a `bool` subclass) - KnownClass::Bool - .when_subclass_of::(db, instance.class(db)) - .negate(db) + C::from_bool( + db, + !instance + .class_if_not_newtype(db) + .is_some_and(|class| KnownClass::Bool.is_subclass_of(db, class)), + ) } (Type::BooleanLiteral(..) | Type::TypeIs(_), _) @@ -2313,9 +2329,12 @@ impl<'db> Type<'db> { | (Type::NominalInstance(instance), Type::IntLiteral(..)) => { // A `Type::IntLiteral()` must be an instance of exactly `int` // (it cannot be an instance of an `int` subclass) - KnownClass::Int - .when_subclass_of::(db, instance.class(db)) - .negate(db) + C::from_bool( + db, + !instance + .class_if_not_newtype(db) + .is_some_and(|class| KnownClass::Int.is_subclass_of(db, class)), + ) } (Type::IntLiteral(..), _) | (_, Type::IntLiteral(..)) => C::always_satisfiable(db), @@ -2327,9 +2346,12 @@ impl<'db> Type<'db> { | (Type::NominalInstance(instance), Type::StringLiteral(..) | Type::LiteralString) => { // A `Type::StringLiteral()` or a `Type::LiteralString` must be an instance of exactly `str` // (it cannot be an instance of a `str` subclass) - KnownClass::Str - .when_subclass_of::(db, instance.class(db)) - .negate(db) + C::from_bool( + db, + !instance + .class_if_not_newtype(db) + .is_some_and(|class| KnownClass::Str.is_subclass_of(db, class)), + ) } (Type::LiteralString, Type::LiteralString) => C::unsatisfiable(db), @@ -2339,9 +2361,12 @@ impl<'db> Type<'db> { | (Type::NominalInstance(instance), Type::BytesLiteral(..)) => { // A `Type::BytesLiteral()` must be an instance of exactly `bytes` // (it cannot be an instance of a `bytes` subclass) - KnownClass::Bytes - .when_subclass_of::(db, instance.class(db)) - .negate(db) + C::from_bool( + db, + !instance + .class_if_not_newtype(db) + .is_some_and(|class| KnownClass::Bytes.is_subclass_of(db, class)), + ) } (Type::EnumLiteral(enum_literal), instance @ Type::NominalInstance(_)) @@ -2373,9 +2398,12 @@ impl<'db> Type<'db> { | (Type::NominalInstance(instance), Type::FunctionLiteral(..)) => { // A `Type::FunctionLiteral()` must be an instance of exactly `types.FunctionType` // (it cannot be an instance of a `types.FunctionType` subclass) - KnownClass::FunctionType - .when_subclass_of::(db, instance.class(db)) - .negate(db) + C::from_bool( + db, + !instance + .class_if_not_newtype(db) + .is_some_and(|class| KnownClass::FunctionType.is_subclass_of(db, class)), + ) } (Type::BoundMethod(_), other) | (other, Type::BoundMethod(_)) => KnownClass::MethodType @@ -2426,7 +2454,7 @@ impl<'db> Type<'db> { | ( instance @ Type::NominalInstance(nominal), Type::Callable(_) | Type::DataclassDecorator(_) | Type::DataclassTransformer(_), - ) if nominal.class(db).is_final(db) => instance + ) if nominal.class_ignoring_newtype(db).is_final(db) => instance .member_lookup_with_policy( db, Name::new_static("__call__"), @@ -2788,7 +2816,9 @@ impl<'db> Type<'db> { // `Type::NominalInstance(type)` is equivalent to looking up the name in the // MRO of the class `object`. Type::NominalInstance(instance) - if instance.class(db).is_known(db, KnownClass::Type) => + if instance + .class_ignoring_newtype(db) + .is_known(db, KnownClass::Type) => { if policy.mro_no_object_fallback() { Some(Place::Unbound.into()) @@ -2908,7 +2938,9 @@ impl<'db> Type<'db> { Type::Dynamic(_) | Type::Never => Place::bound(self).into(), - Type::NominalInstance(instance) => instance.class(db).instance_member(db, name), + Type::NominalInstance(instance) => instance + .class_ignoring_newtype(db) + .instance_member(db, name), Type::ProtocolInstance(protocol) => protocol.instance_member(db, name), @@ -3428,7 +3460,9 @@ impl<'db> Type<'db> { Type::NominalInstance(instance) if matches!(name.as_str(), "major" | "minor") - && instance.class(db).is_known(db, KnownClass::VersionInfo) => + && instance + .class_ignoring_newtype(db) + .is_known(db, KnownClass::VersionInfo) => { let python_version = Program::get(db).python_version(db); let segment = if name == "major" { @@ -3506,7 +3540,7 @@ impl<'db> Type<'db> { // resolve the attribute. if matches!( self.into_nominal_instance() - .and_then(|instance| instance.class(db).known(db)), + .and_then(|instance| instance.class_ignoring_newtype(db).known(db)), Some(KnownClass::ModuleType | KnownClass::GenericAlias) ) { return Place::Unbound.into(); @@ -3826,7 +3860,7 @@ impl<'db> Type<'db> { Type::TypeVar(_) => Truthiness::Ambiguous, Type::NominalInstance(instance) => instance - .class(db) + .class_ignoring_newtype(db) .known(db) .and_then(KnownClass::bool) .map(Ok) @@ -4407,6 +4441,21 @@ impl<'db> Type<'db> { .into() } + Some(KnownClass::NewType) => Binding::single( + self, + Signature::new( + Parameters::new([ + Parameter::positional_or_keyword(Name::new_static("name")) + .with_annotated_type(Type::LiteralString), + Parameter::positional_or_keyword(Name::new_static("tp")), + ]), + // The true return type is a KnownInstanceType::NewType, but each callsite + // gets its own unique...instance...of that type. + None, + ), + ) + .into(), + Some(KnownClass::Object) => { // ```py // class object: @@ -4801,6 +4850,16 @@ impl<'db> Type<'db> { Type::EnumLiteral(enum_literal) => enum_literal.enum_class_instance(db).bindings(db), + Type::KnownInstance(KnownInstanceType::NewType(newtype)) => Binding::single( + self, + Signature::new( + Parameters::new([Parameter::positional_only(None) + .with_annotated_type(newtype.base(db).to_type(db))]), + Some(Type::newtype_nominal_instance(newtype)), + ), + ) + .into(), + Type::KnownInstance(known_instance) => { known_instance.instance_fallback(db).bindings(db) } @@ -5275,9 +5334,10 @@ impl<'db> Type<'db> { }; match self { - Type::NominalInstance(instance) => { - instance.class(db).iter_mro(db).find_map(from_class_base) - } + Type::NominalInstance(instance) => instance + .class_ignoring_newtype(db) + .iter_mro(db) + .find_map(from_class_base), Type::ProtocolInstance(instance) => { if let Protocol::FromClass(class) = instance.inner { class.iter_mro(db).find_map(from_class_base) @@ -5635,6 +5695,7 @@ impl<'db> Type<'db> { invalid_expressions: smallvec::smallvec_inline![InvalidTypeExpression::Generic], fallback_type: Type::unknown(), }), + KnownInstanceType::NewType(newtype) => Ok(Type::newtype_nominal_instance(*newtype)), }, Type::SpecialForm(special_form) => match special_form { @@ -5812,32 +5873,37 @@ impl<'db> Type<'db> { Type::Dynamic(_) => Ok(*self), - Type::NominalInstance(instance) => match instance.class(db).known(db) { - Some(KnownClass::TypeVar) => Ok(todo_type!( - "Support for `typing.TypeVar` instances in type expressions" - )), - Some( - KnownClass::ParamSpec | KnownClass::ParamSpecArgs | KnownClass::ParamSpecKwargs, - ) => Ok(todo_type!("Support for `typing.ParamSpec`")), - Some(KnownClass::TypeVarTuple) => Ok(todo_type!( - "Support for `typing.TypeVarTuple` instances in type expressions" - )), - Some(KnownClass::NewType) => Ok(todo_type!( - "Support for `typing.NewType` instances in type expressions" - )), - Some(KnownClass::GenericAlias) => Ok(todo_type!( - "Support for `typing.GenericAlias` instances in type expressions" - )), - Some(KnownClass::UnionType) => Ok(todo_type!( - "Support for `types.UnionType` instances in type expressions" - )), - _ => Err(InvalidTypeExpressionError { - invalid_expressions: smallvec::smallvec_inline![ - InvalidTypeExpression::InvalidType(*self, scope_id) - ], - fallback_type: Type::unknown(), - }), - }, + Type::NominalInstance(instance) => { + // TODO: Should NewType wrapping be supported here? + match instance.class_ignoring_newtype(db).known(db) { + Some(KnownClass::TypeVar) => Ok(todo_type!( + "Support for `typing.TypeVar` instances in type expressions" + )), + Some( + KnownClass::ParamSpec + | KnownClass::ParamSpecArgs + | KnownClass::ParamSpecKwargs, + ) => Ok(todo_type!("Support for `typing.ParamSpec`")), + Some(KnownClass::TypeVarTuple) => Ok(todo_type!( + "Support for `typing.TypeVarTuple` instances in type expressions" + )), + Some(KnownClass::NewType) => Ok(todo_type!( + "Support for `typing.NewType` instances in type expressions" + )), + Some(KnownClass::GenericAlias) => Ok(todo_type!( + "Support for `typing.GenericAlias` instances in type expressions" + )), + Some(KnownClass::UnionType) => Ok(todo_type!( + "Support for `types.UnionType` instances in type expressions" + )), + _ => Err(InvalidTypeExpressionError { + invalid_expressions: smallvec::smallvec_inline![ + InvalidTypeExpression::InvalidType(*self, scope_id) + ], + fallback_type: Type::unknown(), + }), + } + } Type::Intersection(_) => Ok(todo_type!("Type::Intersection.in_type_expression")), @@ -6342,7 +6408,12 @@ impl<'db> Type<'db> { } Self::GenericAlias(alias) => Some(TypeDefinition::Class(alias.definition(db))), Self::NominalInstance(instance) => { - Some(TypeDefinition::Class(instance.class(db).definition(db))) + let (instance_class_type, instance_newtype) = instance.class_and_newtype(db); + if let Some(newtype) = instance_newtype { + Some(TypeDefinition::NewType(newtype.definition(db))) + } else { + Some(TypeDefinition::Class(instance_class_type.definition(db))) + } } Self::KnownInstance(instance) => match instance { KnownInstanceType::TypeVar(var) => { @@ -6375,17 +6446,18 @@ impl<'db> Type<'db> { | Self::PropertyInstance(_) | Self::BoundSuper(_) => self.to_meta_type(db).definition(db), - Self::NonInferableTypeVar(bound_typevar) | - Self::TypeVar(bound_typevar) => Some(TypeDefinition::TypeVar(bound_typevar.typevar(db).definition(db)?)), + Self::NonInferableTypeVar(bound_typevar) | Self::TypeVar(bound_typevar) => Some( + TypeDefinition::TypeVar(bound_typevar.typevar(db).definition(db)?), + ), Self::ProtocolInstance(protocol) => match protocol.inner { Protocol::FromClass(class) => Some(TypeDefinition::Class(class.definition(db))), Protocol::Synthesized(_) => None, }, - Type::TypedDict(typed_dict) => { - Some(TypeDefinition::Class(typed_dict.defining_class().definition(db))) - } + Type::TypedDict(typed_dict) => Some(TypeDefinition::Class( + typed_dict.defining_class().definition(db), + )), Self::Union(_) | Self::Intersection(_) => None, @@ -6466,7 +6538,7 @@ impl<'db> Type<'db> { match self { Type::GenericAlias(generic) => Some(generic.origin(db)), Type::NominalInstance(instance) => { - if let ClassType::Generic(generic) = instance.class(db) { + if let ClassType::Generic(generic) = instance.class_ignoring_newtype(db) { Some(generic.origin(db)) } else { None @@ -6712,6 +6784,10 @@ pub enum KnownInstanceType<'db> { /// A single instance of `dataclasses.Field` Field(FieldInstance<'db>), + + /// An identity function created with `typing.NewType(name, base)`, which behaves like a + /// subclass of `base` in type expressions. See `NewTypeInstance` for an example. + NewType(NewTypeInstance<'db>), } fn walk_known_instance_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>( @@ -6736,6 +6812,11 @@ fn walk_known_instance_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>( KnownInstanceType::Field(field) => { visitor.visit_type(db, field.default_type(db)); } + KnownInstanceType::NewType(newtype) => { + if let ClassType::Generic(generic_alias) = newtype.base_class_type(db) { + visitor.visit_generic_alias_type(db, generic_alias); + } + } } } @@ -6757,6 +6838,7 @@ impl<'db> KnownInstanceType<'db> { Self::Deprecated(deprecated) } Self::Field(field) => Self::Field(field.normalized_impl(db, visitor)), + Self::NewType(newtype) => Self::NewType(newtype.normalized_impl(db, visitor)), } } @@ -6767,6 +6849,7 @@ impl<'db> KnownInstanceType<'db> { Self::TypeAliasType(_) => KnownClass::TypeAliasType, Self::Deprecated(_) => KnownClass::Deprecated, Self::Field(_) => KnownClass::Field, + Self::NewType(_) => KnownClass::NewType, } } @@ -6817,6 +6900,9 @@ impl<'db> KnownInstanceType<'db> { field.default_type(self.db).display(self.db).fmt(f)?; f.write_str("]") } + KnownInstanceType::NewType(declaration) => { + f.write_str(declaration.name(self.db)) + } } } } @@ -7185,6 +7271,158 @@ impl<'db> FieldInstance<'db> { } } +/// A `typing.NewType` declaration, either from the perspective of the +/// identity-function-that-acts-like-a-subclass-in-type-expressions returned by the call to +/// `NewType`, or from the perspective of instances of that subclass. For example: +/// +/// ```py +/// from typing import NewType +/// Foo = NewType("Foo", int) +/// x = Foo(42) +/// ``` +/// +/// The revealed types there are: +/// - `NewType`: `Type::ClassLiteral(ClassLiteral)` with `KnownClass::NewType`. +/// - `Foo`: `Type::KnownInstance(KnownInstanceType::NewType(NewTypeInstance { .. }))` +/// - `x`: `Type::NominalInstance(...(NominalInstanceInner::NewType(NewTypeInstance { .. }))` +#[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] +#[derive(PartialOrd, Ord)] +pub struct NewTypeInstance<'db> { + /// The name of this NewType (e.g. `"Foo"`) + #[returns(ref)] + pub name: ast::name::Name, + + /// The binding where this NewType is first created. + pub definition: Definition<'db>, + + // The base class of this NewType (e.g. `int`), which could be a (specialized) class type or + // could be another NewType. + pub base: NewTypeBase<'db>, +} + +impl get_size2::GetSize for NewTypeInstance<'_> {} + +impl<'db> NewTypeInstance<'db> { + // Walk the `NewTypeBase` chain to find the underlying `ClassType`. + fn base_class_type(self, db: &'db dyn Db) -> ClassType<'db> { + let mut base = self.base(db); + loop { + match base { + NewTypeBase::ClassType(class) => return class, + NewTypeBase::NewType(newtype) => base = newtype.base(db), + } + } + } + + fn normalized_impl(self, db: &'db dyn Db, visitor: &NormalizedVisitor<'db>) -> Self { + let normalized_base = match self.base(db) { + NewTypeBase::ClassType(base_class) => { + NewTypeBase::ClassType(base_class.normalized_impl(db, visitor)) + } + NewTypeBase::NewType(base_newtype) => { + NewTypeBase::NewType(base_newtype.normalized_impl(db, visitor)) + } + }; + Self::new( + db, + self.name(db).clone(), + self.definition(db), + normalized_base, + ) + } + + fn materialize(self, db: &'db dyn Db, materialization_kind: MaterializationKind) -> Self { + let materialized_base = match self.base(db) { + NewTypeBase::ClassType(base_class) => { + NewTypeBase::ClassType(base_class.materialize(db, materialization_kind)) + } + NewTypeBase::NewType(base_newtype) => { + NewTypeBase::NewType(base_newtype.materialize(db, materialization_kind)) + } + }; + Self::new( + db, + self.name(db).clone(), + self.definition(db), + materialized_base, + ) + } + + fn apply_type_mapping_impl<'a>( + self, + db: &'db dyn Db, + type_mapping: &TypeMapping<'a, 'db>, + visitor: &ApplyTypeMappingVisitor<'db>, + ) -> Self { + let mapped_base = match self.base(db) { + NewTypeBase::ClassType(base_class) => NewTypeBase::ClassType( + base_class.apply_type_mapping_impl(db, type_mapping, visitor), + ), + NewTypeBase::NewType(base_newtype) => NewTypeBase::NewType( + base_newtype.apply_type_mapping_impl(db, type_mapping, visitor), + ), + }; + Self::new(db, self.name(db).clone(), self.definition(db), mapped_base) + } + + fn is_subtype_of(self, db: &'db dyn Db, other: Self) -> bool { + let mut candidate_newtype = self; + loop { + if candidate_newtype == other { + // Either `self` is `other`, or it's a (transitive) NewType wrapper of it. + return true; + } + match candidate_newtype.base(db) { + NewTypeBase::NewType(base_newtype) => candidate_newtype = base_newtype, + // Classes can't inherit from NewTypes, so if we reach the base `ClassType` without + // seeing `other`, then we can't be a subtype of it. + NewTypeBase::ClassType(_) => return false, + } + } + } + + fn has_relation_to_impl>( + self, + db: &'db dyn Db, + other: Self, + _: TypeRelation, + _: &HasRelationToVisitor<'db, C>, + ) -> C { + // TODO: Is this correct? + C::from_bool(db, self.is_subtype_of(db, other)) + } + + pub(super) fn is_disjoint_from_impl>( + self, + db: &'db dyn Db, + other: Self, + _: &IsDisjointVisitor<'db, C>, + ) -> C { + // Two NewTypes are disjoint if they're not equal and neither inherits from the other. + // (NewTypes have single inheritance, and a regular class can't inherit from a NewType, so + // it's not possible for some third type to multiply-inherit from both.) + C::from_bool( + db, + !self.is_subtype_of(db, other) && !other.is_subtype_of(db, self), + ) + } +} + +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, get_size2::GetSize)] +pub enum NewTypeBase<'db> { + ClassType(ClassType<'db>), + NewType(NewTypeInstance<'db>), +} + +impl<'db> NewTypeBase<'db> { + fn to_type(self, db: &'db dyn Db) -> Type<'db> { + match self { + NewTypeBase::ClassType(base_class) => Type::instance(db, base_class), + NewTypeBase::NewType(base_newtype) => Type::newtype_nominal_instance(base_newtype), + } + } +} + /// Whether this typevar was created via the legacy `TypeVar` constructor, using PEP 695 syntax, /// or an implicit typevar like `Self` was used. #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, get_size2::GetSize)] @@ -9976,7 +10214,9 @@ impl<'db> SuperOwnerKind<'db> { Either::Left(ClassBase::Dynamic(dynamic).mro(db, None)) } SuperOwnerKind::Class(class) => Either::Right(class.iter_mro(db)), - SuperOwnerKind::Instance(instance) => Either::Right(instance.class(db).iter_mro(db)), + SuperOwnerKind::Instance(instance) => { + Either::Right(instance.class_ignoring_newtype(db).iter_mro(db)) + } } } @@ -9988,11 +10228,11 @@ impl<'db> SuperOwnerKind<'db> { } } - fn into_class(self, db: &'db dyn Db) -> Option> { + fn into_class_ignoring_newtype(self, db: &'db dyn Db) -> Option> { match self { SuperOwnerKind::Dynamic(_) => None, SuperOwnerKind::Class(class) => Some(class), - SuperOwnerKind::Instance(instance) => Some(instance.class(db)), + SuperOwnerKind::Instance(instance) => Some(instance.class_ignoring_newtype(db)), } } @@ -10111,7 +10351,7 @@ impl<'db> BoundSuperType<'db> { let Some(pivot_class) = pivot_class.into_class() else { return Some(owner); }; - let Some(owner_class) = owner.into_class(db) else { + let Some(owner_class) = owner.into_class_ignoring_newtype(db) else { return Some(owner); }; if owner_class.is_subclass_of(db, pivot_class) { @@ -10212,7 +10452,7 @@ impl<'db> BoundSuperType<'db> { .expect("Calling `find_name_in_mro` on dynamic type should return `Some`"); } SuperOwnerKind::Class(class) => class, - SuperOwnerKind::Instance(instance) => instance.class(db), + SuperOwnerKind::Instance(instance) => instance.class_ignoring_newtype(db), }; let (class_literal, _) = class.class_literal(db); diff --git a/crates/ty_python_semantic/src/types/builder.rs b/crates/ty_python_semantic/src/types/builder.rs index c33ed3fa09..53f9458064 100644 --- a/crates/ty_python_semantic/src/types/builder.rs +++ b/crates/ty_python_semantic/src/types/builder.rs @@ -631,7 +631,14 @@ impl<'db> IntersectionBuilder<'db> { self } Type::NominalInstance(instance) - if enum_metadata(self.db, instance.class(self.db).class_literal(self.db).0) + if !instance.is_newtype() + && enum_metadata( + self.db, + instance + .class_ignoring_newtype(self.db) + .class_literal(self.db) + .0, + ) .is_some() => { let mut contains_enum_literal_as_negative_element = false; @@ -657,9 +664,13 @@ impl<'db> IntersectionBuilder<'db> { self.add_positive_impl( Type::Union(UnionType::new( db, - enum_member_literals(db, instance.class(db).class_literal(db).0, None) - .expect("Calling `enum_member_literals` on an enum class") - .collect::>(), + enum_member_literals( + db, + instance.class_ignoring_newtype(db).class_literal(db).0, + None, + ) + .expect("Calling `enum_member_literals` on an enum class") + .collect::>(), )), seen_aliases, ) @@ -854,7 +865,8 @@ impl<'db> InnerIntersectionBuilder<'db> { _ => { let known_instance = new_positive .into_nominal_instance() - .and_then(|instance| instance.class(db).known(db)); + .and_then(|instance| instance.class_if_not_newtype(db)) + .and_then(|class| class.known(db)); if known_instance == Some(KnownClass::Object) { // `object & T` -> `T`; it is always redundant to add `object` to an intersection @@ -874,7 +886,9 @@ impl<'db> InnerIntersectionBuilder<'db> { new_positive = Type::BooleanLiteral(false); } Type::NominalInstance(instance) - if instance.class(db).is_known(db, KnownClass::Bool) => + if instance + .class_if_not_newtype(db) + .is_some_and(|class| class.is_known(db, KnownClass::Bool)) => { match new_positive { // `bool & AlwaysTruthy` -> `Literal[True]` @@ -968,7 +982,8 @@ impl<'db> InnerIntersectionBuilder<'db> { self.positive .iter() .filter_map(|ty| ty.into_nominal_instance()) - .filter_map(|instance| instance.class(db).known(db)) + .filter_map(|instance| instance.class_if_not_newtype(db)) + .filter_map(|class| class.known(db)) .any(KnownClass::is_bool) }; diff --git a/crates/ty_python_semantic/src/types/call/arguments.rs b/crates/ty_python_semantic/src/types/call/arguments.rs index 3bf78c6a75..db4a32a3c3 100644 --- a/crates/ty_python_semantic/src/types/call/arguments.rs +++ b/crates/ty_python_semantic/src/types/call/arguments.rs @@ -260,7 +260,9 @@ impl<'a, 'db> FromIterator<(Argument<'a>, Option>)> for CallArguments< pub(crate) fn is_expandable_type<'db>(db: &'db dyn Db, ty: Type<'db>) -> bool { match ty { Type::NominalInstance(instance) => { - let class = instance.class(db); + let Some(class) = instance.class_if_not_newtype(db) else { + return false; + }; class.is_known(db, KnownClass::Bool) || instance.tuple_spec(db).is_some_and(|spec| match &*spec { Tuple::Fixed(fixed_length_tuple) => fixed_length_tuple @@ -282,7 +284,7 @@ fn expand_type<'db>(db: &'db dyn Db, ty: Type<'db>) -> Option>> { // NOTE: Update `is_expandable_type` if this logic changes accordingly. match ty { Type::NominalInstance(instance) => { - let class = instance.class(db); + let class = instance.class_if_not_newtype(db)?; if class.is_known(db, KnownClass::Bool) { return Some(vec![ diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index 3bf1f4e679..2bb938e6e8 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -20,7 +20,9 @@ use crate::semantic_index::{ }; use crate::types::constraints::{Constraints, IteratorConstraintsExtension}; use crate::types::context::InferContext; -use crate::types::diagnostic::{INVALID_LEGACY_TYPE_VARIABLE, INVALID_TYPE_ALIAS_TYPE}; +use crate::types::diagnostic::{ + INVALID_LEGACY_TYPE_VARIABLE, INVALID_NEWTYPE, INVALID_TYPE_ALIAS_TYPE, +}; use crate::types::enums::enum_metadata; use crate::types::function::{DataclassTransformerParams, KnownFunction}; use crate::types::generics::{GenericContext, Specialization, walk_specialization}; @@ -31,10 +33,10 @@ use crate::types::typed_dict::typed_dict_params_from_class_def; use crate::types::{ ApplyTypeMappingVisitor, Binding, BoundSuperError, BoundSuperType, CallableType, DataclassParams, DeprecatedInstance, HasRelationToVisitor, IsEquivalentVisitor, - KnownInstanceType, ManualPEP695TypeAliasType, MaterializationKind, NormalizedVisitor, - PropertyInstanceType, StringLiteralType, TypeAliasType, TypeMapping, TypeRelation, - TypeVarBoundOrConstraints, TypeVarInstance, TypeVarKind, TypedDictParams, VarianceInferable, - declaration_type, infer_definition_types, todo_type, + KnownInstanceType, ManualPEP695TypeAliasType, MaterializationKind, NewTypeBase, + NewTypeInstance, NormalizedVisitor, PropertyInstanceType, StringLiteralType, TypeAliasType, + TypeMapping, TypeRelation, TypeVarBoundOrConstraints, TypeVarInstance, TypeVarKind, + TypedDictParams, VarianceInferable, declaration_type, infer_definition_types, todo_type, }; use crate::{ Db, FxIndexMap, FxOrderSet, Program, @@ -1317,7 +1319,8 @@ impl<'db> Field<'db> { pub(crate) fn is_kw_only_sentinel(&self, db: &'db dyn Db) -> bool { self.declared_ty .into_nominal_instance() - .is_some_and(|instance| instance.class(db).is_known(db, KnownClass::KwOnly)) + .and_then(|instance| instance.class_if_not_newtype(db)) + .is_some_and(|class| class.is_known(db, KnownClass::KwOnly)) } } @@ -4245,14 +4248,6 @@ impl KnownClass { .is_ok_and(|class| class.is_subclass_of(db, None, other)) } - pub(super) fn when_subclass_of<'db, C: Constraints<'db>>( - self, - db: &'db dyn Db, - other: ClassType<'db>, - ) -> C { - C::from_bool(db, self.is_subclass_of(db, other)) - } - /// Return the module in which we should look up the definition for this class fn canonical_module(self, db: &dyn Db) -> KnownModule { match self { @@ -4969,6 +4964,69 @@ impl KnownClass { ))); } + KnownClass::NewType => { + let assigned_to = index + .try_expression(ast::ExprRef::from(call_expression)) + .and_then(|expr| expr.assigned_to(db)); + + let Some(target) = assigned_to.as_ref().and_then(|assigned_to| { + match assigned_to.node(module).targets.as_slice() { + [ast::Expr::Name(target)] => Some(target), + _ => None, + } + }) else { + if let Some(builder) = context.report_lint(&INVALID_NEWTYPE, call_expression) { + builder.into_diagnostic( + "A `typing.NewType` must be immediately assigned to a variable", + ); + } + return; + }; + let definition = index.expect_single_definition(target); + + let [Some(name), Some(supertype), ..] = overload.parameter_types() else { + return; + }; + + let Some(name) = name.into_string_literal() else { + if let Some(builder) = context.report_lint(&INVALID_NEWTYPE, call_expression) { + builder.into_diagnostic( + "The name of a `typing.NewType` must be a string literal", + ); + } + return; + }; + + let newtype_base = match supertype { + Type::ClassLiteral(class_literal) => { + NewTypeBase::ClassType(class_literal.default_specialization(db)) + } + Type::GenericAlias(alias) => NewTypeBase::ClassType(ClassType::Generic(*alias)), + Type::KnownInstance(KnownInstanceType::NewType(newtype)) => { + NewTypeBase::NewType(*newtype) + } + _ => { + if let Some(builder) = + context.report_lint(&INVALID_NEWTYPE, call_expression) + { + builder.into_diagnostic( + "The second argument to `typing.NewType` must be a class or another `NewType`", + ); + } + return; + } + }; + + overload.set_return_type(Type::KnownInstance(KnownInstanceType::NewType( + NewTypeInstance::new( + db, + ast::name::Name::new(name.value(db)), + definition, + newtype_base, + ), + ))); + } + _ => {} } } diff --git a/crates/ty_python_semantic/src/types/class_base.rs b/crates/ty_python_semantic/src/types/class_base.rs index 85cadc0b5d..297ddb5c81 100644 --- a/crates/ty_python_semantic/src/types/class_base.rs +++ b/crates/ty_python_semantic/src/types/class_base.rs @@ -80,7 +80,9 @@ impl<'db> ClassBase<'db> { Type::ClassLiteral(literal) => Some(Self::Class(literal.default_specialization(db))), Type::GenericAlias(generic) => Some(Self::Class(ClassType::Generic(generic))), Type::NominalInstance(instance) - if instance.class(db).is_known(db, KnownClass::GenericAlias) => + if instance + .class_if_not_newtype(db) + .is_some_and(|class| class.is_known(db, KnownClass::GenericAlias)) => { Self::try_from_type(db, todo_type!("GenericAlias instance"), subclass) } @@ -164,7 +166,8 @@ impl<'db> ClassBase<'db> { KnownInstanceType::TypeAliasType(_) | KnownInstanceType::TypeVar(_) | KnownInstanceType::Deprecated(_) - | KnownInstanceType::Field(_) => None, + | KnownInstanceType::Field(_) + | KnownInstanceType::NewType(_) => None, }, Type::SpecialForm(special_form) => match special_form { diff --git a/crates/ty_python_semantic/src/types/definition.rs b/crates/ty_python_semantic/src/types/definition.rs index f98d47ba93..9095dcea44 100644 --- a/crates/ty_python_semantic/src/types/definition.rs +++ b/crates/ty_python_semantic/src/types/definition.rs @@ -12,6 +12,7 @@ pub enum TypeDefinition<'db> { Function(Definition<'db>), TypeVar(Definition<'db>), TypeAlias(Definition<'db>), + NewType(Definition<'db>), } impl TypeDefinition<'_> { @@ -21,7 +22,8 @@ impl TypeDefinition<'_> { Self::Class(definition) | Self::Function(definition) | Self::TypeVar(definition) - | Self::TypeAlias(definition) => { + | Self::TypeAlias(definition) + | Self::NewType(definition) => { let module = parsed_module(db, definition.file(db)).load(db); Some(definition.focus_range(db, &module)) } @@ -38,7 +40,8 @@ impl TypeDefinition<'_> { Self::Class(definition) | Self::Function(definition) | Self::TypeVar(definition) - | Self::TypeAlias(definition) => { + | Self::TypeAlias(definition) + | Self::NewType(definition) => { let module = parsed_module(db, definition.file(db)).load(db); Some(definition.full_range(db, &module)) } diff --git a/crates/ty_python_semantic/src/types/diagnostic.rs b/crates/ty_python_semantic/src/types/diagnostic.rs index ca953928ed..5554516871 100644 --- a/crates/ty_python_semantic/src/types/diagnostic.rs +++ b/crates/ty_python_semantic/src/types/diagnostic.rs @@ -61,6 +61,7 @@ pub(crate) fn register_lints(registry: &mut LintRegistryBuilder) { registry.register_lint(&INVALID_GENERIC_CLASS); registry.register_lint(&INVALID_LEGACY_TYPE_VARIABLE); registry.register_lint(&INVALID_TYPE_ALIAS_TYPE); + registry.register_lint(&INVALID_NEWTYPE); registry.register_lint(&INVALID_METACLASS); registry.register_lint(&INVALID_OVERLOAD); registry.register_lint(&INVALID_PARAMETER_DEFAULT); @@ -886,6 +887,27 @@ declare_lint! { } } +declare_lint! { + /// ## What it does + /// Checks for the creation of invalid `NewType`s + /// + /// ## Why is this bad? + /// There are several requirements that you must follow when creating a `NewType`. + /// + /// ## Examples + /// ```python + /// from typing import NewType + /// + /// Foo = NewType("Foo", int) # okay + /// Bar = NewType(get_name(), int) # error: NewType name must be a string literal + /// ``` + pub(crate) static INVALID_NEWTYPE = { + summary: "detects invalid NewType definitions", + status: LintStatus::preview("1.0.0"), + default_level: Level::Error, + } +} + declare_lint! { /// ## What it does /// Checks for arguments to `metaclass=` that are invalid. @@ -1976,7 +1998,8 @@ pub(super) fn report_invalid_assignment( fn type_to_class_literal<'db>(ty: Type<'db>, db: &'db dyn crate::Db) -> Option> { match ty { Type::ClassLiteral(class) => Some(class), - Type::NominalInstance(instance) => match instance.class(db) { + // TODO: How should we handle NewTypes here? + Type::NominalInstance(instance) => match instance.class_ignoring_newtype(db) { crate::types::class::ClassType::NonGeneric(class) => Some(class), crate::types::class::ClassType::Generic(alias) => Some(alias.origin(db)), }, @@ -2591,7 +2614,13 @@ pub(crate) fn report_undeclared_protocol_member( SubclassOfInner::Dynamic(DynamicType::Any) => return true, SubclassOfInner::Dynamic(_) => return false, }, - Type::NominalInstance(instance) => instance.class(db), + Type::NominalInstance(instance) => { + if let Some(class) = instance.class_if_not_newtype(db) { + class + } else { + return false; + } + } _ => return false, }; diff --git a/crates/ty_python_semantic/src/types/display.rs b/crates/ty_python_semantic/src/types/display.rs index 63c3447889..1b42260d11 100644 --- a/crates/ty_python_semantic/src/types/display.rs +++ b/crates/ty_python_semantic/src/types/display.rs @@ -187,21 +187,22 @@ impl Display for DisplayRepresentation<'_> { Type::Dynamic(dynamic) => dynamic.fmt(f), Type::Never => f.write_str("Never"), Type::NominalInstance(instance) => { - let class = instance.class(self.db); + let (class, newtype) = instance.class_and_newtype(self.db); - match (class, class.known(self.db)) { - (_, Some(KnownClass::NoneType)) => f.write_str("None"), - (_, Some(KnownClass::NoDefaultType)) => f.write_str("NoDefault"), - (ClassType::Generic(alias), Some(KnownClass::Tuple)) => alias + match (newtype, class, class.known(self.db)) { + (Some(newtype), _, _) => f.write_str(newtype.name(self.db)), + (None, _, Some(KnownClass::NoneType)) => f.write_str("None"), + (None, _, Some(KnownClass::NoDefaultType)) => f.write_str("NoDefault"), + (None, ClassType::Generic(alias), Some(KnownClass::Tuple)) => alias .specialization(self.db) .tuple(self.db) .expect("Specialization::tuple() should always return `Some()` for `KnownClass::Tuple`") .display_with(self.db, self.settings) .fmt(f), - (ClassType::NonGeneric(class), _) => { + (None, ClassType::NonGeneric(class), _) => { self.write_maybe_qualified_class(f, class) }, - (ClassType::Generic(alias), _) => alias.display_with(self.db, self.settings).fmt(f), + (None, ClassType::Generic(alias), _) => alias.display_with(self.db, self.settings).fmt(f), } } Type::ProtocolInstance(protocol) => match protocol.inner { diff --git a/crates/ty_python_semantic/src/types/enums.rs b/crates/ty_python_semantic/src/types/enums.rs index fb6f5b0c33..71d5570a3c 100644 --- a/crates/ty_python_semantic/src/types/enums.rs +++ b/crates/ty_python_semantic/src/types/enums.rs @@ -130,26 +130,28 @@ pub(crate) fn enum_metadata<'db>( // Some types are specifically disallowed for enum members. return None; } - Type::NominalInstance(instance) => match instance.class(db).known(db) { - // enum.nonmember - Some(KnownClass::Nonmember) => return None, + Type::NominalInstance(instance) => { + match instance.class_ignoring_newtype(db).known(db) { + // enum.nonmember + Some(KnownClass::Nonmember) => return None, - // enum.member - Some(KnownClass::Member) => Some( - ty.member(db, "value") - .place - .ignore_possibly_unbound() - .unwrap_or(Type::unknown()), - ), + // enum.member + Some(KnownClass::Member) => Some( + ty.member(db, "value") + .place + .ignore_possibly_unbound() + .unwrap_or(Type::unknown()), + ), - // enum.auto - Some(KnownClass::Auto) => { - auto_counter += 1; - Some(Type::IntLiteral(auto_counter)) + // enum.auto + Some(KnownClass::Auto) => { + auto_counter += 1; + Some(Type::IntLiteral(auto_counter)) + } + + _ => None, } - - _ => None, - }, + } _ => None, }; @@ -208,7 +210,10 @@ pub(crate) fn enum_metadata<'db>( PlaceAndQualifiers { place: Place::Type(Type::NominalInstance(instance), _), .. - } if instance.class(db).is_known(db, KnownClass::Member) => { + } if instance + .class_ignoring_newtype(db) // TODO: what could it mean to wrap `enum.member` in a NewType? + .is_known(db, KnownClass::Member) => + { // If the attribute is specifically declared with `enum.member`, it is considered a member } _ => { diff --git a/crates/ty_python_semantic/src/types/function.rs b/crates/ty_python_semantic/src/types/function.rs index 8840e00413..766425682d 100644 --- a/crates/ty_python_semantic/src/types/function.rs +++ b/crates/ty_python_semantic/src/types/function.rs @@ -953,7 +953,7 @@ fn is_instance_truthiness<'db>( let is_instance = |ty: &Type<'_>| { if let Type::NominalInstance(instance) = ty { if instance - .class(db) + .class_ignoring_newtype(db) .iter_mro(db) .filter_map(ClassBase::into_class) .any(|c| match c { diff --git a/crates/ty_python_semantic/src/types/generics.rs b/crates/ty_python_semantic/src/types/generics.rs index 2752340eea..9ad9ade28a 100644 --- a/crates/ty_python_semantic/src/types/generics.rs +++ b/crates/ty_python_semantic/src/types/generics.rs @@ -1119,9 +1119,9 @@ impl<'db> SpecializationBuilder<'db> { // Extract formal_alias if this is a generic class let formal_alias = match formal { - Type::NominalInstance(formal_nominal) => { - formal_nominal.class(self.db).into_generic_alias() - } + Type::NominalInstance(formal_nominal) => formal_nominal + .class_if_not_newtype(self.db) + .and_then(ClassType::into_generic_alias), // TODO: This will only handle classes that explicit implement a generic protocol // by listing it as a base class. To handle classes that implicitly implement a // generic protocol, we will need to check the types of the protocol members to be @@ -1135,7 +1135,10 @@ impl<'db> SpecializationBuilder<'db> { if let Some(formal_alias) = formal_alias { let formal_origin = formal_alias.origin(self.db); - for base in actual_nominal.class(self.db).iter_mro(self.db) { + for base in actual_nominal + .class_ignoring_newtype(self.db) + .iter_mro(self.db) + { let ClassBase::Class(ClassType::Generic(base_alias)) = base else { continue; }; diff --git a/crates/ty_python_semantic/src/types/ide_support.rs b/crates/ty_python_semantic/src/types/ide_support.rs index f96401bd2f..85ecafb7e8 100644 --- a/crates/ty_python_semantic/src/types/ide_support.rs +++ b/crates/ty_python_semantic/src/types/ide_support.rs @@ -95,7 +95,8 @@ impl<'db> AllMembers<'db> { ), Type::NominalInstance(instance) => { - let (class_literal, _specialization) = instance.class(db).class_literal(db); + let (class_literal, _specialization) = + instance.class_ignoring_newtype(db).class_literal(db); self.extend_with_instance_members(db, ty, class_literal); } @@ -211,7 +212,9 @@ impl<'db> AllMembers<'db> { match ty { Type::NominalInstance(instance) if matches!( - instance.class(db).known(db), + instance + .class_if_not_newtype(db) + .and_then(|class| class.known(db)), Some( KnownClass::TypeVar | KnownClass::TypeVarTuple diff --git a/crates/ty_python_semantic/src/types/infer.rs b/crates/ty_python_semantic/src/types/infer.rs index 67fb854f17..8868cc5a71 100644 --- a/crates/ty_python_semantic/src/types/infer.rs +++ b/crates/ty_python_semantic/src/types/infer.rs @@ -1823,7 +1823,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { Type::BooleanLiteral(_) | Type::IntLiteral(_) => {} Type::NominalInstance(instance) if matches!( - instance.class(self.db()).known(self.db()), + instance.class_ignoring_newtype(self.db()).known(self.db()), Some(KnownClass::Float | KnownClass::Int | KnownClass::Bool) ) => {} _ => return false, @@ -4004,7 +4004,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // Super instances do not allow attribute assignment Type::NominalInstance(instance) - if instance.class(db).is_known(db, KnownClass::Super) => + if instance + .class_ignoring_newtype(db) + .is_known(db, KnownClass::Super) => { if emit_diagnostics { if let Some(builder) = self.context.report_lint(&INVALID_ASSIGNMENT, target) { @@ -6274,6 +6276,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { | KnownClass::TypeVar | KnownClass::TypeAliasType | KnownClass::Deprecated + | KnownClass::NewType ) ) || ( // Constructor calls to `tuple` and subclasses of `tuple` are handled in `Type::Bindings`, @@ -9157,7 +9160,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { Type::Dynamic(DynamicType::TodoUnpack) => Err(GenericContextError::NotYetSupported), Type::NominalInstance(nominal) if matches!( - nominal.class(self.db()).known(self.db()), + nominal.class_ignoring_newtype(self.db()).known(self.db()), Some(KnownClass::TypeVarTuple | KnownClass::ParamSpec) ) => { @@ -9201,8 +9204,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { Some(ty @ (Type::IntLiteral(_) | Type::BooleanLiteral(_))) => SliceArg::Arg(ty), Some(ty @ Type::NominalInstance(instance)) if instance - .class(self.db()) - .is_known(self.db(), KnownClass::NoneType) => + .class_if_not_newtype(self.db()) + .is_some_and(|class| class.is_known(self.db(), KnownClass::NoneType)) => { SliceArg::Arg(ty) } @@ -10426,6 +10429,16 @@ impl<'db> TypeInferenceBuilder<'db, '_> { self.infer_type_expression(&subscript.slice); todo_type!("Generic PEP-695 type alias") } + KnownInstanceType::NewType(newtype) => { + self.infer_type_expression(&subscript.slice); + if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) { + builder.into_diagnostic(format_args!( + "`{}` is a `NewType` and cannot be specialized", + newtype.name(self.db()) + )); + } + Type::unknown() + } }, Type::Dynamic(DynamicType::Todo(_)) => { self.infer_type_expression(slice); @@ -11141,7 +11154,8 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } Type::NominalInstance(nominal) if nominal - .class(self.db()) + // TODO: What could NewType mean here? + .class_ignoring_newtype(self.db()) .is_known(self.db(), KnownClass::ParamSpec) => { return Some(Parameters::todo()); diff --git a/crates/ty_python_semantic/src/types/instance.rs b/crates/ty_python_semantic/src/types/instance.rs index 825b767b55..32b7d5bb93 100644 --- a/crates/ty_python_semantic/src/types/instance.rs +++ b/crates/ty_python_semantic/src/types/instance.rs @@ -13,8 +13,8 @@ use crate::types::protocol_class::walk_protocol_interface; use crate::types::tuple::{TupleSpec, TupleType}; use crate::types::{ ApplyTypeMappingVisitor, ClassBase, HasRelationToVisitor, IsDisjointVisitor, - IsEquivalentVisitor, MaterializationKind, NormalizedVisitor, TypeMapping, TypeRelation, - VarianceInferable, + IsEquivalentVisitor, MaterializationKind, NewTypeInstance, NormalizedVisitor, TypeMapping, + TypeRelation, VarianceInferable, }; use crate::{Db, FxOrderSet}; @@ -41,6 +41,10 @@ impl<'db> Type<'db> { } } + pub(crate) fn newtype_nominal_instance(newtype: NewTypeInstance<'db>) -> Self { + Type::NominalInstance(NominalInstanceType(NominalInstanceInner::NewType(newtype))) + } + pub(crate) fn tuple(tuple: Option>) -> Self { let Some(tuple) = tuple else { return Type::Never; @@ -75,7 +79,7 @@ impl<'db> Type<'db> { /// **Private** helper function to create a `Type::NominalInstance` from a class that /// is known not to be `Any`, a protocol class, or a typed dict class. fn non_tuple_instance(class: ClassType<'db>) -> Self { - Type::NominalInstance(NominalInstanceType(NominalInstanceInner::NonTuple(class))) + Type::NominalInstance(NominalInstanceType(NominalInstanceInner::Class(class))) } pub(crate) const fn into_nominal_instance(self) -> Option> { @@ -130,56 +134,75 @@ pub(super) fn walk_nominal_instance_type<'db, V: super::visitor::TypeVisitor<'db nominal: NominalInstanceType<'db>, visitor: &V, ) { - visitor.visit_type(db, nominal.class(db).into()); + visitor.visit_type(db, nominal.class_ignoring_newtype(db).into()); } impl<'db> NominalInstanceType<'db> { - pub(super) fn class(&self, db: &'db dyn Db) -> ClassType<'db> { + pub(super) fn class_and_newtype( + &self, + db: &'db dyn Db, + ) -> (ClassType<'db>, Option>) { match self.0 { - NominalInstanceInner::ExactTuple(tuple) => tuple.to_class_type(db), - NominalInstanceInner::NonTuple(class) => class, + NominalInstanceInner::ExactTuple(tuple) => (tuple.to_class_type(db), None), + NominalInstanceInner::NewType(newtype) => (newtype.base_class_type(db), Some(newtype)), + NominalInstanceInner::Class(class) => (class, None), } } + pub(super) fn class_ignoring_newtype(&self, db: &'db dyn Db) -> ClassType<'db> { + self.class_and_newtype(db).0 + } + + pub(super) fn class_if_not_newtype(&self, db: &'db dyn Db) -> Option> { + match self.0 { + NominalInstanceInner::NewType(_) => None, + _ => Some(self.class_ignoring_newtype(db)), + } + } + + pub(super) fn is_newtype(&self) -> bool { + matches!(self.0, NominalInstanceInner::NewType(_)) + } + /// If this is an instance type where the class has a tuple spec, returns the tuple spec. /// /// I.e., for the type `tuple[int, str]`, this will return the tuple spec `[int, str]`. /// For a subclass of `tuple[int, str]`, it will return the same tuple spec. pub(super) fn tuple_spec(&self, db: &'db dyn Db) -> Option>> { + let from_class_type = |class: ClassType<'db>| { + // Avoid an expensive MRO traversal for common stdlib classes. + if class + .known(db) + .is_some_and(|known_class| !known_class.is_tuple_subclass()) + { + return None; + } + class + .iter_mro(db) + .filter_map(ClassBase::into_class) + .find_map(|class| match class.known(db)? { + // N.B. this is a pure optimisation: iterating through the MRO would give us + // the correct tuple spec for `sys._version_info`, since we special-case the class + // in `ClassLiteral::explicit_bases()` so that it is inferred as inheriting from + // a tuple type with the correct spec for the user's configured Python version and platform. + KnownClass::VersionInfo => Some(Cow::Owned(TupleSpec::version_info_spec(db))), + KnownClass::Tuple => Some( + class + .into_generic_alias() + .and_then(|alias| { + Some(Cow::Borrowed(alias.specialization(db).tuple(db)?)) + }) + .unwrap_or_else(|| Cow::Owned(TupleSpec::homogeneous(Type::unknown()))), + ), + _ => None, + }) + }; match self.0 { NominalInstanceInner::ExactTuple(tuple) => Some(Cow::Borrowed(tuple.tuple(db))), - NominalInstanceInner::NonTuple(class) => { - // Avoid an expensive MRO traversal for common stdlib classes. - if class - .known(db) - .is_some_and(|known_class| !known_class.is_tuple_subclass()) - { - return None; - } - class - .iter_mro(db) - .filter_map(ClassBase::into_class) - .find_map(|class| match class.known(db)? { - // N.B. this is a pure optimisation: iterating through the MRO would give us - // the correct tuple spec for `sys._version_info`, since we special-case the class - // in `ClassLiteral::explicit_bases()` so that it is inferred as inheriting from - // a tuple type with the correct spec for the user's configured Python version and platform. - KnownClass::VersionInfo => { - Some(Cow::Owned(TupleSpec::version_info_spec(db))) - } - KnownClass::Tuple => Some( - class - .into_generic_alias() - .and_then(|alias| { - Some(Cow::Borrowed(alias.specialization(db).tuple(db)?)) - }) - .unwrap_or_else(|| { - Cow::Owned(TupleSpec::homogeneous(Type::unknown())) - }), - ), - _ => None, - }) + NominalInstanceInner::NewType(declaration) => { + from_class_type(declaration.base_class_type(db)) } + NominalInstanceInner::Class(class) => from_class_type(class), } } @@ -187,7 +210,10 @@ impl<'db> NominalInstanceType<'db> { pub(super) fn is_object(self, db: &'db dyn Db) -> bool { match self.0 { NominalInstanceInner::ExactTuple(_) => false, - NominalInstanceInner::NonTuple(class) => class.is_object(db), + NominalInstanceInner::NewType(declaration) => { + declaration.base_class_type(db).is_object(db) + } + NominalInstanceInner::Class(class) => class.is_object(db), } } @@ -204,7 +230,8 @@ impl<'db> NominalInstanceType<'db> { pub(super) fn own_tuple_spec(&self, db: &'db dyn Db) -> Option>> { match self.0 { NominalInstanceInner::ExactTuple(tuple) => Some(Cow::Borrowed(tuple.tuple(db))), - NominalInstanceInner::NonTuple(_) => None, + NominalInstanceInner::NewType(_) => None, + NominalInstanceInner::Class(_) => None, } } @@ -216,7 +243,8 @@ impl<'db> NominalInstanceType<'db> { pub(crate) fn slice_literal(self, db: &'db dyn Db) -> Option { let class = match self.0 { NominalInstanceInner::ExactTuple(_) => return None, - NominalInstanceInner::NonTuple(class) => class, + NominalInstanceInner::NewType(_) => return None, + NominalInstanceInner::Class(class) => class, }; let (class, Some(specialization)) = class.class_literal(db) else { return None; @@ -232,7 +260,9 @@ impl<'db> NominalInstanceType<'db> { Type::IntLiteral(n) => i32::try_from(*n).map(Some).ok(), Type::BooleanLiteral(b) => Some(Some(i32::from(*b))), Type::NominalInstance(instance) - if instance.class(db).is_known(db, KnownClass::NoneType) => + if instance + .class_ignoring_newtype(db) + .is_known(db, KnownClass::NoneType) => { Some(None) } @@ -254,7 +284,10 @@ impl<'db> NominalInstanceType<'db> { NominalInstanceInner::ExactTuple(tuple) => { Type::tuple(tuple.normalized_impl(db, visitor)) } - NominalInstanceInner::NonTuple(class) => { + NominalInstanceInner::NewType(newtype) => { + Type::newtype_nominal_instance(newtype.normalized_impl(db, visitor)) + } + NominalInstanceInner::Class(class) => { Type::non_tuple_instance(class.normalized_impl(db, visitor)) } } @@ -269,7 +302,10 @@ impl<'db> NominalInstanceType<'db> { NominalInstanceInner::ExactTuple(tuple) => { Type::tuple(tuple.materialize(db, materialization_kind)) } - NominalInstanceInner::NonTuple(class) => { + NominalInstanceInner::NewType(newtype) => { + Type::newtype_nominal_instance(newtype.materialize(db, materialization_kind)) + } + NominalInstanceInner::Class(class) => { Type::non_tuple_instance(class.materialize(db, materialization_kind)) } } @@ -283,13 +319,25 @@ impl<'db> NominalInstanceType<'db> { visitor: &HasRelationToVisitor<'db, C>, ) -> C { match (self.0, other.0) { + (NominalInstanceInner::NewType(newtype1), NominalInstanceInner::NewType(newtype2)) => { + newtype1.has_relation_to_impl(db, newtype2, relation, visitor) + } + // A non-NewType is never a subtype of a NewType. + (_, NominalInstanceInner::NewType(_)) => C::unsatisfiable(db), + (NominalInstanceInner::NewType(newtype), _) => { + Self(NominalInstanceInner::Class(newtype.base_class_type(db))) + .has_relation_to_impl(db, other, relation, visitor) + } ( NominalInstanceInner::ExactTuple(tuple1), NominalInstanceInner::ExactTuple(tuple2), ) => tuple1.has_relation_to_impl(db, tuple2, relation, visitor), - _ => self - .class(db) - .has_relation_to_impl(db, other.class(db), relation, visitor), + _ => self.class_ignoring_newtype(db).has_relation_to_impl( + db, + other.class_ignoring_newtype(db), + relation, + visitor, + ), } } @@ -304,7 +352,7 @@ impl<'db> NominalInstanceType<'db> { NominalInstanceInner::ExactTuple(tuple1), NominalInstanceInner::ExactTuple(tuple2), ) => tuple1.is_equivalent_to_impl(db, tuple2, visitor), - (NominalInstanceInner::NonTuple(class1), NominalInstanceInner::NonTuple(class2)) => { + (NominalInstanceInner::Class(class1), NominalInstanceInner::Class(class2)) => { class1.is_equivalent_to_impl(db, class2, visitor) } _ => C::unsatisfiable(db), @@ -327,10 +375,14 @@ impl<'db> NominalInstanceType<'db> { } } result.or(db, || { - C::from_bool( - db, - !(self.class(db)).could_coexist_in_mro_with(db, other.class(db)), - ) + let (self_class, self_newtype) = self.class_and_newtype(db); + let (other_class, other_newtype) = other.class_and_newtype(db); + match (self_newtype, other_newtype) { + (Some(self_newtype), Some(other_newtype)) => { + self_newtype.is_disjoint_from_impl(db, other_newtype, visitor) + } + _ => C::from_bool(db, !self_class.could_coexist_in_mro_with(db, other_class)), + } }) } @@ -342,7 +394,8 @@ impl<'db> NominalInstanceType<'db> { // See: // https://docs.python.org/3/reference/expressions.html#parenthesized-forms NominalInstanceInner::ExactTuple(_) => false, - NominalInstanceInner::NonTuple(class) => class + NominalInstanceInner::NewType(_) => false, + NominalInstanceInner::Class(class) => class .known(db) .map(KnownClass::is_singleton) .unwrap_or_else(|| is_single_member_enum(db, class.class_literal(db).0)), @@ -350,18 +403,24 @@ impl<'db> NominalInstanceType<'db> { } pub(super) fn is_single_valued(self, db: &'db dyn Db) -> bool { - match self.0 { - NominalInstanceInner::ExactTuple(tuple) => tuple.is_single_valued(db), - NominalInstanceInner::NonTuple(class) => class + let class_type_is_single_valued = |class: ClassType<'db>| { + class .known(db) .and_then(KnownClass::is_single_valued) .or_else(|| Some(self.tuple_spec(db)?.is_single_valued(db))) - .unwrap_or_else(|| is_single_member_enum(db, class.class_literal(db).0)), + .unwrap_or_else(|| is_single_member_enum(db, class.class_literal(db).0)) + }; + match self.0 { + NominalInstanceInner::ExactTuple(tuple) => tuple.is_single_valued(db), + NominalInstanceInner::NewType(newtype) => { + class_type_is_single_valued(newtype.base_class_type(db)) + } + NominalInstanceInner::Class(class) => class_type_is_single_valued(class), } } pub(super) fn to_meta_type(self, db: &'db dyn Db) -> Type<'db> { - SubclassOfType::from(db, self.class(db)) + SubclassOfType::from(db, self.class_ignoring_newtype(db)) } pub(super) fn apply_type_mapping_impl<'a>( @@ -374,7 +433,10 @@ impl<'db> NominalInstanceType<'db> { NominalInstanceInner::ExactTuple(tuple) => { Type::tuple(tuple.apply_type_mapping_impl(db, type_mapping, visitor)) } - NominalInstanceInner::NonTuple(class) => { + NominalInstanceInner::NewType(newtype) => Type::newtype_nominal_instance( + newtype.apply_type_mapping_impl(db, type_mapping, visitor), + ), + NominalInstanceInner::Class(class) => { Type::non_tuple_instance(class.apply_type_mapping_impl(db, type_mapping, visitor)) } } @@ -390,7 +452,12 @@ impl<'db> NominalInstanceType<'db> { NominalInstanceInner::ExactTuple(tuple) => { tuple.find_legacy_typevars(db, binding_context, typevars); } - NominalInstanceInner::NonTuple(class) => { + NominalInstanceInner::NewType(newtype) => { + newtype + .base_class_type(db) + .find_legacy_typevars(db, binding_context, typevars); + } + NominalInstanceInner::Class(class) => { class.find_legacy_typevars(db, binding_context, typevars); } } @@ -413,12 +480,19 @@ enum NominalInstanceInner<'db> { /// Note that the type `tuple[int, str]` includes subtypes of `tuple[int, str]`, /// but those subtypes would be represented using the `NonTuple` variant. ExactTuple(TupleType<'db>), + + /// A `NewType` instance type, e.g. `NewType("Foo", int)`. + /// + /// These instances are the return values of the return values of (sic) `typing.NewType`. See + /// `NewTypeInstance` for an example. + NewType(NewTypeInstance<'db>), + /// Any instance type that does not represent some kind of instance of the /// builtin `tuple` class. /// /// This variant includes types that are subtypes of "exact tuple" types, /// because they represent "all instances of a class that is a tuple subclass". - NonTuple(ClassType<'db>), + Class(ClassType<'db>), } pub(crate) struct SliceLiteral { @@ -429,7 +503,7 @@ pub(crate) struct SliceLiteral { impl<'db> VarianceInferable<'db> for NominalInstanceType<'db> { fn variance_of(self, db: &'db dyn Db, typevar: BoundTypeVarInstance<'db>) -> TypeVarVariance { - self.class(db).variance_of(db, typevar) + self.class_ignoring_newtype(db).variance_of(db, typevar) } } diff --git a/crates/ty_python_semantic/src/types/narrow.rs b/crates/ty_python_semantic/src/types/narrow.rs index 46a0b5a8f5..2fc871c86d 100644 --- a/crates/ty_python_semantic/src/types/narrow.rs +++ b/crates/ty_python_semantic/src/types/narrow.rs @@ -554,7 +554,9 @@ impl<'db, 'ast> NarrowingConstraintsBuilder<'db, 'ast> { } // Treat `bool` as `Literal[True, False]`. Type::NominalInstance(instance) - if instance.class(db).is_known(db, KnownClass::Bool) => + if instance + .class_if_not_newtype(db) + .is_some_and(|class| class.is_known(db, KnownClass::Bool)) => { UnionType::from_elements( db, @@ -565,13 +567,22 @@ impl<'db, 'ast> NarrowingConstraintsBuilder<'db, 'ast> { } // Treat enums as a union of their members. Type::NominalInstance(instance) - if enum_metadata(db, instance.class(db).class_literal(db).0).is_some() => + if !instance.is_newtype() + && enum_metadata( + db, + instance.class_ignoring_newtype(db).class_literal(db).0, + ) + .is_some() => { UnionType::from_elements( db, - enum_member_literals(db, instance.class(db).class_literal(db).0, None) - .expect("Calling `enum_member_literals` on an enum class") - .map(|ty| filter_to_cannot_be_equal(db, ty, rhs_ty)), + enum_member_literals( + db, + instance.class_ignoring_newtype(db).class_literal(db).0, + None, + ) + .expect("Calling `enum_member_literals` on an enum class") + .map(|ty| filter_to_cannot_be_equal(db, ty, rhs_ty)), ) } _ => { @@ -596,7 +607,9 @@ impl<'db, 'ast> NarrowingConstraintsBuilder<'db, 'ast> { fn evaluate_expr_ne(&mut self, lhs_ty: Type<'db>, rhs_ty: Type<'db>) -> Option> { match (lhs_ty, rhs_ty) { (Type::NominalInstance(instance), Type::IntLiteral(i)) - if instance.class(self.db).is_known(self.db, KnownClass::Bool) => + if instance + .class_if_not_newtype(self.db) + .is_some_and(|class| class.is_known(self.db, KnownClass::Bool)) => { if i == 0 { Some(Type::BooleanLiteral(false).negate(self.db)) diff --git a/crates/ty_python_semantic/src/types/property_tests/type_generation.rs b/crates/ty_python_semantic/src/types/property_tests/type_generation.rs index 0a296088ff..ce1853793f 100644 --- a/crates/ty_python_semantic/src/types/property_tests/type_generation.rs +++ b/crates/ty_python_semantic/src/types/property_tests/type_generation.rs @@ -153,7 +153,7 @@ impl Ty { .place .expect_type(); debug_assert!( - matches!(ty, Type::NominalInstance(instance) if is_single_member_enum(db, instance.class(db).class_literal(db).0)) + matches!(ty, Type::NominalInstance(instance) if is_single_member_enum(db, instance.class_ignoring_newtype(db).class_literal(db).0)) ); ty } diff --git a/crates/ty_python_semantic/src/types/type_ordering.rs b/crates/ty_python_semantic/src/types/type_ordering.rs index d1262802e9..c482907194 100644 --- a/crates/ty_python_semantic/src/types/type_ordering.rs +++ b/crates/ty_python_semantic/src/types/type_ordering.rs @@ -131,7 +131,15 @@ pub(super) fn union_or_intersection_elements_ordering<'db>( (_, Type::TypeIs(_)) => Ordering::Greater, (Type::NominalInstance(left), Type::NominalInstance(right)) => { - left.class(db).cmp(&right.class(db)) + let (left_class, left_newtype) = left.class_and_newtype(db); + let (right_class, right_newtype) = right.class_and_newtype(db); + // NewTypes order after regular classes. + match (left_newtype, right_newtype) { + (None, None) => left_class.cmp(&right_class), + (Some(_), None) => Ordering::Greater, + (None, Some(_)) => Ordering::Less, + (Some(left_newtype), Some(right_newtype)) => left_newtype.cmp(&right_newtype), + } } (Type::NominalInstance(_), _) => Ordering::Less, (_, Type::NominalInstance(_)) => Ordering::Greater, @@ -180,7 +188,17 @@ pub(super) fn union_or_intersection_elements_ordering<'db>( (SuperOwnerKind::Class(_), _) => Ordering::Less, (_, SuperOwnerKind::Class(_)) => Ordering::Greater, (SuperOwnerKind::Instance(left), SuperOwnerKind::Instance(right)) => { - left.class(db).cmp(&right.class(db)) + let (left_class, left_newtype) = left.class_and_newtype(db); + let (right_class, right_newtype) = right.class_and_newtype(db); + // NewTypes order after regular classes. + match (left_newtype, right_newtype) { + (None, None) => left_class.cmp(&right_class), + (Some(_), None) => Ordering::Greater, + (None, Some(_)) => Ordering::Less, + (Some(left_newtype), Some(right_newtype)) => { + left_newtype.cmp(&right_newtype) + } + } } (SuperOwnerKind::Instance(_), _) => Ordering::Less, (_, SuperOwnerKind::Instance(_)) => Ordering::Greater, diff --git a/ty.schema.json b/ty.schema.json index a9261dfefb..2b08f50097 100644 --- a/ty.schema.json +++ b/ty.schema.json @@ -581,6 +581,16 @@ } ] }, + "invalid-newtype": { + "title": "detects invalid NewType definitions", + "description": "## What it does\nChecks for the creation of invalid `NewType`s\n\n## Why is this bad?\nThere are several requirements that you must follow when creating a `NewType`.\n\n## Examples\n```python\nfrom typing import NewType\n\nFoo = NewType(\"Foo\", int) # okay\nBar = NewType(get_name(), int) # error: NewType name must be a string literal\n```", + "default": "error", + "oneOf": [ + { + "$ref": "#/definitions/Level" + } + ] + }, "invalid-overload": { "title": "detects invalid `@overload` usages", "description": "## What it does\nChecks for various invalid `@overload` usages.\n\n## Why is this bad?\nThe `@overload` decorator is used to define functions and methods that accepts different\ncombinations of arguments and return different types based on the arguments passed. This is\nmainly beneficial for type checkers. But, if the `@overload` usage is invalid, the type\nchecker may not be able to provide correct type information.\n\n## Example\n\nDefining only one overload:\n\n```py\nfrom typing import overload\n\n@overload\ndef foo(x: int) -> int: ...\ndef foo(x: int | None) -> int | None:\n return x\n```\n\nOr, not providing an implementation for the overloaded definition:\n\n```py\nfrom typing import overload\n\n@overload\ndef foo() -> None: ...\n@overload\ndef foo(x: int) -> int: ...\n```\n\n## References\n- [Python documentation: `@overload`](https://docs.python.org/3/library/typing.html#typing.overload)",