mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-29 13:24:57 +00:00
[syntax-errors] Detect yield from
inside async function (#20051)
<!-- Thank you for contributing to Ruff/ty! To help us out with reviewing, please consider the following: - Does this pull request include a summary of the change? (See below.) - Does this pull request include a descriptive title? (Please prefix with `[ty]` for ty pull requests.) - Does this pull request include references to any relevant issues? --> ## Summary This PR implements https://docs.astral.sh/ruff/rules/yield-from-in-async-function/ as a syntax semantic error ## Test Plan <!-- How was it tested? --> I have written a simple inline test as directed in [https://github.com/astral-sh/ruff/issues/17412](https://github.com/astral-sh/ruff/issues/17412) --------- Signed-off-by: 11happy <soni5happy@gmail.com> Co-authored-by: Alex Waygood <alex.waygood@gmail.com> Co-authored-by: Brent Westbrook <36778786+ntBre@users.noreply.github.com>
This commit is contained in:
parent
5d7c17c20a
commit
4c3e1930f6
11 changed files with 140 additions and 33 deletions
1
crates/ruff_linter/resources/test/fixtures/syntax_errors/yield_from_in_async_function.py
vendored
Normal file
1
crates/ruff_linter/resources/test/fixtures/syntax_errors/yield_from_in_async_function.py
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
async def f(): yield from x # error
|
|
@ -1319,13 +1319,10 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
|
||||||
pylint::rules::yield_in_init(checker, expr);
|
pylint::rules::yield_in_init(checker, expr);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Expr::YieldFrom(yield_from) => {
|
Expr::YieldFrom(_) => {
|
||||||
if checker.is_rule_enabled(Rule::YieldInInit) {
|
if checker.is_rule_enabled(Rule::YieldInInit) {
|
||||||
pylint::rules::yield_in_init(checker, expr);
|
pylint::rules::yield_in_init(checker, expr);
|
||||||
}
|
}
|
||||||
if checker.is_rule_enabled(Rule::YieldFromInAsyncFunction) {
|
|
||||||
pylint::rules::yield_from_in_async_function(checker, yield_from);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Expr::FString(f_string_expr @ ast::ExprFString { value, .. }) => {
|
Expr::FString(f_string_expr @ ast::ExprFString { value, .. }) => {
|
||||||
if checker.is_rule_enabled(Rule::FStringMissingPlaceholders) {
|
if checker.is_rule_enabled(Rule::FStringMissingPlaceholders) {
|
||||||
|
|
|
@ -71,7 +71,9 @@ use crate::registry::Rule;
|
||||||
use crate::rules::pyflakes::rules::{
|
use crate::rules::pyflakes::rules::{
|
||||||
LateFutureImport, ReturnOutsideFunction, YieldOutsideFunction,
|
LateFutureImport, ReturnOutsideFunction, YieldOutsideFunction,
|
||||||
};
|
};
|
||||||
use crate::rules::pylint::rules::{AwaitOutsideAsync, LoadBeforeGlobalDeclaration};
|
use crate::rules::pylint::rules::{
|
||||||
|
AwaitOutsideAsync, LoadBeforeGlobalDeclaration, YieldFromInAsyncFunction,
|
||||||
|
};
|
||||||
use crate::rules::{flake8_pyi, flake8_type_checking, pyflakes, pyupgrade};
|
use crate::rules::{flake8_pyi, flake8_type_checking, pyflakes, pyupgrade};
|
||||||
use crate::settings::rule_table::RuleTable;
|
use crate::settings::rule_table::RuleTable;
|
||||||
use crate::settings::{LinterSettings, TargetVersion, flags};
|
use crate::settings::{LinterSettings, TargetVersion, flags};
|
||||||
|
@ -668,6 +670,12 @@ impl SemanticSyntaxContext for Checker<'_> {
|
||||||
self.report_diagnostic(AwaitOutsideAsync, error.range);
|
self.report_diagnostic(AwaitOutsideAsync, error.range);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
SemanticSyntaxErrorKind::YieldFromInAsyncFunction => {
|
||||||
|
// PLE1700
|
||||||
|
if self.is_rule_enabled(Rule::YieldFromInAsyncFunction) {
|
||||||
|
self.report_diagnostic(YieldFromInAsyncFunction, error.range);
|
||||||
|
}
|
||||||
|
}
|
||||||
SemanticSyntaxErrorKind::ReboundComprehensionVariable
|
SemanticSyntaxErrorKind::ReboundComprehensionVariable
|
||||||
| SemanticSyntaxErrorKind::DuplicateTypeParameter
|
| SemanticSyntaxErrorKind::DuplicateTypeParameter
|
||||||
| SemanticSyntaxErrorKind::MultipleCaseAssignment(_)
|
| SemanticSyntaxErrorKind::MultipleCaseAssignment(_)
|
||||||
|
|
|
@ -1231,6 +1231,10 @@ mod tests {
|
||||||
)]
|
)]
|
||||||
#[test_case(Rule::AwaitOutsideAsync, Path::new("await_outside_async_function.py"))]
|
#[test_case(Rule::AwaitOutsideAsync, Path::new("await_outside_async_function.py"))]
|
||||||
#[test_case(Rule::AwaitOutsideAsync, Path::new("async_comprehension.py"))]
|
#[test_case(Rule::AwaitOutsideAsync, Path::new("async_comprehension.py"))]
|
||||||
|
#[test_case(
|
||||||
|
Rule::YieldFromInAsyncFunction,
|
||||||
|
Path::new("yield_from_in_async_function.py")
|
||||||
|
)]
|
||||||
fn test_syntax_errors(rule: Rule, path: &Path) -> Result<()> {
|
fn test_syntax_errors(rule: Rule, path: &Path) -> Result<()> {
|
||||||
let snapshot = path.to_string_lossy().to_string();
|
let snapshot = path.to_string_lossy().to_string();
|
||||||
let path = Path::new("resources/test/fixtures/syntax_errors").join(path);
|
let path = Path::new("resources/test/fixtures/syntax_errors").join(path);
|
||||||
|
|
|
@ -1,10 +1,6 @@
|
||||||
use ruff_macros::{ViolationMetadata, derive_message_formats};
|
use ruff_macros::{ViolationMetadata, derive_message_formats};
|
||||||
use ruff_python_ast::{self as ast};
|
|
||||||
use ruff_python_semantic::ScopeKind;
|
|
||||||
use ruff_text_size::Ranged;
|
|
||||||
|
|
||||||
use crate::Violation;
|
use crate::Violation;
|
||||||
use crate::checkers::ast::Checker;
|
|
||||||
|
|
||||||
/// ## What it does
|
/// ## What it does
|
||||||
/// Checks for uses of `yield from` in async functions.
|
/// Checks for uses of `yield from` in async functions.
|
||||||
|
@ -36,13 +32,3 @@ impl Violation for YieldFromInAsyncFunction {
|
||||||
"`yield from` statement in async function; use `async for` instead".to_string()
|
"`yield from` statement in async function; use `async for` instead".to_string()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// PLE1700
|
|
||||||
pub(crate) fn yield_from_in_async_function(checker: &Checker, expr: &ast::ExprYieldFrom) {
|
|
||||||
if matches!(
|
|
||||||
checker.semantic().current_scope().kind,
|
|
||||||
ScopeKind::Function(ast::StmtFunctionDef { is_async: true, .. })
|
|
||||||
) {
|
|
||||||
checker.report_diagnostic(YieldFromInAsyncFunction, expr.range());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
---
|
||||||
|
source: crates/ruff_linter/src/linter.rs
|
||||||
|
---
|
||||||
|
PLE1700 `yield from` statement in async function; use `async for` instead
|
||||||
|
--> resources/test/fixtures/syntax_errors/yield_from_in_async_function.py:1:16
|
||||||
|
|
|
||||||
|
1 | async def f(): yield from x # error
|
||||||
|
| ^^^^^^^^^^^^
|
||||||
|
|
|
|
@ -0,0 +1 @@
|
||||||
|
async def f(): yield from x
|
|
@ -709,6 +709,16 @@ impl SemanticSyntaxChecker {
|
||||||
}
|
}
|
||||||
Expr::YieldFrom(_) => {
|
Expr::YieldFrom(_) => {
|
||||||
Self::yield_outside_function(ctx, expr, YieldOutsideFunctionKind::YieldFrom);
|
Self::yield_outside_function(ctx, expr, YieldOutsideFunctionKind::YieldFrom);
|
||||||
|
if ctx.in_function_scope() && ctx.in_async_context() {
|
||||||
|
// test_err yield_from_in_async_function
|
||||||
|
// async def f(): yield from x
|
||||||
|
|
||||||
|
Self::add_error(
|
||||||
|
ctx,
|
||||||
|
SemanticSyntaxErrorKind::YieldFromInAsyncFunction,
|
||||||
|
expr.range(),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Expr::Await(_) => {
|
Expr::Await(_) => {
|
||||||
Self::yield_outside_function(ctx, expr, YieldOutsideFunctionKind::Await);
|
Self::yield_outside_function(ctx, expr, YieldOutsideFunctionKind::Await);
|
||||||
|
@ -989,6 +999,9 @@ impl Display for SemanticSyntaxError {
|
||||||
SemanticSyntaxErrorKind::AnnotatedNonlocal(name) => {
|
SemanticSyntaxErrorKind::AnnotatedNonlocal(name) => {
|
||||||
write!(f, "annotated name `{name}` can't be nonlocal")
|
write!(f, "annotated name `{name}` can't be nonlocal")
|
||||||
}
|
}
|
||||||
|
SemanticSyntaxErrorKind::YieldFromInAsyncFunction => {
|
||||||
|
f.write_str("`yield from` statement in async function; use `async for` instead")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1346,6 +1359,9 @@ pub enum SemanticSyntaxErrorKind {
|
||||||
|
|
||||||
/// Represents a type annotation on a variable that's been declared nonlocal
|
/// Represents a type annotation on a variable that's been declared nonlocal
|
||||||
AnnotatedNonlocal(String),
|
AnnotatedNonlocal(String),
|
||||||
|
|
||||||
|
/// Represents the use of `yield from` inside an asynchronous function.
|
||||||
|
YieldFromInAsyncFunction,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, get_size2::GetSize)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, get_size2::GetSize)]
|
||||||
|
|
|
@ -465,7 +465,7 @@ impl<'ast> SourceOrderVisitor<'ast> for ValidateAstVisitor<'ast> {
|
||||||
|
|
||||||
enum Scope {
|
enum Scope {
|
||||||
Module,
|
Module,
|
||||||
Function,
|
Function { is_async: bool },
|
||||||
Comprehension { is_async: bool },
|
Comprehension { is_async: bool },
|
||||||
Class,
|
Class,
|
||||||
}
|
}
|
||||||
|
@ -528,7 +528,15 @@ impl SemanticSyntaxContext for SemanticSyntaxCheckerVisitor<'_> {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn in_async_context(&self) -> bool {
|
fn in_async_context(&self) -> bool {
|
||||||
true
|
if let Some(scope) = self.scopes.iter().next_back() {
|
||||||
|
match scope {
|
||||||
|
Scope::Class | Scope::Module => false,
|
||||||
|
Scope::Comprehension { is_async } => *is_async,
|
||||||
|
Scope::Function { is_async } => *is_async,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn in_sync_comprehension(&self) -> bool {
|
fn in_sync_comprehension(&self) -> bool {
|
||||||
|
@ -589,8 +597,10 @@ impl Visitor<'_> for SemanticSyntaxCheckerVisitor<'_> {
|
||||||
self.visit_body(body);
|
self.visit_body(body);
|
||||||
self.scopes.pop().unwrap();
|
self.scopes.pop().unwrap();
|
||||||
}
|
}
|
||||||
ast::Stmt::FunctionDef(ast::StmtFunctionDef { .. }) => {
|
ast::Stmt::FunctionDef(ast::StmtFunctionDef { is_async, .. }) => {
|
||||||
self.scopes.push(Scope::Function);
|
self.scopes.push(Scope::Function {
|
||||||
|
is_async: *is_async,
|
||||||
|
});
|
||||||
ast::visitor::walk_stmt(self, stmt);
|
ast::visitor::walk_stmt(self, stmt);
|
||||||
self.scopes.pop().unwrap();
|
self.scopes.pop().unwrap();
|
||||||
}
|
}
|
||||||
|
@ -604,7 +614,7 @@ impl Visitor<'_> for SemanticSyntaxCheckerVisitor<'_> {
|
||||||
self.with_semantic_checker(|semantic, context| semantic.visit_expr(expr, context));
|
self.with_semantic_checker(|semantic, context| semantic.visit_expr(expr, context));
|
||||||
match expr {
|
match expr {
|
||||||
ast::Expr::Lambda(_) => {
|
ast::Expr::Lambda(_) => {
|
||||||
self.scopes.push(Scope::Function);
|
self.scopes.push(Scope::Function { is_async: false });
|
||||||
ast::visitor::walk_expr(self, expr);
|
ast::visitor::walk_expr(self, expr);
|
||||||
self.scopes.pop().unwrap();
|
self.scopes.pop().unwrap();
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,68 @@
|
||||||
|
---
|
||||||
|
source: crates/ruff_python_parser/tests/fixtures.rs
|
||||||
|
input_file: crates/ruff_python_parser/resources/inline/err/yield_from_in_async_function.py
|
||||||
|
---
|
||||||
|
## AST
|
||||||
|
|
||||||
|
```
|
||||||
|
Module(
|
||||||
|
ModModule {
|
||||||
|
node_index: NodeIndex(None),
|
||||||
|
range: 0..28,
|
||||||
|
body: [
|
||||||
|
FunctionDef(
|
||||||
|
StmtFunctionDef {
|
||||||
|
node_index: NodeIndex(None),
|
||||||
|
range: 0..27,
|
||||||
|
is_async: true,
|
||||||
|
decorator_list: [],
|
||||||
|
name: Identifier {
|
||||||
|
id: Name("f"),
|
||||||
|
range: 10..11,
|
||||||
|
node_index: NodeIndex(None),
|
||||||
|
},
|
||||||
|
type_params: None,
|
||||||
|
parameters: Parameters {
|
||||||
|
range: 11..13,
|
||||||
|
node_index: NodeIndex(None),
|
||||||
|
posonlyargs: [],
|
||||||
|
args: [],
|
||||||
|
vararg: None,
|
||||||
|
kwonlyargs: [],
|
||||||
|
kwarg: None,
|
||||||
|
},
|
||||||
|
returns: None,
|
||||||
|
body: [
|
||||||
|
Expr(
|
||||||
|
StmtExpr {
|
||||||
|
node_index: NodeIndex(None),
|
||||||
|
range: 15..27,
|
||||||
|
value: YieldFrom(
|
||||||
|
ExprYieldFrom {
|
||||||
|
node_index: NodeIndex(None),
|
||||||
|
range: 15..27,
|
||||||
|
value: Name(
|
||||||
|
ExprName {
|
||||||
|
node_index: NodeIndex(None),
|
||||||
|
range: 26..27,
|
||||||
|
id: Name("x"),
|
||||||
|
ctx: Load,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
```
|
||||||
|
## Semantic Syntax Errors
|
||||||
|
|
||||||
|
|
|
||||||
|
1 | async def f(): yield from x
|
||||||
|
| ^^^^^^^^^^^^ Syntax Error: `yield from` statement in async function; use `async for` instead
|
||||||
|
|
|
|
@ -74,8 +74,13 @@ def _(
|
||||||
def bar() -> None:
|
def bar() -> None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def outer_sync(): # `yield` from is only valid syntax inside a synchronous function
|
||||||
|
def _(
|
||||||
|
a: (yield from [1]), # error: [invalid-type-form] "`yield from` expressions are not allowed in type expressions"
|
||||||
|
): ...
|
||||||
|
|
||||||
async def baz(): ...
|
async def baz(): ...
|
||||||
async def outer(): # avoid unrelated syntax errors on yield, yield from, and await
|
async def outer_async(): # avoid unrelated syntax errors on `yield` and `await`
|
||||||
def _(
|
def _(
|
||||||
a: 1, # error: [invalid-type-form] "Int literals are not allowed in this context in a type expression"
|
a: 1, # error: [invalid-type-form] "Int literals are not allowed in this context in a type expression"
|
||||||
b: 2.3, # error: [invalid-type-form] "Float literals are not allowed in type expressions"
|
b: 2.3, # error: [invalid-type-form] "Float literals are not allowed in type expressions"
|
||||||
|
@ -90,11 +95,10 @@ async def outer(): # avoid unrelated syntax errors on yield, yield from, and aw
|
||||||
k: 1 if True else 2, # error: [invalid-type-form] "`if` expressions are not allowed in type expressions"
|
k: 1 if True else 2, # error: [invalid-type-form] "`if` expressions are not allowed in type expressions"
|
||||||
l: await baz(), # error: [invalid-type-form] "`await` expressions are not allowed in type expressions"
|
l: await baz(), # error: [invalid-type-form] "`await` expressions are not allowed in type expressions"
|
||||||
m: (yield 1), # error: [invalid-type-form] "`yield` expressions are not allowed in type expressions"
|
m: (yield 1), # error: [invalid-type-form] "`yield` expressions are not allowed in type expressions"
|
||||||
n: (yield from [1]), # error: [invalid-type-form] "`yield from` expressions are not allowed in type expressions"
|
n: 1 < 2, # error: [invalid-type-form] "Comparison expressions are not allowed in type expressions"
|
||||||
o: 1 < 2, # error: [invalid-type-form] "Comparison expressions are not allowed in type expressions"
|
o: bar(), # error: [invalid-type-form] "Function calls are not allowed in type expressions"
|
||||||
p: bar(), # error: [invalid-type-form] "Function calls are not allowed in type expressions"
|
p: int | f"foo", # error: [invalid-type-form] "F-strings are not allowed in type expressions"
|
||||||
q: int | f"foo", # error: [invalid-type-form] "F-strings are not allowed in type expressions"
|
q: [1, 2, 3][1:2], # error: [invalid-type-form] "Slices are not allowed in type expressions"
|
||||||
r: [1, 2, 3][1:2], # error: [invalid-type-form] "Slices are not allowed in type expressions"
|
|
||||||
):
|
):
|
||||||
reveal_type(a) # revealed: Unknown
|
reveal_type(a) # revealed: Unknown
|
||||||
reveal_type(b) # revealed: Unknown
|
reveal_type(b) # revealed: Unknown
|
||||||
|
@ -107,9 +111,12 @@ async def outer(): # avoid unrelated syntax errors on yield, yield from, and aw
|
||||||
reveal_type(i) # revealed: Unknown
|
reveal_type(i) # revealed: Unknown
|
||||||
reveal_type(j) # revealed: Unknown
|
reveal_type(j) # revealed: Unknown
|
||||||
reveal_type(k) # revealed: Unknown
|
reveal_type(k) # revealed: Unknown
|
||||||
reveal_type(p) # revealed: Unknown
|
reveal_type(l) # revealed: Unknown
|
||||||
reveal_type(q) # revealed: int | Unknown
|
reveal_type(m) # revealed: Unknown
|
||||||
reveal_type(r) # revealed: @Todo(unknown type subscript)
|
reveal_type(n) # revealed: Unknown
|
||||||
|
reveal_type(o) # revealed: Unknown
|
||||||
|
reveal_type(p) # revealed: int | Unknown
|
||||||
|
reveal_type(q) # revealed: @Todo(unknown type subscript)
|
||||||
|
|
||||||
class Mat:
|
class Mat:
|
||||||
def __init__(self, value: int):
|
def __init__(self, value: int):
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue