From 92ecfc908be2ed0d61477ddecf1d51608d3b35d7 Mon Sep 17 00:00:00 2001 From: Brent Westbrook <36778786+ntBre@users.noreply.github.com> Date: Thu, 24 Apr 2025 15:45:54 -0400 Subject: [PATCH] [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. --- .../diagnostics/semantic_syntax_errors.md | 2 +- ...nchronous_comprehensions_-_Python_3.10.snap | 6 +++--- crates/ruff_linter/src/checkers/ast/mod.rs | 2 +- crates/ruff_linter/src/linter.rs | 1 + ...n_sync_comprehension_error_on_310_3.10.snap | 2 +- ...sync_comprehension_false_positive_3.10.snap | 3 +++ ...on_in_sync_comprehension_notebook_3.10.snap | 2 +- .../ruff_python_parser/src/semantic_errors.rs | 18 +++++++++--------- ...ax@nested_async_comprehension_py310.py.snap | 10 +++++----- 9 files changed, 25 insertions(+), 21 deletions(-) create mode 100644 crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__async_comprehension_in_sync_comprehension_false_positive_3.10.snap diff --git a/crates/red_knot_python_semantic/resources/mdtest/diagnostics/semantic_syntax_errors.md b/crates/red_knot_python_semantic/resources/mdtest/diagnostics/semantic_syntax_errors.md index 7dfabdef84..4c755ac6cc 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/diagnostics/semantic_syntax_errors.md +++ b/crates/red_knot_python_semantic/resources/mdtest/diagnostics/semantic_syntax_errors.md @@ -19,7 +19,7 @@ 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)" + # 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)} ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/semantic_syntax_errors.md_-_Semantic_syntax_error_diagnostics_-_`async`_comprehensions_in_synchronous_comprehensions_-_Python_3.10.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/semantic_syntax_errors.md_-_Semantic_syntax_error_diagnostics_-_`async`_comprehensions_in_synchronous_comprehensions_-_Python_3.10.snap index d3ec566124..974595ccfa 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/semantic_syntax_errors.md_-_Semantic_syntax_error_diagnostics_-_`async`_comprehensions_in_synchronous_comprehensions_-_Python_3.10.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/semantic_syntax_errors.md_-_Semantic_syntax_error_diagnostics_-_`async`_comprehensions_in_synchronous_comprehensions_-_Python_3.10.snap @@ -16,7 +16,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/semant 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)" + 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)} 7 | async def test(): 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 | 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)} - | ^^^^^^^^^^^^^^^^^^^^^^^^^^ 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(): 8 | return [[x async for x in elements(n)] async for n in range(3)] | diff --git a/crates/ruff_linter/src/checkers/ast/mod.rs b/crates/ruff_linter/src/checkers/ast/mod.rs index 87169d7a91..7178db784f 100644 --- a/crates/ruff_linter/src/checkers/ast/mod.rs +++ b/crates/ruff_linter/src/checkers/ast/mod.rs @@ -615,7 +615,7 @@ impl SemanticSyntaxContext for Checker<'_> { | SemanticSyntaxErrorKind::DuplicateMatchKey(_) | SemanticSyntaxErrorKind::DuplicateMatchClassAttribute(_) | SemanticSyntaxErrorKind::InvalidStarExpression - | SemanticSyntaxErrorKind::AsyncComprehensionOutsideAsyncFunction(_) + | SemanticSyntaxErrorKind::AsyncComprehensionInSyncComprehension(_) | SemanticSyntaxErrorKind::DuplicateParameter(_) => { if self.settings.preview.is_enabled() { self.semantic_errors.borrow_mut().push(error); diff --git a/crates/ruff_linter/src/linter.rs b/crates/ruff_linter/src/linter.rs index b266a82e54..4af0010c38 100644 --- a/crates/ruff_linter/src/linter.rs +++ b/crates/ruff_linter/src/linter.rs @@ -1022,6 +1022,7 @@ mod tests { ", PythonVersion::PY310 )] + #[test_case("false_positive", "[x async for x in y]", PythonVersion::PY310)] fn test_async_comprehension_in_sync_comprehension( name: &str, contents: &str, diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__async_comprehension_in_sync_comprehension_error_on_310_3.10.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__async_comprehension_in_sync_comprehension_error_on_310_3.10.snap index 86380f014a..9fd781f021 100644 --- a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__async_comprehension_in_sync_comprehension_error_on_310_3.10.snap +++ b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__async_comprehension_in_sync_comprehension_error_on_310_3.10.snap @@ -1,7 +1,7 @@ --- source: crates/ruff_linter/src/linter.rs --- -:1:27: SyntaxError: cannot use an asynchronous comprehension outside of an asynchronous function on Python 3.10 (syntax was added in 3.11) +: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)] | ^^^^^^^^^^^^^^^^^^^^^ diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__async_comprehension_in_sync_comprehension_false_positive_3.10.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__async_comprehension_in_sync_comprehension_false_positive_3.10.snap new file mode 100644 index 0000000000..8d0a8faf7a --- /dev/null +++ b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__async_comprehension_in_sync_comprehension_false_positive_3.10.snap @@ -0,0 +1,3 @@ +--- +source: crates/ruff_linter/src/linter.rs +--- diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__async_comprehension_in_sync_comprehension_notebook_3.10.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__async_comprehension_in_sync_comprehension_notebook_3.10.snap index d55d10cfc6..c573573f70 100644 --- a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__async_comprehension_in_sync_comprehension_notebook_3.10.snap +++ b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__async_comprehension_in_sync_comprehension_notebook_3.10.snap @@ -1,7 +1,7 @@ --- 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 2 | [x async for x in elements(5)] # okay, async at top level diff --git a/crates/ruff_python_parser/src/semantic_errors.rs b/crates/ruff_python_parser/src/semantic_errors.rs index fd41ea2187..a12b61cb06 100644 --- a/crates/ruff_python_parser/src/semantic_errors.rs +++ b/crates/ruff_python_parser/src/semantic_errors.rs @@ -573,7 +573,7 @@ impl SemanticSyntaxChecker { elt, generators, .. }) => { 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) { Self::await_outside_async_function( ctx, @@ -590,7 +590,7 @@ impl SemanticSyntaxChecker { }) => { Self::check_generator_expr(key, 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) { Self::await_outside_async_function( ctx, @@ -801,7 +801,7 @@ impl SemanticSyntaxChecker { } } - fn async_comprehension_outside_async_function( + fn async_comprehension_in_sync_comprehension( ctx: &Ctx, generators: &[ast::Comprehension], ) { @@ -813,7 +813,7 @@ impl SemanticSyntaxChecker { if ctx.in_notebook() && ctx.in_module_scope() { return; } - if ctx.in_async_context() && !ctx.in_sync_comprehension() { + if !ctx.in_sync_comprehension() { return; } 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)] Self::add_error( ctx, - SemanticSyntaxErrorKind::AsyncComprehensionOutsideAsyncFunction(python_version), + SemanticSyntaxErrorKind::AsyncComprehensionInSyncComprehension(python_version), generator.range, ); } @@ -914,11 +914,11 @@ impl Display for SemanticSyntaxError { SemanticSyntaxErrorKind::InvalidStarExpression => { f.write_str("can't use starred expression here") } - SemanticSyntaxErrorKind::AsyncComprehensionOutsideAsyncFunction(python_version) => { + SemanticSyntaxErrorKind::AsyncComprehensionInSyncComprehension(python_version) => { write!( f, - "cannot use an asynchronous comprehension outside of an asynchronous \ - function on Python {python_version} (syntax was added in 3.11)", + "cannot use an asynchronous comprehension inside of a synchronous comprehension \ + on Python {python_version} (syntax was added in 3.11)", ) } SemanticSyntaxErrorKind::YieldOutsideFunction(kind) => { @@ -1187,7 +1187,7 @@ pub enum SemanticSyntaxErrorKind { /// This was discussed in [BPO 33346] and fixed in Python 3.11. /// /// [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. /// diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@nested_async_comprehension_py310.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@nested_async_comprehension_py310.py.snap index 465ef924e7..cec50a8151 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@nested_async_comprehension_py310.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@nested_async_comprehension_py310.py.snap @@ -780,7 +780,7 @@ Module( | 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 - | ^^^^^^^^^^^^^^^^^^^^^ 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 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"} 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 - | ^^^^^^^^^^^^^^^^^^^^^ 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 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 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 - | ^^^^^^^^^^^^^^^^^^^^^ 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)] 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 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)] - | ^^^^^^^^^^^^^^^^^^^^^^^ 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)] | @@ -819,5 +819,5 @@ Module( 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)] 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) |