[syntax-errors] Make async-comprehension-in-sync-comprehension more specific (#17460)

## Summary

While adding semantic error support to red-knot, I noticed duplicate
diagnostics for code like this:

```py
# 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"
 [reveal_type(x) async for x in AsyncIterable()]
```

Beyond the duplication, the first error message doesn't make much sense
because this syntax is _not_ allowed on Python 3.11 either.

To fix this, this PR renames the
`async-comprehension-outside-async-function` semantic syntax error to
`async-comprehension-in-sync-comprehension` and fixes the rule to avoid
applying outside of sync comprehensions at all.

## Test Plan

New linter test demonstrating the false positive. The mdtests from my red-knot 
PR also reflect this change.
This commit is contained in:
Brent Westbrook 2025-04-24 15:45:54 -04:00 committed by GitHub
parent f7b48510b5
commit 92ecfc908b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 25 additions and 21 deletions

View file

@ -19,7 +19,7 @@ async def elements(n):
yield n yield n
async def f(): 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)" # error: 19 [invalid-syntax] "cannot use an asynchronous comprehension inside of a synchronous comprehension on Python 3.10 (syntax was added in 3.11)"
return {n: [x async for x in elements(n)] for n in range(3)} return {n: [x async for x in elements(n)] for n in range(3)}
``` ```

View file

@ -16,7 +16,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/semant
2 | yield n 2 | yield n
3 | 3 |
4 | async def f(): 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)" 5 | # error: 19 [invalid-syntax] "cannot use an asynchronous comprehension inside of a synchronous comprehension 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)} 6 | return {n: [x async for x in elements(n)] for n in range(3)}
7 | async def test(): 7 | async def test():
8 | return [[x async for x in elements(n)] async for n in range(3)] 8 | return [[x async for x in elements(n)] async for n in range(3)]
@ -36,9 +36,9 @@ error: invalid-syntax
--> /src/mdtest_snippet.py:6:19 --> /src/mdtest_snippet.py:6:19
| |
4 | async def f(): 4 | async def f():
5 | # error: 19 [invalid-syntax] "cannot use an asynchronous comprehension outside of an asynchronous function on Python 3.10 (syntax... 5 | # error: 19 [invalid-syntax] "cannot use an asynchronous comprehension inside of a synchronous comprehension on Python 3.10 (synt...
6 | return {n: [x async for x in elements(n)] for n in range(3)} 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) | ^^^^^^^^^^^^^^^^^^^^^^^^^^ cannot use an asynchronous comprehension inside of a synchronous comprehension on Python 3.10 (syntax was added in 3.11)
7 | async def test(): 7 | async def test():
8 | return [[x async for x in elements(n)] async for n in range(3)] 8 | return [[x async for x in elements(n)] async for n in range(3)]
| |

View file

