mirror of
https://github.com/astral-sh/ruff.git
synced 2026-10-28 10:45:37 +08:00
[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:
1 parent
5804ffddcd
commit
4fc78f66fd
9 files changed
+731
-137
No files matched your search
Generated
+4
-4
@@ -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
|
||||
|
||||
-73
@@ -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"]`?
|
||||
|
||||
```
|
||||
+22
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user