diff --git a/crates/ruff_linter/resources/test/fixtures/pylint/await_outside_async.py b/crates/ruff_linter/resources/test/fixtures/pylint/await_outside_async.py index f652428a58..1379d4d7e3 100644 --- a/crates/ruff_linter/resources/test/fixtures/pylint/await_outside_async.py +++ b/crates/ruff_linter/resources/test/fixtures/pylint/await_outside_async.py @@ -72,3 +72,15 @@ def await_generator_target(): # See: https://github.com/astral-sh/ruff/issues/14167 def async_for_list_comprehension_target(): [x for x in await foo()] + + +def async_for_dictionary_comprehension_key(): + {await x: y for x, y in foo()} + + +def async_for_dictionary_comprehension_value(): + {y: await x for x, y in foo()} + + +def async_for_dict_comprehension(): + {x: y async for x, y in foo()} diff --git a/crates/ruff_linter/src/checkers/ast/analyze/comprehension.rs b/crates/ruff_linter/src/checkers/ast/analyze/comprehension.rs index 750501e4b3..356c0ca899 100644 --- a/crates/ruff_linter/src/checkers/ast/analyze/comprehension.rs +++ b/crates/ruff_linter/src/checkers/ast/analyze/comprehension.rs @@ -2,7 +2,7 @@ use ruff_python_ast::Comprehension; use crate::checkers::ast::Checker; use crate::codes::Rule; -use crate::rules::{flake8_simplify, pylint, refurb}; +use crate::rules::{flake8_simplify, refurb}; /// Run lint rules over a [`Comprehension`] syntax nodes. pub(crate) fn comprehension(comprehension: &Comprehension, checker: &Checker) { @@ -12,9 +12,4 @@ pub(crate) fn comprehension(comprehension: &Comprehension, checker: &Checker) { if checker.enabled(Rule::ReadlinesInFor) { refurb::rules::readlines_in_comprehension(checker, comprehension); } - if comprehension.is_async { - if checker.enabled(Rule::AwaitOutsideAsync) { - pylint::rules::await_outside_async(checker, comprehension); - } - } } diff --git a/crates/ruff_linter/src/checkers/ast/analyze/expression.rs b/crates/ruff_linter/src/checkers/ast/analyze/expression.rs index 27e3e3ea45..ebae9fb17d 100644 --- a/crates/ruff_linter/src/checkers/ast/analyze/expression.rs +++ b/crates/ruff_linter/src/checkers/ast/analyze/expression.rs @@ -1215,11 +1215,6 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) { pylint::rules::yield_from_in_async_function(checker, yield_from); } } - Expr::Await(_) => { - if checker.enabled(Rule::AwaitOutsideAsync) { - pylint::rules::await_outside_async(checker, expr); - } - } Expr::FString(f_string_expr @ ast::ExprFString { value, .. }) => { if checker.enabled(Rule::FStringMissingPlaceholders) { pyflakes::rules::f_string_missing_placeholders(checker, f_string_expr); diff --git a/crates/ruff_linter/src/checkers/ast/analyze/statement.rs b/crates/ruff_linter/src/checkers/ast/analyze/statement.rs index 17f8609a16..e503e7df1a 100644 --- a/crates/ruff_linter/src/checkers/ast/analyze/statement.rs +++ b/crates/ruff_linter/src/checkers/ast/analyze/statement.rs @@ -1242,14 +1242,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) { ruff::rules::invalid_assert_message_literal_argument(checker, assert_stmt); } } - Stmt::With( - with_stmt @ ast::StmtWith { - items, - body, - is_async, - .. - }, - ) => { + Stmt::With(with_stmt @ ast::StmtWith { items, body, .. }) => { if checker.enabled(Rule::TooManyNestedBlocks) { pylint::rules::too_many_nested_blocks(checker, stmt); } @@ -1284,11 +1277,6 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) { if checker.enabled(Rule::CancelScopeNoCheckpoint) { flake8_async::rules::cancel_scope_no_checkpoint(checker, with_stmt, items); } - if *is_async { - if checker.enabled(Rule::AwaitOutsideAsync) { - pylint::rules::await_outside_async(checker, stmt); - } - } } Stmt::While(while_stmt @ ast::StmtWhile { body, orelse, .. }) => { if checker.enabled(Rule::TooManyNestedBlocks) { @@ -1377,11 +1365,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) { if checker.enabled(Rule::ReadlinesInFor) { refurb::rules::readlines_in_for(checker, for_stmt); } - if *is_async { - if checker.enabled(Rule::AwaitOutsideAsync) { - pylint::rules::await_outside_async(checker, stmt); - } - } else { + if !*is_async { if checker.enabled(Rule::ReimplementedBuiltin) { flake8_simplify::rules::convert_for_loop_to_any_all(checker, stmt); } diff --git a/crates/ruff_linter/src/checkers/ast/mod.rs b/crates/ruff_linter/src/checkers/ast/mod.rs index 50c2da238b..ebff124383 100644 --- a/crates/ruff_linter/src/checkers/ast/mod.rs +++ b/crates/ruff_linter/src/checkers/ast/mod.rs @@ -69,7 +69,7 @@ use crate::registry::Rule; use crate::rules::pyflakes::rules::{ LateFutureImport, ReturnOutsideFunction, YieldOutsideFunction, }; -use crate::rules::pylint::rules::LoadBeforeGlobalDeclaration; +use crate::rules::pylint::rules::{AwaitOutsideAsync, LoadBeforeGlobalDeclaration}; use crate::rules::{flake8_pyi, flake8_type_checking, pyflakes, pyupgrade}; use crate::settings::{flags, LinterSettings}; use crate::{docstrings, noqa, Locator}; @@ -604,6 +604,11 @@ impl SemanticSyntaxContext for Checker<'_> { self.report_diagnostic(Diagnostic::new(ReturnOutsideFunction, error.range)); } } + SemanticSyntaxErrorKind::AwaitOutsideAsyncFunction(_) => { + if self.settings.rules.enabled(Rule::AwaitOutsideAsync) { + self.report_diagnostic(Diagnostic::new(AwaitOutsideAsync, error.range)); + } + } SemanticSyntaxErrorKind::ReboundComprehensionVariable | SemanticSyntaxErrorKind::DuplicateTypeParameter | SemanticSyntaxErrorKind::MultipleCaseAssignment(_) @@ -680,6 +685,16 @@ impl SemanticSyntaxContext for Checker<'_> { fn in_notebook(&self) -> bool { self.source_type.is_ipynb() } + + fn in_generator_scope(&self) -> bool { + matches!( + &self.semantic.current_scope().kind, + ScopeKind::Generator { + kind: GeneratorKind::Generator, + .. + } + ) + } } impl<'a> Visitor<'a> for Checker<'a> { diff --git a/crates/ruff_linter/src/rules/pylint/rules/await_outside_async.rs b/crates/ruff_linter/src/rules/pylint/rules/await_outside_async.rs index 5d4646d0e3..fd73d2ab69 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/await_outside_async.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/await_outside_async.rs @@ -1,9 +1,5 @@ -use ruff_diagnostics::{Diagnostic, Violation}; +use ruff_diagnostics::Violation; use ruff_macros::{derive_message_formats, ViolationMetadata}; -use ruff_python_semantic::{GeneratorKind, ScopeKind}; -use ruff_text_size::Ranged; - -use crate::checkers::ast::Checker; /// ## What it does /// Checks for uses of `await` outside `async` functions. @@ -47,39 +43,3 @@ impl Violation for AwaitOutsideAsync { "`await` should be used within an async function".to_string() } } - -/// PLE1142 -pub(crate) fn await_outside_async(checker: &Checker, node: T) { - // If we're in an `async` function, we're good. - if checker.semantic().in_async_context() { - return; - } - - // `await` is allowed at the top level of a Jupyter notebook. - // See: https://ipython.readthedocs.io/en/stable/interactive/autoawait.html. - if checker.semantic().current_scope().kind.is_module() && checker.source_type.is_ipynb() { - return; - } - - // Generators are evaluated lazily, so you can use `await` in them. For example: - // ```python - // # This is valid - // (await x for x in y) - // (x async for x in y) - // - // # This is invalid - // (x for x in async y) - // [await x for x in y] - // ``` - if matches!( - checker.semantic().current_scope().kind, - ScopeKind::Generator { - kind: GeneratorKind::Generator, - .. - } - ) { - return; - } - - checker.report_diagnostic(Diagnostic::new(AwaitOutsideAsync, node.range())); -} diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE1142_await_outside_async.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE1142_await_outside_async.py.snap index 9e61d1d6f3..05e0044833 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE1142_await_outside_async.py.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE1142_await_outside_async.py.snap @@ -63,3 +63,24 @@ await_outside_async.py:74:17: PLE1142 `await` should be used within an async fun 74 | [x for x in await foo()] | ^^^^^^^^^^^ PLE1142 | + +await_outside_async.py:78:6: PLE1142 `await` should be used within an async function + | +77 | def async_for_dictionary_comprehension_key(): +78 | {await x: y for x, y in foo()} + | ^^^^^^^ PLE1142 + | + +await_outside_async.py:82:9: PLE1142 `await` should be used within an async function + | +81 | def async_for_dictionary_comprehension_value(): +82 | {y: await x for x, y in foo()} + | ^^^^^^^ PLE1142 + | + +await_outside_async.py:86:11: PLE1142 `await` should be used within an async function + | +85 | def async_for_dict_comprehension(): +86 | {x: y async for x, y in foo()} + | ^^^^^^^^^^^^^^^^^^^^^^^ PLE1142 + | diff --git a/crates/ruff_python_parser/src/semantic_errors.rs b/crates/ruff_python_parser/src/semantic_errors.rs index c41f4af6f3..e61c440fcf 100644 --- a/crates/ruff_python_parser/src/semantic_errors.rs +++ b/crates/ruff_python_parser/src/semantic_errors.rs @@ -103,12 +103,31 @@ impl SemanticSyntaxChecker { Self::add_error(ctx, SemanticSyntaxErrorKind::ReturnOutsideFunction, *range); } } - Stmt::For(ast::StmtFor { target, iter, .. }) => { + Stmt::For(ast::StmtFor { + target, + iter, + is_async, + .. + }) => { // test_err single_star_for // for _ in *x: ... // for *x in xs: ... Self::invalid_star_expression(target, ctx); Self::invalid_star_expression(iter, ctx); + if *is_async { + Self::await_outside_async_function( + ctx, + stmt, + AwaitOutsideAsyncFunctionKind::AsyncFor, + ); + } + } + Stmt::With(ast::StmtWith { is_async: true, .. }) => { + Self::await_outside_async_function( + ctx, + stmt, + AwaitOutsideAsyncFunctionKind::AsyncWith, + ); } _ => {} } @@ -514,11 +533,13 @@ impl SemanticSyntaxChecker { }) => { Self::check_generator_expr(elt, generators, ctx); Self::async_comprehension_outside_async_function(ctx, generators); - } - Expr::Generator(ast::ExprGenerator { - elt, generators, .. - }) => { - Self::check_generator_expr(elt, generators, ctx); + for generator in generators.iter().filter(|g| g.is_async) { + Self::await_outside_async_function( + ctx, + generator, + AwaitOutsideAsyncFunctionKind::AsyncComprehension, + ); + } } Expr::DictComp(ast::ExprDictComp { key, @@ -529,6 +550,20 @@ impl SemanticSyntaxChecker { Self::check_generator_expr(key, generators, ctx); Self::check_generator_expr(value, generators, ctx); Self::async_comprehension_outside_async_function(ctx, generators); + for generator in generators.iter().filter(|g| g.is_async) { + Self::await_outside_async_function( + ctx, + generator, + AwaitOutsideAsyncFunctionKind::AsyncComprehension, + ); + } + } + Expr::Generator(ast::ExprGenerator { + elt, generators, .. + }) => { + Self::check_generator_expr(elt, generators, ctx); + // Note that `await_outside_async_function` is not called here because generators + // are evaluated lazily. See the note in the function for more details. } Expr::Name(ast::ExprName { range, @@ -603,11 +638,53 @@ impl SemanticSyntaxChecker { } Expr::Await(_) => { Self::yield_outside_function(ctx, expr, YieldOutsideFunctionKind::Await); + Self::await_outside_async_function(ctx, expr, AwaitOutsideAsyncFunctionKind::Await); } _ => {} } } + /// PLE1142 + fn await_outside_async_function( + ctx: &Ctx, + node: Node, + kind: AwaitOutsideAsyncFunctionKind, + ) { + if ctx.in_async_context() { + return; + } + // `await` is allowed at the top level of a Jupyter notebook. + // See: https://ipython.readthedocs.io/en/stable/interactive/autoawait.html. + if ctx.in_module_scope() && ctx.in_notebook() { + return; + } + // Generators are evaluated lazily, so you can use `await` in them. For example: + // + // ```python + // # This is valid + // def f(): + // (await x for x in y) + // (x async for x in y) + // + // # This is invalid + // def f(): + // (x for x in await y) + // [await x for x in y] + // ``` + // + // 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() { + return; + } + Self::add_error( + ctx, + SemanticSyntaxErrorKind::AwaitOutsideAsyncFunction(kind), + node.range(), + ); + } + /// F704 fn yield_outside_function( ctx: &Ctx, @@ -803,6 +880,9 @@ impl Display for SemanticSyntaxError { SemanticSyntaxErrorKind::ReturnOutsideFunction => { f.write_str("`return` statement outside of a function") } + SemanticSyntaxErrorKind::AwaitOutsideAsyncFunction(kind) => { + write!(f, "`{kind}` outside of an asynchronous function") + } } } } @@ -1101,6 +1181,38 @@ pub enum SemanticSyntaxErrorKind { /// Represents the use of `return` outside of a function scope. ReturnOutsideFunction, + + /// Represents the use of `await`, `async for`, or `async with` outside of an asynchronous + /// function. + /// + /// ## Examples + /// + /// ```python + /// def f(): + /// await 1 # error + /// async for x in y: ... # error + /// async with x: ... # error + /// ``` + AwaitOutsideAsyncFunction(AwaitOutsideAsyncFunctionKind), +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum AwaitOutsideAsyncFunctionKind { + Await, + AsyncFor, + AsyncWith, + AsyncComprehension, +} + +impl Display for AwaitOutsideAsyncFunctionKind { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(match self { + AwaitOutsideAsyncFunctionKind::Await => "await", + AwaitOutsideAsyncFunctionKind::AsyncFor => "async for", + AwaitOutsideAsyncFunctionKind::AsyncWith => "async with", + AwaitOutsideAsyncFunctionKind::AsyncComprehension => "asynchronous comprehension", + }) + } } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] @@ -1527,6 +1639,12 @@ 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. + /// + /// Note that this refers to an `Expr::Generator` precisely, not to comprehensions more + /// generally. + fn in_generator_scope(&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 5eed806061..a7b0260010 100644 --- a/crates/ruff_python_parser/tests/fixtures.rs +++ b/crates/ruff_python_parser/tests/fixtures.rs @@ -462,7 +462,7 @@ impl<'ast> SourceOrderVisitor<'ast> for ValidateAstVisitor<'ast> { enum Scope { Module, - Function { is_async: bool }, + Function, Comprehension { is_async: bool }, Class, } @@ -529,12 +529,7 @@ impl SemanticSyntaxContext for SemanticSyntaxCheckerVisitor<'_> { } fn in_async_context(&self) -> bool { - for scope in &self.scopes { - if let Scope::Function { is_async } = scope { - return *is_async; - } - } - false + true } fn in_sync_comprehension(&self) -> bool { @@ -561,6 +556,10 @@ impl SemanticSyntaxContext for SemanticSyntaxCheckerVisitor<'_> { fn in_await_allowed_context(&self) -> bool { true } + + fn in_generator_scope(&self) -> bool { + true + } } impl Visitor<'_> for SemanticSyntaxCheckerVisitor<'_> { @@ -587,10 +586,8 @@ impl Visitor<'_> for SemanticSyntaxCheckerVisitor<'_> { self.visit_body(body); self.scopes.pop().unwrap(); } - ast::Stmt::FunctionDef(ast::StmtFunctionDef { is_async, .. }) => { - self.scopes.push(Scope::Function { - is_async: *is_async, - }); + ast::Stmt::FunctionDef(ast::StmtFunctionDef { .. }) => { + self.scopes.push(Scope::Function); ast::visitor::walk_stmt(self, stmt); self.scopes.pop().unwrap(); } @@ -604,7 +601,7 @@ impl Visitor<'_> for SemanticSyntaxCheckerVisitor<'_> { self.with_semantic_checker(|semantic, context| semantic.visit_expr(expr, context)); match expr { ast::Expr::Lambda(_) => { - self.scopes.push(Scope::Function { is_async: false }); + self.scopes.push(Scope::Function); ast::visitor::walk_expr(self, expr); self.scopes.pop().unwrap(); }