@ -615,7 +615,7 @@ impl SemanticSyntaxContext for Checker<'_> {
| SemanticSyntaxErrorKind::DuplicateMatchKey(_) | SemanticSyntaxErrorKind::DuplicateMatchKey(_)
| SemanticSyntaxErrorKind::DuplicateMatchClassAttribute(_) | SemanticSyntaxErrorKind::DuplicateMatchClassAttribute(_)
| SemanticSyntaxErrorKind::InvalidStarExpression | SemanticSyntaxErrorKind::InvalidStarExpression
| SemanticSyntaxErrorKind::AsyncComprehensionOutsideAsyncFunction(_) | SemanticSyntaxErrorKind::AsyncComprehensionInSyncComprehension(_)
| SemanticSyntaxErrorKind::DuplicateParameter(_) => { | SemanticSyntaxErrorKind::DuplicateParameter(_) => {
if self.settings.preview.is_enabled() { if self.settings.preview.is_enabled() {
self.semantic_errors.borrow_mut().push(error); self.semantic_errors.borrow_mut().push(error);

View file

@ -1022,6 +1022,7 @@ mod tests {
", ",
PythonVersion::PY310 PythonVersion::PY310
)] )]
#[test_case("false_positive", "[x async for x in y]", PythonVersion::PY310)]
fn test_async_comprehension_in_sync_comprehension( fn test_async_comprehension_in_sync_comprehension(
name: &str, name: &str,
contents: &str, contents: &str,

View file

@ -1,7 +1,7 @@
--- ---
source: crates/ruff_linter/src/linter.rs source: crates/ruff_linter/src/linter.rs
--- ---
<filename>:1:27: SyntaxError: cannot use an asynchronous comprehension outside of an asynchronous function on Python 3.10 (syntax was added in 3.11) <filename>:1:27: SyntaxError: cannot use an asynchronous comprehension inside of a synchronous comprehension on Python 3.10 (syntax was added in 3.11)
| |
1 | async def f(): return [[x async for x in foo(n)] for n in range(3)] 1 | async def f(): return [[x async for x in foo(n)] for n in range(3)]
| ^^^^^^^^^^^^^^^^^^^^^ | ^^^^^^^^^^^^^^^^^^^^^

View file

@ -0,0 +1,3 @@
---
source: crates/ruff_linter/src/linter.rs
---

View file

@ -1,7 +1,7 @@
--- ---
source: crates/ruff_linter/src/linter.rs source: crates/ruff_linter/src/linter.rs
--- ---
resources/test/fixtures/syntax_errors/async_comprehension.ipynb:3:5: SyntaxError: cannot use an asynchronous comprehension outside of an asynchronous function on Python 3.10 (syntax was added in 3.11) resources/test/fixtures/syntax_errors/async_comprehension.ipynb:3:5: SyntaxError: cannot use an asynchronous comprehension inside of a synchronous comprehension on Python 3.10 (syntax was added in 3.11)
| |
1 | async def elements(n): yield n 1 | async def elements(n): yield n
2 | [x async for x in elements(5)] # okay, async at top level 2 | [x async for x in elements(5)] # okay, async at top level

View file

@ -573,7 +573,7 @@ impl SemanticSyntaxChecker {
elt, generators, .. elt, generators, ..
}) => { }) => {
Self::check_generator_expr(elt, generators, ctx); Self::check_generator_expr(elt, generators, ctx);
Self::async_comprehension_outside_async_function(ctx, generators); Self::async_comprehension_in_sync_comprehension(ctx, generators);
for generator in generators.iter().filter(|g| g.is_async) { for generator in generators.iter().filter(|g| g.is_async) {
Self::await_outside_async_function( Self::await_outside_async_function(
ctx, ctx,
@ -590,7 +590,7 @@ impl SemanticSyntaxChecker {
}) => { }) => {
Self::check_generator_expr(key, generators, ctx); Self::check_generator_expr(key, generators, ctx);
Self::check_generator_expr(value, generators, ctx); Self::check_generator_expr(value, generators, ctx);
Self::async_comprehension_outside_async_function(ctx, generators); Self::async_comprehension_in_sync_comprehension(ctx, generators);
for generator in generators.iter().filter(|g| g.is_async) { for generator in generators.iter().filter(|g| g.is_async) {
Self::await_outside_async_function( Self::await_outside_async_function(
ctx, ctx,
@ -801,7 +801,7 @@ impl SemanticSyntaxChecker {
} }
} }
fn async_comprehension_outside_async_function<Ctx: SemanticSyntaxContext>( fn async_comprehension_in_sync_comprehension<Ctx: SemanticSyntaxContext>(
ctx: &Ctx, ctx: &Ctx,
generators: &[ast::Comprehension], generators: &[ast::Comprehension],
) { ) {
@ -813,7 +813,7 @@ impl SemanticSyntaxChecker {
if ctx.in_notebook() && ctx.in_module_scope() { if ctx.in_notebook() && ctx.in_module_scope() {
return; return;
} }
if ctx.in_async_context() && !ctx.in_sync_comprehension() { if !ctx.in_sync_comprehension() {
return; return;
} }
for generator in generators.iter().filter(|gen| gen.is_async) { for generator in generators.iter().filter(|gen| gen.is_async) {
@ -845,7 +845,7 @@ impl SemanticSyntaxChecker {
// async def j(): return [([y for y in range(1)], [z async for z in range(2)]) for x in range(5)] // async def j(): return [([y for y in range(1)], [z async for z in range(2)]) for x in range(5)]
Self::add_error( Self::add_error(
ctx, ctx,
SemanticSyntaxErrorKind::AsyncComprehensionOutsideAsyncFunction(python_version), SemanticSyntaxErrorKind::AsyncComprehensionInSyncComprehension(python_version),
generator.range, generator.range,
); );
} }
@ -914,11 +914,11 @@ impl Display for SemanticSyntaxError {
SemanticSyntaxErrorKind::InvalidStarExpression => { SemanticSyntaxErrorKind::InvalidStarExpression => {
f.write_str("can't use starred expression here") f.write_str("can't use starred expression here")
} }
SemanticSyntaxErrorKind::AsyncComprehensionOutsideAsyncFunction(python_version) => { SemanticSyntaxErrorKind::AsyncComprehensionInSyncComprehension(python_version) => {
write!( write!(
f, f,
"cannot use an asynchronous comprehension outside of an asynchronous \ "cannot use an asynchronous comprehension inside of a synchronous comprehension \
function on Python {python_version} (syntax was added in 3.11)", on Python {python_version} (syntax was added in 3.11)",
) )
} }
SemanticSyntaxErrorKind::YieldOutsideFunction(kind) => { SemanticSyntaxErrorKind::YieldOutsideFunction(kind) => {
@ -1187,7 +1187,7 @@ pub enum SemanticSyntaxErrorKind {
/// This was discussed in [BPO 33346] and fixed in Python 3.11. /// This was discussed in [BPO 33346] and fixed in Python 3.11.
/// ///
/// [BPO 33346]: https://github.com/python/cpython/issues/77527 /// [BPO 33346]: https://github.com/python/cpython/issues/77527
AsyncComprehensionOutsideAsyncFunction(PythonVersion), AsyncComprehensionInSyncComprehension(PythonVersion),
/// Represents the use of `yield`, `yield from`, or `await` outside of a function scope. /// Represents the use of `yield`, `yield from`, or `await` outside of a function scope.
/// ///

View file

@ -780,7 +780,7 @@ Module(
| |
1 | # parse_options: {"target-version": "3.10"} 1 | # parse_options: {"target-version": "3.10"}
2 | async def f(): return [[x async for x in foo(n)] for n in range(3)] # list 2 | async def f(): return [[x async for x in foo(n)] for n in range(3)] # list
| ^^^^^^^^^^^^^^^^^^^^^ Syntax Error: cannot use an asynchronous comprehension outside of an asynchronous function on Python 3.10 (syntax was added in 3.11) | ^^^^^^^^^^^^^^^^^^^^^ Syntax Error: cannot use an asynchronous comprehension inside of a synchronous comprehension on Python 3.10 (syntax was added in 3.11)
3 | async def g(): return [{x: 1 async for x in foo(n)} for n in range(3)] # dict 3 | async def g(): return [{x: 1 async for x in foo(n)} for n in range(3)] # dict
4 | async def h(): return [{x async for x in foo(n)} for n in range(3)] # set 4 | async def h(): return [{x async for x in foo(n)} for n in range(3)] # set
| |
@ -790,7 +790,7 @@ Module(
1 | # parse_options: {"target-version": "3.10"} 1 | # parse_options: {"target-version": "3.10"}
2 | async def f(): return [[x async for x in foo(n)] for n in range(3)] # list 2 | async def f(): return [[x async for x in foo(n)] for n in range(3)] # list
3 | async def g(): return [{x: 1 async for x in foo(n)} for n in range(3)] # dict 3 | async def g(): return [{x: 1 async for x in foo(n)} for n in range(3)] # dict
| ^^^^^^^^^^^^^^^^^^^^^ Syntax Error: cannot use an asynchronous comprehension outside of an asynchronous function on Python 3.10 (syntax was added in 3.11) | ^^^^^^^^^^^^^^^^^^^^^ Syntax Error: cannot use an asynchronous comprehension inside of a synchronous comprehension on Python 3.10 (syntax was added in 3.11)
4 | async def h(): return [{x async for x in foo(n)} for n in range(3)] # set 4 | async def h(): return [{x async for x in foo(n)} for n in range(3)] # set
5 | async def i(): return [([y async for y in range(1)], [z for z in range(2)]) for x in range(5)] 5 | async def i(): return [([y async for y in range(1)], [z for z in range(2)]) for x in range(5)]
| |
@ -800,7 +800,7 @@ Module(
2 | async def f(): return [[x async for x in foo(n)] for n in range(3)] # list 2 | async def f(): return [[x async for x in foo(n)] for n in range(3)] # list
3 | async def g(): return [{x: 1 async for x in foo(n)} for n in range(3)] # dict 3 | async def g(): return [{x: 1 async for x in foo(n)} for n in range(3)] # dict
4 | async def h(): return [{x async for x in foo(n)} for n in range(3)] # set 4 | async def h(): return [{x async for x in foo(n)} for n in range(3)] # set
| ^^^^^^^^^^^^^^^^^^^^^ Syntax Error: cannot use an asynchronous comprehension outside of an asynchronous function on Python 3.10 (syntax was added in 3.11) | ^^^^^^^^^^^^^^^^^^^^^ Syntax Error: cannot use an asynchronous comprehension inside of a synchronous comprehension on Python 3.10 (syntax was added in 3.11)
5 | async def i(): return [([y async for y in range(1)], [z for z in range(2)]) for x in range(5)] 5 | async def i(): return [([y async for y in range(1)], [z for z in range(2)]) for x in range(5)]
6 | async def j(): return [([y for y in range(1)], [z async for z in range(2)]) for x in range(5)] 6 | async def j(): return [([y for y in range(1)], [z async for z in range(2)]) for x in range(5)]
| |
@ -810,7 +810,7 @@ Module(
3 | async def g(): return [{x: 1 async for x in foo(n)} for n in range(3)] # dict 3 | async def g(): return [{x: 1 async for x in foo(n)} for n in range(3)] # dict
4 | async def h(): return [{x async for x in foo(n)} for n in range(3)] # set 4 | async def h(): return [{x async for x in foo(n)} for n in range(3)] # set
5 | async def i(): return [([y async for y in range(1)], [z for z in range(2)]) for x in range(5)] 5 | async def i(): return [([y async for y in range(1)], [z for z in range(2)]) for x in range(5)]
| ^^^^^^^^^^^^^^^^^^^^^^^ Syntax Error: cannot use an asynchronous comprehension outside of an asynchronous function on Python 3.10 (syntax was added in 3.11) | ^^^^^^^^^^^^^^^^^^^^^^^ Syntax Error: cannot use an asynchronous comprehension inside of a synchronous comprehension on Python 3.10 (syntax was added in 3.11)
6 | async def j(): return [([y for y in range(1)], [z async for z in range(2)]) for x in range(5)] 6 | async def j(): return [([y for y in range(1)], [z async for z in range(2)]) for x in range(5)]
| |
@ -819,5 +819,5 @@ Module(
4 | async def h(): return [{x async for x in foo(n)} for n in range(3)] # set 4 | async def h(): return [{x async for x in foo(n)} for n in range(3)] # set
5 | async def i(): return [([y async for y in range(1)], [z for z in range(2)]) for x in range(5)] 5 | async def i(): return [([y async for y in range(1)], [z for z in range(2)]) for x in range(5)]
6 | async def j(): return [([y for y in range(1)], [z async for z in range(2)]) for x in range(5)] 6 | async def j(): return [([y for y in range(1)], [z async for z in range(2)]) for x in range(5)]
| ^^^^^^^^^^^^^^^^^^^^^^^ Syntax Error: cannot use an asynchronous comprehension outside of an asynchronous function on Python 3.10 (syntax was added in 3.11) | ^^^^^^^^^^^^^^^^^^^^^^^ Syntax Error: cannot use an asynchronous comprehension inside of a synchronous comprehension on Python 3.10 (syntax was added in 3.11)
| |