Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
1b1ff1f
[red-knot] Detect semantic syntax errors
ntBre Apr 16, 2025
74353f0
add SemanticIndexBuilder::seen_module_docstring_boundary
ntBre Apr 16, 2025
b09fcee
implement in_async_context and in_generator_scope
ntBre Apr 16, 2025
f37022a
add failing mdtest
ntBre Apr 17, 2025
46b022e
emit diagnostics and pass mdtest
ntBre Apr 17, 2025
2764c45
pass existing mdtests
ntBre Apr 18, 2025
507b737
add mdtests covering all context methods
ntBre Apr 18, 2025
60acba6
upcast explicitly for msrv tests
ntBre Apr 18, 2025
63fd71d
avoid unrelated errors in invalid.md
ntBre Apr 18, 2025
773e7a1
avoid unrelated errors in comprehensions/basic.md
ntBre Apr 18, 2025
c916bc3
remove backticks on async comprehension and test
ntBre Apr 21, 2025
82f848c
replace current_scope_is_global_scope with context method
ntBre Apr 21, 2025
f6a4213
update notebook check
ntBre Apr 21, 2025
bd1d9b1
remove now-unused is_module method
ntBre Apr 21, 2025
4b2fac9
seen_docstring_boundary -> seen_module_docstring_boundary
ntBre Apr 21, 2025
bfbc79f
use expect_function instead of if-let
ntBre Apr 21, 2025
add4f6b
skip checking a file if it's not open
ntBre Apr 22, 2025
9ac43a9
cache SemanticIndexBuilder::python_version
ntBre Apr 22, 2025
7db6685
add TypeCheckDiagnostic::extend_diagnostics
ntBre Apr 22, 2025
3d11ef7
remove SemanticSyntaxContext::seen_module_docstring_boundary
ntBre Apr 22, 2025
5e172cc
cache is_file_open call
ntBre Apr 22, 2025
af43757
Merge branch 'main' into brent/semantic-errors-red-knot
ntBre Apr 22, 2025
7a1e1ab
revert is_file_open check entirely
ntBre Apr 22, 2025
b32372c
restore is_file_open check, but only for pushing errors
ntBre Apr 22, 2025
02dfc23
move semantic checker fields to their own group
ntBre Apr 22, 2025
e32c188
make semantic_checker optional
ntBre Apr 22, 2025
ebccf41
restore SemanticSyntaxContext::source method and use OnceCell
ntBre Apr 22, 2025
57866a0
delete is_file_open check in favor of Option approach
ntBre Apr 22, 2025
931fcf1
Revert "make semantic_checker optional"
ntBre Apr 23, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -56,40 +56,41 @@ def _(
def bar() -> None:
return None

def _(
a: 1, # error: [invalid-type-form] "Int literals are not allowed in this context in a type expression"
b: 2.3, # error: [invalid-type-form] "Float literals are not allowed in type expressions"
c: 4j, # error: [invalid-type-form] "Complex literals are not allowed in type expressions"
d: True, # error: [invalid-type-form] "Boolean literals are not allowed in this context in a type expression"
e: int | b"foo", # error: [invalid-type-form] "Bytes literals are not allowed in this context in a type expression"
f: 1 and 2, # error: [invalid-type-form] "Boolean operations are not allowed in type expressions"
g: 1 or 2, # error: [invalid-type-form] "Boolean operations are not allowed in type expressions"
h: (foo := 1), # error: [invalid-type-form] "Named expressions are not allowed in type expressions"
i: not 1, # error: [invalid-type-form] "Unary operations are not allowed in type expressions"
j: lambda: 1, # error: [invalid-type-form] "`lambda` expressions are not allowed in type expressions"
k: 1 if True else 2, # error: [invalid-type-form] "`if` expressions are not allowed in type expressions"
l: await 1, # error: [invalid-type-form] "`await` expressions are not allowed in type expressions"
m: (yield 1), # error: [invalid-type-form] "`yield` expressions are not allowed in type expressions"
n: (yield from [1]), # error: [invalid-type-form] "`yield from` expressions are not allowed in type expressions"
o: 1 < 2, # error: [invalid-type-form] "Comparison expressions are not allowed in type expressions"
p: bar(), # error: [invalid-type-form] "Function calls are not allowed in type expressions"
q: int | f"foo", # error: [invalid-type-form] "F-strings are not allowed in type expressions"
r: [1, 2, 3][1:2], # error: [invalid-type-form] "Slices are not allowed in type expressions"
):
reveal_type(a) # revealed: Unknown
reveal_type(b) # revealed: Unknown
reveal_type(c) # revealed: Unknown
reveal_type(d) # revealed: Unknown
reveal_type(e) # revealed: int | Unknown
reveal_type(f) # revealed: Unknown
reveal_type(g) # revealed: Unknown
reveal_type(h) # revealed: Unknown
reveal_type(i) # revealed: Unknown
reveal_type(j) # revealed: Unknown
reveal_type(k) # revealed: Unknown
reveal_type(p) # revealed: Unknown
reveal_type(q) # revealed: int | Unknown
reveal_type(r) # revealed: @Todo(unknown type subscript)
async def outer(): # avoid unrelated syntax errors on yield, yield from, and await
def _(
a: 1, # error: [invalid-type-form] "Int literals are not allowed in this context in a type expression"
b: 2.3, # error: [invalid-type-form] "Float literals are not allowed in type expressions"
c: 4j, # error: [invalid-type-form] "Complex literals are not allowed in type expressions"
d: True, # error: [invalid-type-form] "Boolean literals are not allowed in this context in a type expression"
e: int | b"foo", # error: [invalid-type-form] "Bytes literals are not allowed in this context in a type expression"
f: 1 and 2, # error: [invalid-type-form] "Boolean operations are not allowed in type expressions"
g: 1 or 2, # error: [invalid-type-form] "Boolean operations are not allowed in type expressions"
h: (foo := 1), # error: [invalid-type-form] "Named expressions are not allowed in type expressions"
i: not 1, # error: [invalid-type-form] "Unary operations are not allowed in type expressions"
j: lambda: 1, # error: [invalid-type-form] "`lambda` expressions are not allowed in type expressions"
k: 1 if True else 2, # error: [invalid-type-form] "`if` expressions are not allowed in type expressions"
l: await 1, # error: [invalid-type-form] "`await` expressions are not allowed in type expressions"
m: (yield 1), # error: [invalid-type-form] "`yield` expressions are not allowed in type expressions"
n: (yield from [1]), # error: [invalid-type-form] "`yield from` expressions are not allowed in type expressions"
o: 1 < 2, # error: [invalid-type-form] "Comparison expressions are not allowed in type expressions"
p: bar(), # error: [invalid-type-form] "Function calls are not allowed in type expressions"
q: int | f"foo", # error: [invalid-type-form] "F-strings are not allowed in type expressions"
r: [1, 2, 3][1:2], # error: [invalid-type-form] "Slices are not allowed in type expressions"
):
reveal_type(a) # revealed: Unknown
reveal_type(b) # revealed: Unknown
reveal_type(c) # revealed: Unknown
reveal_type(d) # revealed: Unknown
reveal_type(e) # revealed: int | Unknown
reveal_type(f) # revealed: Unknown
reveal_type(g) # revealed: Unknown
reveal_type(h) # revealed: Unknown
reveal_type(i) # revealed: Unknown
reveal_type(j) # revealed: Unknown
reveal_type(k) # revealed: Unknown
reveal_type(p) # revealed: Unknown
reveal_type(q) # revealed: int | Unknown
reveal_type(r) # revealed: @Todo(unknown type subscript)
```

## Invalid Collection based AST nodes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,8 +127,9 @@ class AsyncIterable:
def __aiter__(self) -> AsyncIterator:
return AsyncIterator()

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

### Invalid async comprehension
Expand All @@ -145,6 +146,7 @@ class Iterable:
def __iter__(self) -> Iterator:
return Iterator()

# revealed: @Todo(async iterables/iterators)
[reveal_type(x) async for x in Iterable()]
async def _():
# revealed: @Todo(async iterables/iterators)
[reveal_type(x) async for x in Iterable()]
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
# Semantic syntax error diagnostics

## `async` comprehensions in synchronous comprehensions

### Python 3.10

<!-- snapshot-diagnostics -->

Before Python 3.11, `async` comprehensions could not be used within outer sync comprehensions, even
within an `async` function ([CPython issue](https://github.com/python/cpython/issues/77527)):

```toml
[environment]
python-version = "3.10"
```

```py
async def elements(n):
yield n

async def f():
# error: 19 [invalid-syntax] "cannot use an asynchronous comprehension outside of an asynchronous function on Python 3.10 (syntax was added in 3.11)"
return {n: [x async for x in elements(n)] for n in range(3)}
```

If all of the comprehensions are `async`, on the other hand, the code was still valid:

```py
async def test():
return [[x async for x in elements(n)] async for n in range(3)]
```

These are a couple of tricky but valid cases to check that nested scope handling is wired up
correctly in the `SemanticSyntaxContext` trait:

```py
async def f():
[x for x in [1]] and [x async for x in elements(1)]

async def f():
def g():
pass
[x async for x in elements(1)]
```

### Python 3.11

All of these same examples are valid after Python 3.11:

```toml
[environment]
python-version = "3.11"
```

```py
async def elements(n):
yield n

async def f():
return {n: [x async for x in elements(n)] for n in range(3)}
```

## Late `__future__` import

```py
from collections import namedtuple

# error: [invalid-syntax] "__future__ imports must be at the top of the file"
from __future__ import print_function
```

## Invalid annotation

This one might be a bit redundant with the `invalid-type-form` error.

```toml
[environment]
python-version = "3.12"
```

```py
from __future__ import annotations

# error: [invalid-type-form] "Named expressions are not allowed in type expressions"
# error: [invalid-syntax] "named expression cannot be used within a type annotation"
def f() -> (y := 3): ...
```

## Duplicate `match` key

```toml
[environment]
python-version = "3.10"
```

```py
match 2:
# error: [invalid-syntax] "mapping pattern checks duplicate key `"x"`"
case {"x": 1, "x": 2}:
...
```

## `return`, `yield`, `yield from`, and `await` outside function

```py
# error: [invalid-syntax] "`return` statement outside of a function"
return

# error: [invalid-syntax] "`yield` statement outside of a function"
yield

# error: [invalid-syntax] "`yield from` statement outside of a function"
yield from []

# error: [invalid-syntax] "`await` statement outside of a function"
# error: [invalid-syntax] "`await` outside of an asynchronous function"
await 1

def f():
# error: [invalid-syntax] "`await` outside of an asynchronous function"
await 1
```

Generators are evaluated lazily, so `await` is allowed, even outside of a function.

```py
async def g():
yield 1

(x async for x in g())
```

## `await` outside async function

This error includes `await`, `async for`, `async with`, and `async` comprehensions.

```python
async def elements(n):
yield n

def _():
# error: [invalid-syntax] "`await` outside of an asynchronous function"
await 1
# error: [invalid-syntax] "`async for` outside of an asynchronous function"
async for _ in elements(1):
...
# error: [invalid-syntax] "`async with` outside of an asynchronous function"
async with elements(1) as x:
...
# error: [invalid-syntax] "cannot use an asynchronous comprehension outside of an asynchronous function on Python 3.9 (syntax was added in 3.11)"
# error: [invalid-syntax] "asynchronous comprehension outside of an asynchronous function"
[x async for x in elements(1)]
```

## Load before `global` declaration

This should be an error, but it's not yet.

TODO implement `SemanticSyntaxContext::global`

```py
def f():
x = 1
global x
```
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ match 42:
...
case [O]:
...
case P | Q:
case P | Q: # error: [invalid-syntax] "name capture `P` makes remaining patterns unreachable"
...
case object(foo=R):
...
Expand Down Expand Up @@ -289,7 +289,7 @@ match 42:
...
case [D]:
...
case E | F:
case E | F: # error: [invalid-syntax] "name capture `E` makes remaining patterns unreachable"
...
case object(foo=G):
...
Expand Down Expand Up @@ -357,7 +357,7 @@ match 42:
...
case [D]:
...
case E | F:
case E | F: # error: [invalid-syntax] "name capture `E` makes remaining patterns unreachable"
...
case object(foo=G):
...
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
---
source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: semantic_syntax_errors.md - Semantic syntax error diagnostics - `async` comprehensions in synchronous comprehensions - Python 3.10
mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/semantic_syntax_errors.md
---

# Python source files

## mdtest_snippet.py

```
1 | async def elements(n):
2 | yield n
3 |
4 | async def f():
5 | # error: 19 [invalid-syntax] "cannot use an asynchronous comprehension outside of an asynchronous function on Python 3.10 (syntax was added in 3.11)"
6 | return {n: [x async for x in elements(n)] for n in range(3)}
7 | async def test():
8 | return [[x async for x in elements(n)] async for n in range(3)]
9 | async def f():
10 | [x for x in [1]] and [x async for x in elements(1)]
11 |
12 | async def f():
13 | def g():
14 | pass
15 | [x async for x in elements(1)]
```

# Diagnostics

```
error: invalid-syntax
--> /src/mdtest_snippet.py:6:19
|
4 | async def f():
5 | # error: 19 [invalid-syntax] "cannot use an asynchronous comprehension outside of an asynchronous function on Python 3.10 (syntax...
6 | return {n: [x async for x in elements(n)] for n in range(3)}
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ cannot use an asynchronous comprehension outside of an asynchronous function on Python 3.10 (syntax was added in 3.11)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a bit confused by this message. Isn't f async?

It's a bit a shame that we can't make use of the multi range diagnostics because it would be nice to have a second frame outlining "why" the context isn't async (which would address my confusion). I don't think this is something we should solve as part of this PR but maybe something to come back once we also use the new diagnostics in Ruff.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the "synchronous function" that the async list comprehension is immediately nested inside of here is the function-like scope created by the synchronous dict comprehension. But I agree that it would be great if we could have a clearer error and a multi-span diagnostic

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, f is async, but the version-related part is that you can't use an async comprehension inside of a sync comprehension before 3.11. This is the error message I'm trying to clean up in #17460, so I definitely agree that it's confusing!

That's a good point about the multi-span diagnostic too. We could definitely highlight the outer sync comprehension, as well as the inner async one.

7 | async def test():
8 | return [[x async for x in elements(n)] async for n in range(3)]
|
```
8 changes: 8 additions & 0 deletions crates/red_knot_python_semantic/src/semantic_index.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use ruff_db::files::File;
use ruff_db::parsed::parsed_module;
use ruff_index::{IndexSlice, IndexVec};

use ruff_python_parser::semantic_errors::SemanticSyntaxError;
use rustc_hash::{FxBuildHasher, FxHashMap, FxHashSet};
use salsa::plumbing::AsId;
use salsa::Update;
Expand Down Expand Up @@ -175,6 +176,9 @@ pub(crate) struct SemanticIndex<'db> {

/// Map of all of the eager bindings that appear in this file.
eager_bindings: FxHashMap<EagerBindingsKey, ScopedEagerBindingsId>,

/// List of all semantic syntax errors in this file.
semantic_syntax_errors: Vec<SemanticSyntaxError>,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could be a good use case for a ThinVec to shrink the size of this struct, considering that most files won't have any SemanticSyntaxError. I'm okay making this change as a separate PR and also applying it to TypeCheckDiagnostics

}

impl<'db> SemanticIndex<'db> {
Expand Down Expand Up @@ -399,6 +403,10 @@ impl<'db> SemanticIndex<'db> {
None => EagerBindingsResult::NotFound,
}
}

pub(crate) fn semantic_syntax_errors(&self) -> &[SemanticSyntaxError] {
&self.semantic_syntax_errors
}
}

pub struct AncestorsIter<'a> {
Expand Down
Loading
Loading