[syntax-errors] await outside async functions (#17363)

Summary
--

This PR implements detecting the use of `await` expressions outside of
async functions. This is a reimplementation of
[await-outside-async
(PLE1142)](https://docs.astral.sh/ruff/rules/await-outside-async/) as a
semantic syntax error.

Despite the rule name, PLE1142 also applies to `async for` and `async
with`, so these are covered here too.

Test Plan
--

Existing PLE1142 tests.

I also deleted more code from the `SemanticSyntaxCheckerVisitor` to
avoid changes in other parser tests.
This commit is contained in:
Brent Westbrook 2025-04-14 13:01:48 -04:00 committed by GitHub
parent e2a38e4c00
commit 014bb526f4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 186 additions and 89 deletions

View file

@ -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()}

View file

@ -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);
}
}
}

View file

@ -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);

View file

@ -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);
}

View file

@ -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> {

View file

@ -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<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()));
}

View file

@ -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
|

View file

@ -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: 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,
@ -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;

View file

@ -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();
}