Skip to content

Commit cb7dae1

Browse files
authored
[red-knot] Add initial set of tests for unreachable code (#17159)
## Summary Add an initial set of tests that will eventually document our behavior around unreachable code. In the last section of this suite, I argue why we should never type check unreachable sections and never emit any diagnostics in these sections.
1 parent 8833484 commit cb7dae1

File tree

1 file changed

+259
-0
lines changed

1 file changed

+259
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
# Unreachable code
2+
3+
## Detecting unreachable code
4+
5+
In this section, we look at various scenarios how sections of code can become unreachable. We should
6+
eventually introduce a new diagnostic that would detect unreachable code.
7+
8+
### Terminal statements
9+
10+
In the following examples, the `print` statements are definitely unreachable.
11+
12+
```py
13+
def f1():
14+
return
15+
16+
# TODO: we should mark this as unreachable
17+
print("unreachable")
18+
19+
def f2():
20+
raise Exception()
21+
22+
# TODO: we should mark this as unreachable
23+
print("unreachable")
24+
25+
def f3():
26+
while True:
27+
break
28+
29+
# TODO: we should mark this as unreachable
30+
print("unreachable")
31+
32+
def f4():
33+
for _ in range(10):
34+
continue
35+
36+
# TODO: we should mark this as unreachable
37+
print("unreachable")
38+
```
39+
40+
### Infinite loops
41+
42+
```py
43+
def f1():
44+
while True:
45+
pass
46+
47+
# TODO: we should mark this as unreachable
48+
print("unreachable")
49+
```
50+
51+
### Statically known branches
52+
53+
In the following examples, the `print` statements are also unreachable, but it requires type
54+
inference to determine that:
55+
56+
```py
57+
def f1():
58+
if 2 + 3 > 10:
59+
# TODO: we should mark this as unreachable
60+
print("unreachable")
61+
62+
def f2():
63+
if True:
64+
return
65+
66+
# TODO: we should mark this as unreachable
67+
print("unreachable")
68+
```
69+
70+
### `Never` / `NoReturn`
71+
72+
If a function is annotated with a return type of `Never` or `NoReturn`, we can consider all code
73+
after the call to that function unreachable.
74+
75+
```py
76+
from typing_extensions import NoReturn
77+
78+
def always_raises() -> NoReturn:
79+
raise Exception()
80+
81+
def f():
82+
always_raises()
83+
84+
# TODO: we should mark this as unreachable
85+
print("unreachable")
86+
```
87+
88+
## Python version and platform checks
89+
90+
It is common to have code that is specific to a certain Python version or platform. This case is
91+
special because whether or not the code is reachable depends on externally configured constants. And
92+
if we are checking for a set of parameters that makes one of these branches unreachable, that is
93+
likely not something that the user wants to be warned about, because there are probably other sets
94+
of parameters that make the branch reachable.
95+
96+
### `sys.version_info` branches
97+
98+
Consider the following example. If we check with a Python version lower than 3.11, the import
99+
statement is unreachable. If we check with a Python version equal to or greater than 3.11, the
100+
import statement is definitely reachable. We should not emit any diagnostics in either case.
101+
102+
#### Checking with Python version 3.10
103+
104+
```toml
105+
[environment]
106+
python-version = "3.10"
107+
```
108+
109+
```py
110+
import sys
111+
112+
if sys.version_info >= (3, 11):
113+
# TODO: we should not emit an error here
114+
# error: [unresolved-import]
115+
from typing import Self
116+
```
117+
118+
#### Checking with Python version 3.12
119+
120+
```toml
121+
[environment]
122+
python-version = "3.12"
123+
```
124+
125+
```py
126+
import sys
127+
128+
if sys.version_info >= (3, 11):
129+
from typing import Self
130+
```
131+
132+
### `sys.platform` branches
133+
134+
The problem is even more pronounced with `sys.platform` branches, since we don't necessarily have
135+
the platform information available.
136+
137+
#### Checking with platform `win32`
138+
139+
```toml
140+
[environment]
141+
python-platform = "win32"
142+
```
143+
144+
```py
145+
import sys
146+
147+
if sys.platform == "win32":
148+
sys.getwindowsversion()
149+
```
150+
151+
#### Checking with platform `linux`
152+
153+
```toml
154+
[environment]
155+
python-platform = "linux"
156+
```
157+
158+
```py
159+
import sys
160+
161+
if sys.platform == "win32":
162+
# TODO: we should not emit an error here
163+
# error: [unresolved-attribute]
164+
sys.getwindowsversion()
165+
```
166+
167+
#### Checking without a specified platform
168+
169+
```toml
170+
[environment]
171+
# python-platform not specified
172+
```
173+
174+
```py
175+
import sys
176+
177+
if sys.platform == "win32":
178+
# TODO: we should not emit an error here
179+
# error: [possibly-unbound-attribute]
180+
sys.getwindowsversion()
181+
```
182+
183+
#### Checking with platform set to `all`
184+
185+
```toml
186+
[environment]
187+
python-platform = "all"
188+
```
189+
190+
```py
191+
import sys
192+
193+
if sys.platform == "win32":
194+
# TODO: we should not emit an error here
195+
# error: [possibly-unbound-attribute]
196+
sys.getwindowsversion()
197+
```
198+
199+
## No false positive diagnostics in unreachable code
200+
201+
In this section, we make sure that we do not emit false positive diagnostics in unreachable code.
202+
203+
### Use of variables in unreachable code
204+
205+
We should not emit any diagnostics for uses of symbols in unreachable code:
206+
207+
```py
208+
def f():
209+
x = 1
210+
return
211+
212+
print("unreachable")
213+
214+
# TODO: we should not emit an error here; we currently do, since there is no control flow path from this
215+
# use of 'x' to any definition of 'x'.
216+
# error: [unresolved-reference]
217+
print(x)
218+
```
219+
220+
### Use of variable in nested function
221+
222+
In the example below, since we use `x` in the `inner` function, we use the "public" type of `x`,
223+
which currently refers to the end-of-scope type of `x`. Since the end of the `outer` scope is
224+
unreachable, we treat `x` as if it was not defined. This behavior can certainly be improved.
225+
226+
```py
227+
def outer():
228+
x = 1
229+
230+
def inner():
231+
# TODO: we should not emit an error here
232+
# error: [unresolved-reference]
233+
return x # Name `x` used when not defined
234+
while True:
235+
pass
236+
```
237+
238+
## No diagnostics in unreachable code
239+
240+
In general, no diagnostics should be emitted in unreachable code. The reasoning is that any issues
241+
inside the unreachable section would not cause problems at runtime. And type checking the
242+
unreachable code under the assumption that it *is* reachable might lead to false positives:
243+
244+
```py
245+
FEATURE_X_ACTIVATED = False
246+
247+
if FEATURE_X_ACTIVATED:
248+
def feature_x():
249+
print("Performing 'X'")
250+
251+
def f():
252+
if FEATURE_X_ACTIVATED:
253+
# Type checking this particular section as if it were reachable would
254+
# lead to a false positive, so we should not emit diagnostics here.
255+
256+
# TODO: no error should be emitted here
257+
# error: [unresolved-reference]
258+
feature_x()
259+
```

0 commit comments

Comments
 (0)