From 744a2ac059bfa5ac8b26045d35888ea8e7238627 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Tue, 25 Aug 2026 12:43:48 +0100 Subject: [PATCH 1/3] [ty] Add more autofixes --- .../resources/mdtest/annotations/callable.md | 69 ++++ .../resources/mdtest/annotations/invalid.md | 330 ++++++++++++++++++ .../resources/mdtest/call/builtins.md | 35 ++ .../diagnostics/unresolved_reference.md | 20 ++ ...iagno\342\200\246_(a97274530a7f61c1).snap" | 32 ++ ..._set-\342\200\246_(15737b0beb194b0e).snap" | 16 + ...ed_wh\342\200\246_(ba5cb09eaa3715d8).snap" | 16 + ...sed_w\342\200\246_(f61204fc81905069).snap" | 48 +++ ..._Pyth\342\200\246_(1028a80959504fc9).snap" | 14 + .../ty_python_semantic/src/semantic_model.rs | 34 ++ .../src/types/diagnostic.rs | 24 +- .../src/types/infer/builder.rs | 19 +- .../types/infer/builder/type_expression.rs | 104 +++++- 13 files changed, 754 insertions(+), 7 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/annotations/callable.md b/crates/ty_python_semantic/resources/mdtest/annotations/callable.md index 6eb3680b52c014..270109079d04eb 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 29f633e706b8d2..1f08c7fa18878a 100644 --- a/crates/ty_python_semantic/resources/mdtest/annotations/invalid.md +++ b/crates/ty_python_semantic/resources/mdtest/annotations/invalid.md @@ -572,3 +572,333 @@ def bad( d: "invalid syntax", ): ... ``` + +## 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 +``` + +## 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 +``` diff --git a/crates/ty_python_semantic/resources/mdtest/call/builtins.md b/crates/ty_python_semantic/resources/mdtest/call/builtins.md index 163523ba4b857b..9baa98bec8088a 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 b7a43defddf69d..6deec9ba4c511e 100644 --- a/crates/ty_python_semantic/resources/mdtest/diagnostics/unresolved_reference.md +++ b/crates/ty_python_semantic/resources/mdtest/diagnostics/unresolved_reference.md @@ -32,6 +32,26 @@ foo: List[int] # error: [unresolved-reference] bar: Type # error: [unresolved-reference] ``` +### 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 3984c55baecf65..d1355a60c5fd07 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" index b0bbb1b6f8cdc7..4bb565e209fc58 100644 --- "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" @@ -29,6 +29,14 @@ error[invalid-type-form]: Dict literals are not allowed in parameter annotations | ^^^^^^^^^^ 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}, # error: [invalid-type-form] +2 + x: dict[int, str], # error: [invalid-type-form] +3 | y: {str}, # error: [invalid-type-form] + | +note: This is an unsafe fix and may change runtime behavior ``` @@ -40,5 +48,13 @@ error[invalid-type-form]: Set literals are not allowed in parameter annotations | ^^^^^ 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}, # error: [invalid-type-form] + - y: {str}, # error: [invalid-type-form] +3 + y: set[str], # error: [invalid-type-form] +4 | ): ... + | +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_-_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" index db6a0ecbd5cb6a..951c3d899f1126 100644 --- "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" @@ -35,6 +35,14 @@ error[invalid-type-form]: List literals are not allowed in this context in a par | ^^^^^ 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], # error: [invalid-type-form] +2 + x: list[int], # error: [invalid-type-form] +3 | ) -> [int]: # error: [invalid-type-form] + | +note: This is an unsafe fix and may change runtime behavior ``` @@ -46,6 +54,14 @@ error[invalid-type-form]: List literals are not allowed in this context in a ret | ^^^^^ 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], # error: [invalid-type-form] + - ) -> [int]: # error: [invalid-type-form] +3 + ) -> list[int]: # error: [invalid-type-form] +4 | return x + | +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_-_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" index 68d261cc833f78..e31feee05b74ac 100644 --- "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" @@ -37,6 +37,14 @@ error[invalid-type-form]: Tuple literals are not allowed in this context in a pa | ^^ 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: (), # error: [invalid-type-form] +2 + x: tuple[()], # error: [invalid-type-form] +3 | ) -> (): # error: [invalid-type-form] + | +note: This is an unsafe fix and may change runtime behavior ``` @@ -48,6 +56,14 @@ error[invalid-type-form]: Tuple literals are not allowed in this context in a re | ^^ 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: (), # error: [invalid-type-form] + - ) -> (): # error: [invalid-type-form] +3 + ) -> tuple[()]: # error: [invalid-type-form] +4 | return x + | +note: This is an unsafe fix and may change runtime behavior ``` @@ -59,6 +75,14 @@ error[invalid-type-form]: Tuple literals are not allowed in this context in a pa | ^^^^^^ 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,), # error: [invalid-type-form] +6 + x: tuple[int], # error: [invalid-type-form] +7 | ) -> (int,): # error: [invalid-type-form] + | +note: This is an unsafe fix and may change runtime behavior ``` @@ -70,6 +94,14 @@ error[invalid-type-form]: Tuple literals are not allowed in this context in a re | ^^^^^^ 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,), # error: [invalid-type-form] + - ) -> (int,): # error: [invalid-type-form] +7 + ) -> tuple[int]: # error: [invalid-type-form] +8 | return x + | +note: This is an unsafe fix and may change runtime behavior ``` @@ -81,6 +113,14 @@ error[invalid-type-form]: Tuple literals are not allowed in this context in a pa | ^^^^^^^^^^ 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), # error: [invalid-type-form] +10 + x: tuple[int, str], # error: [invalid-type-form] +11 | ) -> (int, str): # error: [invalid-type-form] + | +note: This is an unsafe fix and may change runtime behavior ``` @@ -92,5 +132,13 @@ error[invalid-type-form]: Tuple literals are not allowed in this context in a re | ^^^^^^^^^^ 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), # error: [invalid-type-form] + - ) -> (int, str): # error: [invalid-type-form] +11 + ) -> tuple[int, str]: # error: [invalid-type-form] +12 | return x + | +note: This is an unsafe fix and may change runtime behavior ``` 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" index cc2b3758fd563c..b49144625466c8 100644 --- "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" @@ -25,6 +25,13 @@ error[unresolved-reference]: Name `List` used when not defined | 1 | foo: List[int] # error: [unresolved-reference] | ^^^^ Did you mean `list`? +help: Replace with `list` + | + - foo: List[int] # error: [unresolved-reference] +1 + foo: list[int] # error: [unresolved-reference] +2 | bar: Type # error: [unresolved-reference] + | +note: This is an unsafe fix and may change runtime behavior ``` @@ -34,5 +41,12 @@ error[unresolved-reference]: Name `Type` used when not defined | 2 | bar: Type # error: [unresolved-reference] | ^^^^ Did you mean `type`? +help: Replace with `type` + | +1 | foo: List[int] # error: [unresolved-reference] + - bar: Type # error: [unresolved-reference] +2 + bar: type # error: [unresolved-reference] + | +note: This is an unsafe fix and may change runtime behavior ``` diff --git a/crates/ty_python_semantic/src/semantic_model.rs b/crates/ty_python_semantic/src/semantic_model.rs index c6feb53ec200cb..82bb7393728fbe 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 6abff15c094649..da0c5d8846c7af 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 b2e8c2bf46331d..7d013326501da7 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 487265cc8182ab..e1ee4bdfd8ff5f 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,11 @@ 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::{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 +30,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 +558,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 +605,35 @@ 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()) + { + diagnostic.set_fix(Fix::unsafe_edits( + Edit::range_replacement( + "tuple[".to_string(), + TextRange::new(tuple.start(), first_elt.start()), + ), + [Edit::range_replacement( + "]".to_string(), + TextRange::new(last_elt.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 +782,30 @@ 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()) + { + diagnostic.help("Replace with `dict[...]`"); + diagnostic.set_fix(Fix::unsafe_edits( + Edit::range_replacement( + "dict[".to_string(), + TextRange::new(dict.start(), key.start()), + ), + [ + Edit::range_replacement( + ", ".to_string(), + TextRange::new(key.end(), value.start()), + ), + Edit::range_replacement( + "]".to_string(), + TextRange::new(value.end(), dict.end()), + ), + ], + )); + } } Type::unknown() } @@ -764,6 +834,26 @@ 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()) + { + diagnostic.help("Replace with `set[...]`"); + diagnostic.set_fix(Fix::unsafe_edits( + Edit::range_replacement( + "set[".to_string(), + TextRange::new(set.start(), single_element.start()), + ), + [Edit::range_replacement( + "]".to_string(), + TextRange::new(single_element.end(), set.end()), + )], + )); + } } Type::unknown() } @@ -1966,6 +2056,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( From 76ef5ae015305c538ea4db7d64ca99f26d749878 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Tue, 25 Aug 2026 19:14:46 +0100 Subject: [PATCH 2/3] [ty] Preserve parentheses in collection literal autofixes --- .../resources/mdtest/annotations/invalid.md | 558 ++++++++++++++++-- ..._set-\342\200\246_(15737b0beb194b0e).snap" | 60 -- ...ed_wh\342\200\246_(ba5cb09eaa3715d8).snap" | 88 --- ...sed_w\342\200\246_(f61204fc81905069).snap" | 144 ----- .../types/infer/builder/type_expression.rs | 39 +- 5 files changed, 550 insertions(+), 339 deletions(-) delete mode 100644 "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" delete mode 100644 "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" delete mode 100644 "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" diff --git a/crates/ty_python_semantic/resources/mdtest/annotations/invalid.md b/crates/ty_python_semantic/resources/mdtest/annotations/invalid.md index 1f08c7fa18878a..4f1db6a01e7bdc 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,78 +502,492 @@ 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 -# No special hints for these: it's unclear what the user meant: + +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. + +```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 ): ... ``` -### Special-cased diagnostic for `callable` used in a type expression +```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 -# error: [invalid-type-form] -# error: [invalid-type-form] -def decorator(fn: callable) -> callable: - return fn +# 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 ``` -### AST nodes that are only valid inside `Literal` +```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 -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", -): ... +# 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 +#### 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. @@ -627,7 +1041,7 @@ 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 +#### 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. @@ -673,7 +1087,7 @@ 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 +#### 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. @@ -782,7 +1196,7 @@ 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 +#### 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`. @@ -812,7 +1226,43 @@ help: Replace with `set[...]` note: This is an unsafe fix and may change runtime behavior ``` -## Collection literal fixes with project-level builtin overrides +#### 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. @@ -854,7 +1304,7 @@ note: This is an unsafe fix and may change runtime behavior list: object ``` -## Collection literal fixes are omitted in string annotations +#### 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. @@ -902,3 +1352,31 @@ error[invalid-type-form]: Set literals are not allowed in type expressions 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] +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/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 4bb565e209fc58..00000000000000 --- "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,60 +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 -help: Replace with `dict[...]` - | -1 | def _( - - x: {int: str}, # error: [invalid-type-form] -2 + x: dict[int, str], # error: [invalid-type-form] -3 | y: {str}, # error: [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}, # 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 -help: Replace with `set[...]` - | -2 | x: {int: str}, # error: [invalid-type-form] - - y: {str}, # error: [invalid-type-form] -3 + y: set[str], # error: [invalid-type-form] -4 | ): ... - | -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_-_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 951c3d899f1126..00000000000000 --- "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,88 +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 -help: Replace with `list[...]` - | -1 | def _( - - x: [int], # error: [invalid-type-form] -2 + x: list[int], # error: [invalid-type-form] -3 | ) -> [int]: # error: [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]: # 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 -help: Replace with `list[...]` - | -2 | x: [int], # error: [invalid-type-form] - - ) -> [int]: # error: [invalid-type-form] -3 + ) -> list[int]: # error: [invalid-type-form] -4 | return x - | -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 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 e31feee05b74ac..00000000000000 --- "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,144 +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 -help: Replace with `tuple[...]` - | -1 | def _( - - x: (), # error: [invalid-type-form] -2 + x: tuple[()], # error: [invalid-type-form] -3 | ) -> (): # error: [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 | ) -> (): # 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 -help: Replace with `tuple[...]` - | -2 | x: (), # error: [invalid-type-form] - - ) -> (): # error: [invalid-type-form] -3 + ) -> tuple[()]: # error: [invalid-type-form] -4 | return x - | -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 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 -help: Replace with `tuple[...]` - | -5 | def _( - - x: (int,), # error: [invalid-type-form] -6 + x: tuple[int], # error: [invalid-type-form] -7 | ) -> (int,): # error: [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,): # 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 -help: Replace with `tuple[...]` - | -6 | x: (int,), # error: [invalid-type-form] - - ) -> (int,): # error: [invalid-type-form] -7 + ) -> tuple[int]: # error: [invalid-type-form] -8 | return x - | -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 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 -help: Replace with `tuple[...]` - | -9 | def _( - - x: (int, str), # error: [invalid-type-form] -10 + x: tuple[int, str], # error: [invalid-type-form] -11 | ) -> (int, str): # error: [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): # 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 -help: Replace with `tuple[...]` - | -10 | x: (int, str), # error: [invalid-type-form] - - ) -> (int, str): # error: [invalid-type-form] -11 + ) -> tuple[int, str]: # error: [invalid-type-form] -12 | return x - | -note: This is an unsafe fix and may change runtime behavior - -``` 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 e1ee4bdfd8ff5f..5251d3424e6d4f 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 @@ -3,6 +3,7 @@ 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_source_file::LineRanges; use ruff_text_size::{Ranged, TextRange}; @@ -617,14 +618,26 @@ impl<'db> TypeInferenceBuilder<'db, '_> { 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_elt.start()), + TextRange::new(tuple.start(), first_range.start()), ), [Edit::range_replacement( "]".to_string(), - TextRange::new(last_elt.end(), tuple.end()), + TextRange::new(last_range.end(), tuple.end()), )], )); } else { @@ -788,20 +801,26 @@ impl<'db> TypeInferenceBuilder<'db, '_> { && 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.start()), + TextRange::new(dict.start(), key_range.start()), ), [ Edit::range_replacement( ", ".to_string(), - TextRange::new(key.end(), value.start()), + TextRange::new(key_range.end(), value_range.start()), ), Edit::range_replacement( "]".to_string(), - TextRange::new(value.end(), dict.end()), + TextRange::new(value_range.end(), dict.end()), ), ], )); @@ -842,15 +861,21 @@ impl<'db> TypeInferenceBuilder<'db, '_> { && 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(), single_element.start()), + TextRange::new(set.start(), element_range.start()), ), [Edit::range_replacement( "]".to_string(), - TextRange::new(single_element.end(), set.end()), + TextRange::new(element_range.end(), set.end()), )], )); } From f147051014120fb537c478bb6d8c5c933535a897 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Tue, 25 Aug 2026 19:22:00 +0100 Subject: [PATCH 3/3] [ty] Use inline snapshots for builtin replacement diagnostics --- .../diagnostics/unresolved_reference.md | 35 +++++++++++-- ..._Pyth\342\200\246_(1028a80959504fc9).snap" | 52 ------------------- 2 files changed, 31 insertions(+), 56 deletions(-) delete mode 100644 "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" 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 6deec9ba4c511e..11107d7eeadb9e 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,43 @@ 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 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 b49144625466c8..00000000000000 --- "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,52 +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`? -help: Replace with `list` - | - - foo: List[int] # error: [unresolved-reference] -1 + foo: list[int] # error: [unresolved-reference] -2 | bar: Type # error: [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 # error: [unresolved-reference] - | ^^^^ Did you mean `type`? -help: Replace with `type` - | -1 | foo: List[int] # error: [unresolved-reference] - - bar: Type # error: [unresolved-reference] -2 + bar: type # error: [unresolved-reference] - | -note: This is an unsafe fix and may change runtime behavior - -```