diff --git a/crates/ty_python_semantic/resources/mdtest/annotations/callable.md b/crates/ty_python_semantic/resources/mdtest/annotations/callable.md index 6eb3680b52c01..270109079d04e 100644 --- a/crates/ty_python_semantic/resources/mdtest/annotations/callable.md +++ b/crates/ty_python_semantic/resources/mdtest/annotations/callable.md @@ -69,6 +69,75 @@ def _(c: Callable[[...], int]): reveal_type(c) # revealed: (...) -> int ``` +The invalid parameter list also offers an autofix that replaces the list with an ellipsis. + +```py +def fixable(callback: Callable[[...], int]): ... # snapshot: invalid-type-form +``` + +```snapshot +error[invalid-type-form]: `[...]` is not a valid parameter list for `Callable` + --> src/mdtest_snippet.py:17:32 + | +17 | def fixable(callback: Callable[[...], int]): ... # snapshot: invalid-type-form + | ^^^^^ Did you mean `Callable[..., int]`? +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 +help: Replace `[...]` with `...` + | +16 | reveal_type(c) # revealed: (...) -> int + - def fixable(callback: Callable[[...], int]): ... # snapshot: invalid-type-form +17 + def fixable(callback: Callable[..., int]): ... # snapshot: invalid-type-form +18 | def with_comments( + | +note: This is an unsafe fix and may change runtime behavior +``` + +A multiline parameter list can contain comments, so its brackets are not removed automatically. + +```py +def with_comments( + callback: Callable[ + [ # snapshot: invalid-type-form + # The callable accepts arbitrary arguments. + ..., # The parameter description remains documented. + ], + int, + ], +): ... +``` + +```snapshot +error[invalid-type-form]: `[...]` is not a valid parameter list for `Callable` + --> src/mdtest_snippet.py:20:9 + | +20 | / [ # snapshot: invalid-type-form +21 | | # The callable accepts arbitrary arguments. +22 | | ..., # The parameter description remains documented. +23 | | ], + | |_________^ Did you mean `Callable[..., int]`? +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 +``` + +A quoted callable annotation still receives the diagnostic, but its parsed source range cannot be +rewritten directly. + +```py +# snapshot: invalid-type-form +def quoted(callback: "Callable[[...], int]"): ... +``` + +```snapshot +error[invalid-type-form]: `[...]` is not a valid parameter list for `Callable` + --> src/mdtest_snippet.py:28:32 + | +28 | def quoted(callback: "Callable[[...], int]"): ... + | ^^^^^ Did you mean `Callable[..., int]`? +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 +``` + ```py # error: [invalid-type-form] "`...` is not allowed in this context in a parameter annotation" def _(c: Callable[[int, ...], int]): diff --git a/crates/ty_python_semantic/resources/mdtest/annotations/invalid.md b/crates/ty_python_semantic/resources/mdtest/annotations/invalid.md index 29f633e706b8d..4f1db6a01e7bd 100644 --- a/crates/ty_python_semantic/resources/mdtest/annotations/invalid.md +++ b/crates/ty_python_semantic/resources/mdtest/annotations/invalid.md @@ -472,10 +472,10 @@ class name_4[name_1: [{}]]: ## Diagnostics for common errors - - ### Module-literal used when you meant to use a class from that module + + It's pretty common in Python to accidentally use a module-literal type in a type expression when you *meant* to use a class by the same name that comes from that module. We emit a nice subdiagnostic for this case: @@ -502,55 +502,861 @@ from PIL import Image def g(x: Image): ... # error: [invalid-type-form] ``` -### List-literal used when you meant to use a list +### Collection literals used as type expressions + +Collection literals are not valid type expressions. When the intended collection type is clear, we +suggest a subscripted builtin and offer an unsafe fix when that builtin is available. + +#### List literals + +A list literal with one element suggests a `list` annotation. We offer a fix in both parameter and +return annotations. ```py def _( - x: [int], # error: [invalid-type-form] -) -> [int]: # error: [invalid-type-form] + x: [int], # snapshot: invalid-type-form +) -> [int]: # snapshot: invalid-type-form return x +``` + +```snapshot +error[invalid-type-form]: List literals are not allowed in this context in a parameter annotation + --> src/mdtest_snippet.py:2:8 + | +2 | x: [int], # snapshot: invalid-type-form + | ^^^^^ Did you mean `list[int]`? +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 +help: Replace with `list[...]` + | +1 | def _( + - x: [int], # snapshot: invalid-type-form +2 + x: list[int], # snapshot: invalid-type-form +3 | ) -> [int]: # snapshot: invalid-type-form + | +note: This is an unsafe fix and may change runtime behavior + + +error[invalid-type-form]: List literals are not allowed in this context in a return type annotation + --> src/mdtest_snippet.py:3:6 + | +3 | ) -> [int]: # snapshot: invalid-type-form + | ^^^^^ Did you mean `list[int]`? +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 +help: Replace with `list[...]` + | +2 | x: [int], # snapshot: invalid-type-form + - ) -> [int]: # snapshot: invalid-type-form +3 + ) -> list[int]: # snapshot: invalid-type-form +4 | return x + | +note: This is an unsafe fix and may change runtime behavior +``` + +A list literal with several elements is ambiguous, so we do not suggest a replacement. -# No special hints for these: it's unclear what the user meant: +```py def _( - x: [int, str], # error: [invalid-type-form] -) -> [int, str]: # error: [invalid-type-form] + x: [int, str], # snapshot: invalid-type-form +) -> [int, str]: # snapshot: invalid-type-form return x ``` -### Tuple-literal used when you meant to use a tuple +```snapshot +error[invalid-type-form]: List literals are not allowed in this context in a parameter annotation + --> src/mdtest_snippet.py:6:8 + | +6 | x: [int, str], # snapshot: invalid-type-form + | ^^^^^^^^^^ +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 + + +error[invalid-type-form]: List literals are not allowed in this context in a return type annotation + --> src/mdtest_snippet.py:7:6 + | +7 | ) -> [int, str]: # snapshot: invalid-type-form + | ^^^^^^^^^^ +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 +``` + +#### Tuple literals + +An empty tuple literal suggests `tuple[()]`, the type of an empty tuple. ```py def _( - x: (), # error: [invalid-type-form] -) -> (): # error: [invalid-type-form] + x: (), # snapshot: invalid-type-form +) -> (): # snapshot: invalid-type-form return x ``` +```snapshot +error[invalid-type-form]: Tuple literals are not allowed in this context in a parameter annotation + --> src/mdtest_snippet.py:2:8 + | +2 | x: (), # snapshot: invalid-type-form + | ^^ Did you mean `tuple[()]`? +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 +help: Replace with `tuple[...]` + | +1 | def _( + - x: (), # snapshot: invalid-type-form +2 + x: tuple[()], # snapshot: invalid-type-form +3 | ) -> (): # snapshot: invalid-type-form + | +note: This is an unsafe fix and may change runtime behavior + + +error[invalid-type-form]: Tuple literals are not allowed in this context in a return type annotation + --> src/mdtest_snippet.py:3:6 + | +3 | ) -> (): # snapshot: invalid-type-form + | ^^ Did you mean `tuple[()]`? +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 +help: Replace with `tuple[...]` + | +2 | x: (), # snapshot: invalid-type-form + - ) -> (): # snapshot: invalid-type-form +3 + ) -> tuple[()]: # snapshot: invalid-type-form +4 | return x + | +note: This is an unsafe fix and may change runtime behavior +``` + +A tuple literal with one element suggests a fixed-length tuple with one element. + ```py def _( - x: (int,), # error: [invalid-type-form] -) -> (int,): # error: [invalid-type-form] + x: (int,), # snapshot: invalid-type-form +) -> (int,): # snapshot: invalid-type-form return x ``` +```snapshot +error[invalid-type-form]: Tuple literals are not allowed in this context in a parameter annotation + --> src/mdtest_snippet.py:6:8 + | +6 | x: (int,), # snapshot: invalid-type-form + | ^^^^^^ Did you mean `tuple[int]`? +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 +help: Replace with `tuple[...]` + | +5 | def _( + - x: (int,), # snapshot: invalid-type-form +6 + x: tuple[int], # snapshot: invalid-type-form +7 | ) -> (int,): # snapshot: invalid-type-form + | +note: This is an unsafe fix and may change runtime behavior + + +error[invalid-type-form]: Tuple literals are not allowed in this context in a return type annotation + --> src/mdtest_snippet.py:7:6 + | +7 | ) -> (int,): # snapshot: invalid-type-form + | ^^^^^^ Did you mean `tuple[int]`? +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 +help: Replace with `tuple[...]` + | +6 | x: (int,), # snapshot: invalid-type-form + - ) -> (int,): # snapshot: invalid-type-form +7 + ) -> tuple[int]: # snapshot: invalid-type-form +8 | return x + | +note: This is an unsafe fix and may change runtime behavior +``` + +A tuple literal with several elements suggests a fixed-length tuple with the corresponding element +types. + ```py def _( - x: (int, str), # error: [invalid-type-form] -) -> (int, str): # error: [invalid-type-form] + x: (int, str), # snapshot: invalid-type-form +) -> (int, str): # snapshot: invalid-type-form return x ``` -### Dict-literal or set-literal when you meant to use `dict[]`/`set[]` +```snapshot +error[invalid-type-form]: Tuple literals are not allowed in this context in a parameter annotation + --> src/mdtest_snippet.py:10:8 + | +10 | x: (int, str), # snapshot: invalid-type-form + | ^^^^^^^^^^ Did you mean `tuple[int, str]`? +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 +help: Replace with `tuple[...]` + | +9 | def _( + - x: (int, str), # snapshot: invalid-type-form +10 + x: tuple[int, str], # snapshot: invalid-type-form +11 | ) -> (int, str): # snapshot: invalid-type-form + | +note: This is an unsafe fix and may change runtime behavior + + +error[invalid-type-form]: Tuple literals are not allowed in this context in a return type annotation + --> src/mdtest_snippet.py:11:6 + | +11 | ) -> (int, str): # snapshot: invalid-type-form + | ^^^^^^^^^^ Did you mean `tuple[int, str]`? +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 +help: Replace with `tuple[...]` + | +10 | x: (int, str), # snapshot: invalid-type-form + - ) -> (int, str): # snapshot: invalid-type-form +11 + ) -> tuple[int, str]: # snapshot: invalid-type-form +12 | return x + | +note: This is an unsafe fix and may change runtime behavior +``` + +#### Dict and set literals + +A dictionary literal with one entry suggests `dict[Key, Value]`, and a set literal with one element +suggests `set[Element]`. ```py def _( - x: {int: str}, # error: [invalid-type-form] - y: {str}, # error: [invalid-type-form] + x: {int: str}, # snapshot: invalid-type-form + y: {str}, # snapshot: invalid-type-form ): ... ``` +```snapshot +error[invalid-type-form]: Dict literals are not allowed in parameter annotations + --> src/mdtest_snippet.py:2:8 + | +2 | x: {int: str}, # snapshot: invalid-type-form + | ^^^^^^^^^^ Did you mean `dict[int, str]`? +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 +help: Replace with `dict[...]` + | +1 | def _( + - x: {int: str}, # snapshot: invalid-type-form +2 + x: dict[int, str], # snapshot: invalid-type-form +3 | y: {str}, # snapshot: invalid-type-form + | +note: This is an unsafe fix and may change runtime behavior + + +error[invalid-type-form]: Set literals are not allowed in parameter annotations + --> src/mdtest_snippet.py:3:8 + | +3 | y: {str}, # snapshot: invalid-type-form + | ^^^^^ Did you mean `set[str]`? +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 +help: Replace with `set[...]` + | +2 | x: {int: str}, # snapshot: invalid-type-form + - y: {str}, # snapshot: invalid-type-form +3 + y: set[str], # snapshot: invalid-type-form +4 | ): ... + | +note: This is an unsafe fix and may change runtime behavior +``` + +#### Parenthesized collection elements + +Rewriting a tuple literal preserves parentheses around its first and last elements, including nested +parentheses and parentheses around the entire tuple. + +```py +# fmt: off +first: ((int), str) # snapshot: invalid-type-form +last: (int, (str)) # snapshot: invalid-type-form +single: (((int)),) # snapshot: invalid-type-form +outer: (((int), (str))) # snapshot: invalid-type-form +``` + +```snapshot +error[invalid-type-form]: Tuple literals are not allowed in this context in a type expression + --> src/mdtest_snippet.py:2:8 + | +2 | first: ((int), str) # snapshot: invalid-type-form + | ^^^^^^^^^^^^ Did you mean `tuple[int, str]`? +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 +help: Replace with `tuple[...]` + | +1 | # fmt: off + - first: ((int), str) # snapshot: invalid-type-form +2 + first: tuple[(int), str] # snapshot: invalid-type-form +3 | last: (int, (str)) # snapshot: invalid-type-form + | +note: This is an unsafe fix and may change runtime behavior + + +error[invalid-type-form]: Tuple literals are not allowed in this context in a type expression + --> src/mdtest_snippet.py:3:7 + | +3 | last: (int, (str)) # snapshot: invalid-type-form + | ^^^^^^^^^^^^ Did you mean `tuple[int, str]`? +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 +help: Replace with `tuple[...]` + | +2 | first: ((int), str) # snapshot: invalid-type-form + - last: (int, (str)) # snapshot: invalid-type-form +3 + last: tuple[int, (str)] # snapshot: invalid-type-form +4 | single: (((int)),) # snapshot: invalid-type-form + | +note: This is an unsafe fix and may change runtime behavior + + +error[invalid-type-form]: Tuple literals are not allowed in this context in a type expression + --> src/mdtest_snippet.py:4:9 + | +4 | single: (((int)),) # snapshot: invalid-type-form + | ^^^^^^^^^^ Did you mean `tuple[int]`? +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 +help: Replace with `tuple[...]` + | +3 | last: (int, (str)) # snapshot: invalid-type-form + - single: (((int)),) # snapshot: invalid-type-form +4 + single: tuple[((int))] # snapshot: invalid-type-form +5 | outer: (((int), (str))) # snapshot: invalid-type-form + | +note: This is an unsafe fix and may change runtime behavior + + +error[invalid-type-form]: Tuple literals are not allowed in this context in a type expression + --> src/mdtest_snippet.py:5:9 + | +5 | outer: (((int), (str))) # snapshot: invalid-type-form + | ^^^^^^^^^^^^^^ Did you mean `tuple[int, str]`? +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 +help: Replace with `tuple[...]` + | +4 | single: (((int)),) # snapshot: invalid-type-form + - outer: (((int), (str))) # snapshot: invalid-type-form +5 + outer: (tuple[(int), (str)]) # snapshot: invalid-type-form +6 | # fmt: off + | +note: This is an unsafe fix and may change runtime behavior +``` + +Dictionary keys and values, set elements, and list elements also retain their parentheses. + +```py +# fmt: off +key: {(int): str} # snapshot: invalid-type-form +value: {int: ((str)),} # snapshot: invalid-type-form +items: {((int)),} # snapshot: invalid-type-form +values: [((int))] # snapshot: invalid-type-form +``` + +```snapshot +error[invalid-type-form]: Dict literals are not allowed in type expressions + --> src/mdtest_snippet.py:7:6 + | +7 | key: {(int): str} # snapshot: invalid-type-form + | ^^^^^^^^^^^^ Did you mean `dict[int, str]`? +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 +help: Replace with `dict[...]` + | +6 | # fmt: off + - key: {(int): str} # snapshot: invalid-type-form +7 + key: dict[(int), str] # snapshot: invalid-type-form +8 | value: {int: ((str)),} # snapshot: invalid-type-form + | +note: This is an unsafe fix and may change runtime behavior + + +error[invalid-type-form]: Dict literals are not allowed in type expressions + --> src/mdtest_snippet.py:8:8 + | +8 | value: {int: ((str)),} # snapshot: invalid-type-form + | ^^^^^^^^^^^^^^^ Did you mean `dict[int, str]`? +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 +help: Replace with `dict[...]` + | +7 | key: {(int): str} # snapshot: invalid-type-form + - value: {int: ((str)),} # snapshot: invalid-type-form +8 + value: dict[int, ((str))] # snapshot: invalid-type-form +9 | items: {((int)),} # snapshot: invalid-type-form + | +note: This is an unsafe fix and may change runtime behavior + + +error[invalid-type-form]: Set literals are not allowed in type expressions + --> src/mdtest_snippet.py:9:8 + | +9 | items: {((int)),} # snapshot: invalid-type-form + | ^^^^^^^^^^ Did you mean `set[int]`? +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 +help: Replace with `set[...]` + | +8 | value: {int: ((str)),} # snapshot: invalid-type-form + - items: {((int)),} # snapshot: invalid-type-form +9 + items: set[((int))] # snapshot: invalid-type-form +10 | values: [((int))] # snapshot: invalid-type-form + | +note: This is an unsafe fix and may change runtime behavior + + +error[invalid-type-form]: List literals are not allowed in this context in a type expression + --> src/mdtest_snippet.py:10:9 + | +10 | values: [((int))] # snapshot: invalid-type-form + | ^^^^^^^^^ Did you mean `list[int]`? +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 +help: Replace with `list[...]` + | +9 | items: {((int)),} # snapshot: invalid-type-form + - values: [((int))] # snapshot: invalid-type-form +10 + values: list[((int))] # snapshot: invalid-type-form + | +note: This is an unsafe fix and may change runtime behavior +``` + +#### Required parentheses in collection elements + +Some expressions require parentheses inside a subscript. Although `yield` expressions are not valid +type expressions, the fix preserves their parentheses to avoid introducing a syntax error. + +```py +def generator(): + yielded_key: {(yield int): str} # snapshot: invalid-type-form + yielded_value: {int: (yield str)} # snapshot: invalid-type-form + yielded_element: {(yield int)} # snapshot: invalid-type-form +``` + +```snapshot +error[invalid-type-form]: Dict literals are not allowed in type expressions + --> src/mdtest_snippet.py:2:18 + | +2 | yielded_key: {(yield int): str} # snapshot: invalid-type-form + | ^^^^^^^^^^^^^^^^^^ +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 +help: Replace with `dict[...]` + | +1 | def generator(): + - yielded_key: {(yield int): str} # snapshot: invalid-type-form +2 + yielded_key: dict[(yield int), str] # snapshot: invalid-type-form +3 | yielded_value: {int: (yield str)} # snapshot: invalid-type-form + | +note: This is an unsafe fix and may change runtime behavior + + +error[invalid-type-form]: Dict literals are not allowed in type expressions + --> src/mdtest_snippet.py:3:20 + | +3 | yielded_value: {int: (yield str)} # snapshot: invalid-type-form + | ^^^^^^^^^^^^^^^^^^ +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 +help: Replace with `dict[...]` + | +2 | yielded_key: {(yield int): str} # snapshot: invalid-type-form + - yielded_value: {int: (yield str)} # snapshot: invalid-type-form +3 + yielded_value: dict[int, (yield str)] # snapshot: invalid-type-form +4 | yielded_element: {(yield int)} # snapshot: invalid-type-form + | +note: This is an unsafe fix and may change runtime behavior + + +error[invalid-type-form]: Set literals are not allowed in type expressions + --> src/mdtest_snippet.py:4:22 + | +4 | yielded_element: {(yield int)} # snapshot: invalid-type-form + | ^^^^^^^^^^^^^ +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 +help: Replace with `set[...]` + | +3 | yielded_value: {int: (yield str)} # snapshot: invalid-type-form + - yielded_element: {(yield int)} # snapshot: invalid-type-form +4 + yielded_element: set[(yield int)] # snapshot: invalid-type-form + | +note: This is an unsafe fix and may change runtime behavior +``` + +#### Collection literal fixes require Python 3.9 or later + +Builtin collection types cannot be subscripted on Python 3.8, so their literals do not receive fixes +that would introduce unsupported subscripts. + +```toml +[environment] +python-version = "3.8" +``` + +```py +as_list: [int] # snapshot: invalid-type-form +as_tuple: (int,) # snapshot: invalid-type-form +as_dict: {str: int} # snapshot: invalid-type-form +as_set: {int} # snapshot: invalid-type-form +``` + +```snapshot +error[invalid-type-form]: List literals are not allowed in this context in a type expression + --> src/mdtest_snippet.py:1:10 + | +1 | as_list: [int] # snapshot: invalid-type-form + | ^^^^^ Did you mean `list[int]`? +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 + + +error[invalid-type-form]: Tuple literals are not allowed in this context in a type expression + --> src/mdtest_snippet.py:2:11 + | +2 | as_tuple: (int,) # snapshot: invalid-type-form + | ^^^^^^ Did you mean `tuple[int]`? +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 + + +error[invalid-type-form]: Dict literals are not allowed in type expressions + --> src/mdtest_snippet.py:3:10 + | +3 | as_dict: {str: int} # snapshot: invalid-type-form + | ^^^^^^^^^^ Did you mean `dict[str, int]`? +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 + + +error[invalid-type-form]: Set literals are not allowed in type expressions + --> src/mdtest_snippet.py:4:9 + | +4 | as_set: {int} # snapshot: invalid-type-form + | ^^^^^ Did you mean `set[int]`? +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 +``` + +#### Collection literal fixes are omitted for starred elements + +Starred subscripts are unavailable before Python 3.11, so starred collection elements cannot be +rewritten into subscripts when targeting Python 3.10. + +```toml +[environment] +python-version = "3.10" +``` + +```py +types = (int, str) + +as_list: [*types] # snapshot: invalid-type-form +as_tuple: (*types,) # snapshot: invalid-type-form +as_set: {*types} # snapshot: invalid-type-form +``` + +```snapshot +error[invalid-type-form]: List literals are not allowed in this context in a type expression + --> src/mdtest_snippet.py:3:10 + | +3 | as_list: [*types] # snapshot: invalid-type-form + | ^^^^^^^^ Did you mean `list[tuple[Unknown, ...]]`? +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 + + +error[invalid-type-form]: Tuple literals are not allowed in this context in a type expression + --> src/mdtest_snippet.py:4:11 + | +4 | as_tuple: (*types,) # snapshot: invalid-type-form + | ^^^^^^^^^ Did you mean `tuple[tuple[Unknown, ...]]`? +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 + + +error[invalid-type-form]: Set literals are not allowed in type expressions + --> src/mdtest_snippet.py:5:9 + | +5 | as_set: {*types} # snapshot: invalid-type-form + | ^^^^^^^^ Did you mean `set[tuple[Unknown, ...]]`? +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 +``` + +#### Collection literal fixes are omitted for multiline annotations + +Multiline collection literals can contain comments that would be removed by replacing their +delimiters, so we do not offer an autofix. + +`list.py`: + +```py +values: [ # snapshot: invalid-type-form + # The element must not be discarded. + int, +] +``` + +```snapshot +error[invalid-type-form]: List literals are not allowed in this context in a type expression + --> src/list.py:1:9 + | +1 | values: [ # snapshot: invalid-type-form + | _________^ +2 | | # The element must not be discarded. +3 | | int, +4 | | ] + | |_^ Did you mean `list[int]`? +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 +``` + +The same restriction applies to tuple literals. + +`tuple.py`: + +```py +value: ( # snapshot: invalid-type-form + # The first type remains documented. + int, + str, # The final type remains documented. +) +``` + +```snapshot +error[invalid-type-form]: Tuple literals are not allowed in this context in a type expression + --> src/tuple.py:1:8 + | +1 | value: ( # snapshot: invalid-type-form + | ________^ +2 | | # The first type remains documented. +3 | | int, +4 | | str, # The final type remains documented. +5 | | ) + | |_^ Did you mean `tuple[int, str]`? +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 +``` + +A dictionary literal may have comments around its key, colon, value, or trailing comma. + +`dict.py`: + +```py +mapping: { # snapshot: invalid-type-form + # The key remains documented. + str: # The separator remains documented. + # The value remains documented. + int, # The trailing comma remains documented. +} +``` + +```snapshot +error[invalid-type-form]: Dict literals are not allowed in type expressions + --> src/dict.py:1:10 + | +1 | mapping: { # snapshot: invalid-type-form + | __________^ +2 | | # The key remains documented. +3 | | str: # The separator remains documented. +4 | | # The value remains documented. +5 | | int, # The trailing comma remains documented. +6 | | } + | |_^ Did you mean `dict[str, int]`? +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 +``` + +Set literals can likewise contain comments around their element. + +`set.py`: + +```py +items: { # snapshot: invalid-type-form + # The element remains documented. + int, # The trailing comma remains documented. +} +``` + +```snapshot +error[invalid-type-form]: Set literals are not allowed in type expressions + --> src/set.py:1:8 + | +1 | items: { # snapshot: invalid-type-form + | ________^ +2 | | # The element remains documented. +3 | | int, # The trailing comma remains documented. +4 | | } + | |_^ Did you mean `set[int]`? +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 +``` + +#### Class attributes do not shadow collection builtins in methods + +A class attribute named `set` is not visible when resolving names in a method body, so it does not +prevent an annotation from being rewritten with the builtin `set`. + +```py +class Container: + set = 42 + + def check(self): + value: {int} # snapshot: invalid-type-form +``` + +```snapshot +error[invalid-type-form]: Set literals are not allowed in type expressions + --> src/mdtest_snippet.py:5:16 + | +5 | value: {int} # snapshot: invalid-type-form + | ^^^^^ Did you mean `set[int]`? +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 +help: Replace with `set[...]` + | +4 | def check(self): + - value: {int} # snapshot: invalid-type-form +5 + value: set[int] # snapshot: invalid-type-form + | +note: This is an unsafe fix and may change runtime behavior +``` + +#### Class attributes in nested annotation scopes + +A generic type alias can access attributes of its enclosing class through its type-parameter scope. +A class attribute named `list` therefore shadows the builtin in the alias's value. + +```toml +[environment] +python-version = "3.12" +``` + +```py +class C: + list = 42 + + # TODO: `visible_ancestor_scopes` skips the class through nested annotation scopes, + # so we incorrectly offer a fix that resolves `list` to `C.list`. + type Alias[T] = [int] # snapshot: invalid-type-form +``` + +```snapshot +error[invalid-type-form]: List literals are not allowed in this context in a type alias value + --> src/mdtest_snippet.py:6:21 + | +6 | type Alias[T] = [int] # snapshot: invalid-type-form + | ^^^^^ Did you mean `list[int]`? +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 +help: Replace with `list[...]` + | +5 | # so we incorrectly offer a fix that resolves `list` to `C.list`. + - type Alias[T] = [int] # snapshot: invalid-type-form +6 + type Alias[T] = list[int] # snapshot: invalid-type-form + | +note: This is an unsafe fix and may change runtime behavior +``` + +#### Collection literal fixes with project-level builtin overrides + +A project-level `__builtins__.pyi` can replace `list` while leaving the standard `set` builtin +available. We suppress only the fix that would reference the overridden builtin. + +```py +overridden: [int] # snapshot: invalid-type-form +standard: {int} # snapshot: invalid-type-form +``` + +```snapshot +error[invalid-type-form]: List literals are not allowed in this context in a type expression + --> src/mdtest_snippet.py:1:13 + | +1 | overridden: [int] # snapshot: invalid-type-form + | ^^^^^ Did you mean `list[int]`? +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 + + +error[invalid-type-form]: Set literals are not allowed in type expressions + --> src/mdtest_snippet.py:2:11 + | +2 | standard: {int} # snapshot: invalid-type-form + | ^^^^^ Did you mean `set[int]`? +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 +help: Replace with `set[...]` + | +1 | overridden: [int] # snapshot: invalid-type-form + - standard: {int} # snapshot: invalid-type-form +2 + standard: set[int] # snapshot: invalid-type-form + | +note: This is an unsafe fix and may change runtime behavior +``` + +`__builtins__.pyi`: + +```pyi +list: object +``` + +#### Collection literal fixes are omitted in string annotations + +Collection literals parsed from quoted annotations do not have source ranges that can be rewritten +directly, so their diagnostics do not offer collection-literal fixes. + +```py +quoted_list: "[int]" # snapshot: invalid-type-form +quoted_tuple: "(int, str)" # snapshot: invalid-type-form +quoted_dict: "{int: str}" # snapshot: invalid-type-form +quoted_set: "{int}" # snapshot: invalid-type-form +``` + +```snapshot +error[invalid-type-form]: List literals are not allowed in this context in a type expression + --> src/mdtest_snippet.py:1:15 + | +1 | quoted_list: "[int]" # snapshot: invalid-type-form + | ^^^^^ Did you mean `list[int]`? +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 + + +error[invalid-type-form]: Tuple literals are not allowed in this context in a type expression + --> src/mdtest_snippet.py:2:16 + | +2 | quoted_tuple: "(int, str)" # snapshot: invalid-type-form + | ^^^^^^^^^^ Did you mean `tuple[int, str]`? +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 + + +error[invalid-type-form]: Dict literals are not allowed in type expressions + --> src/mdtest_snippet.py:3:15 + | +3 | quoted_dict: "{int: str}" # snapshot: invalid-type-form + | ^^^^^^^^^^ Did you mean `dict[int, str]`? +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 + + +error[invalid-type-form]: Set literals are not allowed in type expressions + --> src/mdtest_snippet.py:4:14 + | +4 | quoted_set: "{int}" # snapshot: invalid-type-form + | ^^^^^ Did you mean `set[int]`? +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 +``` + ### Special-cased diagnostic for `callable` used in a type expression + + ```py # error: [invalid-type-form] # error: [invalid-type-form] @@ -560,6 +1366,8 @@ def decorator(fn: callable) -> callable: ### AST nodes that are only valid inside `Literal` + + ```py def bad( # error: [invalid-type-form] diff --git a/crates/ty_python_semantic/resources/mdtest/call/builtins.md b/crates/ty_python_semantic/resources/mdtest/call/builtins.md index 163523ba4b857..9baa98bec8088 100644 --- a/crates/ty_python_semantic/resources/mdtest/call/builtins.md +++ b/crates/ty_python_semantic/resources/mdtest/call/builtins.md @@ -460,6 +460,14 @@ error[call-non-callable]: `NotImplemented` is not callable | --------------^^ | | | Did you mean `NotImplementedError`? +help: Use `NotImplementedError` instead + | +2 | # snapshot: call-non-callable + - raise NotImplemented() +3 + raise NotImplementedError() +4 | def _(): + | +note: This is an unsafe fix and may change runtime behavior ``` ```py @@ -476,6 +484,33 @@ error[call-non-callable]: `NotImplemented` is not callable | --------------^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | | | Did you mean `NotImplementedError`? +help: Use `NotImplementedError` instead + | +5 | # snapshot: call-non-callable + - raise NotImplemented("this module is not implemented yet!!!") +6 + raise NotImplementedError("this module is not implemented yet!!!") +7 | def _(NotImplementedError: object): + | +note: This is an unsafe fix and may change runtime behavior +``` + +When a local binding shadows `NotImplementedError`, replacing `NotImplemented` with that name would +not necessarily produce an exception, so we omit the fix. + +```py +def _(NotImplementedError: object): + # snapshot: call-non-callable + raise NotImplemented() +``` + +```snapshot +error[call-non-callable]: `NotImplemented` is not callable + --> src/mdtest_snippet.py:9:11 + | +9 | raise NotImplemented() + | --------------^^ + | | + | Did you mean `NotImplementedError`? ``` ## `map` with generic callbacks diff --git a/crates/ty_python_semantic/resources/mdtest/diagnostics/unresolved_reference.md b/crates/ty_python_semantic/resources/mdtest/diagnostics/unresolved_reference.md index b7a43defddf69..11107d7eeadb9 100644 --- a/crates/ty_python_semantic/resources/mdtest/diagnostics/unresolved_reference.md +++ b/crates/ty_python_semantic/resources/mdtest/diagnostics/unresolved_reference.md @@ -20,16 +20,63 @@ version of Python. (full diagnostic captured in snapshot) ### Info present in Python 3.9+ - - ```toml [environment] python-version = "3.9" ``` ```py -foo: List[int] # error: [unresolved-reference] -bar: Type # error: [unresolved-reference] +foo: List[int] # snapshot: unresolved-reference +bar: Type # snapshot: unresolved-reference +``` + +```snapshot +error[unresolved-reference]: Name `List` used when not defined + --> src/mdtest_snippet.py:1:6 + | +1 | foo: List[int] # snapshot: unresolved-reference + | ^^^^ Did you mean `list`? +help: Replace with `list` + | + - foo: List[int] # snapshot: unresolved-reference +1 + foo: list[int] # snapshot: unresolved-reference +2 | bar: Type # snapshot: unresolved-reference + | +note: This is an unsafe fix and may change runtime behavior + + +error[unresolved-reference]: Name `Type` used when not defined + --> src/mdtest_snippet.py:2:6 + | +2 | bar: Type # snapshot: unresolved-reference + | ^^^^ Did you mean `type`? +help: Replace with `type` + | +1 | foo: List[int] # snapshot: unresolved-reference + - bar: Type # snapshot: unresolved-reference +2 + bar: type # snapshot: unresolved-reference + | +note: This is an unsafe fix and may change runtime behavior +``` + +### Builtin replacement shadowed at module scope + +A module-level binding named `list` also shadows the standard builtin inside a nested function, so +the unresolved `List` annotation cannot safely be replaced with `list`. + +```py +list = object + +def check(): + value: List[int] # snapshot: unresolved-reference +``` + +```snapshot +error[unresolved-reference]: Name `List` used when not defined + --> src/mdtest_snippet.py:4:12 + | +4 | value: List[int] # snapshot: unresolved-reference + | ^^^^ Did you mean `list`? ``` ### Info not present before Python 3.9 diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/basic.md_-_Exception_Handling_-_Special-cased_diagno\342\200\246_(a97274530a7f61c1).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/basic.md_-_Exception_Handling_-_Special-cased_diagno\342\200\246_(a97274530a7f61c1).snap" index 3984c55baecf6..d1355a60c5fd0 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/basic.md_-_Exception_Handling_-_Special-cased_diagno\342\200\246_(a97274530a7f61c1).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/basic.md_-_Exception_Handling_-_Special-cased_diagno\342\200\246_(a97274530a7f61c1).snap" @@ -34,6 +34,14 @@ error[invalid-raise]: Cannot raise `NotImplemented` 4 | raise NotImplemented from NotImplemented | ^^^^^^^^^^^^^^ Did you mean `NotImplementedError`? info: Can only raise an instance or subclass of `BaseException` +help: Use `NotImplementedError` instead + | +3 | # error: [invalid-raise] + - raise NotImplemented from NotImplemented +4 + raise NotImplementedError from NotImplemented +5 | # error: [invalid-exception-caught] + | +note: This is an unsafe fix and may change runtime behavior ``` @@ -43,7 +51,15 @@ error[invalid-raise]: Cannot use `NotImplemented` as an exception cause | 4 | raise NotImplemented from NotImplemented | ^^^^^^^^^^^^^^ Did you mean `NotImplementedError`? +help: Use `NotImplementedError` instead info: An exception cause must be an instance of `BaseException`, subclass of `BaseException`, or `None` + | +3 | # error: [invalid-raise] + - raise NotImplemented from NotImplemented +4 + raise NotImplemented from NotImplementedError +5 | # error: [invalid-exception-caught] + | +note: This is an unsafe fix and may change runtime behavior ``` @@ -53,7 +69,15 @@ error[invalid-exception-caught]: Cannot catch `NotImplemented` in an exception h | 6 | except NotImplemented: | ^^^^^^^^^^^^^^ Did you mean `NotImplementedError`? +help: Use `NotImplementedError` instead info: Can only catch a subclass of `BaseException` or tuple of `BaseException` subclasses + | +5 | # error: [invalid-exception-caught] + - except NotImplemented: +6 + except NotImplementedError: +7 | pass + | +note: This is an unsafe fix and may change runtime behavior ``` @@ -66,6 +90,14 @@ error[invalid-exception-caught]: Invalid tuple caught in an exception handler | | | Invalid element of type `NotImplementedType` | Did you mean `NotImplementedError`? +help: Use `NotImplementedError` instead info: Can only catch a subclass of `BaseException` or tuple of `BaseException` subclasses + | +8 | # error: [invalid-exception-caught] + - except (TypeError, NotImplemented): +9 + except (TypeError, NotImplementedError): +10 | pass + | +note: This is an unsafe fix and may change runtime behavior ``` 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" deleted file mode 100644 index b0bbb1b6f8cdc..0000000000000 --- "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" +++ /dev/null @@ -1,44 +0,0 @@ ---- -source: crates/mdtest/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 parameter annotations - --> src/mdtest_snippet.py:2:8 - | -2 | x: {int: str}, # error: [invalid-type-form] - | ^^^^^^^^^^ Did you mean `dict[int, str]`? -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 - -``` - -``` -error[invalid-type-form]: Set literals are not allowed in parameter annotations - --> src/mdtest_snippet.py:3:8 - | -3 | y: {str}, # error: [invalid-type-form] - | ^^^^^ Did you mean `set[str]`? -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 - -``` 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_-_List-literal_used_wh\342\200\246_(ba5cb09eaa3715d8).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid.md_-_Tests_for_invalid_ty\342\200\246_-_Diagnostics_for_comm\342\200\246_-_List-literal_used_wh\342\200\246_(ba5cb09eaa3715d8).snap" deleted file mode 100644 index db6a0ecbd5cb6..0000000000000 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid.md_-_Tests_for_invalid_ty\342\200\246_-_Diagnostics_for_comm\342\200\246_-_List-literal_used_wh\342\200\246_(ba5cb09eaa3715d8).snap" +++ /dev/null @@ -1,72 +0,0 @@ ---- -source: crates/mdtest/src/lib.rs -expression: snapshot ---- - ---- -mdtest name: invalid.md - Tests for invalid types in type expressions - Diagnostics for common errors - List-literal used when you meant to use a list -mdtest path: crates/ty_python_semantic/resources/mdtest/annotations/invalid.md ---- - -# Python source files - -## mdtest_snippet.py - -``` - 1 | def _( - 2 | x: [int], # error: [invalid-type-form] - 3 | ) -> [int]: # error: [invalid-type-form] - 4 | return x - 5 | - 6 | # No special hints for these: it's unclear what the user meant: - 7 | def _( - 8 | x: [int, str], # error: [invalid-type-form] - 9 | ) -> [int, str]: # error: [invalid-type-form] -10 | return x -``` - -# Diagnostics - -``` -error[invalid-type-form]: List literals are not allowed in this context in a parameter annotation - --> src/mdtest_snippet.py:2:8 - | -2 | x: [int], # error: [invalid-type-form] - | ^^^^^ Did you mean `list[int]`? -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 - -``` - -``` -error[invalid-type-form]: List literals are not allowed in this context in a return type annotation - --> src/mdtest_snippet.py:3:6 - | -3 | ) -> [int]: # error: [invalid-type-form] - | ^^^^^ Did you mean `list[int]`? -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 - -``` - -``` -error[invalid-type-form]: List literals are not allowed in this context in a parameter annotation - --> src/mdtest_snippet.py:8:8 - | -8 | x: [int, str], # error: [invalid-type-form] - | ^^^^^^^^^^ -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 - -``` - -``` -error[invalid-type-form]: List literals are not allowed in this context in a return type annotation - --> src/mdtest_snippet.py:9:6 - | -9 | ) -> [int, str]: # error: [invalid-type-form] - | ^^^^^^^^^^ -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 - -``` 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_-_Tuple-literal_used_w\342\200\246_(f61204fc81905069).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid.md_-_Tests_for_invalid_ty\342\200\246_-_Diagnostics_for_comm\342\200\246_-_Tuple-literal_used_w\342\200\246_(f61204fc81905069).snap" deleted file mode 100644 index 68d261cc833f7..0000000000000 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid.md_-_Tests_for_invalid_ty\342\200\246_-_Diagnostics_for_comm\342\200\246_-_Tuple-literal_used_w\342\200\246_(f61204fc81905069).snap" +++ /dev/null @@ -1,96 +0,0 @@ ---- -source: crates/mdtest/src/lib.rs -expression: snapshot ---- - ---- -mdtest name: invalid.md - Tests for invalid types in type expressions - Diagnostics for common errors - Tuple-literal used when you meant to use a tuple -mdtest path: crates/ty_python_semantic/resources/mdtest/annotations/invalid.md ---- - -# Python source files - -## mdtest_snippet.py - -``` - 1 | def _( - 2 | x: (), # error: [invalid-type-form] - 3 | ) -> (): # error: [invalid-type-form] - 4 | return x - 5 | def _( - 6 | x: (int,), # error: [invalid-type-form] - 7 | ) -> (int,): # error: [invalid-type-form] - 8 | return x - 9 | def _( -10 | x: (int, str), # error: [invalid-type-form] -11 | ) -> (int, str): # error: [invalid-type-form] -12 | return x -``` - -# Diagnostics - -``` -error[invalid-type-form]: Tuple literals are not allowed in this context in a parameter annotation - --> src/mdtest_snippet.py:2:8 - | -2 | x: (), # error: [invalid-type-form] - | ^^ Did you mean `tuple[()]`? -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 - -``` - -``` -error[invalid-type-form]: Tuple literals are not allowed in this context in a return type annotation - --> src/mdtest_snippet.py:3:6 - | -3 | ) -> (): # error: [invalid-type-form] - | ^^ Did you mean `tuple[()]`? -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 - -``` - -``` -error[invalid-type-form]: Tuple literals are not allowed in this context in a parameter annotation - --> src/mdtest_snippet.py:6:8 - | -6 | x: (int,), # error: [invalid-type-form] - | ^^^^^^ Did you mean `tuple[int]`? -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 - -``` - -``` -error[invalid-type-form]: Tuple literals are not allowed in this context in a return type annotation - --> src/mdtest_snippet.py:7:6 - | -7 | ) -> (int,): # error: [invalid-type-form] - | ^^^^^^ Did you mean `tuple[int]`? -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 - -``` - -``` -error[invalid-type-form]: Tuple literals are not allowed in this context in a parameter annotation - --> src/mdtest_snippet.py:10:8 - | -10 | x: (int, str), # error: [invalid-type-form] - | ^^^^^^^^^^ Did you mean `tuple[int, str]`? -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 - -``` - -``` -error[invalid-type-form]: Tuple literals are not allowed in this context in a return type annotation - --> src/mdtest_snippet.py:11:6 - | -11 | ) -> (int, str): # error: [invalid-type-form] - | ^^^^^^^^^^ Did you mean `tuple[int, str]`? -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 - -``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/unresolved_reference\342\200\246_-_Diagnostics_for_unre\342\200\246_-_Typing_builtin_has_I\342\200\246_-_Info_present_in_Pyth\342\200\246_(1028a80959504fc9).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/unresolved_reference\342\200\246_-_Diagnostics_for_unre\342\200\246_-_Typing_builtin_has_I\342\200\246_-_Info_present_in_Pyth\342\200\246_(1028a80959504fc9).snap" deleted file mode 100644 index cc2b3758fd563..0000000000000 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/unresolved_reference\342\200\246_-_Diagnostics_for_unre\342\200\246_-_Typing_builtin_has_I\342\200\246_-_Info_present_in_Pyth\342\200\246_(1028a80959504fc9).snap" +++ /dev/null @@ -1,38 +0,0 @@ ---- -source: crates/mdtest/src/lib.rs -expression: snapshot ---- - ---- -mdtest name: unresolved_reference.md - Diagnostics for unresolved references - Typing builtin has Info help - Info present in Python 3.9+ -mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/unresolved_reference.md ---- - -# Python source files - -## mdtest_snippet.py - -``` -1 | foo: List[int] # error: [unresolved-reference] -2 | bar: Type # error: [unresolved-reference] -``` - -# Diagnostics - -``` -error[unresolved-reference]: Name `List` used when not defined - --> src/mdtest_snippet.py:1:6 - | -1 | foo: List[int] # error: [unresolved-reference] - | ^^^^ Did you mean `list`? - -``` - -``` -error[unresolved-reference]: Name `Type` used when not defined - --> src/mdtest_snippet.py:2:6 - | -2 | bar: Type # error: [unresolved-reference] - | ^^^^ Did you mean `type`? - -``` diff --git a/crates/ty_python_semantic/src/semantic_model.rs b/crates/ty_python_semantic/src/semantic_model.rs index c6feb53ec200c..82bb7393728fb 100644 --- a/crates/ty_python_semantic/src/semantic_model.rs +++ b/crates/ty_python_semantic/src/semantic_model.rs @@ -16,6 +16,7 @@ use ty_module_resolver::{ use crate::Db; use crate::place::implicit_globals::all_implicit_module_globals; +use crate::place::{builtins_module_scope, implicit_builtins_symbol_scope}; use crate::types::ide_support::{ImportAliasResolution, definition_for_name}; use crate::types::list_members::{all_members, all_reachable_members}; use crate::types::{ @@ -89,6 +90,39 @@ impl<'db> SemanticModel<'db> { line_index(self.db, self.file()) } + /// Returns whether `name` refers to a standard builtin in the scope containing `node`. + /// + /// This method uses a simplified implementation of name resolution: any binding or declaration + /// in a visible scope shadows the builtin, even if it does not reach `node`. As a result, it + /// can return `false` when the builtin is actually available. That is acceptable when deciding + /// whether to offer an autofix: we can safely omit the fix in edge cases where resolving the + /// name precisely would require more complex analysis. + /// + /// Definitions in a project-level `__builtins__.pyi` also shadow standard builtins. + pub(crate) fn definitely_has_builtin_binding( + &self, + name: &str, + node: ast::AnyNodeRef<'_>, + ) -> bool { + let index = semantic_index(self.db, self.program_file()); + let Some(scope) = self.scope(node) else { + return false; + }; + + if index.visible_ancestor_scopes(scope).any(|(scope, _)| { + index + .place_table(scope) + .symbol_by_name(name) + .is_some_and(|symbol| symbol.is_bound() || symbol.is_declared()) + }) { + return false; + } + + let env = self.program_environment(); + implicit_builtins_symbol_scope(self.db, &env, name) + .is_some_and(|scope| Some(scope) == builtins_module_scope(self.db, &env)) + } + /// Returns a map from symbol name to that symbol's /// type and definition site (if available). /// diff --git a/crates/ty_python_semantic/src/types/diagnostic.rs b/crates/ty_python_semantic/src/types/diagnostic.rs index 6abff15c09464..da0c5d8846c7a 100644 --- a/crates/ty_python_semantic/src/types/diagnostic.rs +++ b/crates/ty_python_semantic/src/types/diagnostic.rs @@ -32,7 +32,7 @@ use crate::types::{ protocol_class::ProtocolClass, }; use crate::types::{KnownInstanceType, MemberLookupPolicy, TypeVarKind, TypedDictType, UnionType}; -use crate::{Db, DisplaySettings, FxIndexMap, ProgramEnvironment, declare_lint}; +use crate::{Db, DisplaySettings, FxIndexMap, ProgramEnvironment, SemanticModel, declare_lint}; use itertools::Itertools; use ruff_db::source::source_text; use ruff_db::{ @@ -2857,6 +2857,24 @@ pub(super) fn report_possibly_missing_attribute( }; } +/// Add an autofix to `diagnostic` that replaces the given node with `NotImplementedError` +/// iff `NotImplementedError` definitely has a builtin binding from the given scope. +pub(crate) fn autofix_with_notimplementederror( + context: &InferContext, + diagnostic: &mut Diagnostic, + node: &ast::Expr, +) { + if SemanticModel::new(context.db(), context.program_file()) + .definitely_has_builtin_binding("NotImplementedError", node.into()) + { + diagnostic.help("Use `NotImplementedError` instead"); + diagnostic.set_fix(Fix::unsafe_edit(Edit::range_replacement( + "NotImplementedError".to_string(), + node.range(), + ))); + } +} + pub(super) fn report_invalid_exception_tuple_caught<'db, 'ast>( context: &InferContext<'db, 'ast>, node: &'ast ast::ExprTuple, @@ -2885,6 +2903,7 @@ pub(super) fn report_invalid_exception_tuple_caught<'db, 'ast>( diagnostic.annotate( Annotation::secondary(span).message("Did you mean `NotImplementedError`?"), ); + autofix_with_notimplementederror(context, &mut diagnostic, sub_node); } } @@ -2904,6 +2923,7 @@ pub(super) fn report_invalid_exception_caught(context: &InferContext, node: &ast let mut diag = builder.into_diagnostic("Cannot catch `NotImplemented` in an exception handler"); diag.set_primary_annotation_message("Did you mean `NotImplementedError`?"); + autofix_with_notimplementederror(context, &mut diag, node); diag } else { let mut diag = builder.into_diagnostic(format_args!( @@ -2940,6 +2960,7 @@ pub(crate) fn report_invalid_exception_raised( let mut diagnostic = builder.into_diagnostic(format_args!("Cannot raise `NotImplemented`")); diagnostic.set_primary_annotation_message("Did you mean `NotImplementedError`?"); diagnostic.info("Can only raise an instance or subclass of `BaseException`"); + autofix_with_notimplementederror(context, &mut diagnostic, raised_node); } else { let mut diagnostic = builder.into_diagnostic(format_args!( "Cannot raise object of type `{}`", @@ -2960,6 +2981,7 @@ pub(crate) fn report_invalid_exception_cause(context: &InferContext, node: &ast: "Cannot use `NotImplemented` as an exception cause", )); diag.set_primary_annotation_message("Did you mean `NotImplementedError`?"); + autofix_with_notimplementederror(context, &mut diag, node); diag } else { builder.into_diagnostic(format_args!( diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index b2e8c2bf46331..7d013326501da 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -8,6 +8,7 @@ use ruff_db::diagnostic::Span; use ruff_db::files::File; use ruff_db::parsed::ParsedModuleRef; use ruff_db::source::source_text; +use ruff_diagnostics::{Edit, Fix}; use ruff_python_ast::helpers::is_dotted_name; use ruff_python_ast::name::Name; use ruff_python_ast::{ @@ -68,9 +69,9 @@ use crate::types::diagnostic::{ INVALID_TYPE_VARIABLE_DEFAULT, POSSIBLY_MISSING_IMPLICIT_CALL, POSSIBLY_MISSING_SUBMODULE, TypeCheckDiagnostics, UNDEFINED_REVEAL, UNRESOLVED_ATTRIBUTE, UNRESOLVED_GLOBAL, UNRESOLVED_REFERENCE, UNSOUND_ASSIGNMENT, UNSOUND_YIELD, UNSUPPORTED_OPERATOR, - UNUSED_AWAITABLE, YieldKind, hint_if_stdlib_attribute_exists_on_other_versions, - report_attempted_protocol_instantiation, report_bad_dunder_delattr_call, - report_bad_dunder_delete_call, report_call_to_abstract_method, + UNUSED_AWAITABLE, YieldKind, autofix_with_notimplementederror, + hint_if_stdlib_attribute_exists_on_other_versions, report_attempted_protocol_instantiation, + report_bad_dunder_delattr_call, report_bad_dunder_delete_call, report_call_to_abstract_method, report_cannot_pop_required_field_on_typed_dict, report_dynamic_function_decorator_return, report_invalid_assignment, report_invalid_class_match_pattern, report_invalid_exception_caught, report_invalid_exception_cause, report_invalid_exception_raised, @@ -125,7 +126,7 @@ use crate::types::{ any_over_type, binding_type, extract_fixed_length_iterable_element_types, infer_complete_scope_types, infer_scope_types, is_discarded_dict_key_assignment, todo_type, }; -use crate::{AnalysisSettings, Db, DisplaySettings, FxIndexSet, FxOrderSet}; +use crate::{AnalysisSettings, Db, DisplaySettings, FxIndexSet, FxOrderSet, SemanticModel}; use ty_python_core::definition::{ AnnotatedAssignmentDefinitionKind, AssignmentDefinitionKind, BindingsOwner, ComprehensionDefinitionKind, Definition, DefinitionKind, DefinitionNodeKey, DefinitionState, @@ -9080,6 +9081,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { diagnostic.set_concise_message( "`NotImplemented` is not callable - did you mean `NotImplementedError`?", ); + autofix_with_notimplementederror(&self.context, &mut diagnostic, func); } return Type::unknown(); } @@ -10224,6 +10226,15 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { if let Some(("", builtin_name)) = as_pep_585_generic("typing", id) { diagnostic .set_primary_annotation_message(format_args!("Did you mean `{builtin_name}`?")); + if SemanticModel::new(db, self.program_file()) + .definitely_has_builtin_binding(builtin_name, expr_name_node.into()) + { + diagnostic.help(format_args!("Replace with `{builtin_name}`")); + diagnostic.set_fix(Fix::unsafe_edit(Edit::range_replacement( + builtin_name.to_string(), + expr_name_node.range(), + ))); + } } } 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 487265cc8182a..5251d3424e6d4 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 @@ -1,8 +1,12 @@ use itertools::Either; +use ruff_db::source::source_text; +use ruff_diagnostics::{Edit, Fix}; use ruff_python_ast::helpers::is_dotted_name; use ruff_python_ast::name::Name; +use ruff_python_ast::token::parenthesized_range; use ruff_python_ast::{self as ast, PythonVersion}; -use ruff_text_size::Ranged; +use ruff_source_file::LineRanges; +use ruff_text_size::{Ranged, TextRange}; use super::{DeferredExpressionState, TypeInferenceBuilder}; use crate::types::call::CallArguments; @@ -27,7 +31,7 @@ use crate::types::{ TypeGuardType, TypeIsType, TypeMapping, TypeVarKind, UnionBuilder, UnionType, any_over_type, todo_type, }; -use crate::{FxOrderSet, add_inferred_python_version_hint_to_diagnostic}; +use crate::{FxOrderSet, SemanticModel, add_inferred_python_version_hint_to_diagnostic}; /// Type expressions impl<'db> TypeInferenceBuilder<'db, '_> { @@ -555,6 +559,20 @@ impl<'db> TypeInferenceBuilder<'db, '_> { hinted_type.display(db, env), )); } + + if !self.in_string_annotation() + && env.python_version(db) >= PythonVersion::PY39 + && !single_element.is_starred_expr() + && !source_text(db, self.file()).contains_line_break(list.range()) + && SemanticModel::new(db, self.program_file()) + .definitely_has_builtin_binding("list", expression.into()) + { + diagnostic.help("Replace with `list[...]`"); + diagnostic.set_fix(Fix::unsafe_edit(Edit::insertion( + "list".to_string(), + expression.start(), + ))); + } } Type::unknown() } @@ -588,6 +606,47 @@ impl<'db> TypeInferenceBuilder<'db, '_> { hinted_type.display(db, env), )); } + + if !self.in_string_annotation() + && !source_text(db, self.file()).contains_line_break(tuple.range()) + && env.python_version(db) >= PythonVersion::PY39 + && !tuple.elts.iter().any(ast::Expr::is_starred_expr) + && SemanticModel::new(db, self.program_file()) + .definitely_has_builtin_binding("tuple", tuple.into()) + { + diagnostic.help("Replace with `tuple[...]`"); + if let (Some(first_elt), Some(last_elt)) = + (tuple.elts.first(), tuple.elts.last()) + { + let first_range = parenthesized_range( + first_elt.into(), + tuple.into(), + self.module().tokens(), + ) + .unwrap_or(first_elt.range()); + let last_range = parenthesized_range( + last_elt.into(), + tuple.into(), + self.module().tokens(), + ) + .unwrap_or(last_elt.range()); + diagnostic.set_fix(Fix::unsafe_edits( + Edit::range_replacement( + "tuple[".to_string(), + TextRange::new(tuple.start(), first_range.start()), + ), + [Edit::range_replacement( + "]".to_string(), + TextRange::new(last_range.end(), tuple.end()), + )], + )); + } else { + diagnostic.set_fix(Fix::unsafe_edit(Edit::range_replacement( + "tuple[()]".to_string(), + tuple.range(), + ))); + } + } } } else { for element in tuple { @@ -736,6 +795,36 @@ impl<'db> TypeInferenceBuilder<'db, '_> { hinted_type.display(db, env), )); } + if !self.in_string_annotation() + && env.python_version(db) >= PythonVersion::PY39 + && !source_text(db, self.file()).contains_line_break(dict.range()) + && SemanticModel::new(db, self.program_file()) + .definitely_has_builtin_binding("dict", dict.into()) + { + let key_range = + parenthesized_range(key.into(), dict.into(), self.module().tokens()) + .unwrap_or(key.range()); + let value_range = + parenthesized_range(value.into(), dict.into(), self.module().tokens()) + .unwrap_or(value.range()); + diagnostic.help("Replace with `dict[...]`"); + diagnostic.set_fix(Fix::unsafe_edits( + Edit::range_replacement( + "dict[".to_string(), + TextRange::new(dict.start(), key_range.start()), + ), + [ + Edit::range_replacement( + ", ".to_string(), + TextRange::new(key_range.end(), value_range.start()), + ), + Edit::range_replacement( + "]".to_string(), + TextRange::new(value_range.end(), dict.end()), + ), + ], + )); + } } Type::unknown() } @@ -764,6 +853,32 @@ impl<'db> TypeInferenceBuilder<'db, '_> { hinted_type.display(db, env), )); } + + if !self.in_string_annotation() + && env.python_version(db) >= PythonVersion::PY39 + && !single_element.is_starred_expr() + && !source_text(db, self.file()).contains_line_break(set.range()) + && SemanticModel::new(db, self.program_file()) + .definitely_has_builtin_binding("set", set.into()) + { + let element_range = parenthesized_range( + single_element.into(), + set.into(), + self.module().tokens(), + ) + .unwrap_or(single_element.range()); + diagnostic.help("Replace with `set[...]`"); + diagnostic.set_fix(Fix::unsafe_edits( + Edit::range_replacement( + "set[".to_string(), + TextRange::new(set.start(), element_range.start()), + ), + [Edit::range_replacement( + "]".to_string(), + TextRange::new(element_range.end(), set.end()), + )], + )); + } } Type::unknown() } @@ -1966,6 +2081,16 @@ impl<'db> TypeInferenceBuilder<'db, '_> { "Did you mean `Callable[..., {}]`?", returns.display(db, builder.program_environment()) )); + if !builder.in_string_annotation() + && !source_text(db, builder.file()) + .contains_line_break(first_argument.range()) + { + diagnostic.help("Replace `[...]` with `...`"); + diagnostic.set_fix(Fix::unsafe_edit(Edit::range_replacement( + "...".to_string(), + first_argument.range(), + ))); + } } } Type::single_callable(