Skip to content

Commit f1fb77c

Browse files
committed
Add tests for string annotations
1 parent ed806ba commit f1fb77c

File tree

3 files changed

+243
-10
lines changed

3 files changed

+243
-10
lines changed
Lines changed: 216 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,223 @@
11
# String annotations
22

3+
## Simple
4+
35
```py
46
def f() -> "int":
57
return 1
68

7-
# TODO: We do not support string annotations, but we should not panic if we encounter them
8-
reveal_type(f()) # revealed: @Todo
9+
reveal_type(f()) # revealed: int
10+
```
11+
12+
## Nested
13+
14+
```py
15+
def f() -> "'int'":
16+
return 1
17+
18+
reveal_type(f()) # revealed: int
19+
```
20+
21+
## Type expression
22+
23+
```py
24+
def f1() -> "int | str":
25+
return 1
26+
27+
28+
def f2() -> "tuple[int, str]":
29+
return 1
30+
31+
32+
reveal_type(f1()) # revealed: int | str
33+
reveal_type(f2()) # revealed: tuple[int, str]
34+
```
35+
36+
## Partial
37+
38+
```py
39+
def f() -> tuple[int, "str"]:
40+
return 1
41+
42+
reveal_type(f()) # revealed: tuple[int, str]
43+
```
44+
45+
## Deferred
46+
47+
```py
48+
def f() -> "Foo":
49+
return Foo()
50+
51+
52+
class Foo:
53+
pass
54+
55+
56+
reveal_type(f()) # revealed: Foo
957
```
58+
59+
## Deferred (undefined)
60+
61+
```py
62+
# error: [unresolved-reference]
63+
def f() -> "Foo":
64+
pass
65+
66+
67+
reveal_type(f()) # revealed: Unknown
68+
```
69+
70+
## Partial deferred
71+
72+
```py
73+
def f() -> int | "Foo":
74+
return 1
75+
76+
77+
class Foo:
78+
pass
79+
80+
81+
reveal_type(f()) # revealed: int | Foo
82+
```
83+
84+
## `typing.Literal`
85+
86+
```py
87+
from typing import Literal
88+
89+
90+
def f1() -> Literal["Foo", "Bar"]:
91+
return "Foo"
92+
93+
94+
def f2() -> 'Literal["Foo", "Bar"]':
95+
return "Foo"
96+
97+
98+
class Foo:
99+
pass
100+
101+
102+
reveal_type(f1()) # revealed: Literal["Foo", "Bar"]
103+
reveal_type(f2()) # revealed: Literal["Foo", "Bar"]
104+
```
105+
106+
## Various string kinds
107+
108+
```py
109+
# error: [annotation-raw-string] "Type expressions cannot use raw string literal"
110+
def f1() -> r"int":
111+
return 1
112+
113+
114+
# error: [annotation-f-string] "Type expressions cannot use f-strings"
115+
def f2() -> f"int":
116+
return 1
117+
118+
119+
# error: [annotation-byte-string] "Type expressions cannot use bytes literal"
120+
def f3() -> b"int":
121+
return 1
122+
123+
124+
def f4() -> "int":
125+
return 1
126+
127+
128+
# error: [annotation-implicit-concat] "Type expressions cannot span multiple string literals"
129+
def f5() -> "in" "t":
130+
return 1
131+
132+
133+
# error: [annotation-escape-character] "Type expressions cannot contain escape characters"
134+
def f6() -> "\N{LATIN SMALL LETTER I}nt":
135+
return 1
136+
137+
138+
# error: [annotation-escape-character] "Type expressions cannot contain escape characters"
139+
def f7() -> "\x69nt":
140+
return 1
141+
142+
143+
def f8() -> """int""":
144+
return 1
145+
146+
147+
# error: [annotation-byte-string] "Type expressions cannot use bytes literal"
148+
def f9() -> "b'int'":
149+
return 1
150+
151+
152+
def f10() -> u"int":
153+
return 1
154+
155+
156+
reveal_type(f1()) # revealed: Unknown
157+
reveal_type(f2()) # revealed: Unknown
158+
reveal_type(f3()) # revealed: Unknown
159+
reveal_type(f4()) # revealed: int
160+
reveal_type(f5()) # revealed: Unknown
161+
reveal_type(f6()) # revealed: Unknown
162+
reveal_type(f7()) # revealed: Unknown
163+
reveal_type(f8()) # revealed: int
164+
reveal_type(f9()) # revealed: Unknown
165+
reveal_type(f10()) # revealed: int
166+
```
167+
168+
## Various string kinds in `typing.Literal`
169+
170+
```py
171+
from typing import Literal
172+
173+
174+
def f() -> Literal["a", r"b", b"c", "d" "e", "\N{LATIN SMALL LETTER F}", "\x67", """h""", u"i"]:
175+
return "normal"
176+
177+
178+
reveal_type(f()) # revealed: Literal["a", "b", "de", "f", "g", "h", "i"] | Literal[b"c"]
179+
```
180+
181+
## Class variables
182+
183+
```py
184+
MyType = int
185+
186+
187+
class Aliases:
188+
MyType = str
189+
190+
forward: "MyType"
191+
not_forward: MyType
192+
193+
194+
reveal_type(Aliases.forward) # revealed: str
195+
reveal_type(Aliases.not_forward) # revealed: str
196+
```
197+
198+
## Annotated assignment
199+
200+
```py
201+
a: "int" = 1
202+
b: "'int'" = 1
203+
c: "Foo"
204+
# error: [invalid-assignment] "Object of type `Literal[1]` is not assignable to `Foo`"
205+
d: "Foo" = 1
206+
207+
208+
class Foo:
209+
pass
210+
211+
212+
c = Foo()
213+
214+
reveal_type(a) # revealed: Literal[1]
215+
reveal_type(b) # revealed: Literal[1]
216+
reveal_type(c) # revealed: Foo
217+
reveal_type(d) # revealed: Foo
218+
```
219+
220+
## Parameter
221+
222+
TODO: Add tests once parameter inference is supported
223+

