mirror of
https://github.com/astral-sh/ruff.git
synced 2025-08-08 04:38:26 +00:00
[syntax-errors] yield
, yield from
, and await
outside functions (#17298)
Summary -- This PR reimplements [yield-outside-function (F704)](https://docs.astral.sh/ruff/rules/yield-outside-function/) as a semantic syntax error. Despite the name, this rule covers `yield from` and `await` in addition to `yield`. Test Plan -- New linter tests, along with the existing F704 test. --------- Co-authored-by: Dhruv Manilawala <dhruvmanila@gmail.com>
This commit is contained in:
parent
7e571791c0
commit
ffef71d106
10 changed files with 538 additions and 57 deletions
|
@ -587,17 +587,53 @@ impl SemanticSyntaxChecker {
|
|||
}
|
||||
}
|
||||
}
|
||||
Expr::Yield(ast::ExprYield {
|
||||
value: Some(value), ..
|
||||
}) => {
|
||||
// test_err single_star_yield
|
||||
// def f(): yield *x
|
||||
Self::invalid_star_expression(value, ctx);
|
||||
Expr::Yield(ast::ExprYield { value, .. }) => {
|
||||
if let Some(value) = value {
|
||||
// test_err single_star_yield
|
||||
// def f(): yield *x
|
||||
Self::invalid_star_expression(value, ctx);
|
||||
}
|
||||
Self::yield_outside_function(ctx, expr, YieldOutsideFunctionKind::Yield);
|
||||
}
|
||||
Expr::YieldFrom(_) => {
|
||||
Self::yield_outside_function(ctx, expr, YieldOutsideFunctionKind::YieldFrom);
|
||||
}
|
||||
Expr::Await(_) => {
|
||||
Self::yield_outside_function(ctx, expr, YieldOutsideFunctionKind::Await);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// F704
|
||||
fn yield_outside_function<Ctx: SemanticSyntaxContext>(
|
||||
ctx: &Ctx,
|
||||
expr: &Expr,
|
||||
kind: YieldOutsideFunctionKind,
|
||||
) {
|
||||
// We are intentionally not inspecting the async status of the scope for now to mimic F704.
|
||||
// await-outside-async is PLE1142 instead, so we'll end up emitting both syntax errors for
|
||||
// cases that trigger F704
|
||||
if kind.is_await() {
|
||||
if ctx.in_await_allowed_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;
|
||||
}
|
||||
} else if ctx.in_function_scope() {
|
||||
return;
|
||||
}
|
||||
|
||||
Self::add_error(
|
||||
ctx,
|
||||
SemanticSyntaxErrorKind::YieldOutsideFunction(kind),
|
||||
expr.range(),
|
||||
);
|
||||
}
|
||||
|
||||
/// Add a [`SyntaxErrorKind::ReboundComprehensionVariable`] if `expr` rebinds an iteration
|
||||
/// variable in `generators`.
|
||||
fn check_generator_expr<Ctx: SemanticSyntaxContext>(
|
||||
|
@ -758,6 +794,9 @@ impl Display for SemanticSyntaxError {
|
|||
function on Python {python_version} (syntax was added in 3.11)",
|
||||
)
|
||||
}
|
||||
SemanticSyntaxErrorKind::YieldOutsideFunction(kind) => {
|
||||
write!(f, "`{kind}` statement outside of a function")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1013,6 +1052,69 @@ pub enum SemanticSyntaxErrorKind {
|
|||
///
|
||||
/// [BPO 33346]: https://github.com/python/cpython/issues/77527
|
||||
AsyncComprehensionOutsideAsyncFunction(PythonVersion),
|
||||
|
||||
/// Represents the use of `yield`, `yield from`, or `await` outside of a function scope.
|
||||
///
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// `yield` and `yield from` are only allowed if the immediately-enclosing scope is a function
|
||||
/// or lambda and not allowed otherwise:
|
||||
///
|
||||
/// ```python
|
||||
/// yield 1 # error
|
||||
///
|
||||
/// def f():
|
||||
/// [(yield 1) for x in y] # error
|
||||
/// ```
|
||||
///
|
||||
/// `await` is additionally allowed in comprehensions, if the comprehension itself is in a
|
||||
/// function scope:
|
||||
///
|
||||
/// ```python
|
||||
/// await 1 # error
|
||||
///
|
||||
/// async def f():
|
||||
/// await 1 # okay
|
||||
/// [await 1 for x in y] # also okay
|
||||
/// ```
|
||||
///
|
||||
/// This last case _is_ an error, but it has to do with the lambda not being an async function.
|
||||
/// For the sake of this error kind, this is okay.
|
||||
///
|
||||
/// ## References
|
||||
///
|
||||
/// See [PEP 255] for details on `yield`, [PEP 380] for the extension to `yield from`, [PEP 492]
|
||||
/// for async-await syntax, and [PEP 530] for async comprehensions.
|
||||
///
|
||||
/// [PEP 255]: https://peps.python.org/pep-0255/
|
||||
/// [PEP 380]: https://peps.python.org/pep-0380/
|
||||
/// [PEP 492]: https://peps.python.org/pep-0492/
|
||||
/// [PEP 530]: https://peps.python.org/pep-0530/
|
||||
YieldOutsideFunction(YieldOutsideFunctionKind),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum YieldOutsideFunctionKind {
|
||||
Yield,
|
||||
YieldFrom,
|
||||
Await,
|
||||
}
|
||||
|
||||
impl YieldOutsideFunctionKind {
|
||||
pub fn is_await(&self) -> bool {
|
||||
matches!(self, Self::Await)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for YieldOutsideFunctionKind {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(match self {
|
||||
YieldOutsideFunctionKind::Yield => "yield",
|
||||
YieldOutsideFunctionKind::YieldFrom => "yield from",
|
||||
YieldOutsideFunctionKind::Await => "await",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
|
@ -1326,6 +1428,40 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
/// Information needed from a parent visitor to emit semantic syntax errors.
|
||||
///
|
||||
/// Note that the `in_*_scope` methods should refer to the immediately-enclosing scope. For example,
|
||||
/// `in_function_scope` should return true for this case:
|
||||
///
|
||||
/// ```python
|
||||
/// def f():
|
||||
/// x # here
|
||||
/// ```
|
||||
///
|
||||
/// but not for this case:
|
||||
///
|
||||
/// ```python
|
||||
/// def f():
|
||||
/// class C:
|
||||
/// x # here
|
||||
/// ```
|
||||
///
|
||||
/// In contrast, the `in_*_context` methods should traverse parent scopes. For example,
|
||||
/// `in_function_context` should return true for this case:
|
||||
///
|
||||
/// ```python
|
||||
/// def f():
|
||||
/// [x # here
|
||||
/// for x in range(3)]
|
||||
/// ```
|
||||
///
|
||||
/// but not here:
|
||||
///
|
||||
/// ```python
|
||||
/// def f():
|
||||
/// class C:
|
||||
/// x # here, classes break function scopes
|
||||
/// ```
|
||||
pub trait SemanticSyntaxContext {
|
||||
/// Returns `true` if a module's docstring boundary has been passed.
|
||||
fn seen_docstring_boundary(&self) -> bool;
|
||||
|
@ -1345,6 +1481,29 @@ pub trait SemanticSyntaxContext {
|
|||
/// Returns `true` if the visitor is currently in an async context, i.e. an async function.
|
||||
fn in_async_context(&self) -> bool;
|
||||
|
||||
/// Returns `true` if the visitor is currently in a context where the `await` keyword is
|
||||
/// allowed.
|
||||
///
|
||||
/// Note that this is method is primarily used to report `YieldOutsideFunction` errors for
|
||||
/// `await` outside function scopes, irrespective of their async status. As such, this differs
|
||||
/// from `in_async_context` in two ways:
|
||||
///
|
||||
/// 1. `await` is allowed in a lambda, despite it not being async
|
||||
/// 2. `await` is allowed in any function, regardless of its async status
|
||||
///
|
||||
/// In short, only nested class definitions should cause this method to return `false`, for
|
||||
/// example:
|
||||
///
|
||||
/// ```python
|
||||
/// def f():
|
||||
/// await 1 # okay, in a function
|
||||
/// class C:
|
||||
/// await 1 # error
|
||||
/// ```
|
||||
///
|
||||
/// See the trait-level documentation for more details.
|
||||
fn in_await_allowed_context(&self) -> bool;
|
||||
|
||||
/// Returns `true` if the visitor is currently inside of a synchronous comprehension.
|
||||
///
|
||||
/// This method is necessary because `in_async_context` only checks for the nearest, enclosing
|
||||
|
@ -1356,6 +1515,9 @@ pub trait SemanticSyntaxContext {
|
|||
/// Returns `true` if the visitor is at the top-level module scope.
|
||||
fn in_module_scope(&self) -> bool;
|
||||
|
||||
/// Returns `true` if the visitor is in a function scope.
|
||||
fn in_function_scope(&self) -> bool;
|
||||
|
||||
/// Returns `true` if the source file is a Jupyter notebook.
|
||||
fn in_notebook(&self) -> bool;
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue