Files
ruff/crates/red_knot_python_semantic/resources/mdtest/comprehensions/basic.md
Brent Westbrook e7f38fe74b [red-knot] Detect semantic syntax errors (#17463)
Summary
--

This PR extends semantic syntax error detection to red-knot. The main
changes here are:

1. Adding `SemanticSyntaxChecker` and `Vec<SemanticSyntaxError>` fields
to the `SemanticIndexBuilder`
2. Calling `SemanticSyntaxChecker::visit_stmt` and `visit_expr` in the
`SemanticIndexBuilder`'s `visit_stmt` and `visit_expr` methods
3. Implementing `SemanticSyntaxContext` for `SemanticIndexBuilder`
4. Adding new mdtests to test the context implementation and show
diagnostics

(3) is definitely the trickiest and required (I think) a minor addition
to the `SemanticIndexBuilder`. I tried to look around for existing code
performing the necessary checks, but I definitely could have missed
something or misused the existing code even when I found it.

There's still one TODO around `global` statement handling. I don't think
there's an existing way to look this up, but I'm happy to work on that
here or in a separate PR. This currently only affects detection of one
error (`LoadBeforeGlobalDeclaration` or
[PLE0118](https://docs.astral.sh/ruff/rules/load-before-global-declaration/)
in ruff), so it's not too big of a problem even if we leave the TODO.

Test Plan
--

New mdtests, as well as new errors for existing mdtests

---------

Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2025-04-23 09:52:58 -04:00

3.1 KiB

Comprehensions

Basic comprehensions

class IntIterator:
    def __next__(self) -> int:
        return 42

class IntIterable:
    def __iter__(self) -> IntIterator:
        return IntIterator()

# revealed: int
[reveal_type(x) for x in IntIterable()]

class IteratorOfIterables:
    def __next__(self) -> IntIterable:
        return IntIterable()

class IterableOfIterables:
    def __iter__(self) -> IteratorOfIterables:
        return IteratorOfIterables()

# revealed: tuple[int, IntIterable]
[reveal_type((x, y)) for y in IterableOfIterables() for x in y]

# revealed: int
{reveal_type(x): 0 for x in IntIterable()}

# revealed: int
{0: reveal_type(x) for x in IntIterable()}

Nested comprehension

class IntIterator:
    def __next__(self) -> int:
        return 42

class IntIterable:
    def __iter__(self) -> IntIterator:
        return IntIterator()

# revealed: tuple[int, int]
[[reveal_type((x, y)) for x in IntIterable()] for y in IntIterable()]

Comprehension referencing outer comprehension

class IntIterator:
    def __next__(self) -> int:
        return 42

class IntIterable:
    def __iter__(self) -> IntIterator:
        return IntIterator()

class IteratorOfIterables:
    def __next__(self) -> IntIterable:
        return IntIterable()

class IterableOfIterables:
    def __iter__(self) -> IteratorOfIterables:
        return IteratorOfIterables()

# revealed: tuple[int, IntIterable]
[[reveal_type((x, y)) for x in y] for y in IterableOfIterables()]

Comprehension with unbound iterable

Iterating over an unbound iterable yields Unknown:

# error: [unresolved-reference] "Name `x` used when not defined"
# revealed: Unknown
[reveal_type(z) for z in x]

class IntIterator:
    def __next__(self) -> int:
        return 42

class IntIterable:
    def __iter__(self) -> IntIterator:
        return IntIterator()

# error: [not-iterable] "Object of type `int` is not iterable"
# revealed: tuple[int, Unknown]
[reveal_type((x, z)) for x in IntIterable() for z in x]

Starred expressions

Starred expressions must be iterable

class NotIterable: ...

class Iterator:
    def __next__(self) -> int:
        return 42

class Iterable:
    def __iter__(self) -> Iterator:
        return Iterator()

# This is fine:
x = [*Iterable()]

# error: [not-iterable] "Object of type `NotIterable` is not iterable"
y = [*NotIterable()]

Async comprehensions

Basic

class AsyncIterator:
    async def __anext__(self) -> int:
        return 42

class AsyncIterable:
    def __aiter__(self) -> AsyncIterator:
        return AsyncIterator()

async def _():
    # revealed: @Todo(async iterables/iterators)
    [reveal_type(x) async for x in AsyncIterable()]

Invalid async comprehension

This tests that we understand that async comprehensions do not work according to the synchronous iteration protocol

class Iterator:
    def __next__(self) -> int:
        return 42

class Iterable:
    def __iter__(self) -> Iterator:
        return Iterator()

async def _():
    # revealed: @Todo(async iterables/iterators)
    [reveal_type(x) async for x in Iterable()]