crates/red_knot_python_semantic/src/types/infer.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4103,6 +4103,24 @@ impl<'db> TypeInferenceBuilder<'db> {
41034103
// Annotation expressions also get special handling for `*args` and `**kwargs`.
41044104
ast::Expr::Starred(starred) => self.infer_starred_expression(starred),
41054105

4106+
ast::Expr::BytesLiteral(bytes) => {
4107+
self.diagnostics.add(
4108+
bytes.into(),
4109+
"annotation-byte-string",
4110+
format_args!("Type expressions cannot use bytes literal"),
4111+
);
4112+
Type::Unknown
4113+
}
4114+
4115+
ast::Expr::FString(fstring) => {
4116+
self.diagnostics.add(
4117+
fstring.into(),
4118+
"annotation-f-string",
4119+
format_args!("Type expressions cannot use f-strings"),
4120+
);
4121+
Type::Unknown
4122+
}
4123+
41064124
// All other annotation expressions are (possibly) valid type expressions, so handle
41074125
// them there instead.
41084126
type_expr => self.infer_type_expression_no_store(type_expr),

crates/red_knot_python_semantic/src/types/string_annotation.rs

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,6 @@ use crate::Db;
1111
type AnnotationParseResult = Result<Parsed<ModExpression>, TypeCheckDiagnostics>;
1212

1313
/// Parses the given expression as a string annotation.
14-
///
15-
/// # Panics
16-
///
17-
/// Panics if the expression is not a string literal.
1814
pub(crate) fn parse_string_annotation(
1915
db: &dyn Db,
2016
file: File,
@@ -32,20 +28,25 @@ pub(crate) fn parse_string_annotation(
3228
diagnostics.add(
3329
string_literal.into(),
3430
"annotation-raw-string",
35-
format_args!("Type expressions cannot be use raw string literal"),
31+
format_args!("Type expressions cannot use raw string literal"),
3632
);
37-
}
38-
3933
// Compare the raw contents (without quotes) of the expression with the parsed contents
4034
// contained in the string literal.
41-
if raw_contents(node_text)
35+
} else if raw_contents(node_text)
4236
.is_some_and(|raw_contents| raw_contents == string_literal.as_str())
4337
{
4438
let range_excluding_quotes = string_literal
4539
.range()
4640
.add_start(string_literal.flags.opener_len())
4741
.sub_end(string_literal.flags.closer_len());
4842

43+
// TODO: Support multiline strings like:
44+
// ```py
45+
// x: """
46+
// int
47+
// | float
48+
// """ = 1
49+
// ```
4950
match parse_expression_range(source.as_str(), range_excluding_quotes) {
5051
Ok(parsed) => return Ok(parsed),
5152
Err(parse_error) => diagnostics.add(

0 commit comments

Comments
 (0)