## 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.
5.7 KiB
Unreachable code
Detecting unreachable code
In this section, we look at various scenarios how sections of code can become unreachable. We should eventually introduce a new diagnostic that would detect unreachable code.
Terminal statements
In the following examples, the print statements are definitely unreachable.
def f1():
return
# TODO: we should mark this as unreachable
print("unreachable")
def f2():
raise Exception()
# TODO: we should mark this as unreachable
print("unreachable")
def f3():
while True:
break
# TODO: we should mark this as unreachable
print("unreachable")
def f4():
for _ in range(10):
continue
# TODO: we should mark this as unreachable
print("unreachable")
Infinite loops
def f1():
while True:
pass
# TODO: we should mark this as unreachable
print("unreachable")
Statically known branches
In the following examples, the print statements are also unreachable, but it requires type
inference to determine that:
def f1():
if 2 + 3 > 10:
# TODO: we should mark this as unreachable
print("unreachable")
def f2():
if True:
return
# TODO: we should mark this as unreachable
print("unreachable")
Never / NoReturn
If a function is annotated with a return type of Never or NoReturn, we can consider all code
after the call to that function unreachable.
from typing_extensions import NoReturn
def always_raises() -> NoReturn:
raise Exception()
def f():
always_raises()
# TODO: we should mark this as unreachable
print("unreachable")
Python version and platform checks
It is common to have code that is specific to a certain Python version or platform. This case is special because whether or not the code is reachable depends on externally configured constants. And if we are checking for a set of parameters that makes one of these branches unreachable, that is likely not something that the user wants to be warned about, because there are probably other sets of parameters that make the branch reachable.
sys.version_info branches
Consider the following example. If we check with a Python version lower than 3.11, the import statement is unreachable. If we check with a Python version equal to or greater than 3.11, the import statement is definitely reachable. We should not emit any diagnostics in either case.
Checking with Python version 3.10
[environment]
python-version = "3.10"
import sys
if sys.version_info >= (3, 11):
# TODO: we should not emit an error here
# error: [unresolved-import]
from typing import Self
Checking with Python version 3.12
[environment]
python-version = "3.12"
import sys
if sys.version_info >= (3, 11):
from typing import Self
sys.platform branches
The problem is even more pronounced with sys.platform branches, since we don't necessarily have
the platform information available.
Checking with platform win32
[environment]
python-platform = "win32"
import sys
if sys.platform == "win32":
sys.getwindowsversion()
Checking with platform linux
[environment]
python-platform = "linux"
import sys
if sys.platform == "win32":
# TODO: we should not emit an error here
# error: [unresolved-attribute]
sys.getwindowsversion()
Checking without a specified platform
[environment]
# python-platform not specified
import sys
if sys.platform == "win32":
# TODO: we should not emit an error here
# error: [possibly-unbound-attribute]
sys.getwindowsversion()
Checking with platform set to all
[environment]
python-platform = "all"
import sys
if sys.platform == "win32":
# TODO: we should not emit an error here
# error: [possibly-unbound-attribute]
sys.getwindowsversion()
No false positive diagnostics in unreachable code
In this section, we make sure that we do not emit false positive diagnostics in unreachable code.
Use of variables in unreachable code
We should not emit any diagnostics for uses of symbols in unreachable code:
def f():
x = 1
return
print("unreachable")
# TODO: we should not emit an error here; we currently do, since there is no control flow path from this
# use of 'x' to any definition of 'x'.
# error: [unresolved-reference]
print(x)
Use of variable in nested function
In the example below, since we use x in the inner function, we use the "public" type of x,
which currently refers to the end-of-scope type of x. Since the end of the outer scope is
unreachable, we treat x as if it was not defined. This behavior can certainly be improved.
def outer():
x = 1
def inner():
# TODO: we should not emit an error here
# error: [unresolved-reference]
return x # Name `x` used when not defined
while True:
pass
No diagnostics in unreachable code
In general, no diagnostics should be emitted in unreachable code. The reasoning is that any issues inside the unreachable section would not cause problems at runtime. And type checking the unreachable code under the assumption that it is reachable might lead to false positives:
FEATURE_X_ACTIVATED = False
if FEATURE_X_ACTIVATED:
def feature_x():
print("Performing 'X'")
def f():
if FEATURE_X_ACTIVATED:
# Type checking this particular section as if it were reachable would
# lead to a false positive, so we should not emit diagnostics here.
# TODO: no error should be emitted here
# error: [unresolved-reference]
feature_x()