Skip to content

Commit 34d54b6

Browse files
authored
[ty] Improve consistency and quality of diagnostics relating to invalid type forms (astral-sh#24325)
1 parent da7b958 commit 34d54b6

12 files changed

Lines changed: 396 additions & 329 deletions

‎crates/ty/docs/rules.md‎

Lines changed: 109 additions & 171 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎crates/ty_python_semantic/resources/mdtest/annotations/invalid.md‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -420,6 +420,15 @@ def _(
420420
return x
421421
```
422422

423+
### Dict-literal or set-literal when you meant to use `dict[]`/`set[]`
424+
425+
```py
426+
def _(
427+
x: {int: str}, # error: [invalid-type-form]
428+
y: {str}, # error: [invalid-type-form]
429+
): ...
430+
```
431+
423432
### Special-cased diagnostic for `callable` used in a type expression
424433

425434
```py
@@ -428,3 +437,18 @@ def _(
428437
def decorator(fn: callable) -> callable:
429438
return fn
430439
```
440+
441+
### AST nodes that are only valid inside `Literal`
442+
443+
```py
444+
def bad(
445+
# error: [invalid-type-form]
446+
a: 42,
447+
# error: [invalid-type-form]
448+
b: b"42",
449+
# error: [invalid-type-form]
450+
c: True,
451+
# error: [invalid-syntax-in-forward-annotation]
452+
d: "invalid syntax",
453+
): ...
454+
```

‎crates/ty_python_semantic/resources/mdtest/annotations/string.md‎

Lines changed: 36 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -217,32 +217,44 @@ class Foo: ...
217217

218218
```py
219219
def f1(
220-
# error: [raw-string-type-annotation] "Type expressions cannot use raw string literal"
220+
# error: [raw-string-type-annotation] "Raw string literals are not allowed in type expressions"
221221
a: r"int",
222-
# error: [fstring-type-annotation] "Type expressions cannot use f-strings"
223-
b: f"int",
224-
# error: [byte-string-type-annotation] "Type expressions cannot use bytes literal"
225-
c: b"int",
226-
d: "int",
222+
# error: [raw-string-type-annotation] "Raw string literals are not allowed in type expressions"
223+
b: list[r"int"],
224+
# error: [invalid-type-form] "F-strings are not allowed in type expressions"
225+
c: f"int",
226+
# error: [invalid-type-form] "F-strings are not allowed in type expressions"
227+
d: list[f"int"],
228+
# error: [invalid-type-form] "Bytes literals are not allowed in this context in a type expression"
229+
e: b"int",
230+
f: "int",
227231
# error: [implicit-concatenated-string-type-annotation] "Type expressions cannot span multiple string literals"
228-
e: "in" "t",
229-
# error: [escape-character-in-forward-annotation] "Type expressions cannot contain escape characters"
230-
f: "\N{LATIN SMALL LETTER I}nt",
231-
# error: [escape-character-in-forward-annotation] "Type expressions cannot contain escape characters"
232-
g: "\x69nt",
233-
h: """int""",
234-
# error: [byte-string-type-annotation] "Type expressions cannot use bytes literal"
235-
i: "b'int'",
232+
g: "in" "t",
233+
# error: [implicit-concatenated-string-type-annotation] "Type expressions cannot span multiple string literals"
234+
h: list["in" "t"],
235+
# error: [escape-character-in-forward-annotation] "Escape characters are not allowed in type expressions"
236+
i: "\N{LATIN SMALL LETTER I}nt",
237+
# error: [escape-character-in-forward-annotation] "Escape characters are not allowed in type expressions"
238+
j: "\x69nt",
239+
k: """int""",
240+
# error: [invalid-type-form] "Bytes literals are not allowed in this context in a type expression"
241+
l: "b'int'",
242+
# error: [invalid-type-form] "Bytes literals are not allowed in this context in a type expression"
243+
m: list[b"int"],
236244
): # fmt:skip
237245
reveal_type(a) # revealed: Unknown
238-
reveal_type(b) # revealed: Unknown
246+
reveal_type(b) # revealed: list[Unknown]
239247
reveal_type(c) # revealed: Unknown
240-
reveal_type(d) # revealed: int
248+
reveal_type(d) # revealed: list[Unknown]
241249
reveal_type(e) # revealed: Unknown
242-
reveal_type(f) # revealed: Unknown
250+
reveal_type(f) # revealed: int
243251
reveal_type(g) # revealed: Unknown
244-
reveal_type(h) # revealed: int
252+
reveal_type(h) # revealed: list[Unknown]
245253
reveal_type(i) # revealed: Unknown
254+
reveal_type(j) # revealed: Unknown
255+
reveal_type(k) # revealed: int
256+
reveal_type(l) # revealed: Unknown
257+
reveal_type(m) # revealed: list[Unknown]
246258
```
247259

248260
## Various string kinds in `typing.Literal`
@@ -305,17 +317,17 @@ shouldn't panic.
305317

306318
```py
307319
# Regression test for https://github.com/astral-sh/ty/issues/1865
308-
# error: [fstring-type-annotation]
320+
# error: [invalid-type-form]
309321
stringified_fstring_with_conditional: "f'{1 if 1 else 1}'"
310-
# error: [fstring-type-annotation]
322+
# error: [invalid-type-form]
311323
stringified_fstring_with_boolean_expression: "f'{1 or 2}'"
312-
# error: [fstring-type-annotation]
324+
# error: [invalid-type-form]
313325
stringified_fstring_with_generator_expression: "f'{(i for i in range(5))}'"
314-
# error: [fstring-type-annotation]
326+
# error: [invalid-type-form]
315327
stringified_fstring_with_list_comprehension: "f'{[i for i in range(5)]}'"
316-
# error: [fstring-type-annotation]
328+
# error: [invalid-type-form]
317329
stringified_fstring_with_dict_comprehension: "f'{ {i: i for i in range(5)} }'"
318-
# error: [fstring-type-annotation]
330+
# error: [invalid-type-form]
319331
stringified_fstring_with_set_comprehension: "f'{ {i for i in range(5)} }'"
320332

321333
# error: [invalid-type-form]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
---
2+
source: crates/ty_test/src/lib.rs
3+
expression: snapshot
4+
---
5+
6+
---
7+
mdtest name: invalid.md - Tests for invalid types in type expressions - Diagnostics for common errors - AST nodes that are only valid inside `Literal`
8+
mdtest path: crates/ty_python_semantic/resources/mdtest/annotations/invalid.md
9+
---
10+
11+
# Python source files
12+
13+
## mdtest_snippet.py
14+
15+
```
16+
1 | def bad(
17+
2 | # error: [invalid-type-form]
18+
3 | a: 42,
19+
4 | # error: [invalid-type-form]
20+
5 | b: b"42",
21+
6 | # error: [invalid-type-form]
22+
7 | c: True,
23+
8 | # error: [invalid-syntax-in-forward-annotation]
24+
9 | d: "invalid syntax",
25+
10 | ): ...
26+
```
27+
28+
# Diagnostics
29+
30+
```
31+
error[invalid-type-form]: Int literals are not allowed in this context in a type expression
32+
--> src/mdtest_snippet.py:3:8
33+
|
34+
1 | def bad(
35+
2 | # error: [invalid-type-form]
36+
3 | a: 42,
37+
| ^^ Did you mean `typing.Literal[42]`?
38+
4 | # error: [invalid-type-form]
39+
5 | b: b"42",
40+
|
41+
info: See the following page for a reference on valid type expressions:
42+
info: https://typing.python.org/en/latest/spec/annotations.html#type-and-annotation-expressions
43+
info: rule `invalid-type-form` is enabled by default
44+
45+
```
46+
47+
```
48+
error[invalid-type-form]: Bytes literals are not allowed in this context in a type expression
49+
--> src/mdtest_snippet.py:5:8
50+
|
51+
3 | a: 42,
52+
4 | # error: [invalid-type-form]
53+
5 | b: b"42",
54+
| ^^^^^ Did you mean `typing.Literal[b"42"]`?
55+
6 | # error: [invalid-type-form]
56+
7 | c: True,
57+
|
58+
info: See the following page for a reference on valid type expressions:
59+
info: https://typing.python.org/en/latest/spec/annotations.html#type-and-annotation-expressions
60+
info: rule `invalid-type-form` is enabled by default
61+
62+
```
63+
64+
```
65+
error[invalid-type-form]: Boolean literals are not allowed in this context in a type expression
66+
--> src/mdtest_snippet.py:7:8
67+
|
68+
5 | b: b"42",
69+
6 | # error: [invalid-type-form]
70+
7 | c: True,
71+
| ^^^^ Did you mean `typing.Literal[True]`?
72+
8 | # error: [invalid-syntax-in-forward-annotation]
73+
9 | d: "invalid syntax",
74+
|
75+
info: See the following page for a reference on valid type expressions:
76+
info: https://typing.python.org/en/latest/spec/annotations.html#type-and-annotation-expressions
77+
info: rule `invalid-type-form` is enabled by default
78+
79+
```
80+
81+
```
82+
error[invalid-syntax-in-forward-annotation]: Syntax error in forward annotation: Unexpected token at the end of an expression
83+
--> src/mdtest_snippet.py:9:8
84+
|
85+
7 | c: True,
86+
8 | # error: [invalid-syntax-in-forward-annotation]
87+
9 | d: "invalid syntax",
88+
| ^^^^^^^^^^^^^^^^ Did you mean `typing.Literal["invalid syntax"]`?
89+
10 | ): ...
90+
|
91+
info: rule `invalid-syntax-in-forward-annotation` is enabled by default
92+
93+
```
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
---
2+
source: crates/ty_test/src/lib.rs
3+
expression: snapshot
4+
---
5+
6+
---
7+
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[]`
8+
mdtest path: crates/ty_python_semantic/resources/mdtest/annotations/invalid.md
9+
---
10+
11+
# Python source files
12+
13+
## mdtest_snippet.py
14+
15+
```
16+
1 | def _(
17+
2 | x: {int: str}, # error: [invalid-type-form]
18+
3 | y: {str}, # error: [invalid-type-form]
19+
4 | ): ...
20+
```
21+
22+
# Diagnostics
23+
24+
```
25+
error[invalid-type-form]: Dict literals are not allowed in type expressions
26+
--> src/mdtest_snippet.py:2:8
27+
|
28+
1 | def _(
29+
2 | x: {int: str}, # error: [invalid-type-form]
30+
| ^^^^^^^^^^ Did you mean `dict[int, str]`?
31+
3 | y: {str}, # error: [invalid-type-form]
32+
4 | ): ...
33+
|
34+
info: See the following page for a reference on valid type expressions:
35+
info: https://typing.python.org/en/latest/spec/annotations.html#type-and-annotation-expressions
36+
info: rule `invalid-type-form` is enabled by default
37+
38+
```
39+
40+
```
41+
error[invalid-type-form]: Set literals are not allowed in type expressions
42+
--> src/mdtest_snippet.py:3:8
43+
|
44+
1 | def _(
45+
2 | x: {int: str}, # error: [invalid-type-form]
46+
3 | y: {str}, # error: [invalid-type-form]
47+
| ^^^^^ Did you mean `set[str]`?
48+
4 | ): ...
49+
|
50+
info: See the following page for a reference on valid type expressions:
51+
info: https://typing.python.org/en/latest/spec/annotations.html#type-and-annotation-expressions
52+
info: rule `invalid-type-form` is enabled by default
53+
54+
```

‎crates/ty_python_semantic/resources/mdtest/snapshots/typed_dict.md_-_`TypedDict`_-_Function_syntax_with…_(4b18755412dfaff1).snap‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -531,7 +531,7 @@ error[invalid-type-form]: Int literals are not allowed in this context in a type
531531
86 | # error: [invalid-argument-type] "Expected a string-literal key in the `fields` dict of `TypedDict()`"
532532
87 | # error: [invalid-type-form]
533533
88 | Bad10 = TypedDict("Bad10", {name: 42})
534-
| ^^
534+
| ^^ Did you mean `typing.Literal[42]`?
535535
89 |
536536
90 | # error: [invalid-argument-type] "Expected a string-literal key in the `fields` dict of `TypedDict()`"
537537
|
@@ -563,7 +563,7 @@ error[invalid-type-form]: Int literals are not allowed in this context in a type
563563
90 | # error: [invalid-argument-type] "Expected a string-literal key in the `fields` dict of `TypedDict()`"
564564
91 | # error: [invalid-type-form] "Int literals are not allowed in this context in a type expression"
565565
92 | class Bad11(TypedDict("Bad11", {name: 42})): ...
566-
| ^^
566+
| ^^ Did you mean `typing.Literal[42]`?
567567
93 |
568568
94 | # error: [invalid-argument-type] "Invalid argument to parameter `typename` of `TypedDict()`: Expected `str`, found `Literal[123]`"
569569
|

‎crates/ty_python_semantic/resources/mdtest/suppressions/ty_ignore.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ a = 10 / 0 # ty: ignore[invalid-assignment, unresolved-reference, division-by-z
7070

7171
```py
7272
# fmt: off
73-
def test(a: f"f-string type annotation", b: b"byte-string-type-annotation"): ... # ty: ignore[fstring-type-annotation, byte-string-type-annotation]
73+
def test(a: f"f-string type annotation", b: unresolved_ref): ... # ty: ignore[invalid-type-form, unresolved-reference]
7474
```
7575

7676
## Can't suppress syntax errors

‎crates/ty_python_semantic/src/types/diagnostic.rs‎

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,8 @@ use crate::types::infer::UnsupportedComparisonError;
2020
use crate::types::overrides::MethodKind;
2121
use crate::types::protocol_class::ProtocolMember;
2222
use crate::types::string_annotation::{
23-
BYTE_STRING_TYPE_ANNOTATION, ESCAPE_CHARACTER_IN_FORWARD_ANNOTATION, FSTRING_TYPE_ANNOTATION,
24-
IMPLICIT_CONCATENATED_STRING_TYPE_ANNOTATION, INVALID_SYNTAX_IN_FORWARD_ANNOTATION,
25-
RAW_STRING_TYPE_ANNOTATION,
23+
ESCAPE_CHARACTER_IN_FORWARD_ANNOTATION, IMPLICIT_CONCATENATED_STRING_TYPE_ANNOTATION,
24+
INVALID_SYNTAX_IN_FORWARD_ANNOTATION, RAW_STRING_TYPE_ANNOTATION,
2625
};
2726
use crate::types::tuple::TupleSpec;
2827
use crate::types::typed_dict::TypedDictSchema;
@@ -160,9 +159,7 @@ pub(crate) fn register_lints(registry: &mut LintRegistryBuilder) {
160159
registry.register_lint(&INVALID_LEGACY_POSITIONAL_PARAMETER);
161160

162161
// String annotations
163-
registry.register_lint(&BYTE_STRING_TYPE_ANNOTATION);
164162
registry.register_lint(&ESCAPE_CHARACTER_IN_FORWARD_ANNOTATION);
165-
registry.register_lint(&FSTRING_TYPE_ANNOTATION);
166163
registry.register_lint(&IMPLICIT_CONCATENATED_STRING_TYPE_ANNOTATION);
167164
registry.register_lint(&INVALID_SYNTAX_IN_FORWARD_ANNOTATION);
168165
registry.register_lint(&RAW_STRING_TYPE_ANNOTATION);

‎crates/ty_python_semantic/src/types/infer/builder/annotation_expression.rs‎

Lines changed: 2 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,7 @@ use crate::types::diagnostic::{INVALID_TYPE_FORM, REDUNDANT_FINAL_CLASSVAR};
77
use crate::types::infer::builder::InferenceFlags;
88
use crate::types::infer::builder::subscript::AnnotatedExprContext;
99
use crate::types::infer::nearest_enclosing_class;
10-
use crate::types::string_annotation::{
11-
BYTE_STRING_TYPE_ANNOTATION, FSTRING_TYPE_ANNOTATION, parse_string_annotation,
12-
};
10+
use crate::types::string_annotation::parse_string_annotation;
1311
use crate::types::{
1412
SpecialFormType, Type, TypeAndQualifiers, TypeContext, TypeQualifier, TypeQualifiers, todo_type,
1513
};
@@ -161,34 +159,6 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
161159
// String annotations: https://typing.python.org/en/latest/spec/annotations.html#string-annotations
162160
ast::Expr::StringLiteral(string) => self.infer_string_annotation_expression(string),
163161

164-
// Annotation expressions also get special handling for `*args` and `**kwargs`.
165-
ast::Expr::Starred(starred) => TypeAndQualifiers::declared(
166-
self.infer_starred_expression(starred, TypeContext::default()),
167-
),
168-
169-
ast::Expr::BytesLiteral(bytes) => {
170-
if let Some(builder) = self
171-
.context
172-
.report_lint(&BYTE_STRING_TYPE_ANNOTATION, bytes)
173-
{
174-
builder.into_diagnostic("Type expressions cannot use bytes literal");
175-
}
176-
if !self.in_string_annotation() {
177-
self.infer_bytes_literal_expression(bytes);
178-
}
179-
TypeAndQualifiers::declared(Type::unknown())
180-
}
181-
182-
ast::Expr::FString(fstring) => {
183-
if let Some(builder) = self.context.report_lint(&FSTRING_TYPE_ANNOTATION, fstring) {
184-
builder.into_diagnostic("Type expressions cannot use f-strings");
185-
}
186-
if !self.in_string_annotation() {
187-
self.infer_fstring_expression(fstring);
188-
}
189-
TypeAndQualifiers::declared(Type::unknown())
190-
}
191-
192162
ast::Expr::Attribute(attribute) => {
193163
if !is_dotted_name(annotation) {
194164
return TypeAndQualifiers::declared(self.infer_type_expression(annotation));
@@ -357,8 +327,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
357327
}
358328
}
359329

360-
// All other annotation expressions are (possibly) valid type expressions, so handle
361-
// them there instead.
330+
// Fallback to `infer_type_expression_no_store` for everything else
362331
type_expr => {
363332
TypeAndQualifiers::declared(self.infer_type_expression_no_store(type_expr))
364333
}

0 commit comments

Comments
 (0)