diff --git a/crates/ty/docs/rules.md b/crates/ty/docs/rules.md index 81f3cd8c71d1e..1e62ff958d6b9 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 @@ -120,44 +120,13 @@ def _(x: int): # which excludes types like `Literal[0]` ``` -## `byte-string-type-annotation` - - -Default level: error · -Added in 0.0.1-alpha.1 · -Related issues · -View source - - - -**What it does** - -Checks for byte-strings in type annotation positions. - -**Why is this bad?** - -Static analysis tools like ty can't analyze type annotations that use byte-string notation. - -**Examples** - -```python -def test(): -> b"int": - ... -``` - -Use instead: -```python -def test(): -> "int": - ... -``` - ## `call-abstract-method` Default level: error · Preview (since 0.0.16) · Related issues · -View source +View source @@ -206,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 @@ -230,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 @@ -261,7 +230,7 @@ def f(x: object): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -293,7 +262,7 @@ f(int) # error Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -324,7 +293,7 @@ a = 1 Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -356,7 +325,7 @@ class C(A, B): ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -388,7 +357,7 @@ class B(A): ... Default level: error · Added in 0.0.1-alpha.29 · Related issues · -View source +View source @@ -416,7 +385,7 @@ type B = A Default level: error · Added in 0.0.15 · Related issues · -View source +View source @@ -448,7 +417,7 @@ class Example: Default level: warn · Added in 0.0.1-alpha.16 · Related issues · -View source +View source @@ -475,7 +444,7 @@ old_func() # emits [deprecated] diagnostic Default level: ignore · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -504,7 +473,7 @@ false positives it can produce. Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -531,7 +500,7 @@ class B(A, A): ... Default level: error · Added in 0.0.1-alpha.12 · Related issues · -View source +View source @@ -569,7 +538,7 @@ class A: # Crash at runtime Default level: error · Added in 0.0.14 · Related issues · -View source +View source @@ -615,7 +584,7 @@ def bar() -> str: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -640,7 +609,7 @@ def foo() -> "intt\b": ... Default level: error · Added in 0.0.20 · Related issues · -View source +View source @@ -672,7 +641,7 @@ def my_function() -> int: Default level: error · Added in 0.0.15 · Related issues · -View source +View source @@ -699,37 +668,6 @@ MY_CONSTANT: Final[int] MY_CONSTANT: Final[int] = 1 ``` -## `fstring-type-annotation` - - -Default level: error · -Added in 0.0.1-alpha.1 · -Related issues · -View source - - - -**What it does** - -Checks for f-strings in type annotation positions. - -**Why is this bad?** - -Static analysis tools like ty can't analyze type annotations that use f-string notation. - -**Examples** - -```python -def test(): -> f"int": - ... -``` - -Use instead: -```python -def test(): -> "int": - ... -``` - ## `ignore-comment-unknown-rule` @@ -767,7 +705,7 @@ a = 20 / 0 # ty: ignore[division-by-zero] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -798,7 +736,7 @@ def test(): -> "Literal[5]": Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -828,7 +766,7 @@ class C(A, B): ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -854,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 @@ -888,7 +826,7 @@ class MyClass: ... Default level: error · Added in 0.0.1-alpha.12 · Related issues · -View source +View source @@ -977,7 +915,7 @@ an atypical memory layout. Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1004,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 @@ -1032,7 +970,7 @@ a: int = '' Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1066,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 @@ -1102,7 +1040,7 @@ asyncio.run(main()) Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1126,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 @@ -1153,7 +1091,7 @@ with 1: Default level: error · Added in 0.0.12 · Related issues · -View source +View source @@ -1190,7 +1128,7 @@ class Foo(NamedTuple): Default level: error · Added in 0.0.13 · Related issues · -View source +View source @@ -1222,7 +1160,7 @@ class A: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1251,7 +1189,7 @@ a: str Default level: warn · Added in 0.0.20 · Related issues · -View source +View source @@ -1300,7 +1238,7 @@ class Pet(Enum): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1344,7 +1282,7 @@ except ZeroDivisionError: Default level: error · Added in 0.0.1-alpha.28 · Related issues · -View source +View source @@ -1386,7 +1324,7 @@ class D(A): Default level: error · Added in 0.0.1-alpha.35 · Related issues · -View source +View source @@ -1430,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 @@ -1468,7 +1406,7 @@ class D(Generic[U, T]): ... Default level: error · Added in 0.0.12 · Related issues · -View source +View source @@ -1547,7 +1485,7 @@ a = 20 / 0 # type: ignore Default level: error · Added in 0.0.1-alpha.17 · Related issues · -View source +View source @@ -1586,7 +1524,7 @@ carol = Person(name="Carol", aeg=25) # typo! Default level: warn · Added in 0.0.15 · Related issues · -View source +View source @@ -1647,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 @@ -1682,7 +1620,7 @@ def f(t: TypeVar("U")): ... Default level: error · Added in 0.0.18 · Related issues · -View source +View source @@ -1710,7 +1648,7 @@ match x: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1744,7 +1682,7 @@ class B(metaclass=f): ... Default level: error · Added in 0.0.1-alpha.20 · Related issues · -View source +View source @@ -1851,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 @@ -1905,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 @@ -1935,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 @@ -1985,7 +1923,7 @@ def foo(x: int) -> int: ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2011,7 +1949,7 @@ def f(a: int = ''): ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2042,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 @@ -2076,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 @@ -2125,7 +2063,7 @@ def g(): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2154,7 +2092,7 @@ def func() -> int: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2200,7 +2138,7 @@ super(B, A) # error: `A` does not satisfy `issubclass(A, B)` Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2250,7 +2188,7 @@ class C: ... Default level: error · Added in 0.0.10 · Related issues · -View source +View source @@ -2296,7 +2234,7 @@ class MyClass: Default level: error · Added in 0.0.1-alpha.6 · Related issues · -View source +View source @@ -2323,7 +2261,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 @@ -2370,7 +2308,7 @@ Bar[int] # error: too few arguments Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2400,7 +2338,7 @@ TYPE_CHECKING = '' Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2430,7 +2368,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 @@ -2464,7 +2402,7 @@ f(10) # Error Default level: error · Added in 0.0.1-alpha.11 · Related issues · -View source +View source @@ -2498,7 +2436,7 @@ class C: Default level: error · Added in 0.0.15 · Related issues · -View source +View source @@ -2529,7 +2467,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 @@ -2576,7 +2514,7 @@ U = TypeVar('U', list[int], int) # valid constrained Type Default level: error · Added in 0.0.16 · Related issues · -View source +View source @@ -2608,7 +2546,7 @@ U = TypeVar("U", int, str, default=bytes) # error: [invalid-type-variable-defau Default level: error · Added in 0.0.14 · Related issues · -View source +View source @@ -2643,7 +2581,7 @@ def f(x: dict): Default level: error · Added in 0.0.9 · Related issues · -View source +View source @@ -2674,7 +2612,7 @@ class Foo(TypedDict): Default level: error · Added in 0.0.25 · Related issues · -View source +View source @@ -2705,7 +2643,7 @@ def gen() -> Iterator[int]: Default level: error · Added in 0.0.14 · Related issues · -View source +View source @@ -2760,7 +2698,7 @@ def h(arg2: type): Default level: error · Added in 0.0.15 · Related issues · -View source +View source @@ -2803,7 +2741,7 @@ def g(arg: object): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2828,7 +2766,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 @@ -2861,7 +2799,7 @@ alice["age"] # KeyError Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2890,7 +2828,7 @@ func("string") # error: [no-matching-overload] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2916,7 +2854,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 @@ -2940,7 +2878,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 @@ -2973,7 +2911,7 @@ class B(A): Default level: error · Added in 0.0.16 · Related issues · -View source +View source @@ -3006,7 +2944,7 @@ class B(A): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3033,7 +2971,7 @@ f(1, x=2) # Error raised here Default level: error · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -3060,7 +2998,7 @@ f(x=1) # Error raised here Default level: ignore · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -3093,7 +3031,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 @@ -3125,7 +3063,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 @@ -3162,7 +3100,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 @@ -3189,7 +3127,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 +3160,7 @@ print(x) # NameError: name 'x' is not defined Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3253,7 +3191,7 @@ def test(): -> "int": Default level: warn · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3280,7 +3218,7 @@ cast(int, f()) # Redundant Default level: warn · Added in 0.0.18 · Related issues · -View source +View source @@ -3312,7 +3250,7 @@ class C: Default level: error · Added in 0.0.20 · Related issues · -View source +View source @@ -3346,7 +3284,7 @@ class Outer[T]: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3376,7 +3314,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 @@ -3405,7 +3343,7 @@ class B(A): ... # Error raised here Default level: error · Added in 0.0.1-alpha.30 · Related issues · -View source +View source @@ -3439,7 +3377,7 @@ class F(NamedTuple): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3466,7 +3404,7 @@ f("foo") # Error raised here Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3494,7 +3432,7 @@ def _(x: int): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3540,7 +3478,7 @@ class A: Default level: error · Added in 0.0.20 · Related issues · -View source +View source @@ -3577,7 +3515,7 @@ class C(Generic[T]): Default level: warn · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3601,7 +3539,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 @@ -3628,7 +3566,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 @@ -3656,7 +3594,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 @@ -3714,7 +3652,7 @@ def g(): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3739,7 +3677,7 @@ import foo # ModuleNotFoundError: No module named 'foo' Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3764,7 +3702,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 @@ -3803,7 +3741,7 @@ class D(C): ... # error: [unsupported-base] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3840,7 +3778,7 @@ b1 < b2 < b1 # exception raised here Default level: ignore · Added in 0.0.12 · Related issues · -View source +View source @@ -3880,7 +3818,7 @@ def factory(base: type[Base]) -> type: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3908,7 +3846,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 @@ -4014,7 +3952,7 @@ to `false`. Default level: warn · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -4077,7 +4015,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/annotations/invalid.md b/crates/ty_python_semantic/resources/mdtest/annotations/invalid.md index 0ec87ba0940c5..79df38114ebbb 100644 --- a/crates/ty_python_semantic/resources/mdtest/annotations/invalid.md +++ b/crates/ty_python_semantic/resources/mdtest/annotations/invalid.md @@ -420,6 +420,15 @@ def _( return x ``` +### Dict-literal or set-literal when you meant to use `dict[]`/`set[]` + +```py +def _( + x: {int: str}, # error: [invalid-type-form] + y: {str}, # error: [invalid-type-form] +): ... +``` + ### Special-cased diagnostic for `callable` used in a type expression ```py @@ -428,3 +437,18 @@ def _( def decorator(fn: callable) -> callable: return fn ``` + +### AST nodes that are only valid inside `Literal` + +```py +def bad( + # error: [invalid-type-form] + a: 42, + # error: [invalid-type-form] + b: b"42", + # error: [invalid-type-form] + c: True, + # error: [invalid-syntax-in-forward-annotation] + d: "invalid syntax", +): ... +``` diff --git a/crates/ty_python_semantic/resources/mdtest/annotations/string.md b/crates/ty_python_semantic/resources/mdtest/annotations/string.md index 31078f9fe76c9..1dc6a8ce18c78 100644 --- a/crates/ty_python_semantic/resources/mdtest/annotations/string.md +++ b/crates/ty_python_semantic/resources/mdtest/annotations/string.md @@ -217,32 +217,44 @@ class Foo: ... ```py def f1( - # error: [raw-string-type-annotation] "Type expressions cannot use raw string literal" + # error: [raw-string-type-annotation] "Raw string literals are not allowed in type expressions" a: r"int", - # error: [fstring-type-annotation] "Type expressions cannot use f-strings" - b: f"int", - # error: [byte-string-type-annotation] "Type expressions cannot use bytes literal" - c: b"int", - d: "int", + # error: [raw-string-type-annotation] "Raw string literals are not allowed in type expressions" + b: list[r"int"], + # error: [invalid-type-form] "F-strings are not allowed in type expressions" + c: f"int", + # error: [invalid-type-form] "F-strings are not allowed in type expressions" + d: list[f"int"], + # error: [invalid-type-form] "Bytes literals are not allowed in this context in a type expression" + e: b"int", + f: "int", # error: [implicit-concatenated-string-type-annotation] "Type expressions cannot span multiple string literals" - e: "in" "t", - # error: [escape-character-in-forward-annotation] "Type expressions cannot contain escape characters" - f: "\N{LATIN SMALL LETTER I}nt", - # error: [escape-character-in-forward-annotation] "Type expressions cannot contain escape characters" - g: "\x69nt", - h: """int""", - # error: [byte-string-type-annotation] "Type expressions cannot use bytes literal" - i: "b'int'", + g: "in" "t", + # error: [implicit-concatenated-string-type-annotation] "Type expressions cannot span multiple string literals" + h: list["in" "t"], + # error: [escape-character-in-forward-annotation] "Escape characters are not allowed in type expressions" + i: "\N{LATIN SMALL LETTER I}nt", + # error: [escape-character-in-forward-annotation] "Escape characters are not allowed in type expressions" + j: "\x69nt", + k: """int""", + # error: [invalid-type-form] "Bytes literals are not allowed in this context in a type expression" + l: "b'int'", + # error: [invalid-type-form] "Bytes literals are not allowed in this context in a type expression" + m: list[b"int"], ): # fmt:skip reveal_type(a) # revealed: Unknown - reveal_type(b) # revealed: Unknown + reveal_type(b) # revealed: list[Unknown] reveal_type(c) # revealed: Unknown - reveal_type(d) # revealed: int + reveal_type(d) # revealed: list[Unknown] reveal_type(e) # revealed: Unknown - reveal_type(f) # revealed: Unknown + reveal_type(f) # revealed: int reveal_type(g) # revealed: Unknown - reveal_type(h) # revealed: int + reveal_type(h) # revealed: list[Unknown] reveal_type(i) # revealed: Unknown + reveal_type(j) # revealed: Unknown + reveal_type(k) # revealed: int + reveal_type(l) # revealed: Unknown + reveal_type(m) # revealed: list[Unknown] ``` ## Various string kinds in `typing.Literal` @@ -305,17 +317,17 @@ shouldn't panic. ```py # Regression test for https://github.com/astral-sh/ty/issues/1865 -# error: [fstring-type-annotation] +# error: [invalid-type-form] stringified_fstring_with_conditional: "f'{1 if 1 else 1}'" -# error: [fstring-type-annotation] +# error: [invalid-type-form] stringified_fstring_with_boolean_expression: "f'{1 or 2}'" -# error: [fstring-type-annotation] +# error: [invalid-type-form] stringified_fstring_with_generator_expression: "f'{(i for i in range(5))}'" -# error: [fstring-type-annotation] +# error: [invalid-type-form] stringified_fstring_with_list_comprehension: "f'{[i for i in range(5)]}'" -# error: [fstring-type-annotation] +# error: [invalid-type-form] stringified_fstring_with_dict_comprehension: "f'{ {i: i for i in range(5)} }'" -# error: [fstring-type-annotation] +# error: [invalid-type-form] stringified_fstring_with_set_comprehension: "f'{ {i for i in range(5)} }'" # error: [invalid-type-form] diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid.md_-_Tests_for_invalid_ty\342\200\246_-_Diagnostics_for_comm\342\200\246_-_AST_nodes_that_are_o\342\200\246_(58a3839a9bc7026d).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid.md_-_Tests_for_invalid_ty\342\200\246_-_Diagnostics_for_comm\342\200\246_-_AST_nodes_that_are_o\342\200\246_(58a3839a9bc7026d).snap" new file mode 100644 index 0000000000000..29dacab84a8ac --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid.md_-_Tests_for_invalid_ty\342\200\246_-_Diagnostics_for_comm\342\200\246_-_AST_nodes_that_are_o\342\200\246_(58a3839a9bc7026d).snap" @@ -0,0 +1,93 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- + +--- +mdtest name: invalid.md - Tests for invalid types in type expressions - Diagnostics for common errors - AST nodes that are only valid inside `Literal` +mdtest path: crates/ty_python_semantic/resources/mdtest/annotations/invalid.md +--- + +# Python source files + +## mdtest_snippet.py + +``` + 1 | def bad( + 2 | # error: [invalid-type-form] + 3 | a: 42, + 4 | # error: [invalid-type-form] + 5 | b: b"42", + 6 | # error: [invalid-type-form] + 7 | c: True, + 8 | # error: [invalid-syntax-in-forward-annotation] + 9 | d: "invalid syntax", +10 | ): ... +``` + +# Diagnostics + +``` +error[invalid-type-form]: Int literals are not allowed in this context in a type expression + --> src/mdtest_snippet.py:3:8 + | +1 | def bad( +2 | # error: [invalid-type-form] +3 | a: 42, + | ^^ Did you mean `typing.Literal[42]`? +4 | # error: [invalid-type-form] +5 | b: b"42", + | +info: See the following page for a reference on valid type expressions: +info: https://typing.python.org/en/latest/spec/annotations.html#type-and-annotation-expressions +info: rule `invalid-type-form` is enabled by default + +``` + +``` +error[invalid-type-form]: Bytes literals are not allowed in this context in a type expression + --> src/mdtest_snippet.py:5:8 + | +3 | a: 42, +4 | # error: [invalid-type-form] +5 | b: b"42", + | ^^^^^ Did you mean `typing.Literal[b"42"]`? +6 | # error: [invalid-type-form] +7 | c: True, + | +info: See the following page for a reference on valid type expressions: +info: https://typing.python.org/en/latest/spec/annotations.html#type-and-annotation-expressions +info: rule `invalid-type-form` is enabled by default + +``` + +``` +error[invalid-type-form]: Boolean literals are not allowed in this context in a type expression + --> src/mdtest_snippet.py:7:8 + | +5 | b: b"42", +6 | # error: [invalid-type-form] +7 | c: True, + | ^^^^ Did you mean `typing.Literal[True]`? +8 | # error: [invalid-syntax-in-forward-annotation] +9 | d: "invalid syntax", + | +info: See the following page for a reference on valid type expressions: +info: https://typing.python.org/en/latest/spec/annotations.html#type-and-annotation-expressions +info: rule `invalid-type-form` is enabled by default + +``` + +``` +error[invalid-syntax-in-forward-annotation]: Syntax error in forward annotation: Unexpected token at the end of an expression + --> src/mdtest_snippet.py:9:8 + | + 7 | c: True, + 8 | # error: [invalid-syntax-in-forward-annotation] + 9 | d: "invalid syntax", + | ^^^^^^^^^^^^^^^^ Did you mean `typing.Literal["invalid syntax"]`? +10 | ): ... + | +info: rule `invalid-syntax-in-forward-annotation` is enabled by default + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid.md_-_Tests_for_invalid_ty\342\200\246_-_Diagnostics_for_comm\342\200\246_-_Dict-literal_or_set-\342\200\246_(15737b0beb194b0e).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid.md_-_Tests_for_invalid_ty\342\200\246_-_Diagnostics_for_comm\342\200\246_-_Dict-literal_or_set-\342\200\246_(15737b0beb194b0e).snap" new file mode 100644 index 0000000000000..87da04f6651d4 --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid.md_-_Tests_for_invalid_ty\342\200\246_-_Diagnostics_for_comm\342\200\246_-_Dict-literal_or_set-\342\200\246_(15737b0beb194b0e).snap" @@ -0,0 +1,54 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- + +--- +mdtest name: invalid.md - Tests for invalid types in type expressions - Diagnostics for common errors - Dict-literal or set-literal when you meant to use `dict[]`/`set[]` +mdtest path: crates/ty_python_semantic/resources/mdtest/annotations/invalid.md +--- + +# Python source files + +## mdtest_snippet.py + +``` +1 | def _( +2 | x: {int: str}, # error: [invalid-type-form] +3 | y: {str}, # error: [invalid-type-form] +4 | ): ... +``` + +# Diagnostics + +``` +error[invalid-type-form]: Dict literals are not allowed in type expressions + --> src/mdtest_snippet.py:2:8 + | +1 | def _( +2 | x: {int: str}, # error: [invalid-type-form] + | ^^^^^^^^^^ Did you mean `dict[int, str]`? +3 | y: {str}, # error: [invalid-type-form] +4 | ): ... + | +info: See the following page for a reference on valid type expressions: +info: https://typing.python.org/en/latest/spec/annotations.html#type-and-annotation-expressions +info: rule `invalid-type-form` is enabled by default + +``` + +``` +error[invalid-type-form]: Set literals are not allowed in type expressions + --> src/mdtest_snippet.py:3:8 + | +1 | def _( +2 | x: {int: str}, # error: [invalid-type-form] +3 | y: {str}, # error: [invalid-type-form] + | ^^^^^ Did you mean `set[str]`? +4 | ): ... + | +info: See the following page for a reference on valid type expressions: +info: https://typing.python.org/en/latest/spec/annotations.html#type-and-annotation-expressions +info: rule `invalid-type-form` is enabled by default + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/typed_dict.md_-_`TypedDict`_-_Function_syntax_with\342\200\246_(4b18755412dfaff1).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/typed_dict.md_-_`TypedDict`_-_Function_syntax_with\342\200\246_(4b18755412dfaff1).snap" index 0d8882f1fa9c6..3e48f7b7ecced 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/typed_dict.md_-_`TypedDict`_-_Function_syntax_with\342\200\246_(4b18755412dfaff1).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/typed_dict.md_-_`TypedDict`_-_Function_syntax_with\342\200\246_(4b18755412dfaff1).snap" @@ -531,7 +531,7 @@ error[invalid-type-form]: Int literals are not allowed in this context in a type 86 | # error: [invalid-argument-type] "Expected a string-literal key in the `fields` dict of `TypedDict()`" 87 | # error: [invalid-type-form] 88 | Bad10 = TypedDict("Bad10", {name: 42}) - | ^^ + | ^^ Did you mean `typing.Literal[42]`? 89 | 90 | # error: [invalid-argument-type] "Expected a string-literal key in the `fields` dict of `TypedDict()`" | @@ -563,7 +563,7 @@ error[invalid-type-form]: Int literals are not allowed in this context in a type 90 | # error: [invalid-argument-type] "Expected a string-literal key in the `fields` dict of `TypedDict()`" 91 | # error: [invalid-type-form] "Int literals are not allowed in this context in a type expression" 92 | class Bad11(TypedDict("Bad11", {name: 42})): ... - | ^^ + | ^^ Did you mean `typing.Literal[42]`? 93 | 94 | # error: [invalid-argument-type] "Invalid argument to parameter `typename` of `TypedDict()`: Expected `str`, found `Literal[123]`" | diff --git a/crates/ty_python_semantic/resources/mdtest/suppressions/ty_ignore.md b/crates/ty_python_semantic/resources/mdtest/suppressions/ty_ignore.md index c1c0172e4256f..79a8c84cc87c1 100644 --- a/crates/ty_python_semantic/resources/mdtest/suppressions/ty_ignore.md +++ b/crates/ty_python_semantic/resources/mdtest/suppressions/ty_ignore.md @@ -70,7 +70,7 @@ a = 10 / 0 # ty: ignore[invalid-assignment, unresolved-reference, division-by-z ```py # fmt: off -def test(a: f"f-string type annotation", b: b"byte-string-type-annotation"): ... # ty: ignore[fstring-type-annotation, byte-string-type-annotation] +def test(a: f"f-string type annotation", b: unresolved_ref): ... # ty: ignore[invalid-type-form, unresolved-reference] ``` ## Can't suppress syntax errors diff --git a/crates/ty_python_semantic/src/types/diagnostic.rs b/crates/ty_python_semantic/src/types/diagnostic.rs index 67116b5f21bb9..04567593b60cb 100644 --- a/crates/ty_python_semantic/src/types/diagnostic.rs +++ b/crates/ty_python_semantic/src/types/diagnostic.rs @@ -20,9 +20,8 @@ use crate::types::infer::UnsupportedComparisonError; use crate::types::overrides::MethodKind; use crate::types::protocol_class::ProtocolMember; use crate::types::string_annotation::{ - BYTE_STRING_TYPE_ANNOTATION, ESCAPE_CHARACTER_IN_FORWARD_ANNOTATION, FSTRING_TYPE_ANNOTATION, - IMPLICIT_CONCATENATED_STRING_TYPE_ANNOTATION, INVALID_SYNTAX_IN_FORWARD_ANNOTATION, - RAW_STRING_TYPE_ANNOTATION, + ESCAPE_CHARACTER_IN_FORWARD_ANNOTATION, IMPLICIT_CONCATENATED_STRING_TYPE_ANNOTATION, + INVALID_SYNTAX_IN_FORWARD_ANNOTATION, RAW_STRING_TYPE_ANNOTATION, }; use crate::types::tuple::TupleSpec; use crate::types::typed_dict::TypedDictSchema; @@ -160,9 +159,7 @@ pub(crate) fn register_lints(registry: &mut LintRegistryBuilder) { registry.register_lint(&INVALID_LEGACY_POSITIONAL_PARAMETER); // String annotations - registry.register_lint(&BYTE_STRING_TYPE_ANNOTATION); registry.register_lint(&ESCAPE_CHARACTER_IN_FORWARD_ANNOTATION); - registry.register_lint(&FSTRING_TYPE_ANNOTATION); registry.register_lint(&IMPLICIT_CONCATENATED_STRING_TYPE_ANNOTATION); registry.register_lint(&INVALID_SYNTAX_IN_FORWARD_ANNOTATION); registry.register_lint(&RAW_STRING_TYPE_ANNOTATION); diff --git a/crates/ty_python_semantic/src/types/infer/builder/annotation_expression.rs b/crates/ty_python_semantic/src/types/infer/builder/annotation_expression.rs index 0a8f7f9ca9cb6..f2bbc379d6722 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/annotation_expression.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/annotation_expression.rs @@ -7,9 +7,7 @@ use crate::types::diagnostic::{INVALID_TYPE_FORM, REDUNDANT_FINAL_CLASSVAR}; use crate::types::infer::builder::InferenceFlags; use crate::types::infer::builder::subscript::AnnotatedExprContext; use crate::types::infer::nearest_enclosing_class; -use crate::types::string_annotation::{ - BYTE_STRING_TYPE_ANNOTATION, FSTRING_TYPE_ANNOTATION, parse_string_annotation, -}; +use crate::types::string_annotation::parse_string_annotation; use crate::types::{ SpecialFormType, Type, TypeAndQualifiers, TypeContext, TypeQualifier, TypeQualifiers, todo_type, }; @@ -161,34 +159,6 @@ impl<'db> TypeInferenceBuilder<'db, '_> { // String annotations: https://typing.python.org/en/latest/spec/annotations.html#string-annotations ast::Expr::StringLiteral(string) => self.infer_string_annotation_expression(string), - // Annotation expressions also get special handling for `*args` and `**kwargs`. - ast::Expr::Starred(starred) => TypeAndQualifiers::declared( - self.infer_starred_expression(starred, TypeContext::default()), - ), - - ast::Expr::BytesLiteral(bytes) => { - if let Some(builder) = self - .context - .report_lint(&BYTE_STRING_TYPE_ANNOTATION, bytes) - { - builder.into_diagnostic("Type expressions cannot use bytes literal"); - } - if !self.in_string_annotation() { - self.infer_bytes_literal_expression(bytes); - } - TypeAndQualifiers::declared(Type::unknown()) - } - - ast::Expr::FString(fstring) => { - if let Some(builder) = self.context.report_lint(&FSTRING_TYPE_ANNOTATION, fstring) { - builder.into_diagnostic("Type expressions cannot use f-strings"); - } - if !self.in_string_annotation() { - self.infer_fstring_expression(fstring); - } - TypeAndQualifiers::declared(Type::unknown()) - } - ast::Expr::Attribute(attribute) => { if !is_dotted_name(annotation) { return TypeAndQualifiers::declared(self.infer_type_expression(annotation)); @@ -357,8 +327,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } } - // All other annotation expressions are (possibly) valid type expressions, so handle - // them there instead. + // Fallback to `infer_type_expression_no_store` for everything else type_expr => { TypeAndQualifiers::declared(self.infer_type_expression_no_store(type_expr)) } diff --git a/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs b/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs index bed7330df6e90..c8bc1513c161b 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs @@ -330,29 +330,38 @@ impl<'db> TypeInferenceBuilder<'db, '_> { // nested expressions as normal expressions, but the type of the top-level expression is // always `Type::unknown` in these cases. // ===================================================================================== - - // TODO: add a subdiagnostic linking to type-expression grammar - // and stating that it is only valid in `typing.Literal[]` or `typing.Annotated[]` - ast::Expr::BytesLiteral(_) => { - self.report_invalid_type_expression( + ast::Expr::BytesLiteral(bytes) => { + if let Some(mut diagnostic) = self.report_invalid_type_expression( expression, - format_args!( - "Bytes literals are not allowed in this context in a type expression" - ), - ); + "Bytes literals are not allowed in this context in a type expression", + ) { + if let Some(single_element) = bytes.as_single_part_bytestring() + && let Ok(valid_string) = String::from_utf8(single_element.value.to_vec()) + { + diagnostic.set_primary_message(format_args!( + "Did you mean `typing.Literal[b\"{valid_string}\"]`?" + )); + } + } Type::unknown() } ast::Expr::NumberLiteral(ast::ExprNumberLiteral { - value: ast::Number::Int(_), + value: ast::Number::Int(int), .. }) => { - self.report_invalid_type_expression( + if let Some(mut diagnostic) = self.report_invalid_type_expression( expression, format_args!( "Int literals are not allowed in this context in a type expression" ), - ); + ) { + if let Some(int) = int.as_i64() { + diagnostic.set_primary_message(format_args!( + "Did you mean `typing.Literal[{int}]`?" + )); + } + } Type::unknown() } @@ -379,13 +388,18 @@ impl<'db> TypeInferenceBuilder<'db, '_> { Type::unknown() } - ast::Expr::BooleanLiteral(_) => { - self.report_invalid_type_expression( + ast::Expr::BooleanLiteral(bool_value) => { + if let Some(mut diagnostic) = self.report_invalid_type_expression( expression, format_args!( "Boolean literals are not allowed in this context in a type expression" ), - ); + ) { + diagnostic.set_primary_message(format_args!( + "Did you mean `typing.Literal[{}]`?", + if bool_value.value { "True" } else { "False" } + )); + } Type::unknown() } @@ -516,10 +530,28 @@ impl<'db> TypeInferenceBuilder<'db, '_> { if !self.in_string_annotation() { self.infer_dict_expression(dict, TypeContext::default()); } - self.report_invalid_type_expression( + if let Some(mut diagnostic) = self.report_invalid_type_expression( expression, format_args!("Dict literals are not allowed in type expressions"), - ); + ) && let [ + ast::DictItem { + key: Some(key), + value, + }, + ] = &*dict.items + { + let mut speculative = self.speculate(); + let key_type = speculative.infer_type_expression(key); + let value_type = speculative.infer_type_expression(value); + if key_type.is_hintable(self.db()) && value_type.is_hintable(self.db()) { + let hinted_type = KnownClass::Dict + .to_specialized_instance(self.db(), &[key_type, value_type]); + diagnostic.set_primary_message(format_args!( + "Did you mean `{}`?", + hinted_type.display(self.db()), + )); + } + } Type::unknown() } @@ -527,10 +559,24 @@ impl<'db> TypeInferenceBuilder<'db, '_> { if !self.in_string_annotation() { self.infer_set_expression(set, TypeContext::default()); } - self.report_invalid_type_expression( + if let Some(mut diagnostic) = self.report_invalid_type_expression( expression, format_args!("Set literals are not allowed in type expressions"), - ); + ) && let [single_element] = &*set.elts + { + let mut speculative_builder = self.speculate(); + let inner_type = speculative_builder.infer_type_expression(single_element); + + if inner_type.is_hintable(self.db()) { + let hinted_type = + KnownClass::Set.to_specialized_instance(self.db(), &[inner_type]); + + diagnostic.set_primary_message(format_args!( + "Did you mean `{}`?", + hinted_type.display(self.db()), + )); + } + } Type::unknown() } @@ -639,7 +685,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } self.report_invalid_type_expression( expression, - format_args!("F-strings are not allowed in type expressions"), + "F-strings are not allowed in type expressions", ); Type::unknown() } diff --git a/crates/ty_python_semantic/src/types/string_annotation.rs b/crates/ty_python_semantic/src/types/string_annotation.rs index d13e8a4ca64a9..4b730a55be7b3 100644 --- a/crates/ty_python_semantic/src/types/string_annotation.rs +++ b/crates/ty_python_semantic/src/types/string_annotation.rs @@ -9,56 +9,6 @@ use crate::lint::{Level, LintStatus}; use super::context::InferContext; -declare_lint! { - /// ## What it does - /// Checks for f-strings in type annotation positions. - /// - /// ## Why is this bad? - /// Static analysis tools like ty can't analyze type annotations that use f-string notation. - /// - /// ## Examples - /// ```python - /// def test(): -> f"int": - /// ... - /// ``` - /// - /// Use instead: - /// ```python - /// def test(): -> "int": - /// ... - /// ``` - pub(crate) static FSTRING_TYPE_ANNOTATION = { - summary: "detects F-strings in type annotation positions", - status: LintStatus::stable("0.0.1-alpha.1"), - default_level: Level::Error, - } -} - -declare_lint! { - /// ## What it does - /// Checks for byte-strings in type annotation positions. - /// - /// ## Why is this bad? - /// Static analysis tools like ty can't analyze type annotations that use byte-string notation. - /// - /// ## Examples - /// ```python - /// def test(): -> b"int": - /// ... - /// ``` - /// - /// Use instead: - /// ```python - /// def test(): -> "int": - /// ... - /// ``` - pub(crate) static BYTE_STRING_TYPE_ANNOTATION = { - summary: "detects byte strings in type annotation positions", - status: LintStatus::stable("0.0.1-alpha.1"), - default_level: Level::Error, - } -} - declare_lint! { /// ## What it does /// Checks for raw-strings in type annotation positions. @@ -189,7 +139,7 @@ pub(crate) fn parse_string_annotation( if prefix.is_raw() { if let Some(builder) = context.report_lint(&RAW_STRING_TYPE_ANNOTATION, string_literal) { - builder.into_diagnostic("Type expressions cannot use raw string literal"); + builder.into_diagnostic("Raw string literals are not allowed in type expressions"); } // Compare the raw contents (without quotes) of the expression with the parsed contents // contained in the string literal. @@ -200,10 +150,14 @@ pub(crate) fn parse_string_annotation( if let Some(builder) = context.report_lint(&INVALID_SYNTAX_IN_FORWARD_ANNOTATION, string_literal) { - builder.into_diagnostic(format_args!( + let mut diagnostic = builder.into_diagnostic(format_args!( "Syntax error in forward annotation: {}", parse_error.error )); + diagnostic.set_primary_message(format_args!( + "Did you mean `typing.Literal[\"{}\"]`?", + string_literal.as_str() + )); } } } @@ -212,7 +166,7 @@ pub(crate) fn parse_string_annotation( { // The raw contents of the string doesn't match the parsed content. This could be the // case for annotations that contain escape sequences. - builder.into_diagnostic("Type expressions cannot contain escape characters"); + builder.into_diagnostic("Escape characters are not allowed in type expressions"); } } else if let Some(builder) = context.report_lint(&IMPLICIT_CONCATENATED_STRING_TYPE_ANNOTATION, string_expr) diff --git a/ty.schema.json b/ty.schema.json index 8a5df89aa26f5..37337fb26f7d3 100644 --- a/ty.schema.json +++ b/ty.schema.json @@ -404,16 +404,6 @@ } ] }, - "byte-string-type-annotation": { - "title": "detects byte strings in type annotation positions", - "description": "## What it does\nChecks for byte-strings in type annotation positions.\n\n## Why is this bad?\nStatic analysis tools like ty can't analyze type annotations that use byte-string notation.\n\n## Examples\n```python\ndef test(): -> b\"int\":\n ...\n```\n\nUse instead:\n```python\ndef test(): -> \"int\":\n ...\n```", - "default": "error", - "oneOf": [ - { - "$ref": "#/definitions/Level" - } - ] - }, "call-abstract-method": { "title": "detects calls to abstract methods with trivial bodies on class objects", "description": "## What it does\nChecks for calls to abstract `@classmethod`s or `@staticmethod`s\nwith \"trivial bodies\" when accessed on the class object itself.\n\n\"Trivial bodies\" are bodies that solely consist of `...`, `pass`,\na docstring, and/or `raise NotImplementedError`.\n\n## Why is this bad?\nAn abstract method with a trivial body has no concrete implementation\nto execute, so calling such a method directly on the class will probably\nnot have the desired effect.\n\nIt is also unsound to call these methods directly on the class. Unlike\nother methods, ty permits abstract methods with trivial bodies to have\nnon-`None` return types even though they always return `None` at runtime.\nThis is because it is expected that these methods will always be\noverridden rather than being called directly. As a result of this\nexception to the normal rule, ty may infer an incorrect type if one of\nthese methods is called directly, which may then mean that type errors\nelsewhere in your code go undetected by ty.\n\nCalling abstract classmethods or staticmethods via `type[X]` is allowed,\nsince the actual runtime type could be a concrete subclass with an implementation.\n\n## Example\n```python\nfrom abc import ABC, abstractmethod\n\nclass Foo(ABC):\n @classmethod\n @abstractmethod\n def method(cls) -> int: ...\n\nFoo.method() # Error: cannot call abstract classmethod\n```", @@ -584,16 +574,6 @@ } ] }, - "fstring-type-annotation": { - "title": "detects F-strings in type annotation positions", - "description": "## What it does\nChecks for f-strings in type annotation positions.\n\n## Why is this bad?\nStatic analysis tools like ty can't analyze type annotations that use f-string notation.\n\n## Examples\n```python\ndef test(): -> f\"int\":\n ...\n```\n\nUse instead:\n```python\ndef test(): -> \"int\":\n ...\n```", - "default": "error", - "oneOf": [ - { - "$ref": "#/definitions/Level" - } - ] - }, "ignore-comment-unknown-rule": { "title": "detects `ty: ignore` comments that reference unknown rules", "description": "## What it does\nChecks for `ty: ignore[code]` or `type: ignore[ty:code]` comments where `code` isn't a known lint rule.\n\n## Why is this bad?\nA `ty: ignore[code]` or a `type:ignore[ty:code] directive with a `code` that doesn't match\nany known rule will not suppress any type errors, and is probably a mistake.\n\n## Examples\n```py\na = 20 / 0 # ty: ignore[division-by-zer]\n```\n\nUse instead:\n\n```py\na = 20 / 0 # ty: ignore[division-by-zero]\n```",