Skip to content

Commit 014bb52

Browse files
authored
[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.
1 parent e2a38e4 commit 014bb52

File tree

9 files changed

+186
-89
lines changed

9 files changed

+186
-89
lines changed

crates/ruff_linter/resources/test/fixtures/pylint/await_outside_async.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,3 +72,15 @@ def await_generator_target():
7272
# See: https://github.com/astral-sh/ruff/issues/14167
7373
def async_for_list_comprehension_target():
7474
[x for x in await foo()]
75+
76+
77+
def async_for_dictionary_comprehension_key():
78+
{await x: y for x, y in foo()}
79+
80+
81+
def async_for_dictionary_comprehension_value():
82+
{y: await x for x, y in foo()}
83+
84+
85+
def async_for_dict_comprehension():
86+
{x: y async for x, y in foo()}

crates/ruff_linter/src/checkers/ast/analyze/comprehension.rs

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use ruff_python_ast::Comprehension;
22

33
use crate::checkers::ast::Checker;
44
use crate::codes::Rule;
5-
use crate::rules::{flake8_simplify, pylint, refurb};
5+
use crate::rules::{flake8_simplify, refurb};
66

77
/// Run lint rules over a [`Comprehension`] syntax nodes.
88
pub(crate) fn comprehension(comprehension: &Comprehension, checker: &Checker) {
@@ -12,9 +12,4 @@ pub(crate) fn comprehension(comprehension: &Comprehension, checker: &Checker) {
1212
if checker.enabled(Rule::ReadlinesInFor) {
1313
refurb::rules::readlines_in_comprehension(checker, comprehension);
1414
}
15-
if comprehension.is_async {
16-
if checker.enabled(Rule::AwaitOutsideAsync) {
17-
pylint::rules::await_outside_async(checker, comprehension);
18-
}
19-
}
2015
}

crates/ruff_linter/src/checkers/ast/analyze/expression.rs

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1215,11 +1215,6 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
12151215
pylint::rules::yield_from_in_async_function(checker, yield_from);
12161216
}
12171217
}
1218-
Expr::Await(_) => {
1219-
if checker.enabled(Rule::AwaitOutsideAsync) {
1220-
pylint::rules::await_outside_async(checker, expr);
1221-
}
1222-
}
12231218
Expr::FString(f_string_expr @ ast::ExprFString { value, .. }) => {
12241219
if checker.enabled(Rule::FStringMissingPlaceholders) {
12251220
pyflakes::rules::f_string_missing_placeholders(checker, f_string_expr);

crates/ruff_linter/src/checkers/ast/analyze/statement.rs

Lines changed: 2 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1242,14 +1242,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
12421242
ruff::rules::invalid_assert_message_literal_argument(checker, assert_stmt);
12431243
}
12441244
}
1245-
Stmt::With(
1246-
with_stmt @ ast::StmtWith {
1247-
items,
1248-
body,
1249-
is_async,
1250-
..
1251-
},
1252-
) => {
1245+
Stmt::With(with_stmt @ ast::StmtWith { items, body, .. }) => {
12531246
if checker.enabled(Rule::TooManyNestedBlocks) {
12541247
pylint::rules::too_many_nested_blocks(checker, stmt);
12551248
}
@@ -1284,11 +1277,6 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
12841277
if checker.enabled(Rule::CancelScopeNoCheckpoint) {
12851278
flake8_async::rules::cancel_scope_no_checkpoint(checker, with_stmt, items);
12861279
}
1287-
if *is_async {
1288-
if checker.enabled(Rule::AwaitOutsideAsync) {
1289-
pylint::rules::await_outside_async(checker, stmt);
1290-
}
1291-
}
12921280
}
12931281
Stmt::While(while_stmt @ ast::StmtWhile { body, orelse, .. }) => {
12941282
if checker.enabled(Rule::TooManyNestedBlocks) {
@@ -1377,11 +1365,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
13771365
if checker.enabled(Rule::ReadlinesInFor) {
13781366
refurb::rules::readlines_in_for(checker, for_stmt);
13791367
}
1380-
if *is_async {
1381-
if checker.enabled(Rule::AwaitOutsideAsync) {
1382-
pylint::rules::await_outside_async(checker, stmt);
1383-
}
1384-
} else {
1368+
if !*is_async {
13851369
if checker.enabled(Rule::ReimplementedBuiltin) {
13861370
flake8_simplify::rules::convert_for_loop_to_any_all(checker, stmt);
13871371
}

crates/ruff_linter/src/checkers/ast/mod.rs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ use crate::registry::Rule;
6969
use crate::rules::pyflakes::rules::{
7070
LateFutureImport, ReturnOutsideFunction, YieldOutsideFunction,
7171
};
72-
use crate::rules::pylint::rules::LoadBeforeGlobalDeclaration;
72+
use crate::rules::pylint::rules::{AwaitOutsideAsync, LoadBeforeGlobalDeclaration};
7373
use crate::rules::{flake8_pyi, flake8_type_checking, pyflakes, pyupgrade};
7474
use crate::settings::{flags, LinterSettings};
7575
use crate::{docstrings, noqa, Locator};
@@ -604,6 +604,11 @@ impl SemanticSyntaxContext for Checker<'_> {
604604
self.report_diagnostic(Diagnostic::new(ReturnOutsideFunction, error.range));
605605
}
606606
}
607+
SemanticSyntaxErrorKind::AwaitOutsideAsyncFunction(_) => {
608+
if self.settings.rules.enabled(Rule::AwaitOutsideAsync) {
609+
self.report_diagnostic(Diagnostic::new(AwaitOutsideAsync, error.range));
610+
}
611+
}
607612
SemanticSyntaxErrorKind::ReboundComprehensionVariable
608613
| SemanticSyntaxErrorKind::DuplicateTypeParameter
609614
| SemanticSyntaxErrorKind::MultipleCaseAssignment(_)
@@ -680,6 +685,16 @@ impl SemanticSyntaxContext for Checker<'_> {
680685
fn in_notebook(&self) -> bool {
681686
self.source_type.is_ipynb()
682687
}
688+
689+
fn in_generator_scope(&self) -> bool {
690+
matches!(
691+
&self.semantic.current_scope().kind,
692+
ScopeKind::Generator {
693+
kind: GeneratorKind::Generator,
694+
..
695+
}
696+
)
697+
}
683698
}
684699

685700
impl<'a> Visitor<'a> for Checker<'a> {
Lines changed: 1 addition & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,5 @@
1-
use ruff_diagnostics::{Diagnostic, Violation};
1+
use ruff_diagnostics::Violation;
22
use ruff_macros::{derive_message_formats, ViolationMetadata};
3-
use ruff_python_semantic::{GeneratorKind, ScopeKind};
4-
use ruff_text_size::Ranged;
5-
6-
use crate::checkers::ast::Checker;
73

84
/// ## What it does
95
/// Checks for uses of `await` outside `async` functions.
@@ -47,39 +43,3 @@ impl Violation for AwaitOutsideAsync {
4743
"`await` should be used within an async function".to_string()
4844
}
4945
}
50-
51-
/// PLE1142
52-
pub(crate) fn await_outside_async<T: Ranged>(checker: &Checker, node: T) {
53-
// If we're in an `async` function, we're good.
54-
if checker.semantic().in_async_context() {
55-
return;
56-
}
57-
58-
// `await` is allowed at the top level of a Jupyter notebook.
59-
// See: https://ipython.readthedocs.io/en/stable/interactive/autoawait.html.
60-
if checker.semantic().current_scope().kind.is_module() && checker.source_type.is_ipynb() {
61-
return;
62-
}
63-
64-
// Generators are evaluated lazily, so you can use `await` in them. For example:
65-
// ```python
66-
// # This is valid
67-
// (await x for x in y)
68-
// (x async for x in y)
69-
//
70-
// # This is invalid
71-
// (x for x in async y)
72-
// [await x for x in y]
73-
// ```
74-
if matches!(
75-
checker.semantic().current_scope().kind,
76-
ScopeKind::Generator {
77-
kind: GeneratorKind::Generator,
78-
..
79-
}
80-
) {
81-
return;
82-
}
83-
84-
checker.report_diagnostic(Diagnostic::new(AwaitOutsideAsync, node.range()));
85-
}

crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE1142_await_outside_async.py.snap

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,3 +63,24 @@ await_outside_async.py:74:17: PLE1142 `await` should be used within an async fun
6363
74 | [x for x in await foo()]
6464
| ^^^^^^^^^^^ PLE1142
6565
|
66+
67+
await_outside_async.py:78:6: PLE1142 `await` should be used within an async function
68+
|
69+
77 | def async_for_dictionary_comprehension_key():
70+
78 | {await x: y for x, y in foo()}
71+
| ^^^^^^^ PLE1142
72+
|
73+
74+
await_outside_async.py:82:9: PLE1142 `await` should be used within an async function
75+
|
76+
81 | def async_for_dictionary_comprehension_value():
77+
82 | {y: await x for x, y in foo()}
78+
| ^^^^^^^ PLE1142
79+
|
80+
81+
await_outside_async.py:86:11: PLE1142 `await` should be used within an async function
82+
|
83+
85 | def async_for_dict_comprehension():
84+
86 | {x: y async for x, y in foo()}
85+
| ^^^^^^^^^^^^^^^^^^^^^^^ PLE1142
86+
|

crates/ruff_python_parser/src/semantic_errors.rs

Lines changed: 124 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -103,12 +103,31 @@ impl SemanticSyntaxChecker {
103103
Self::add_error(ctx, SemanticSyntaxErrorKind::ReturnOutsideFunction, *range);
104104
}
105105
}
106-
Stmt::For(ast::StmtFor { target, iter, .. }) => {
106+
Stmt::For(ast::StmtFor {
107+
target,
108+
iter,
109+
is_async,
110+
..
111+
}) => {
107112
// test_err single_star_for
108113
// for _ in *x: ...
109114
// for *x in xs: ...
110115
Self::invalid_star_expression(target, ctx);
111116
Self::invalid_star_expression(iter, ctx);
117+
if *is_async {
118+
Self::await_outside_async_function(
119+
ctx,
120+
stmt,
121+
AwaitOutsideAsyncFunctionKind::AsyncFor,
122+
);
123+
}
124+
}
125+
Stmt::With(ast::StmtWith { is_async: true, .. }) => {
126+
Self::await_outside_async_function(
127+
ctx,
128+
stmt,
129+
AwaitOutsideAsyncFunctionKind::AsyncWith,
130+
);
112131
}
113132
_ => {}
114133
}
@@ -514,11 +533,13 @@ impl SemanticSyntaxChecker {
514533
}) => {
515534
Self::check_generator_expr(elt, generators, ctx);
516535
Self::async_comprehension_outside_async_function(ctx, generators);
517-
}
518-
Expr::Generator(ast::ExprGenerator {
519-
elt, generators, ..
520-
}) => {
521-
Self::check_generator_expr(elt, generators, ctx);
536+
for generator in generators.iter().filter(|g| g.is_async) {
537+
Self::await_outside_async_function(
538+
ctx,
539+
generator,
540+
AwaitOutsideAsyncFunctionKind::AsyncComprehension,
541+
);
542+
}
522543
}
523544
Expr::DictComp(ast::ExprDictComp {
524545
key,
@@ -529,6 +550,20 @@ impl SemanticSyntaxChecker {
529550
Self::check_generator_expr(key, generators, ctx);
530551
Self::check_generator_expr(value, generators, ctx);
531552
Self::async_comprehension_outside_async_function(ctx, generators);
553+
for generator in generators.iter().filter(|g| g.is_async) {
554+
Self::await_outside_async_function(
555+
ctx,
556+
generator,
557+
AwaitOutsideAsyncFunctionKind::AsyncComprehension,
558+
);
559+
}
560+
}
561+
Expr::Generator(ast::ExprGenerator {
562+
elt, generators, ..
563+
}) => {
564+
Self::check_generator_expr(elt, generators, ctx);
565+
// Note that `await_outside_async_function` is not called here because generators
566+
// are evaluated lazily. See the note in the function for more details.
532567
}
533568
Expr::Name(ast::ExprName {
534569
range,
@@ -603,11 +638,53 @@ impl SemanticSyntaxChecker {
603638
}
604639
Expr::Await(_) => {
605640
Self::yield_outside_function(ctx, expr, YieldOutsideFunctionKind::Await);
641+
Self::await_outside_async_function(ctx, expr, AwaitOutsideAsyncFunctionKind::Await);
606642
}
607643
_ => {}
608644
}
609645
}
610646

647+
/// PLE1142
648+
fn await_outside_async_function<Ctx: SemanticSyntaxContext, Node: Ranged>(
649+
ctx: &Ctx,
650+
node: Node,
651+
kind: AwaitOutsideAsyncFunctionKind,
652+
) {
653+
if ctx.in_async_context() {
654+
return;
655+
}
656+
// `await` is allowed at the top level of a Jupyter notebook.
657+
// See: https://ipython.readthedocs.io/en/stable/interactive/autoawait.html.
658+
if ctx.in_module_scope() && ctx.in_notebook() {
659+
return;
660+
}
661+
// Generators are evaluated lazily, so you can use `await` in them. For example:
662+
//
663+
// ```python
664+
// # This is valid
665+
// def f():
666+
// (await x for x in y)
667+
// (x async for x in y)
668+
//
669+
// # This is invalid
670+
// def f():
671+
// (x for x in await y)
672+
// [await x for x in y]
673+
// ```
674+
//
675+
// This check is required in addition to avoiding calling this function in `visit_expr`
676+
// because the generator scope applies to nested parts of the `Expr::Generator` that are
677+
// visited separately.
678+
if ctx.in_generator_scope() {
679+
return;
680+
}
681+
Self::add_error(
682+
ctx,
683+
SemanticSyntaxErrorKind::AwaitOutsideAsyncFunction(kind),
684+
node.range(),
685+
);
686+
}
687+
611688
/// F704
612689
fn yield_outside_function<Ctx: SemanticSyntaxContext>(
613690
ctx: &Ctx,
@@ -803,6 +880,9 @@ impl Display for SemanticSyntaxError {
803880
SemanticSyntaxErrorKind::ReturnOutsideFunction => {
804881
f.write_str("`return` statement outside of a function")
805882
}
883+
SemanticSyntaxErrorKind::AwaitOutsideAsyncFunction(kind) => {
884+
write!(f, "`{kind}` outside of an asynchronous function")
885+
}
806886
}
807887
}
808888
}
@@ -1101,6 +1181,38 @@ pub enum SemanticSyntaxErrorKind {
11011181

11021182
/// Represents the use of `return` outside of a function scope.
11031183
ReturnOutsideFunction,
1184+
1185+
/// Represents the use of `await`, `async for`, or `async with` outside of an asynchronous
1186+
/// function.
1187+
///
1188+
/// ## Examples
1189+
///
1190+
/// ```python
1191+
/// def f():
1192+
/// await 1 # error
1193+
/// async for x in y: ... # error
1194+
/// async with x: ... # error
1195+
/// ```
1196+
AwaitOutsideAsyncFunction(AwaitOutsideAsyncFunctionKind),
1197+
}
1198+
1199+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1200+
pub enum AwaitOutsideAsyncFunctionKind {
1201+
Await,
1202+
AsyncFor,
1203+
AsyncWith,
1204+
AsyncComprehension,
1205+
}
1206+
1207+
impl Display for AwaitOutsideAsyncFunctionKind {
1208+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1209+
f.write_str(match self {
1210+
AwaitOutsideAsyncFunctionKind::Await => "await",
1211+
AwaitOutsideAsyncFunctionKind::AsyncFor => "async for",
1212+
AwaitOutsideAsyncFunctionKind::AsyncWith => "async with",
1213+
AwaitOutsideAsyncFunctionKind::AsyncComprehension => "asynchronous comprehension",
1214+
})
1215+
}
11041216
}
11051217

11061218
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
@@ -1527,6 +1639,12 @@ pub trait SemanticSyntaxContext {
15271639
/// Returns `true` if the visitor is in a function scope.
15281640
fn in_function_scope(&self) -> bool;
15291641

1642+
/// Returns `true` if the visitor is in a generator scope.
1643+
///
1644+
/// Note that this refers to an `Expr::Generator` precisely, not to comprehensions more
1645+
/// generally.
1646+
fn in_generator_scope(&self) -> bool;
1647+
15301648
/// Returns `true` if the source file is a Jupyter notebook.
15311649
fn in_notebook(&self) -> bool;
15321650

0 commit comments

Comments
 (0)