diff --git a/crates/ruff/resources/test/fixtures/pylint/yield_from_in_async_function.py b/crates/ruff/resources/test/fixtures/pylint/yield_from_in_async_function.py new file mode 100644 index 0000000000000..8130c177277cf --- /dev/null +++ b/crates/ruff/resources/test/fixtures/pylint/yield_from_in_async_function.py @@ -0,0 +1,7 @@ +async def success(): + yield 42 + + +async def fail(): + l = (1, 2, 3) + yield from l diff --git a/crates/ruff/src/checkers/ast/mod.rs b/crates/ruff/src/checkers/ast/mod.rs index 1028f33de4b18..bcdc2f89f8d27 100644 --- a/crates/ruff/src/checkers/ast/mod.rs +++ b/crates/ruff/src/checkers/ast/mod.rs @@ -3077,13 +3077,16 @@ where pylint::rules::yield_in_init(self, expr); } } - Expr::YieldFrom(_) => { + Expr::YieldFrom(yield_from) => { if self.enabled(Rule::YieldOutsideFunction) { pyflakes::rules::yield_outside_function(self, expr); } if self.enabled(Rule::YieldInInit) { pylint::rules::yield_in_init(self, expr); } + if self.enabled(Rule::YieldFromInAsyncFunction) { + pylint::rules::yield_from_in_async_function(self, yield_from); + } } Expr::Await(_) => { if self.enabled(Rule::YieldOutsideFunction) { diff --git a/crates/ruff/src/codes.rs b/crates/ruff/src/codes.rs index 2b55ab6d0e36b..83cf212572885 100644 --- a/crates/ruff/src/codes.rs +++ b/crates/ruff/src/codes.rs @@ -165,6 +165,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { (Pylint, "E1307") => (RuleGroup::Unspecified, Rule::BadStringFormatType), (Pylint, "E1310") => (RuleGroup::Unspecified, Rule::BadStrStripCall), (Pylint, "E1507") => (RuleGroup::Unspecified, Rule::InvalidEnvvarValue), + (Pylint, "E1700") => (RuleGroup::Unspecified, Rule::YieldFromInAsyncFunction), (Pylint, "E2502") => (RuleGroup::Unspecified, Rule::BidirectionalUnicode), (Pylint, "E2510") => (RuleGroup::Unspecified, Rule::InvalidCharacterBackspace), (Pylint, "E2512") => (RuleGroup::Unspecified, Rule::InvalidCharacterSub), diff --git a/crates/ruff/src/registry.rs b/crates/ruff/src/registry.rs index 1f975cff38c88..3fb67075629fc 100644 --- a/crates/ruff/src/registry.rs +++ b/crates/ruff/src/registry.rs @@ -117,6 +117,7 @@ ruff_macros::register_rules!( // pylint rules::pylint::rules::AssertOnStringLiteral, rules::pylint::rules::UselessReturn, + rules::pylint::rules::YieldFromInAsyncFunction, rules::pylint::rules::YieldInInit, rules::pylint::rules::InvalidAllObject, rules::pylint::rules::InvalidAllFormat, diff --git a/crates/ruff/src/rules/pylint/mod.rs b/crates/ruff/src/rules/pylint/mod.rs index 6534a3efd34e4..6381344785504 100644 --- a/crates/ruff/src/rules/pylint/mod.rs +++ b/crates/ruff/src/rules/pylint/mod.rs @@ -73,6 +73,7 @@ mod tests { #[test_case(Rule::UselessElseOnLoop, Path::new("useless_else_on_loop.py"); "PLW0120")] #[test_case(Rule::UselessImportAlias, Path::new("import_aliasing.py"); "PLC0414")] #[test_case(Rule::UselessReturn, Path::new("useless_return.py"); "PLR1711")] + #[test_case(Rule::YieldFromInAsyncFunction, Path::new("yield_from_in_async_function.py"); "PLE1700")] #[test_case(Rule::YieldInInit, Path::new("yield_in_init.py"); "PLE0100")] #[test_case(Rule::NestedMinMax, Path::new("nested_min_max.py"); "PLW3301")] fn rules(rule_code: Rule, path: &Path) -> Result<()> { diff --git a/crates/ruff/src/rules/pylint/rules/mod.rs b/crates/ruff/src/rules/pylint/rules/mod.rs index a75c21d0765d2..f59172ad6a277 100644 --- a/crates/ruff/src/rules/pylint/rules/mod.rs +++ b/crates/ruff/src/rules/pylint/rules/mod.rs @@ -48,6 +48,9 @@ pub(crate) use unnecessary_direct_lambda_call::{ pub(crate) use useless_else_on_loop::{useless_else_on_loop, UselessElseOnLoop}; pub(crate) use useless_import_alias::{useless_import_alias, UselessImportAlias}; pub(crate) use useless_return::{useless_return, UselessReturn}; +pub(crate) use yield_from_in_async_function::{ + yield_from_in_async_function, YieldFromInAsyncFunction, +}; pub(crate) use yield_in_init::{yield_in_init, YieldInInit}; mod assert_on_string_literal; @@ -91,4 +94,5 @@ mod unnecessary_direct_lambda_call; mod useless_else_on_loop; mod useless_import_alias; mod useless_return; +mod yield_from_in_async_function; mod yield_in_init; diff --git a/crates/ruff/src/rules/pylint/rules/yield_from_in_async_function.rs b/crates/ruff/src/rules/pylint/rules/yield_from_in_async_function.rs new file mode 100644 index 0000000000000..eeadc7572c79d --- /dev/null +++ b/crates/ruff/src/rules/pylint/rules/yield_from_in_async_function.rs @@ -0,0 +1,47 @@ +use rustpython_parser::ast::{ExprYieldFrom, Ranged}; + +use ruff_diagnostics::{Diagnostic, Violation}; +use ruff_macros::{derive_message_formats, violation}; + +use crate::checkers::ast::Checker; + +/// ## What it does +/// Checks for uses of `yield from` in async functions. +/// +/// ## Why is this bad? +/// Python doesn't support the use of `yield from` in async functions, and will +/// raise a `SyntaxError` in such cases. +/// +/// Instead, considering refactoring the code to use an `async for` loop instead. +/// +/// ## Example +/// ```python +/// async def numbers(): +/// yield from [1, 2, 3, 4, 5] +/// ``` +/// +/// Use instead: +/// ```python +/// async def numbers(): +/// async for number in [1, 2, 3, 4, 5]: +/// yield number +/// ``` +#[violation] +pub struct YieldFromInAsyncFunction; + +impl Violation for YieldFromInAsyncFunction { + #[derive_message_formats] + fn message(&self) -> String { + format!("`yield from` statement in async function; use `async for` instead") + } +} + +/// PLE1700 +pub(crate) fn yield_from_in_async_function(checker: &mut Checker, expr: &ExprYieldFrom) { + let scope = checker.semantic_model().scope(); + if scope.kind.is_async_function() { + checker + .diagnostics + .push(Diagnostic::new(YieldFromInAsyncFunction, expr.range())); + } +} diff --git a/crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLE1700_yield_from_in_async_function.py.snap b/crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLE1700_yield_from_in_async_function.py.snap new file mode 100644 index 0000000000000..6832eb464095b --- /dev/null +++ b/crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLE1700_yield_from_in_async_function.py.snap @@ -0,0 +1,12 @@ +--- +source: crates/ruff/src/rules/pylint/mod.rs +--- +yield_from_in_async_function.py:7:5: PLE1700 `yield from` statement in async function; use `async for` instead + | +7 | async def fail(): +8 | l = (1, 2, 3) +9 | yield from l + | ^^^^^^^^^^^^ PLE1700 + | + + diff --git a/ruff.schema.json b/ruff.schema.json index 5bf96af3b8c82..d3547d8c391fb 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -2048,6 +2048,9 @@ "PLE15", "PLE150", "PLE1507", + "PLE17", + "PLE170", + "PLE1700", "PLE2", "PLE25", "PLE250",