From 4d2b1bdbd7f36a3bbf30d20c2f182aab2fe9c00e Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 31 Mar 2026 18:41:56 -0400 Subject: [PATCH 1/3] [ty] Validate PEP 695 type alias scope and redeclaration rules --- crates/ty/docs/rules.md | 241 ++++++++++-------- .../resources/mdtest/class/super.md | 1 + .../diagnostics/semantic_syntax_errors.md | 4 +- .../mdtest/generics/pep695/aliases.md | 12 +- .../resources/mdtest/pep695_type_aliases.md | 34 ++- .../resources/mdtest/promotion.md | 4 +- ...edeclared_aliases_(7e8460432a17eeae).snap" | 35 +++ .../src/semantic_index/builder.rs | 7 + .../src/types/diagnostic.rs | 24 ++ .../src/types/infer/builder.rs | 88 ++++++- ty.schema.json | 10 + 11 files changed, 334 insertions(+), 126 deletions(-) create mode 100644 "crates/ty_python_semantic/resources/mdtest/snapshots/pep695_type_aliases.\342\200\246_-_PEP_695_type_aliases_-_Redeclared_aliases_(7e8460432a17eeae).snap" diff --git a/crates/ty/docs/rules.md b/crates/ty/docs/rules.md index 3c1938378cd17..302cbb038fb05 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 e449c46907bec..7a359d78ec046 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 1941be3e9bf33..9c5d8a0051f89 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,12 +321,14 @@ 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-form] "`yield` expressions are not allowed in type alias values" + # 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 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 396181bfc4db5..3798b5d4dbff3 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 3596742438767..4b9681566c5f9 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,34 @@ 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 ``` ## Generic type aliases diff --git a/crates/ty_python_semantic/resources/mdtest/promotion.md b/crates/ty_python_semantic/resources/mdtest/promotion.md index 5de65428207cd..343373721cf0f 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 0000000000000..a4c095558cd5c --- /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,35 @@ +--- +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 +``` + +# 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 + | ^^^^^^^^^^ + | +info: rule `invalid-type-alias` is enabled by default + +``` diff --git a/crates/ty_python_semantic/src/semantic_index/builder.rs b/crates/ty_python_semantic/src/semantic_index/builder.rs index ec11cf3ab045e..7f4cc874b55fa 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 3a87c9fbaae29..68a4d0f02fe05 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 74bb3c7d39402..eb11c3b027bae 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -64,11 +64,12 @@ 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, + 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, @@ -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,80 @@ 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)); + let use_id = type_alias_name.scoped_use_id(db, scope); + + let Some(previous_definition) = use_def + .bindings_at_use(use_id) + .filter_map(|binding| binding.binding.definition()) + .filter(|definition| definition.scope(db) == scope) + .filter(|definition| { + matches!( + 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 ea736ca892a15..8def277450727 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```", From c7a570ffe551be1803324d1c4f77cbdd2ff31584 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 1 Apr 2026 13:24:30 -0400 Subject: [PATCH 2/3] Include same scope --- .../resources/mdtest/pep695_type_aliases.md | 11 +++++++ ...edeclared_aliases_(7e8460432a17eeae).snap" | 32 +++++++++++++++++-- .../src/types/infer/builder.rs | 13 ++++---- 3 files changed, 47 insertions(+), 9 deletions(-) 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 4b9681566c5f9..59e1886e26f36 100644 --- a/crates/ty_python_semantic/resources/mdtest/pep695_type_aliases.md +++ b/crates/ty_python_semantic/resources/mdtest/pep695_type_aliases.md @@ -153,6 +153,17 @@ type Redeclared = int 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 ```py 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" index a4c095558cd5c..bb5b369f0d772 100644 --- "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" @@ -13,9 +13,17 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/pep695_type_aliases.md ## 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 + 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 @@ -29,7 +37,25 @@ error[invalid-type-alias]: Type alias `Redeclared` is already defined in this sc 2 | # error: [invalid-type-alias] "Type alias `Redeclared` is already defined in this scope" 3 | type Redeclared = str | ^^^^^^^^^^ +4 | from random import random | info: rule `invalid-type-alias` is enabled by default ``` + +``` +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 + | ^^^^^^^^^^^^^^^^ + | +info: rule `invalid-type-alias` is enabled by default + +``` diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index eb11c3b027bae..1c96674c2171e 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -1523,15 +1523,16 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let db = self.db(); let scope = definition.scope(db); let use_def = self.index.use_def_map(scope.file_scope_id(db)); - let use_id = type_alias_name.scoped_use_id(db, scope); + // 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 - .bindings_at_use(use_id) - .filter_map(|binding| binding.binding.definition()) - .filter(|definition| definition.scope(db) == scope) - .filter(|definition| { + .all_definitions_with_usage() + .filter_map(|(_, state, _)| state.definition()) + .filter(|previous_definition| previous_definition.place(db) == definition.place(db)) + .filter(|previous_definition| { matches!( - definition.kind(db), + previous_definition.kind(db), DefinitionKind::TypeAlias(previous_type_alias) if previous_type_alias .node(self.module()) From 82a549517403b7988320183b6b7cedda59b9e874 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 7 Apr 2026 15:30:46 -0400 Subject: [PATCH 3/3] [ty] Fix post-rebase test expectations --- .../diagnostics/semantic_syntax_errors.md | 2 +- ...edeclared_aliases_(7e8460432a17eeae).snap" | 2 -- .../src/types/infer/builder.rs | 22 +++++++++---------- 3 files changed, 12 insertions(+), 14 deletions(-) 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 9c5d8a0051f89..44e8580da7827 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 @@ -328,7 +328,7 @@ def _(): 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-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/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" index bb5b369f0d772..1170537a46708 100644 --- "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" @@ -39,7 +39,6 @@ error[invalid-type-alias]: Type alias `Redeclared` is already defined in this sc | ^^^^^^^^^^ 4 | from random import random | -info: rule `invalid-type-alias` is enabled by default ``` @@ -56,6 +55,5 @@ error[invalid-type-alias]: Type alias `BranchRedeclared` is already defined in t 11 | type BranchRedeclared = str | ^^^^^^^^^^^^^^^^ | -info: rule `invalid-type-alias` is enabled by default ``` diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 1c96674c2171e..298faf2ac6428 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -66,17 +66,17 @@ use crate::types::diagnostic::{ INVALID_ATTRIBUTE_ACCESS, INVALID_DECLARATION, INVALID_ENUM_MEMBER_ANNOTATION, 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, + 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,