Files
ruff/crates/red_knot_python_semantic/resources/mdtest/unreachable.md
David Peter cb7dae1e96 [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.
2025-04-02 19:39:44 +02:00

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()