diff --git a/crates/ty/docs/rules.md b/crates/ty/docs/rules.md index 3c1938378cd176..302cbb038fb05d 100644 --- a/crates/ty/docs/rules.md +++ b/crates/ty/docs/rules.md @@ -8,7 +8,7 @@ Default level: error · Added in 0.0.13 · Related issues · -View source +View source @@ -49,7 +49,7 @@ class Derived(Base): # Error: `Derived` does not implement `method` Default level: warn · Added in 0.0.1-alpha.20 · Related issues · -View source +View source @@ -90,7 +90,7 @@ class SubProto(BaseProto, Protocol): Default level: error · Added in 0.0.14 · Related issues · -View source +View source @@ -126,7 +126,7 @@ def _(x: int): Default level: error · Preview (since 0.0.16) · Related issues · -View source +View source @@ -175,7 +175,7 @@ Foo.method() # Error: cannot call abstract classmethod Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -199,7 +199,7 @@ Calling a non-callable object will raise a `TypeError` at runtime. Default level: error · Added in 0.0.7 · Related issues · -View source +View source @@ -230,7 +230,7 @@ def f(x: object): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -262,7 +262,7 @@ f(int) # error Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -293,7 +293,7 @@ a = 1 Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -325,7 +325,7 @@ class C(A, B): ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -357,7 +357,7 @@ class B(A): ... Default level: error · Added in 0.0.1-alpha.29 · Related issues · -View source +View source @@ -385,7 +385,7 @@ type B = A Default level: error · Added in 0.0.15 · Related issues · -View source +View source @@ -417,7 +417,7 @@ class Example: Default level: warn · Added in 0.0.1-alpha.16 · Related issues · -View source +View source @@ -444,7 +444,7 @@ old_func() # emits [deprecated] diagnostic Default level: ignore · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -473,7 +473,7 @@ false positives it can produce. Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -500,7 +500,7 @@ class B(A, A): ... Default level: error · Added in 0.0.1-alpha.12 · Related issues · -View source +View source @@ -538,7 +538,7 @@ class A: # Crash at runtime Default level: error · Added in 0.0.14 · Related issues · -View source +View source @@ -609,7 +609,7 @@ def foo() -> "intt\b": ... Default level: error · Added in 0.0.20 · Related issues · -View source +View source @@ -641,7 +641,7 @@ def my_function() -> int: Default level: error · Added in 0.0.15 · Related issues · -View source +View source @@ -736,7 +736,7 @@ def test(): -> "Literal[5]": Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -766,7 +766,7 @@ class C(A, B): ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -792,7 +792,7 @@ t[3] # IndexError: tuple index out of range Default level: warn · Added in 0.0.1-alpha.33 · Related issues · -View source +View source @@ -826,7 +826,7 @@ class MyClass: ... Default level: error · Added in 0.0.1-alpha.12 · Related issues · -View source +View source @@ -915,7 +915,7 @@ an atypical memory layout. Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -942,7 +942,7 @@ func("foo") # error: [invalid-argument-type] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -970,7 +970,7 @@ a: int = '' Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1004,7 +1004,7 @@ C.instance_var = 3 # error: Cannot assign to instance variable Default level: error · Added in 0.0.1-alpha.19 · Related issues · -View source +View source @@ -1040,7 +1040,7 @@ asyncio.run(main()) Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1064,7 +1064,7 @@ class A(42): ... # error: [invalid-base] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1091,7 +1091,7 @@ with 1: Default level: error · Added in 0.0.12 · Related issues · -View source +View source @@ -1128,7 +1128,7 @@ class Foo(NamedTuple): Default level: error · Added in 0.0.13 · Related issues · -View source +View source @@ -1160,7 +1160,7 @@ class A: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1189,7 +1189,7 @@ a: str Default level: warn · Added in 0.0.20 · Related issues · -View source +View source @@ -1238,7 +1238,7 @@ class Pet(Enum): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1282,7 +1282,7 @@ except ZeroDivisionError: Default level: error · Added in 0.0.1-alpha.28 · Related issues · -View source +View source @@ -1324,7 +1324,7 @@ class D(A): Default level: error · Added in 0.0.1-alpha.35 · Related issues · -View source +View source @@ -1368,7 +1368,7 @@ class NonFrozenChild(FrozenBase): # Error raised here Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1406,7 +1406,7 @@ class D(Generic[U, T]): ... Default level: error · Added in 0.0.12 · Related issues · -View source +View source @@ -1485,7 +1485,7 @@ a = 20 / 0 # type: ignore Default level: error · Added in 0.0.1-alpha.17 · Related issues · -View source +View source @@ -1524,7 +1524,7 @@ carol = Person(name="Carol", aeg=25) # typo! Default level: warn · Added in 0.0.15 · Related issues · -View source +View source @@ -1585,7 +1585,7 @@ def f(x, y, /): # Python 3.8+ syntax Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1620,7 +1620,7 @@ def f(t: TypeVar("U")): ... Default level: error · Added in 0.0.18 · Related issues · -View source +View source @@ -1648,7 +1648,7 @@ match x: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1682,7 +1682,7 @@ class B(metaclass=f): ... Default level: error · Added in 0.0.1-alpha.20 · Related issues · -View source +View source @@ -1789,7 +1789,7 @@ Correct use of `@override` is enforced by ty's [`invalid-explicit-override`](#in Default level: error · Added in 0.0.1-alpha.19 · Related issues · -View source +View source @@ -1843,7 +1843,7 @@ AttributeError: Cannot overwrite NamedTuple attribute _asdict Default level: error · Added in 0.0.1-alpha.27 · Related issues · -View source +View source @@ -1873,7 +1873,7 @@ Baz = NewType("Baz", int | str) # error: invalid base for `typing.NewType` Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1923,7 +1923,7 @@ def foo(x: int) -> int: ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1949,7 +1949,7 @@ def f(a: int = ''): ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1980,7 +1980,7 @@ P2 = ParamSpec("S2") # error: ParamSpec name must match the variable it's assig Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2014,7 +2014,7 @@ TypeError: Protocols can only inherit from other protocols, got Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2063,7 +2063,7 @@ def g(): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2092,7 +2092,7 @@ def func() -> int: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2188,7 +2188,7 @@ class C: ... Default level: error · Added in 0.0.10 · Related issues · -View source +View source @@ -2228,13 +2228,42 @@ class MyClass: return True ``` +## `invalid-type-alias` + + +Default level: error · +Added in 0.0.28 · +Related issues · +View source + + + +**What it does** + +Checks for invalid PEP 695 `type` statements. + +**Why is this bad?** + +A `type` statement is only valid in module and class scopes, and a type alias name should +not be redeclared in the same scope. + +**Examples** + +```python +type Alias = int +type Alias = str # error: type alias already defined + +def f(): + type Local = int # error: type statements are not allowed in function scopes +``` + ## `invalid-type-alias-type` Default level: error · Added in 0.0.1-alpha.6 · Related issues · -View source +View source @@ -2261,7 +2290,7 @@ NewAlias = TypeAliasType(get_name(), int) # error: TypeAliasType name mus Default level: error · Added in 0.0.1-alpha.29 · Related issues · -View source +View source @@ -2308,7 +2337,7 @@ Bar[int] # error: too few arguments Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2338,7 +2367,7 @@ TYPE_CHECKING = '' Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2368,7 +2397,7 @@ b: Annotated[int] # `Annotated` expects at least two arguments Default level: error · Added in 0.0.1-alpha.11 · Related issues · -View source +View source @@ -2402,7 +2431,7 @@ f(10) # Error Default level: error · Added in 0.0.1-alpha.11 · Related issues · -View source +View source @@ -2436,7 +2465,7 @@ class C: Default level: error · Added in 0.0.15 · Related issues · -View source +View source @@ -2467,7 +2496,7 @@ def g[U, T: U](): ... # error: [invalid-type-variable-bound] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2514,7 +2543,7 @@ U = TypeVar('U', list[int], int) # valid constrained Type Default level: error · Added in 0.0.16 · Related issues · -View source +View source @@ -2546,7 +2575,7 @@ U = TypeVar("U", int, str, default=bytes) # error: [invalid-type-variable-defau Default level: error · Added in 0.0.28 · Related issues · -View source +View source @@ -2577,7 +2606,7 @@ class Child(Base): Default level: error · Added in 0.0.14 · Related issues · -View source +View source @@ -2612,7 +2641,7 @@ def f(x: dict): Default level: error · Added in 0.0.9 · Related issues · -View source +View source @@ -2643,7 +2672,7 @@ class Foo(TypedDict): Default level: error · Added in 0.0.25 · Related issues · -View source +View source @@ -2674,7 +2703,7 @@ def gen() -> Iterator[int]: Default level: error · Added in 0.0.14 · Related issues · -View source +View source @@ -2729,7 +2758,7 @@ def h(arg2: type): Default level: error · Added in 0.0.15 · Related issues · -View source +View source @@ -2772,7 +2801,7 @@ def g(arg: object): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2797,7 +2826,7 @@ func() # TypeError: func() missing 1 required positional argument: 'x' Default level: error · Added in 0.0.1-alpha.20 · Related issues · -View source +View source @@ -2830,7 +2859,7 @@ alice["age"] # KeyError Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2859,7 +2888,7 @@ func("string") # error: [no-matching-overload] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2885,7 +2914,7 @@ for i in 34: # TypeError: 'int' object is not iterable Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2909,7 +2938,7 @@ Subscripting an object that does not support it will raise a `TypeError` at runt Default level: error · Added in 0.0.1-alpha.29 · Related issues · -View source +View source @@ -2942,7 +2971,7 @@ class B(A): Default level: error · Added in 0.0.16 · Related issues · -View source +View source @@ -2975,7 +3004,7 @@ class B(A): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3002,7 +3031,7 @@ f(1, x=2) # Error raised here Default level: error · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -3029,7 +3058,7 @@ f(x=1) # Error raised here Default level: ignore · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -3062,7 +3091,7 @@ A.c # AttributeError: type object 'A' has no attribute 'c' Default level: warn · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -3094,7 +3123,7 @@ A()[0] # TypeError: 'A' object is not subscriptable Default level: ignore · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -3131,7 +3160,7 @@ from module import a # ImportError: cannot import name 'a' from 'module' Default level: warn · Added in 0.0.23 · Related issues · -View source +View source @@ -3158,7 +3187,7 @@ html.parser # AttributeError: module 'html' has no attribute 'parser' Default level: ignore · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3222,7 +3251,7 @@ def test(): -> "int": Default level: warn · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3249,7 +3278,7 @@ cast(int, f()) # Redundant Default level: warn · Added in 0.0.18 · Related issues · -View source +View source @@ -3281,7 +3310,7 @@ class C: Default level: error · Added in 0.0.20 · Related issues · -View source +View source @@ -3315,7 +3344,7 @@ class Outer[T]: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3345,7 +3374,7 @@ static_assert(int(2.0 * 3.0) == 6) # error: does not have a statically known tr Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3374,7 +3403,7 @@ class B(A): ... # Error raised here Default level: error · Added in 0.0.1-alpha.30 · Related issues · -View source +View source @@ -3408,7 +3437,7 @@ class F(NamedTuple): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3435,7 +3464,7 @@ f("foo") # Error raised here Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3463,7 +3492,7 @@ def _(x: int): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3509,7 +3538,7 @@ class A: Default level: error · Added in 0.0.20 · Related issues · -View source +View source @@ -3546,7 +3575,7 @@ class C(Generic[T]): Default level: warn · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3570,7 +3599,7 @@ reveal_type(1) # NameError: name 'reveal_type' is not defined Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3597,7 +3626,7 @@ f(x=1, y=2) # Error raised here Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3625,7 +3654,7 @@ A().foo # AttributeError: 'A' object has no attribute 'foo' Default level: warn · Added in 0.0.1-alpha.15 · Related issues · -View source +View source @@ -3683,7 +3712,7 @@ def g(): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3708,7 +3737,7 @@ import foo # ModuleNotFoundError: No module named 'foo' Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3733,7 +3762,7 @@ print(x) # NameError: name 'x' is not defined Default level: warn · Added in 0.0.1-alpha.7 · Related issues · -View source +View source @@ -3772,7 +3801,7 @@ class D(C): ... # error: [unsupported-base] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3809,7 +3838,7 @@ b1 < b2 < b1 # exception raised here Default level: ignore · Added in 0.0.12 · Related issues · -View source +View source @@ -3849,7 +3878,7 @@ def factory(base: type[Base]) -> type: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3877,7 +3906,7 @@ A() + A() # TypeError: unsupported operand type(s) for +: 'A' and 'A' Default level: warn · Preview (since 0.0.21) · Related issues · -View source +View source @@ -3983,7 +4012,7 @@ to `false`. Default level: warn · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -4046,7 +4075,7 @@ def foo(x: int | str) -> int | str: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source diff --git a/crates/ty_python_semantic/resources/mdtest/class/super.md b/crates/ty_python_semantic/resources/mdtest/class/super.md index e449c46907becb..7a359d78ec0468 100644 --- a/crates/ty_python_semantic/resources/mdtest/class/super.md +++ b/crates/ty_python_semantic/resources/mdtest/class/super.md @@ -579,6 +579,7 @@ def f(x: int): # error: [invalid-super-argument] "`int` is not a valid class" super(x, x) + # error: [invalid-type-alias] "`type` statements are not allowed in function scopes" type IntAlias = int # error: [invalid-super-argument] "`TypeAliasType` is not a valid class" super(IntAlias, 0) diff --git a/crates/ty_python_semantic/resources/mdtest/diagnostics/semantic_syntax_errors.md b/crates/ty_python_semantic/resources/mdtest/diagnostics/semantic_syntax_errors.md index 1941be3e9bf331..44e8580da7827d 100644 --- a/crates/ty_python_semantic/resources/mdtest/diagnostics/semantic_syntax_errors.md +++ b/crates/ty_python_semantic/resources/mdtest/diagnostics/semantic_syntax_errors.md @@ -321,11 +321,13 @@ python-version = "3.12" ```py def _(): + # error: [invalid-type-alias] "`type` statements are not allowed in function scopes" # error: [invalid-type-form] "`yield` expressions are not allowed in type expressions" # error: [invalid-syntax] "yield expression cannot be used within a TypeVar bound" type X[T: (yield 1)] = int def _(): + # error: [invalid-type-alias] "`type` statements are not allowed in function scopes" # error: [invalid-type-form] "`yield` expressions are not allowed in type alias values" # error: [invalid-syntax] "yield expression cannot be used within a type alias" type Y = (yield 1) diff --git a/crates/ty_python_semantic/resources/mdtest/generics/pep695/aliases.md b/crates/ty_python_semantic/resources/mdtest/generics/pep695/aliases.md index 396181bfc4db5a..3798b5d4dbff3d 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/pep695/aliases.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/aliases.md @@ -251,9 +251,9 @@ def _(g: G): Unless a type default was provided: ```py -type G[T = int] = list[T] +type GWithDefault[T = int] = list[T] -def _(g: G): +def _(g: GWithDefault): reveal_type(g) # revealed: list[int] ``` @@ -268,9 +268,9 @@ A self-referential default that does not reference itself in the alias body shou even when the default is evaluated (e.g., by omitting the type argument): ```py -type B[T = B] = list[T] +type SelfDefaultB[T = SelfDefaultB] = list[T] -def _(x: B) -> None: +def _(x: SelfDefaultB) -> None: pass ``` @@ -427,12 +427,12 @@ reveal_type(get_value(d, "a")) # revealed: int It also works in the reverse direction, where the type alias is used as the argument type: ```py -type MyList[T] = list[T] +type MyListAlias[T] = list[T] def head[T](l: list[T]) -> T: return l[0] -def _(x: MyList[int]): +def _(x: MyListAlias[int]): reveal_type(head(x)) # revealed: int ``` diff --git a/crates/ty_python_semantic/resources/mdtest/pep695_type_aliases.md b/crates/ty_python_semantic/resources/mdtest/pep695_type_aliases.md index 35967424387674..59e1886e26f360 100644 --- a/crates/ty_python_semantic/resources/mdtest/pep695_type_aliases.md +++ b/crates/ty_python_semantic/resources/mdtest/pep695_type_aliases.md @@ -123,12 +123,45 @@ class Foo: But narrowing of names used in the type alias is still respected: ```py -def _(flag: bool): - t = int if flag else None - if t is not None: - type Alias = t | str - def f(x: Alias): - reveal_type(x) # revealed: int | str +flag = True +t = int if flag else None +if t is not None: + type NarrowedAlias = t | str + +def f(x: NarrowedAlias): + reveal_type(x) # revealed: int | str +``` + +`type` statements are only allowed in module and class scopes: + +```py +class C: + type Alias = int + +def _(): + # error: [invalid-type-alias] "`type` statements are not allowed in function scopes" + type Local = int +``` + +## Redeclared aliases + + + +```py +type Redeclared = int +# error: [invalid-type-alias] "Type alias `Redeclared` is already defined in this scope" +type Redeclared = str +``` + +```py +from random import random + +flag = random() > 0.5 +if flag: + type BranchRedeclared = int +else: + # error: [invalid-type-alias] "Type alias `BranchRedeclared` is already defined in this scope" + type BranchRedeclared = str ``` ## Generic type aliases diff --git a/crates/ty_python_semantic/resources/mdtest/promotion.md b/crates/ty_python_semantic/resources/mdtest/promotion.md index 5de65428207cd8..343373721cf0fe 100644 --- a/crates/ty_python_semantic/resources/mdtest/promotion.md +++ b/crates/ty_python_semantic/resources/mdtest/promotion.md @@ -511,9 +511,9 @@ def _(flag: bool): reveal_type(promotable4 or unpromotable4) # revealed: Literal[True] reveal_type([promotable4 or unpromotable4]) # revealed: list[Literal[True]] -type X = Literal[b"bar"] +type XBytes = Literal[b"bar"] -def _(x1: X | None, x2: X): +def _(x1: XBytes | None, x2: XBytes): reveal_type([x1, x2]) # revealed: list[Literal[b"bar"] | None] reveal_type([x1 or x2]) # revealed: list[Literal[b"bar"]] ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/pep695_type_aliases.\342\200\246_-_PEP_695_type_aliases_-_Redeclared_aliases_(7e8460432a17eeae).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/pep695_type_aliases.\342\200\246_-_PEP_695_type_aliases_-_Redeclared_aliases_(7e8460432a17eeae).snap" new file mode 100644 index 00000000000000..1170537a467083 --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/pep695_type_aliases.\342\200\246_-_PEP_695_type_aliases_-_Redeclared_aliases_(7e8460432a17eeae).snap" @@ -0,0 +1,59 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- + +--- +mdtest name: pep695_type_aliases.md - PEP 695 type aliases - Redeclared aliases +mdtest path: crates/ty_python_semantic/resources/mdtest/pep695_type_aliases.md +--- + +# Python source files + +## mdtest_snippet.py + +``` + 1 | type Redeclared = int + 2 | # error: [invalid-type-alias] "Type alias `Redeclared` is already defined in this scope" + 3 | type Redeclared = str + 4 | from random import random + 5 | + 6 | flag = random() > 0.5 + 7 | if flag: + 8 | type BranchRedeclared = int + 9 | else: +10 | # error: [invalid-type-alias] "Type alias `BranchRedeclared` is already defined in this scope" +11 | type BranchRedeclared = str +``` + +# Diagnostics + +``` +error[invalid-type-alias]: Type alias `Redeclared` is already defined in this scope + --> src/mdtest_snippet.py:1:6 + | +1 | type Redeclared = int + | ---------- `Redeclared` previously defined here +2 | # error: [invalid-type-alias] "Type alias `Redeclared` is already defined in this scope" +3 | type Redeclared = str + | ^^^^^^^^^^ +4 | from random import random + | + +``` + +``` +error[invalid-type-alias]: Type alias `BranchRedeclared` is already defined in this scope + --> src/mdtest_snippet.py:8:10 + | + 6 | flag = random() > 0.5 + 7 | if flag: + 8 | type BranchRedeclared = int + | ---------------- `BranchRedeclared` previously defined here + 9 | else: +10 | # error: [invalid-type-alias] "Type alias `BranchRedeclared` is already defined in this scope" +11 | type BranchRedeclared = str + | ^^^^^^^^^^^^^^^^ + | + +``` diff --git a/crates/ty_python_semantic/src/semantic_index/builder.rs b/crates/ty_python_semantic/src/semantic_index/builder.rs index ec11cf3ab045e6..7f4cc874b55fa6 100644 --- a/crates/ty_python_semantic/src/semantic_index/builder.rs +++ b/crates/ty_python_semantic/src/semantic_index/builder.rs @@ -1920,6 +1920,13 @@ impl<'ast> Visitor<'ast> for SemanticIndexBuilder<'_, 'ast> { .map(|name| name.id.clone()) .unwrap_or("".into()), ); + + if type_alias.name.as_name_expr().is_some() { + let use_id = self.current_ast_ids().record_use(&*type_alias.name); + self.current_use_def_map_mut() + .record_use(symbol.into(), use_id); + } + self.add_definition(symbol.into(), type_alias); self.visit_expr(&type_alias.name); diff --git a/crates/ty_python_semantic/src/types/diagnostic.rs b/crates/ty_python_semantic/src/types/diagnostic.rs index 3a87c9fbaae292..68a4d0f02fe05f 100644 --- a/crates/ty_python_semantic/src/types/diagnostic.rs +++ b/crates/ty_python_semantic/src/types/diagnostic.rs @@ -89,6 +89,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_PARAMSPEC); + registry.register_lint(&INVALID_TYPE_ALIAS); registry.register_lint(&INVALID_TYPE_ALIAS_TYPE); registry.register_lint(&INVALID_NEWTYPE); registry.register_lint(&INVALID_METACLASS); @@ -1387,6 +1388,29 @@ declare_lint! { } } +declare_lint! { + /// ## What it does + /// Checks for invalid PEP 695 `type` statements. + /// + /// ## Why is this bad? + /// A `type` statement is only valid in module and class scopes, and a type alias name should + /// not be redeclared in the same scope. + /// + /// ## Examples + /// ```python + /// type Alias = int + /// type Alias = str # error: type alias already defined + /// + /// def f(): + /// type Local = int # error: type statements are not allowed in function scopes + /// ``` + pub(crate) static INVALID_TYPE_ALIAS = { + summary: "detects invalid PEP 695 `type` statements", + status: LintStatus::stable("0.0.28"), + default_level: Level::Error, + } +} + declare_lint! { /// ## What it does /// Checks for the creation of invalid `TypeAliasType`s diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 74bb3c7d394028..298faf2ac64280 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -64,18 +64,19 @@ use crate::types::diagnostic::{ self, CALL_NON_CALLABLE, CONFLICTING_DECLARATIONS, CYCLIC_TYPE_ALIAS_DEFINITION, GeneratorMismatchKind, INEFFECTIVE_FINAL, INVALID_ARGUMENT_TYPE, INVALID_ASSIGNMENT, INVALID_ATTRIBUTE_ACCESS, INVALID_DECLARATION, INVALID_ENUM_MEMBER_ANNOTATION, - INVALID_LEGACY_TYPE_VARIABLE, INVALID_NEWTYPE, INVALID_PARAMSPEC, INVALID_TYPE_ALIAS_TYPE, - INVALID_TYPE_FORM, INVALID_TYPE_GUARD_CALL, INVALID_TYPE_VARIABLE_BOUND, - INVALID_TYPE_VARIABLE_CONSTRAINTS, POSSIBLY_MISSING_IMPLICIT_CALL, POSSIBLY_MISSING_SUBMODULE, - UNDEFINED_REVEAL, UNRESOLVED_ATTRIBUTE, UNRESOLVED_GLOBAL, UNRESOLVED_REFERENCE, - UNSUPPORTED_OPERATOR, UNUSED_AWAITABLE, hint_if_stdlib_attribute_exists_on_other_versions, - report_attempted_protocol_instantiation, report_bad_dunder_set_call, - report_call_to_abstract_method, report_cannot_pop_required_field_on_typed_dict, - report_invalid_assignment, report_invalid_attribute_assignment, - report_invalid_class_match_pattern, report_invalid_exception_caught, - report_invalid_exception_cause, report_invalid_exception_raised, - report_invalid_exception_tuple_caught, report_invalid_generator_yield_type, - report_invalid_key_on_typed_dict, report_invalid_type_checking_constant, + INVALID_LEGACY_TYPE_VARIABLE, INVALID_NEWTYPE, INVALID_PARAMSPEC, INVALID_TYPE_ALIAS, + INVALID_TYPE_ALIAS_TYPE, INVALID_TYPE_FORM, INVALID_TYPE_GUARD_CALL, + INVALID_TYPE_VARIABLE_BOUND, INVALID_TYPE_VARIABLE_CONSTRAINTS, POSSIBLY_MISSING_IMPLICIT_CALL, + POSSIBLY_MISSING_SUBMODULE, UNDEFINED_REVEAL, UNRESOLVED_ATTRIBUTE, UNRESOLVED_GLOBAL, + UNRESOLVED_REFERENCE, UNSUPPORTED_OPERATOR, UNUSED_AWAITABLE, + hint_if_stdlib_attribute_exists_on_other_versions, report_attempted_protocol_instantiation, + report_bad_dunder_set_call, report_call_to_abstract_method, + report_cannot_pop_required_field_on_typed_dict, report_invalid_assignment, + report_invalid_attribute_assignment, report_invalid_class_match_pattern, + report_invalid_exception_caught, report_invalid_exception_cause, + report_invalid_exception_raised, report_invalid_exception_tuple_caught, + report_invalid_generator_yield_type, report_invalid_key_on_typed_dict, + report_invalid_type_checking_constant, report_match_pattern_against_non_runtime_checkable_protocol, report_match_pattern_against_typed_dict, report_possibly_missing_attribute, report_possibly_unresolved_reference, report_unsupported_augmented_assignment, @@ -1455,6 +1456,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { type_alias: &ast::StmtTypeAlias, definition: Definition<'db>, ) { + self.report_invalid_type_alias_scope(type_alias, definition); + self.report_redeclared_type_alias(type_alias, definition); + self.infer_expression(&type_alias.name, TypeContext::default()); // Check that no type parameter with a default follows a TypeVarTuple @@ -1487,6 +1491,81 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { ); } + fn report_invalid_type_alias_scope( + &mut self, + type_alias: &ast::StmtTypeAlias, + definition: Definition<'db>, + ) { + let db = self.db(); + if !definition + .scope(db) + .scope(db) + .kind() + .is_non_lambda_function() + { + return; + } + + if let Some(builder) = self.context.report_lint(&INVALID_TYPE_ALIAS, type_alias) { + builder.into_diagnostic("`type` statements are not allowed in function scopes"); + } + } + + fn report_redeclared_type_alias( + &mut self, + type_alias: &ast::StmtTypeAlias, + definition: Definition<'db>, + ) { + let Some(type_alias_name) = type_alias.name.as_name_expr() else { + return; + }; + + let db = self.db(); + let scope = definition.scope(db); + let use_def = self.index.use_def_map(scope.file_scope_id(db)); + + // Type alias redeclarations are a scope-level property, so we need all earlier + // definitions for this symbol in the scope, not just the bindings reachable here. + let Some(previous_definition) = use_def + .all_definitions_with_usage() + .filter_map(|(_, state, _)| state.definition()) + .filter(|previous_definition| previous_definition.place(db) == definition.place(db)) + .filter(|previous_definition| { + matches!( + previous_definition.kind(db), + DefinitionKind::TypeAlias(previous_type_alias) + if previous_type_alias + .node(self.module()) + .node_index() + .load() + < type_alias.node_index().load() + ) + }) + .max_by_key(|definition| definition.focus_range(db, self.module()).start()) + else { + return; + }; + + let Some(builder) = self + .context + .report_lint(&INVALID_TYPE_ALIAS, &*type_alias.name) + else { + return; + }; + + let mut diagnostic = builder.into_diagnostic(format_args!( + "Type alias `{}` is already defined in this scope", + type_alias_name.id + )); + diagnostic.annotate( + Annotation::secondary(previous_definition.focus_range(db, self.module()).into()) + .message(format_args!( + "`{}` previously defined here", + type_alias_name.id + )), + ); + } + fn infer_if_statement(&mut self, if_statement: &ast::StmtIf) { let ast::StmtIf { range: _, diff --git a/ty.schema.json b/ty.schema.json index ea736ca892a153..8def2774507271 100644 --- a/ty.schema.json +++ b/ty.schema.json @@ -964,6 +964,16 @@ } ] }, + "invalid-type-alias": { + "title": "detects invalid PEP 695 `type` statements", + "description": "## What it does\nChecks for invalid PEP 695 `type` statements.\n\n## Why is this bad?\nA `type` statement is only valid in module and class scopes, and a type alias name should\nnot be redeclared in the same scope.\n\n## Examples\n```python\ntype Alias = int\ntype Alias = str # error: type alias already defined\n\ndef f():\n type Local = int # error: type statements are not allowed in function scopes\n```", + "default": "error", + "oneOf": [ + { + "$ref": "#/definitions/Level" + } + ] + }, "invalid-type-alias-type": { "title": "detects invalid TypeAliasType definitions", "description": "## What it does\nChecks for the creation of invalid `TypeAliasType`s\n\n## Why is this bad?\nThere are several requirements that you must follow when creating a `TypeAliasType`.\n\n## Examples\n```python\nfrom typing import TypeAliasType\n\nIntOrStr = TypeAliasType(\"IntOrStr\", int | str) # okay\nNewAlias = TypeAliasType(get_name(), int) # error: TypeAliasType name must be a string literal\n```",