diff --git a/crates/ruff_linter/resources/test/fixtures/syntax_errors/yield_from_in_async_function.py b/crates/ruff_linter/resources/test/fixtures/syntax_errors/yield_from_in_async_function.py new file mode 100644 index 0000000000..59e11eabe1 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/syntax_errors/yield_from_in_async_function.py @@ -0,0 +1 @@ +async def f(): yield from x # error \ No newline at end of file diff --git a/crates/ruff_linter/src/checkers/ast/analyze/expression.rs b/crates/ruff_linter/src/checkers/ast/analyze/expression.rs index 39c1b460ee..1851959029 100644 --- a/crates/ruff_linter/src/checkers/ast/analyze/expression.rs +++ b/crates/ruff_linter/src/checkers/ast/analyze/expression.rs @@ -1319,13 +1319,10 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) { pylint::rules::yield_in_init(checker, expr); } } - Expr::YieldFrom(yield_from) => { + Expr::YieldFrom(_) => { if checker.is_rule_enabled(Rule::YieldInInit) { 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, .. }) => { if checker.is_rule_enabled(Rule::FStringMissingPlaceholders) { diff --git a/crates/ruff_linter/src/checkers/ast/mod.rs b/crates/ruff_linter/src/checkers/ast/mod.rs index 906fe2491b..a38086137e 100644 --- a/crates/ruff_linter/src/checkers/ast/mod.rs +++ b/crates/ruff_linter/src/checkers/ast/mod.rs @@ -71,7 +71,9 @@ use crate::registry::Rule; use crate::rules::pyflakes::rules::{ 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::settings::rule_table::RuleTable; use crate::settings::{LinterSettings, TargetVersion, flags}; @@ -668,6 +670,12 @@ impl SemanticSyntaxContext for Checker<'_> { 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::DuplicateTypeParameter | SemanticSyntaxErrorKind::MultipleCaseAssignment(_) diff --git a/crates/ruff_linter/src/linter.rs b/crates/ruff_linter/src/linter.rs index e6dc3f3658..eeb321cab9 100644 --- a/crates/ruff_linter/src/linter.rs +++ b/crates/ruff_linter/src/linter.rs @@ -1231,6 +1231,10 @@ mod tests { )] #[test_case(Rule::AwaitOutsideAsync, Path::new("await_outside_async_function.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<()> { let snapshot = path.to_string_lossy().to_string(); let path = Path::new("resources/test/fixtures/syntax_errors").join(path); diff --git a/crates/ruff_linter/src/rules/pylint/rules/yield_from_in_async_function.rs b/crates/ruff_linter/src/rules/pylint/rules/yield_from_in_async_function.rs index 60fc10bc52..fd9e2ee618 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/yield_from_in_async_function.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/yield_from_in_async_function.rs @@ -1,10 +1,6 @@ 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::checkers::ast::Checker; /// ## What it does /// 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() } } - -/// 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()); - } -} diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__yield_from_in_async_function.py.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__yield_from_in_async_function.py.snap new file mode 100644 index 0000000000..ab60900419 --- /dev/null +++ b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__yield_from_in_async_function.py.snap @@ -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 + | ^^^^^^^^^^^^ + | diff --git a/crates/ruff_python_parser/resources/inline/err/yield_from_in_async_function.py b/crates/ruff_python_parser/resources/inline/err/yield_from_in_async_function.py new file mode 100644 index 0000000000..79354bc05c --- /dev/null +++ b/crates/ruff_python_parser/resources/inline/err/yield_from_in_async_function.py @@ -0,0 +1 @@ +async def f(): yield from x diff --git a/crates/ruff_python_parser/src/semantic_errors.rs b/crates/ruff_python_parser/src/semantic_errors.rs index 6a3ce2a850..9b4d42ffd5 100644 --- a/crates/ruff_python_parser/src/semantic_errors.rs +++ b/crates/ruff_python_parser/src/semantic_errors.rs @@ -709,6 +709,16 @@ impl SemanticSyntaxChecker { } Expr::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(_) => { Self::yield_outside_function(ctx, expr, YieldOutsideFunctionKind::Await); @@ -989,6 +999,9 @@ impl Display for SemanticSyntaxError { SemanticSyntaxErrorKind::AnnotatedNonlocal(name) => { 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 AnnotatedNonlocal(String), + + /// Represents the use of `yield from` inside an asynchronous function. + YieldFromInAsyncFunction, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, get_size2::GetSize)] diff --git a/crates/ruff_python_parser/tests/fixtures.rs b/crates/ruff_python_parser/tests/fixtures.rs index 7aa7a6f2e2..e3679ea50a 100644 --- a/crates/ruff_python_parser/tests/fixtures.rs +++ b/crates/ruff_python_parser/tests/fixtures.rs @@ -465,7 +465,7 @@ impl<'ast> SourceOrderVisitor<'ast> for ValidateAstVisitor<'ast> { enum Scope { Module, - Function, + Function { is_async: bool }, Comprehension { is_async: bool }, Class, } @@ -528,7 +528,15 @@ impl SemanticSyntaxContext for SemanticSyntaxCheckerVisitor<'_> { } 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 { @@ -589,8 +597,10 @@ impl Visitor<'_> for SemanticSyntaxCheckerVisitor<'_> { self.visit_body(body); self.scopes.pop().unwrap(); } - ast::Stmt::FunctionDef(ast::StmtFunctionDef { .. }) => { - self.scopes.push(Scope::Function); + ast::Stmt::FunctionDef(ast::StmtFunctionDef { is_async, .. }) => { + self.scopes.push(Scope::Function { + is_async: *is_async, + }); ast::visitor::walk_stmt(self, stmt); self.scopes.pop().unwrap(); } @@ -604,7 +614,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); + self.scopes.push(Scope::Function { is_async: false }); ast::visitor::walk_expr(self, expr); self.scopes.pop().unwrap(); } diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@yield_from_in_async_function.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@yield_from_in_async_function.py.snap new file mode 100644 index 0000000000..86372e56d9 --- /dev/null +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@yield_from_in_async_function.py.snap @@ -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 + | diff --git a/crates/ty_python_semantic/resources/mdtest/annotations/invalid.md b/crates/ty_python_semantic/resources/mdtest/annotations/invalid.md index 91d55f7352..5202b0921d 100644 --- a/crates/ty_python_semantic/resources/mdtest/annotations/invalid.md +++ b/crates/ty_python_semantic/resources/mdtest/annotations/invalid.md @@ -74,8 +74,13 @@ def _( def bar() -> 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 outer(): # avoid unrelated syntax errors on yield, yield from, and await +async def outer_async(): # avoid unrelated syntax errors on `yield` and `await` def _( 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" @@ -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" 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" - n: (yield from [1]), # error: [invalid-type-form] "`yield from` expressions are not allowed in type expressions" - o: 1 < 2, # error: [invalid-type-form] "Comparison expressions are not allowed in type expressions" - p: bar(), # error: [invalid-type-form] "Function calls are not allowed in type expressions" - q: int | f"foo", # error: [invalid-type-form] "F-strings are not allowed in type expressions" - r: [1, 2, 3][1:2], # error: [invalid-type-form] "Slices are not allowed in type expressions" + n: 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: 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" ): reveal_type(a) # 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(j) # revealed: Unknown reveal_type(k) # revealed: Unknown - reveal_type(p) # revealed: Unknown - reveal_type(q) # revealed: int | Unknown - reveal_type(r) # revealed: @Todo(unknown type subscript) + reveal_type(l) # revealed: Unknown + reveal_type(m) # revealed: Unknown + 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: def __init__(self, value: int):