[ty] Add Literal fixes for diagnostics flagging invalid type annotations (#28185)

## Summary

Add unsafe fixes to diagnostics that suggest `Literal`: wrap integer,
bytes, and boolean literals and strings that cannot be parsed as forward
annotations in `Literal[...]`, and replace parameterized `LiteralString`
with `Literal` when its arguments are valid literal parameters.

The fixes preserve literal spelling and reuse unshadowed import aliases.
They import from `typing` on Python 3.8+, or from `typing_extensions` on
Python 3.7 when it is a declared direct dependency and the installed
module exports `Literal`. The typing-module availability checks are
shared with the `reveal_type` and `assert_never` fixes.
This commit is contained in:
Alex Waygood authored and GitHub committed 2026-09-06 08:35:58 +00:00
1 parent 5804ffddcd
commit 4fc78f66fd
9 files changed
+731 -137

No files matched your search

+4 -4
View File
@@ -1004,7 +1004,7 @@ def bar() -> str: # error: [empty-body]
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20%22escape-character-in-forward-annotation%22" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fstring_annotation.rs#L40" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fstring_annotation.rs#L41" target="_blank">View source</a>
</small>
@@ -1178,7 +1178,7 @@ a = 20 / 0 # ty: ignore[division-by-zero]
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20%22implicit-concatenated-string-type-annotation%22" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fstring_annotation.rs#L22" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fstring_annotation.rs#L23" target="_blank">View source</a>
</small>
@@ -3063,7 +3063,7 @@ super(B, A) # error
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20%22invalid-syntax-in-forward-annotation%22" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fstring_annotation.rs#L31" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fstring_annotation.rs#L32" target="_blank">View source</a>
</small>
@@ -4689,7 +4689,7 @@ argument, or explicitly configure the model with `extra="allow"`.
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20%22raw-string-type-annotation%22" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fstring_annotation.rs#L13" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fstring_annotation.rs#L14" target="_blank">View source</a>
</small>
@@ -1366,17 +1366,498 @@ def decorator(fn: callable) -> callable:
### AST nodes that are only valid inside `Literal`
<!-- snapshot-diagnostics -->
An annotation such as `x: 42` is invalid. To restrict `x` to the value `42`, a user must write
`x: Literal[42]` instead. The same applies to bytes and boolean literals. We offer an unsafe fix
that wraps these values in `Literal[...]`, importing `Literal` if needed. We also offer this fix for
a string that cannot be parsed as a forward annotation.
```toml
[environment]
python-version = "3.8"
```
#### Literal values
`Literal` can be imported from `typing` starting with Python 3.8.
```py
def bad(
# error: [invalid-type-form]
# snapshot: invalid-type-form
a: 42,
# error: [invalid-type-form]
# snapshot: invalid-type-form
b: b"42",
# error: [invalid-type-form]
# snapshot: invalid-type-form
c: True,
# error: [invalid-syntax-in-forward-annotation]
# snapshot: invalid-syntax-in-forward-annotation
d: "invalid syntax",
): ...
```
```snapshot
error[invalid-type-form]: Int literals are not allowed in this context in a parameter annotation
--> src/mdtest_snippet.py:3:8
|
3 | a: 42,
| ^^ Did you mean `typing.Literal[42]`?
info: See the following page for a reference on valid type expressions:
info: https://typing.python.org/en/latest/spec/annotations.html#type-and-annotation-expressions
help: Wrap in `Literal[...]`
|
1 + from typing import Literal
2 | def bad(
3 | # snapshot: invalid-type-form
- a: 42,
4 + a: Literal[42],
5 | # snapshot: invalid-type-form
|
note: This is an unsafe fix and may change runtime behavior
error[invalid-type-form]: Bytes literals are not allowed in this context in a parameter annotation
--> src/mdtest_snippet.py:5:8
|
5 | b: b"42",
| ^^^^^ Did you mean `typing.Literal[b"42"]`?
info: See the following page for a reference on valid type expressions:
info: https://typing.python.org/en/latest/spec/annotations.html#type-and-annotation-expressions
help: Wrap in `Literal[...]`
|
1 + from typing import Literal
2 | def bad(
3 | # snapshot: invalid-type-form
4 | a: 42,
5 | # snapshot: invalid-type-form
- b: b"42",
6 + b: Literal[b"42"],
7 | # snapshot: invalid-type-form
|
note: This is an unsafe fix and may change runtime behavior
error[invalid-type-form]: Boolean literals are not allowed in this context in a parameter annotation
--> src/mdtest_snippet.py:7:8
|
7 | c: True,
| ^^^^ Did you mean `typing.Literal[True]`?
info: See the following page for a reference on valid type expressions:
info: https://typing.python.org/en/latest/spec/annotations.html#type-and-annotation-expressions
help: Wrap in `Literal[...]`
|
1 + from typing import Literal
2 | def bad(
--------------------------------------------------------------------------------
7 | # snapshot: invalid-type-form
- c: True,
8 + c: Literal[True],
9 | # snapshot: invalid-syntax-in-forward-annotation
|
note: This is an unsafe fix and may change runtime behavior
error[invalid-syntax-in-forward-annotation]: Syntax error in forward annotation
--> src/mdtest_snippet.py:9:17
|
9 | d: "invalid syntax",
| --------^^^^^^
| |
| Unexpected token at the end of an expression
help: Did you mean `typing.Literal["invalid syntax"]`?
help: Wrap in `Literal[...]`
|
1 + from typing import Literal
2 | def bad(
--------------------------------------------------------------------------------
9 | # snapshot: invalid-syntax-in-forward-annotation
- d: "invalid syntax",
10 + d: Literal["invalid syntax"],
11 | ): ...
|
note: This is an unsafe fix and may change runtime behavior
```
#### Preserving literal syntax
The fix preserves numeric spelling, bytes escapes, and string delimiters. It also works inside
forward annotations:
```py
def bad(
# snapshot: invalid-type-form
integer: (0x_FF),
# snapshot: invalid-type-form
data: b"\x41",
# snapshot: invalid-type-form
quoted: "False",
# snapshot: invalid-syntax-in-forward-annotation
string: """can't parse this""",
): ...
```
```snapshot
error[invalid-type-form]: Int literals are not allowed in this context in a parameter annotation
--> src/mdtest_snippet.py:3:15
|
3 | integer: (0x_FF),
| ^^^^^ Did you mean `typing.Literal[255]`?
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: Wrap in `Literal[...]`
|
1 + from typing import Literal
2 | def bad(
3 | # snapshot: invalid-type-form
- integer: (0x_FF),
4 + integer: (Literal[0x_FF]),
5 | # snapshot: invalid-type-form
|
note: This is an unsafe fix and may change runtime behavior
error[invalid-type-form]: Bytes literals are not allowed in this context in a parameter annotation
--> src/mdtest_snippet.py:5:11
|
5 | data: b"/x41",
| ^^^^^^^ Did you mean `typing.Literal[b"A"]`?
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: Wrap in `Literal[...]`
|
1 + from typing import Literal
2 | def bad(
3 | # snapshot: invalid-type-form
4 | integer: (0x_FF),
5 | # snapshot: invalid-type-form
- data: b"/x41",
6 + data: Literal[b"/x41"],
7 | # snapshot: invalid-type-form
|
note: This is an unsafe fix and may change runtime behavior
error[invalid-type-form]: Boolean literals are not allowed in this context in a parameter annotation
--> src/mdtest_snippet.py:7:14
|
7 | quoted: "False",
| ^^^^^ Did you mean `typing.Literal[False]`?
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: Wrap in `Literal[...]`
|
1 + from typing import Literal
2 | def bad(
--------------------------------------------------------------------------------
7 | # snapshot: invalid-type-form
- quoted: "False",
8 + quoted: "Literal[False]",
9 | # snapshot: invalid-syntax-in-forward-annotation
|
note: This is an unsafe fix and may change runtime behavior
error[invalid-syntax-in-forward-annotation]: Syntax error in forward annotation
--> src/mdtest_snippet.py:9:19
|
9 | string: """can't parse this""",
| ---^^^^^^^^^^^^^
| |
| missing closing quote in string literal
help: Did you mean `typing.Literal["can't parse this"]`?
help: Wrap in `Literal[...]`
|
1 + from typing import Literal
2 | def bad(
--------------------------------------------------------------------------------
9 | # snapshot: invalid-syntax-in-forward-annotation
- string: """can't parse this""",
10 + string: Literal["""can't parse this"""],
11 | ): ...
|
note: This is an unsafe fix and may change runtime behavior
```
#### Reusing imports
Existing aliases for `Literal` or its module are reused in the replacement.
`member.py`:
```py
from typing import Literal as L
# snapshot: invalid-type-form
value: True
```
```snapshot
error[invalid-type-form]: Boolean literals are not allowed in this context in a type expression
--> src/member.py:4:8
|
4 | value: True
| ^^^^ Did you mean `typing.Literal[True]`?
info: See the following page for a reference on valid type expressions:
info: https://typing.python.org/en/latest/spec/annotations.html#type-and-annotation-expressions
help: Wrap in `Literal[...]`
|
3 | # snapshot: invalid-type-form
- value: True
4 + value: L[True]
|
note: This is an unsafe fix and may change runtime behavior
```
`module.py`:
```py
import typing as t
# snapshot: invalid-type-form
value: 42
```
```snapshot
error[invalid-type-form]: Int literals are not allowed in this context in a type expression
--> src/module.py:4:8
|
4 | value: 42
| ^^ Did you mean `typing.Literal[42]`?
info: See the following page for a reference on valid type expressions:
info: https://typing.python.org/en/latest/spec/annotations.html#type-and-annotation-expressions
help: Wrap in `Literal[...]`
|
3 | # snapshot: invalid-type-form
- value: 42
4 + value: t.Literal[42]
|
note: This is an unsafe fix and may change runtime behavior
```
#### Shadowed import names
When `Literal` is shadowed, the fix uses a qualified import:
```py
def qualified(Literal: int):
# snapshot: invalid-type-form
value: True
```
```snapshot
error[invalid-type-form]: Boolean literals are not allowed in this context in a type expression
--> src/mdtest_snippet.py:3:12
|
3 | value: True
| ^^^^ Did you mean `typing.Literal[True]`?
info: See the following page for a reference on valid type expressions:
info: https://typing.python.org/en/latest/spec/annotations.html#type-and-annotation-expressions
help: Wrap in `Literal[...]`
|
1 + import typing
2 | def qualified(Literal: int):
3 | # snapshot: invalid-type-form
- value: True
4 + value: typing.Literal[True]
5 | def unavailable(Literal: int, typing: int):
|
note: This is an unsafe fix and may change runtime behavior
```
If both names are occupied, no fix is offered:
```py
def unavailable(Literal: int, typing: int):
# snapshot: invalid-type-form
value: False
```
```snapshot
error[invalid-type-form]: Boolean literals are not allowed in this context in a type expression
--> src/mdtest_snippet.py:6:12
|
6 | value: False
| ^^^^^ Did you mean `typing.Literal[False]`?
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
```
#### Python 3.7 without dependency metadata
`Literal` is unavailable in `typing` on Python 3.7. The presence of `typing_extensions` stubs in our
vendored typeshed stubs for the stdlib do not, on their own, justify an autofix that would add an
import from `typing_extensions` -- the package may not be available at runtime:
```toml
[environment]
python-version = "3.7"
```
```py
# snapshot: invalid-type-form
value: True
```
```snapshot
error[invalid-type-form]: Boolean literals are not allowed in this context in a type expression
--> src/mdtest_snippet.py:2:8
|
2 | value: True
| ^^^^ Did you mean `typing.Literal[True]`?
info: See the following page for a reference on valid type expressions:
info: https://typing.python.org/en/latest/spec/annotations.html#type-and-annotation-expressions
```
#### Python 3.7 with a declared backport dependency
If `typing_extensions` is declared as a direct dependency, present in `site-packages`, and the
installed version exports a `Literal` symbol, the fix will import `Literal` from `typing_extensions`
on Python 3.7.
```toml
[environment]
python-version = "3.7"
python = "/.venv"
[dependency-metadata]
projects = [{ path = "/src", dependencies = ["extensions"] }]
[dependency-metadata.distributions]
extensions = { name = "typing-extensions" }
[dependency-metadata.module-owners]
typing_extensions = ["extensions"]
```
##### Available runtime export
The installed version of `typing_extensions` provides `Literal`, so the fix can import it:
`/.venv/<path-to-site-packages>/typing_extensions.py`:
```py
Literal = object()
```
`main.py`:
```py
# snapshot: invalid-type-form
value: True
```
```snapshot
error[invalid-type-form]: Boolean literals are not allowed in this context in a type expression
--> src/main.py:2:8
|
2 | value: True
| ^^^^ Did you mean `typing.Literal[True]`?
info: See the following page for a reference on valid type expressions:
info: https://typing.python.org/en/latest/spec/annotations.html#type-and-annotation-expressions
help: Wrap in `Literal[...]`
|
1 | # snapshot: invalid-type-form
- value: True
2 + from typing_extensions import Literal
3 + value: Literal[True]
|
note: This is an unsafe fix and may change runtime behavior
```
##### Missing runtime export
If the installed version of `typing_extensions` does not expose `Literal`, we don't offer the
autofix even if `Literal` exists in typeshed's stdlib stub for `typing_extensions`:
`/.venv/<path-to-site-packages>/typing_extensions.py`:
```py
```
`main.py`:
```py
# snapshot: invalid-type-form
value: True
```
```snapshot
error[invalid-type-form]: Boolean literals are not allowed in this context in a type expression
--> src/main.py:2:8
|
2 | value: True
| ^^^^ Did you mean `typing.Literal[True]`?
info: See the following page for a reference on valid type expressions:
info: https://typing.python.org/en/latest/spec/annotations.html#type-and-annotation-expressions
```
##### Missing runtime module
`typing_extensions` must be present in `site-packages` as well as declared as a direct dependency
for the autofix to be offered:
`/.venv/<path-to-site-packages>/unrelated.py`:
```py
```
`main.py`:
```py
# snapshot: invalid-type-form
value: True
```
```snapshot
error[invalid-type-form]: Boolean literals are not allowed in this context in a type expression
--> src/main.py:2:8
|
2 | value: True
| ^^^^ Did you mean `typing.Literal[True]`?
info: See the following page for a reference on valid type expressions:
info: https://typing.python.org/en/latest/spec/annotations.html#type-and-annotation-expressions
```
#### Python 3.7 with an indirect backport dependency
It's not sufficient for `typing_extensions` to be installed into `site-packages`. If the project
does not declare a direct dependency on `typing_extensions`, no autofix will be offered. A parent
project's declared dependencies do not apply to a nested project.
```toml
[environment]
python-version = "3.7"
python = "/.venv"
[dependency-metadata]
projects = [
{ path = "/src", dependencies = ["extensions"] },
{ path = "/src/member", dependencies = [] },
]
[dependency-metadata.distributions]
extensions = { name = "typing-extensions" }
[dependency-metadata.module-owners]
typing_extensions = ["extensions"]
```
`/.venv/<path-to-site-packages>/typing_extensions.py`:
```py
Literal = object()
```
`member/main.py`:
```py
# snapshot: invalid-type-form
value: True
```
```snapshot
error[invalid-type-form]: Boolean literals are not allowed in this context in a type expression
--> src/member/main.py:2:8
|
2 | value: True
| ^^^^ Did you mean `typing.Literal[True]`?
info: See the following page for a reference on valid type expressions:
info: https://typing.python.org/en/latest/spec/annotations.html#type-and-annotation-expressions
```
@@ -31,7 +31,8 @@ bad_nesting: Literal[LiteralString] # error: [invalid-type-form]
### Parameterized
`LiteralString` cannot be parameterized.
`LiteralString` cannot be parameterized. When its arguments are valid literal values, the diagnostic
offers to replace it with `Literal`, importing that name if needed.
```py
from typing_extensions import LiteralString
@@ -61,6 +62,86 @@ error[invalid-type-form]: `LiteralString` expects no type parameter
| -------------^^^^^^^
| |
| Did you mean `Literal`?
help: Replace `LiteralString` with `Literal`
|
1 + from typing import Literal
2 | from typing_extensions import LiteralString
--------------------------------------------------------------------------------
6 | # snapshot: invalid-type-form
- b: LiteralString["foo"]
7 + b: Literal["foo"]
|
note: This is an unsafe fix and may change runtime behavior
```
### Literal-valued variables are not valid literal arguments
A variable can have a literal type without being a valid argument to `Literal`. We do not suggest
`Literal` in this case.
```py
from typing_extensions import LiteralString
name = "value"
# snapshot: invalid-type-form
value: LiteralString[name]
```
```snapshot
error[invalid-type-form]: `LiteralString` expects no type parameter
--> src/mdtest_snippet.py:6:8
|
6 | value: LiteralString[name]
| ^^^^^^^^^^^^^^^^^^^
```
### Fixing a parameterized module member
The fix replaces the complete attribute access, preserving the surrounding parentheses, comments,
and arguments.
```toml
[environment]
python-version = "3.11"
```
```py
import typing as t
# snapshot: invalid-type-form
value: (t.LiteralString)[
"first", # A supported value.
b"second",
True,
None,
42,
]
```
```snapshot
error[invalid-type-form]: `LiteralString` expects no type parameter
--> src/mdtest_snippet.py:4:8
|
4 | value: (t.LiteralString)[
| ^--------------- Did you mean `Literal`?
| ________|
| |
5 | | "first", # A supported value.
6 | | b"second",
7 | | True,
8 | | None,
9 | | 42,
10 | | ]
| |_^
help: Replace `LiteralString` with `Literal`
|
3 | # snapshot: invalid-type-form
- value: (t.LiteralString)[
4 + value: (t.Literal)[
5 | "first", # A supported value.
|
note: This is an unsafe fix and may change runtime behavior
```
### Parameterized string annotations
@@ -176,6 +257,17 @@ error[invalid-type-form]: `LiteralString` expects no type parameter
| -------------^^^^^^^
| |
| Did you mean `Literal`?
help: Replace `LiteralString` with `Literal`
|
1 + import typing
2 | from typing_extensions import Literal, LiteralString
--------------------------------------------------------------------------------
6 | # snapshot: invalid-type-form
- alias: "LiteralString[Alias]"
7 + alias: "typing.Literal[Alias]"
8 | MultipleValues = Literal["a", "b"]
|
note: This is an unsafe fix and may change runtime behavior
```
Aliases containing multiple literal values are also valid `Literal` arguments.
@@ -195,6 +287,17 @@ error[invalid-type-form]: `LiteralString` expects no type parameter
| -------------^^^^^^^^^^^^^^^^
| |
| Did you mean `Literal`?
help: Replace `LiteralString` with `Literal`
|
1 + import typing
2 | from typing_extensions import Literal, LiteralString
--------------------------------------------------------------------------------
10 | # snapshot: invalid-type-form
- multiple_values: "LiteralString[MultipleValues]"
11 + multiple_values: "typing.Literal[MultipleValues]"
12 | value = "value"
|
note: This is an unsafe fix and may change runtime behavior
```
Ordinary variables are not valid `Literal` arguments, even if their values are strings.
@@ -432,6 +432,18 @@ error[invalid-syntax-in-forward-annotation]: Syntax error in forward annotation
43 | m: "yield 1"
| ^^^^^^^ Yield expression cannot be used here
help: Did you mean `typing.Literal["yield 1"]`?
help: Wrap in `Literal[...]`
|
2 | # error: [invalid-type-form]
3 + from typing import Literal
4 | stringified_fstring_with_conditional: "f'{1 if 1 else 1}'"
--------------------------------------------------------------------------------
43 | # snapshot
- m: "yield 1"
44 + m: Literal["yield 1"]
45 | # snapshot
|
note: This is an unsafe fix and may change runtime behavior
error[invalid-syntax-in-forward-annotation]: Syntax error in forward annotation
@@ -440,6 +452,18 @@ error[invalid-syntax-in-forward-annotation]: Syntax error in forward annotation
45 | n: "yield from 1"
| ^^^^^^^^^^^^ Yield expression cannot be used here
help: Did you mean `typing.Literal["yield from 1"]`?
help: Wrap in `Literal[...]`
|
2 | # error: [invalid-type-form]
3 + from typing import Literal
4 | stringified_fstring_with_conditional: "f'{1 if 1 else 1}'"
--------------------------------------------------------------------------------
45 | # snapshot
- n: "yield from 1"
46 + n: Literal["yield from 1"]
47 | # error: [invalid-type-form]
|
note: This is an unsafe fix and may change runtime behavior
error[invalid-syntax-in-forward-annotation]: Syntax error in forward annotation
@@ -450,6 +474,18 @@ error[invalid-syntax-in-forward-annotation]: Syntax error in forward annotation
| |
| Yield expression cannot be used here
help: Did you mean `typing.Literal["list[yield from 1]"]`?
help: Wrap in `Literal[...]`
|
2 | # error: [invalid-type-form]
3 + from typing import Literal
4 | stringified_fstring_with_conditional: "f'{1 if 1 else 1}'"
--------------------------------------------------------------------------------
55 | # snapshot
- t: "list[yield from 1]"
56 + t: Literal["list[yield from 1]"]
57 | # snapshot
|
note: This is an unsafe fix and may change runtime behavior
error[invalid-syntax-in-forward-annotation]: Syntax error in forward annotation
@@ -460,6 +496,17 @@ error[invalid-syntax-in-forward-annotation]: Syntax error in forward annotation
| |
| Unexpected token at the end of an expression
help: Did you mean `typing.Literal["type]"]`?
help: Wrap in `Literal[...]`
|
2 | # error: [invalid-type-form]
3 + from typing import Literal
4 | stringified_fstring_with_conditional: "f'{1 if 1 else 1}'"
--------------------------------------------------------------------------------
57 | # snapshot
- u: "type]"
58 + u: Literal["type]"]
|
note: This is an unsafe fix and may change runtime behavior
```
## Multi line annotation
@@ -1,73 +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 - AST nodes that are only valid inside `Literal`
mdtest path: crates/ty_python_semantic/resources/mdtest/annotations/invalid.md
---
# Python source files
## mdtest_snippet.py
```
1 | def bad(
2 | # error: [invalid-type-form]
3 | a: 42,
4 | # error: [invalid-type-form]
5 | b: b"42",
6 | # error: [invalid-type-form]
7 | c: True,
8 | # error: [invalid-syntax-in-forward-annotation]
9 | d: "invalid syntax",
10 | ): ...
```
# Diagnostics
```
error[invalid-type-form]: Int literals are not allowed in this context in a parameter annotation
--> src/mdtest_snippet.py:3:8
|
3 | a: 42,
| ^^ Did you mean `typing.Literal[42]`?
info: See the following page for a reference on valid type expressions:
info: https://typing.python.org/en/latest/spec/annotations.html#type-and-annotation-expressions
```
```
error[invalid-type-form]: Bytes literals are not allowed in this context in a parameter annotation
--> src/mdtest_snippet.py:5:8
|
5 | b: b"42",
| ^^^^^ Did you mean `typing.Literal[b"42"]`?
info: See the following page for a reference on valid type expressions:
info: https://typing.python.org/en/latest/spec/annotations.html#type-and-annotation-expressions
```
```
error[invalid-type-form]: Boolean literals are not allowed in this context in a parameter annotation
--> src/mdtest_snippet.py:7:8
|
7 | c: True,
| ^^^^ Did you mean `typing.Literal[True]`?
info: See the following page for a reference on valid type expressions:
info: https://typing.python.org/en/latest/spec/annotations.html#type-and-annotation-expressions
```
```
error[invalid-syntax-in-forward-annotation]: Syntax error in forward annotation
--> src/mdtest_snippet.py:9:17
|
9 | d: "invalid syntax",
| --------^^^^^^
| |
| Unexpected token at the end of an expression
help: Did you mean `typing.Literal["invalid syntax"]`?
```
@@ -377,6 +377,17 @@ error[invalid-type-form]: Int literals are not allowed in this context in a type
| ^^ Did you mean `typing.Literal[42]`?
info: See the following page for a reference on valid type expressions:
info: https://typing.python.org/en/latest/spec/annotations.html#type-and-annotation-expressions
help: Wrap in `Literal[...]`
|
1 + from typing import Literal
2 | from typing_extensions import TypedDict
--------------------------------------------------------------------------------
89 | # error: [invalid-type-form]
- Bad10 = TypedDict("Bad10", {name: 42})
90 + Bad10 = TypedDict("Bad10", {name: Literal[42]})
91 |
|
note: This is an unsafe fix and may change runtime behavior
```
@@ -397,6 +408,17 @@ error[invalid-type-form]: Int literals are not allowed in this context in a type
| ^^ Did you mean `typing.Literal[42]`?
info: See the following page for a reference on valid type expressions:
info: https://typing.python.org/en/latest/spec/annotations.html#type-and-annotation-expressions
help: Wrap in `Literal[...]`
|
1 + from typing import Literal
2 | from typing_extensions import TypedDict
--------------------------------------------------------------------------------
93 | # error: [invalid-type-form] "Int literals are not allowed in this context in a type expression"
- class Bad11(TypedDict("Bad11", {name: 42})): ...
94 + class Bad11(TypedDict("Bad11", {name: Literal[42]})): ...
95 |
|
note: This is an unsafe fix and may change runtime behavior
```
@@ -11,7 +11,7 @@ use super::{
};
use crate::dependency::is_direct_dependency;
use crate::diagnostic::{did_you_mean, format_enumeration};
use crate::importer::{ImportRequest, MembersInScope};
use crate::importer::{ImportAction, ImportRequest, MembersInScope};
use crate::lint::{Level, LintRegistryBuilder, LintStatus};
use crate::place::{DefinedPlace, Place, imported_symbol, place_from_bindings};
use crate::suppression::FileSuppressionId;
@@ -52,7 +52,7 @@ use ruff_python_ast::name::Name;
use ruff_python_ast::token::parentheses_iterator;
use ruff_python_ast::{self as ast, AnyNodeRef, HasNodeIndex, PythonVersion, StringFlags};
use ruff_source_file::LineRanges;
use ruff_text_size::{Ranged, TextRange};
use ruff_text_size::{Ranged, TextRange, TextSize};
use rustc_hash::{FxHashMap, FxHashSet};
use std::fmt::{self, Formatter};
use ty_module_resolver::{
@@ -3103,6 +3103,37 @@ pub(super) fn typing_module_for_fix(
Some(module)
}
pub(super) fn import_literal_for_fix(context: &InferContext, at: TextSize) -> Option<ImportAction> {
let module = typing_module_for_fix(context, "Literal", PythonVersion::PY38)?;
context.importer().import_for_diagnostic(
ImportRequest::import_from(module.as_str(), "Literal"),
context.scope().file_scope_id(context.db()),
at,
)
}
/// Wraps a literal in `Literal[...]`, preserving its spelling, quotes, and escapes.
/// String annotations retain their original source offsets: `parse_string_annotation` rejects
/// contents that require unescaping, and parses accepted strings directly from the source file.
pub(super) fn autofix_with_literal(
context: &InferContext,
diagnostic: &mut Diagnostic,
node: impl Ranged,
) {
let Some(action) = import_literal_for_fix(context, node.start()) else {
return;
};
let source = source_text(context.db(), context.file());
diagnostic.help("Wrap in `Literal[...]`");
diagnostic.set_fix(Fix::unsafe_edits(
Edit::range_replacement(
format!("{}[{}]", action.symbol_text(), &source[node.range()]),
node.range(),
),
action.import().cloned(),
));
}
pub(super) fn report_undefined_reveal(context: &InferContext, name: &ast::ExprName) {
let Some(builder) = context.report_lint(&UNDEFINED_REVEAL, name) else {
return;
@@ -28,9 +28,9 @@ use ty_python_core::scope::ScopeKind;
use crate::types::{
BindingContext, CallableType, DynamicType, GenericContext, IntersectionBuilder,
IntersectionType, InvalidTypeExpression, KnownClass, KnownInstanceType, LintDiagnosticGuard,
LiteralValueTypeKind, Parameter, Parameters, SpecialFormType, SubclassOfType, Type,
TypeContext, TypeFormType, TypeGuardType, TypeIsType, TypeMapping, TypeVarKind, UnionBuilder,
UnionType, any_over_type, todo_type,
Parameter, Parameters, SpecialFormType, SubclassOfType, Type, TypeContext, TypeFormType,
TypeGuardType, TypeIsType, TypeMapping, TypeVarKind, UnionBuilder, UnionType, any_over_type,
todo_type,
};
use crate::{FxOrderSet, SemanticModel, add_inferred_python_version_hint_to_diagnostic};
@@ -473,6 +473,11 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
diagnostic.set_primary_annotation_message(format_args!(
"Did you mean `typing.Literal[b\"{valid_string}\"]`?"
));
diagnostic::autofix_with_literal(
&self.context,
&mut diagnostic,
expression,
);
}
}
Type::unknown()
@@ -493,6 +498,11 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
diagnostic.set_primary_annotation_message(format_args!(
"Did you mean `typing.Literal[{int}]`?"
));
diagnostic::autofix_with_literal(
&self.context,
&mut diagnostic,
expression,
);
}
}
@@ -539,6 +549,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
"Did you mean `typing.Literal[{}]`?",
if bool_value.value { "True" } else { "False" }
));
diagnostic::autofix_with_literal(&self.context, &mut diagnostic, expression);
}
Type::unknown()
}
@@ -2757,59 +2768,16 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
Type::unknown()
}
SpecialFormType::LiteralString => {
let arguments = self.infer_expression(arguments_slice, TypeContext::default());
self.infer_expression(arguments_slice, TypeContext::default());
if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) {
let mut diag =
builder.into_diagnostic("`LiteralString` expects no type parameter");
let argument_elements = if self.in_string_annotation() {
let argument_expressions = match arguments_slice {
ast::Expr::Tuple(tuple) => tuple.elts.as_slice(),
_ => std::slice::from_ref(arguments_slice),
};
let mut builder = self.speculate_without_diagnostics();
argument_expressions
.iter()
.map(|argument| {
builder
.infer_literal_parameter_type(argument)
.unwrap_or(Type::unknown())
})
.collect::<Vec<_>>()
} else {
let arguments_as_tuple = arguments.exact_tuple_instance_spec(db);
arguments_as_tuple.as_ref().map_or_else(
|| vec![arguments],
|tuple| tuple.iter_element_types(db).collect(),
)
};
let probably_meant_literal = argument_elements.into_iter().all(|ty| {
let elements = match ty {
Type::Union(union) => union.elements(db),
_ => std::slice::from_ref(&ty),
};
elements.iter().all(|ty| match ty {
Type::LiteralValue(literal)
if matches!(
literal.kind(),
LiteralValueTypeKind::String(_)
| LiteralValueTypeKind::Bytes(_)
| LiteralValueTypeKind::Enum(_)
| LiteralValueTypeKind::Bool(_)
) =>
{
true
}
Type::NominalInstance(instance) => {
instance.has_known_class(db, KnownClass::NoneType)
}
_ => false,
})
});
if probably_meant_literal {
if self
.speculate_without_diagnostics()
.infer_literal_parameter_type(arguments_slice)
.is_ok()
{
diag.annotate(
self.context
.secondary(&*subscript.value)
@@ -2818,6 +2786,19 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
diag.set_concise_message(
"`LiteralString` expects no type parameter - did you mean `Literal`?",
);
if let Some(action) = diagnostic::import_literal_for_fix(
&self.context,
subscript.value.start(),
) {
diag.help("Replace `LiteralString` with `Literal`");
diag.set_fix(Fix::unsafe_edits(
Edit::range_replacement(
action.symbol_text().to_string(),
subscript.value.range(),
),
action.import().cloned(),
));
}
}
}
Type::unknown()
@@ -6,6 +6,7 @@ use ruff_text_size::Ranged;
use crate::declare_lint;
use crate::lint::{Level, LintStatus};
use crate::types::diagnostic::autofix_with_literal;
use crate::types::infer::InferenceFlags;
use super::context::InferContext;
@@ -102,6 +103,7 @@ pub(crate) fn parse_string_annotation(
"Did you mean `typing.Literal[\"{}\"]`?",
string_literal.as_str()
));
autofix_with_literal(context, &mut diagnostic, string_expr);
}
}
}