[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:
Brent Westbrook 2025-04-11 10:16:23 -04:00 committed by GitHub
parent 7e571791c0
commit ffef71d106
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 538 additions and 57 deletions

View file

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