Skip to content

Commit 9ec690b

Browse files
authored
[red-knot] Add support for string annotations (#14151)
## Summary This PR adds support for parsing and inferring types within string annotations. ### Implementation (attempt 1) This is preserved in 6217f48. The implementation here would separate the inference of string annotations in the deferred query. This requires the following: * Two ways of evaluating the deferred definitions - lazily and eagerly. * An eager evaluation occurs right outside the definition query which in this case would be in `binding_ty` and `declaration_ty`. * A lazy evaluation occurs on demand like using the `definition_expression_ty` to determine the function return type and class bases. * The above point means that when trying to get the binding type for a variable in an annotated assignment, the definition query won't include the type. So, it'll require going through the deferred query to get the type. This has the following limitations: * Nested string annotations, although not necessarily a useful feature, is difficult to implement unless we convert the implementation in an infinite loop * Partial string annotations require complex layout because inferring the types for stringified and non-stringified parts of the annotation are done in separate queries. This means we need to maintain additional information ### Implementation (attempt 2) This is the final diff in this PR. The implementation here does the complete inference of string annotation in the same definition query by maintaining certain state while trying to infer different parts of an expression and take decisions accordingly. These are: * Allow names that are part of a string annotation to not exists in the symbol table. For example, in `x: "Foo"`, if the "Foo" symbol is not defined then it won't exists in the symbol table even though it's being used. This is an invariant which is being allowed only for symbols in a string annotation. * Similarly, lookup name is updated to do the same and if the symbol doesn't exists, then it's not bounded. * Store the final type of a string annotation on the string expression itself and not for any of the sub-expressions that are created after parsing. This is because those sub-expressions won't exists in the semantic index. Design document: https://www.notion.so/astral-sh/String-Annotations-12148797e1ca801197a9f146641e5b71?pvs=4 Closes: #13796 ## Test Plan * Add various test cases in our markdown framework * Run `red_knot` on LibCST (contains a lot of string annotations, specifically https://github.com/Instagram/LibCST/blob/main/libcst/matchers/_matcher_base.py), FastAPI (good amount of annotated code including `typing.Literal`) and compare against the `main` branch output
1 parent a48d779 commit 9ec690b

File tree

6 files changed

+569
-87
lines changed

6 files changed

+569
-87
lines changed

crates/red_knot_python_semantic/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ license = { workspace = true }
1414
ruff_db = { workspace = true }
1515
ruff_index = { workspace = true }
1616
ruff_python_ast = { workspace = true, features = ["salsa"] }
17+
ruff_python_parser = { workspace = true }
1718
ruff_python_stdlib = { workspace = true }
1819
ruff_source_file = { workspace = true }
1920
ruff_text_size = { workspace = true }
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,191 @@
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+
def f2() -> "tuple[int, str]":
28+
return 1
29+
30+
reveal_type(f1()) # revealed: int | str
31+
reveal_type(f2()) # revealed: tuple[int, str]
32+
```
33+
34+
## Partial
35+
36+
```py
37+
def f() -> tuple[int, "str"]:
38+
return 1
39+
40+
reveal_type(f()) # revealed: tuple[int, str]
41+
```
42+
43+
## Deferred
44+
45+
```py
46+
def f() -> "Foo":
47+
return Foo()
48+
49+
class Foo:
50+
pass
51+
52+
reveal_type(f()) # revealed: Foo
53+
```
54+
55+
## Deferred (undefined)
56+
57+
```py
58+
# error: [unresolved-reference]
59+
def f() -> "Foo":
60+
pass
61+
62+
reveal_type(f()) # revealed: Unknown
63+
```
64+
65+
## Partial deferred
66+
67+
```py
68+
def f() -> int | "Foo":
69+
return 1
70+
71+
class Foo:
72+
pass
73+
74+
reveal_type(f()) # revealed: int | Foo
975
```
76+
77+
## `typing.Literal`
78+
79+
```py
80+
from typing import Literal
81+
82+
def f1() -> Literal["Foo", "Bar"]:
83+
return "Foo"
84+
85+
def f2() -> 'Literal["Foo", "Bar"]':
86+
return "Foo"
87+
88+
class Foo:
89+
pass
90+
91+
reveal_type(f1()) # revealed: Literal["Foo", "Bar"]
92+
reveal_type(f2()) # revealed: Literal["Foo", "Bar"]
93+
```
94+
95+
## Various string kinds
96+
97+
```py
98+
# error: [annotation-raw-string] "Type expressions cannot use raw string literal"
99+
def f1() -> r"int":
100+
return 1
101+
102+
# error: [annotation-f-string] "Type expressions cannot use f-strings"
103+
def f2() -> f"int":
104+
return 1
105+
106+
# error: [annotation-byte-string] "Type expressions cannot use bytes literal"
107+
def f3() -> b"int":
108+
return 1
109+
110+
def f4() -> "int":
111+
return 1
112+
113+
# error: [annotation-implicit-concat] "Type expressions cannot span multiple string literals"
114+
def f5() -> "in" "t":
115+
return 1
116+
117+
# error: [annotation-escape-character] "Type expressions cannot contain escape characters"
118+
def f6() -> "\N{LATIN SMALL LETTER I}nt":
119+
return 1
120+
121+
# error: [annotation-escape-character] "Type expressions cannot contain escape characters"
122+
def f7() -> "\x69nt":
123+
return 1
124+
125+
def f8() -> """int""":
126+
return 1
127+
128+
# error: [annotation-byte-string] "Type expressions cannot use bytes literal"
129+
def f9() -> "b'int'":
130+
return 1
131+
132+
reveal_type(f1()) # revealed: Unknown
133+
reveal_type(f2()) # revealed: Unknown
134+
reveal_type(f3()) # revealed: Unknown
135+
reveal_type(f4()) # revealed: int
136+
reveal_type(f5()) # revealed: Unknown
137+
reveal_type(f6()) # revealed: Unknown
138+
reveal_type(f7()) # revealed: Unknown
139+
reveal_type(f8()) # revealed: int
140+
reveal_type(f9()) # revealed: Unknown
141+
```
142+
143+
## Various string kinds in `typing.Literal`
144+
145+
```py
146+
from typing import Literal
147+
148+
def f() -> Literal["a", r"b", b"c", "d" "e", "\N{LATIN SMALL LETTER F}", "\x67", """h"""]:
149+
return "normal"
150+
151+
reveal_type(f()) # revealed: Literal["a", "b", "de", "f", "g", "h"] | Literal[b"c"]
152+
```
153+
154+
## Class variables
155+
156+
```py
157+
MyType = int
158+
159+
class Aliases:
160+
MyType = str
161+
162+
forward: "MyType"
163+
not_forward: MyType
164+
165+
reveal_type(Aliases.forward) # revealed: str
166+
reveal_type(Aliases.not_forward) # revealed: str
167+
```
168+
169+
## Annotated assignment
170+
171+
```py
172+
a: "int" = 1
173+
b: "'int'" = 1
174+
c: "Foo"
175+
# error: [invalid-assignment] "Object of type `Literal[1]` is not assignable to `Foo`"
176+
d: "Foo" = 1
177+
178+
class Foo:
179+
pass
180+
181+
c = Foo()
182+
183+
reveal_type(a) # revealed: Literal[1]
184+
reveal_type(b) # revealed: Literal[1]
185+
reveal_type(c) # revealed: Foo
186+
reveal_type(d) # revealed: Foo
187+
```
188+
189+
## Parameter
190+
191+
TODO: Add tests once parameter inference is supported

crates/red_knot_python_semantic/resources/mdtest/assignment/annotations.md

+26
Original file line numberDiff line numberDiff line change
@@ -110,3 +110,29 @@ c: builtins.tuple[builtins.tuple[builtins.int, builtins.int], builtins.int] = ((
110110
# error: [invalid-assignment] "Object of type `Literal["foo"]` is not assignable to `tuple[tuple[int, int], int]`"
111111
c: builtins.tuple[builtins.tuple[builtins.int, builtins.int], builtins.int] = "foo"
112112
```
113+
114+
## Future annotations are deferred
115+
116+
```py
117+
from __future__ import annotations
118+
119+
x: Foo
120+
121+
class Foo:
122+
pass
123+
124+
x = Foo()
125+
reveal_type(x) # revealed: Foo
126+
```
127+
128+
## Annotations in stub files are deferred
129+
130+
```pyi path=main.pyi
131+
x: Foo
132+
133+
class Foo:
134+
pass
135+
136+
x = Foo()
137+
reveal_type(x) # revealed: Foo
138+
```

crates/red_knot_python_semantic/src/types.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ mod infer;
3737
mod mro;
3838
mod narrow;
3939
mod signatures;
40+
mod string_annotation;
4041
mod unpacker;
4142

4243
#[salsa::tracked(return_ref)]
@@ -58,7 +59,7 @@ pub fn check_types(db: &dyn Db, file: File) -> TypeCheckDiagnostics {
5859

5960
/// Infer the public type of a symbol (its type as seen from outside its scope).
6061
fn symbol_by_id<'db>(db: &'db dyn Db, scope: ScopeId<'db>, symbol: ScopedSymbolId) -> Symbol<'db> {
61-
let _span = tracing::trace_span!("symbol_ty_by_id", ?symbol).entered();
62+
let _span = tracing::trace_span!("symbol_by_id", ?symbol).entered();
6263

6364
let use_def = use_def_map(db, scope);
6465

0 commit comments

Comments
 (0)