diff --git a/crates/ruff_linter/resources/test/fixtures/pyflakes/F704.py b/crates/ruff_linter/resources/test/fixtures/pyflakes/F704.py index 70a1272d42..819c59c4b9 100644 --- a/crates/ruff_linter/resources/test/fixtures/pyflakes/F704.py +++ b/crates/ruff_linter/resources/test/fixtures/pyflakes/F704.py @@ -17,3 +17,24 @@ def _(): # Valid yield scope yield 3 + + +# await is valid in any generator, sync or async +(await cor async for cor in f()) # ok +(await cor for cor in f()) # ok + +# but not in comprehensions +[await cor async for cor in f()] # F704 +{await cor async for cor in f()} # F704 +{await cor: 1 async for cor in f()} # F704 +[await cor for cor in f()] # F704 +{await cor for cor in f()} # F704 +{await cor: 1 for cor in f()} # F704 + +# or in the iterator of an async generator, which is evaluated in the parent +# scope +(cor async for cor in await f()) # F704 +(await cor async for cor in [await c for c in f()]) # F704 + +# this is also okay because the comprehension is within the generator scope +([await c for c in cor] async for cor in f()) # ok diff --git a/crates/ruff_linter/resources/test/fixtures/syntax_errors/await_outside_async_function.py b/crates/ruff_linter/resources/test/fixtures/syntax_errors/await_outside_async_function.py index dd49cb05f9..6285677609 100644 --- a/crates/ruff_linter/resources/test/fixtures/syntax_errors/await_outside_async_function.py +++ b/crates/ruff_linter/resources/test/fixtures/syntax_errors/await_outside_async_function.py @@ -3,3 +3,5 @@ def func(): # Top-level await await 1 + +([await c for c in cor] async for cor in func()) # ok diff --git a/crates/ruff_linter/src/checkers/ast/mod.rs b/crates/ruff_linter/src/checkers/ast/mod.rs index 8c360448a4..c1d98b6d50 100644 --- a/crates/ruff_linter/src/checkers/ast/mod.rs +++ b/crates/ruff_linter/src/checkers/ast/mod.rs @@ -780,6 +780,10 @@ impl SemanticSyntaxContext for Checker<'_> { match scope.kind { ScopeKind::Class(_) => return false, ScopeKind::Function(_) | ScopeKind::Lambda(_) => return true, + ScopeKind::Generator { + kind: GeneratorKind::Generator, + .. + } => return true, ScopeKind::Generator { .. } | ScopeKind::Module | ScopeKind::Type @@ -829,14 +833,19 @@ impl SemanticSyntaxContext for Checker<'_> { self.source_type.is_ipynb() } - fn in_generator_scope(&self) -> bool { - matches!( - &self.semantic.current_scope().kind, - ScopeKind::Generator { - kind: GeneratorKind::Generator, - .. + fn in_generator_context(&self) -> bool { + for scope in self.semantic.current_scopes() { + if matches!( + scope.kind, + ScopeKind::Generator { + kind: GeneratorKind::Generator, + .. + } + ) { + return true; } - ) + } + false } fn in_loop_context(&self) -> bool { diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F704_F704.py.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F704_F704.py.snap index 25a0fe321a..8d3bff4284 100644 --- a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F704_F704.py.snap +++ b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F704_F704.py.snap @@ -37,3 +37,88 @@ F704 `await` statement outside of a function 12 | 13 | def _(): | + +F704 `await` statement outside of a function + --> F704.py:27:2 + | +26 | # but not in comprehensions +27 | [await cor async for cor in f()] # F704 + | ^^^^^^^^^ +28 | {await cor async for cor in f()} # F704 +29 | {await cor: 1 async for cor in f()} # F704 + | + +F704 `await` statement outside of a function + --> F704.py:28:2 + | +26 | # but not in comprehensions +27 | [await cor async for cor in f()] # F704 +28 | {await cor async for cor in f()} # F704 + | ^^^^^^^^^ +29 | {await cor: 1 async for cor in f()} # F704 +30 | [await cor for cor in f()] # F704 + | + +F704 `await` statement outside of a function + --> F704.py:29:2 + | +27 | [await cor async for cor in f()] # F704 +28 | {await cor async for cor in f()} # F704 +29 | {await cor: 1 async for cor in f()} # F704 + | ^^^^^^^^^ +30 | [await cor for cor in f()] # F704 +31 | {await cor for cor in f()} # F704 + | + +F704 `await` statement outside of a function + --> F704.py:30:2 + | +28 | {await cor async for cor in f()} # F704 +29 | {await cor: 1 async for cor in f()} # F704 +30 | [await cor for cor in f()] # F704 + | ^^^^^^^^^ +31 | {await cor for cor in f()} # F704 +32 | {await cor: 1 for cor in f()} # F704 + | + +F704 `await` statement outside of a function + --> F704.py:31:2 + | +29 | {await cor: 1 async for cor in f()} # F704 +30 | [await cor for cor in f()] # F704 +31 | {await cor for cor in f()} # F704 + | ^^^^^^^^^ +32 | {await cor: 1 for cor in f()} # F704 + | + +F704 `await` statement outside of a function + --> F704.py:32:2 + | +30 | [await cor for cor in f()] # F704 +31 | {await cor for cor in f()} # F704 +32 | {await cor: 1 for cor in f()} # F704 + | ^^^^^^^^^ +33 | +34 | # or in the iterator of an async generator, which is evaluated in the parent + | + +F704 `await` statement outside of a function + --> F704.py:36:23 + | +34 | # or in the iterator of an async generator, which is evaluated in the parent +35 | # scope +36 | (cor async for cor in await f()) # F704 + | ^^^^^^^^^ +37 | (await cor async for cor in [await c for c in f()]) # F704 + | + +F704 `await` statement outside of a function + --> F704.py:37:30 + | +35 | # scope +36 | (cor async for cor in await f()) # F704 +37 | (await cor async for cor in [await c for c in f()]) # F704 + | ^^^^^^^ +38 | +39 | # this is also okay because the comprehension is within the generator scope + | diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__await_outside_async_function.py.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__await_outside_async_function.py.snap index fbab466fc5..e6e38743fa 100644 --- a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__await_outside_async_function.py.snap +++ b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__await_outside_async_function.py.snap @@ -17,4 +17,6 @@ PLE1142 `await` should be used within an async function 4 | # Top-level await 5 | await 1 | ^^^^^^^ +6 | +7 | ([await c for c in cor] async for cor in func()) # ok | diff --git a/crates/ruff_python_parser/src/semantic_errors.rs b/crates/ruff_python_parser/src/semantic_errors.rs index a9bc9c9101..cd7335bdbe 100644 --- a/crates/ruff_python_parser/src/semantic_errors.rs +++ b/crates/ruff_python_parser/src/semantic_errors.rs @@ -896,7 +896,7 @@ impl SemanticSyntaxChecker { // This check is required in addition to avoiding calling this function in `visit_expr` // because the generator scope applies to nested parts of the `Expr::Generator` that are // visited separately. - if ctx.in_generator_scope() { + if ctx.in_generator_context() { return; } Self::add_error( @@ -2096,11 +2096,11 @@ pub trait SemanticSyntaxContext { /// Returns `true` if the visitor is in a function scope. fn in_function_scope(&self) -> bool; - /// Returns `true` if the visitor is in a generator scope. + /// Returns `true` if the visitor is within a generator scope. /// /// Note that this refers to an `Expr::Generator` precisely, not to comprehensions more /// generally. - fn in_generator_scope(&self) -> bool; + fn in_generator_context(&self) -> bool; /// Returns `true` if the source file is a Jupyter notebook. fn in_notebook(&self) -> bool; diff --git a/crates/ruff_python_parser/tests/fixtures.rs b/crates/ruff_python_parser/tests/fixtures.rs index 8f9a2994db..2c89ba7aad 100644 --- a/crates/ruff_python_parser/tests/fixtures.rs +++ b/crates/ruff_python_parser/tests/fixtures.rs @@ -573,7 +573,7 @@ impl SemanticSyntaxContext for SemanticSyntaxCheckerVisitor<'_> { true } - fn in_generator_scope(&self) -> bool { + fn in_generator_context(&self) -> bool { true } diff --git a/crates/ty_python_semantic/resources/mdtest/diagnostics/semantic_syntax_errors.md b/crates/ty_python_semantic/resources/mdtest/diagnostics/semantic_syntax_errors.md index f1c043478b..b4e0b1ae24 100644 --- a/crates/ty_python_semantic/resources/mdtest/diagnostics/semantic_syntax_errors.md +++ b/crates/ty_python_semantic/resources/mdtest/diagnostics/semantic_syntax_errors.md @@ -143,6 +143,10 @@ await C() def f(): # error: [invalid-syntax] "`await` outside of an asynchronous function" await C() + +(await cor async for cor in f()) # ok +(await cor for cor in f()) # ok +([await c for c in cor] async for cor in f()) # ok ``` Generators are evaluated lazily, so `await` is allowed, even outside of a function. diff --git a/crates/ty_python_semantic/src/semantic_index/builder.rs b/crates/ty_python_semantic/src/semantic_index/builder.rs index b729862f2b..66a0f6f428 100644 --- a/crates/ty_python_semantic/src/semantic_index/builder.rs +++ b/crates/ty_python_semantic/src/semantic_index/builder.rs @@ -2845,6 +2845,11 @@ impl SemanticSyntaxContext for SemanticIndexBuilder<'_, '_> { match scope.kind() { ScopeKind::Class => return false, ScopeKind::Function | ScopeKind::Lambda => return true, + ScopeKind::Comprehension + if matches!(scope.node(), NodeWithScopeKind::GeneratorExpression(_)) => + { + return true; + } ScopeKind::Comprehension | ScopeKind::Module | ScopeKind::TypeAlias @@ -2894,11 +2899,14 @@ impl SemanticSyntaxContext for SemanticIndexBuilder<'_, '_> { matches!(kind, ScopeKind::Function | ScopeKind::Lambda) } - fn in_generator_scope(&self) -> bool { - matches!( - self.scopes[self.current_scope()].node(), - NodeWithScopeKind::GeneratorExpression(_) - ) + fn in_generator_context(&self) -> bool { + for scope_info in &self.scope_stack { + let scope = &self.scopes[scope_info.file_scope_id]; + if matches!(scope.node(), NodeWithScopeKind::GeneratorExpression(_)) { + return true; + } + } + false } fn in_notebook(&self) -> bool {