mirror of
https://github.com/astral-sh/ruff.git
synced 2025-08-04 02:39:12 +00:00
[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:
parent
e2a38e4c00
commit
014bb526f4
9 changed files with 186 additions and 89 deletions
|
@ -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()}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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()));
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue