Skip to content

[syntax-errors] await outside async functions #17363

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Apr 14, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -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()}
7 changes: 1 addition & 6 deletions crates/ruff_linter/src/checkers/ast/analyze/comprehension.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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);
}
}
}
5 changes: 0 additions & 5 deletions crates/ruff_linter/src/checkers/ast/analyze/expression.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
20 changes: 2 additions & 18 deletions crates/ruff_linter/src/checkers/ast/analyze/statement.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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);
}
Expand Down
17 changes: 16 additions & 1 deletion crates/ruff_linter/src/checkers/ast/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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(_)
Expand Down Expand Up @@ -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> {
Expand Down
42 changes: 1 addition & 41 deletions crates/ruff_linter/src/rules/pylint/rules/await_outside_async.rs
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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<T: Ranged>(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()));
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
|
130 changes: 124 additions & 6 deletions crates/ruff_python_parser/src/semantic_errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
);
}
_ => {}
}
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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: SemanticSyntaxContext, Node: Ranged>(
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: SemanticSyntaxContext>(
ctx: &Ctx,
Expand Down Expand Up @@ -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")
}
}
}
}
Expand Down Expand Up @@ -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)]
Expand Down Expand Up @@ -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;

Expand Down
Loading
Loading