diff --git a/crates/ruff_python_parser/resources/inline/err/for_in_target_postfix_expr.py b/crates/ruff_python_parser/resources/inline/err/for_in_target_postfix_expr.py new file mode 100644 index 0000000000..6b20f4f0fc --- /dev/null +++ b/crates/ruff_python_parser/resources/inline/err/for_in_target_postfix_expr.py @@ -0,0 +1 @@ +for d(x in y) in target: ... diff --git a/crates/ruff_python_parser/resources/inline/err/parenthesized_compare_expr_in_for.py b/crates/ruff_python_parser/resources/inline/err/parenthesized_compare_expr_in_for.py index 13d613dad9..fd06f2e422 100644 --- a/crates/ruff_python_parser/resources/inline/err/parenthesized_compare_expr_in_for.py +++ b/crates/ruff_python_parser/resources/inline/err/parenthesized_compare_expr_in_for.py @@ -1,2 +1,5 @@ for (x in y)() in iter: ... for (x in y) in iter: ... +for (x in y, z) in iter: ... +for [x in y, z] in iter: ... +for {x in y, z} in iter: ... diff --git a/crates/ruff_python_parser/resources/inline/ok/for_in_target_postfix_expr.py b/crates/ruff_python_parser/resources/inline/ok/for_in_target_postfix_expr.py new file mode 100644 index 0000000000..bf905dbdc8 --- /dev/null +++ b/crates/ruff_python_parser/resources/inline/ok/for_in_target_postfix_expr.py @@ -0,0 +1 @@ +for d[x in y] in target: ... diff --git a/crates/ruff_python_parser/src/parser/expression.rs b/crates/ruff_python_parser/src/parser/expression.rs index 4bc07ac01e..cac035541b 100644 --- a/crates/ruff_python_parser/src/parser/expression.rs +++ b/crates/ruff_python_parser/src/parser/expression.rs @@ -360,7 +360,7 @@ impl<'src> Parser<'src> { } // Don't parse a `CompareExpr` if we are parsing a `Comprehension` or `ForStmt` - if token.is_compare_operator() && self.has_ctx(ParserCtxFlags::FOR_TARGET) { + if matches!(token, TokenKind::In) && self.has_ctx(ParserCtxFlags::FOR_TARGET) { break; } @@ -659,11 +659,38 @@ impl<'src> Parser<'src> { Expr::IpyEscapeCommand(self.parse_ipython_escape_command_expression()) } TokenKind::String | TokenKind::FStringStart => self.parse_strings(), - TokenKind::Lpar => { - return self.parse_parenthesized_expression(); + tok @ (TokenKind::Lpar | TokenKind::Lsqb | TokenKind::Lbrace) => { + // We need to unset the `FOR_TARGET` in the context when parsing an expression + // inside a parentheses, curly brace or brackets otherwise the `in` operator of a + // comparison expression will not be parsed in a `for` target. + + // test_ok parenthesized_compare_expr_in_for + // for (x in y)[0] in iter: ... + // for (x in y).attr in iter: ... + + // test_err parenthesized_compare_expr_in_for + // for (x in y)() in iter: ... + // for (x in y) in iter: ... + // for (x in y, z) in iter: ... + // for [x in y, z] in iter: ... + // for {x in y, z} in iter: ... + let current_context = self.ctx - ParserCtxFlags::FOR_TARGET; + let saved_context = self.set_ctx(current_context); + + let expr = match tok { + TokenKind::Lpar => { + let parsed_expr = self.parse_parenthesized_expression(); + self.restore_ctx(current_context, saved_context); + return parsed_expr; + } + TokenKind::Lsqb => self.parse_list_like_expression(), + TokenKind::Lbrace => self.parse_set_or_dict_like_expression(), + _ => unreachable!(), + }; + + self.restore_ctx(current_context, saved_context); + expr } - TokenKind::Lsqb => self.parse_list_like_expression(), - TokenKind::Lbrace => self.parse_set_or_dict_like_expression(), kind => { if kind.is_keyword() { @@ -692,14 +719,25 @@ impl<'src> Parser<'src> { /// /// This method does nothing if the current token is not a candidate for a postfix expression. pub(super) fn parse_postfix_expression(&mut self, mut lhs: Expr, start: TextSize) -> Expr { - loop { + // test_ok for_in_target_postfix_expr + // for d[x in y] in target: ... + + // test_err for_in_target_postfix_expr + // for d(x in y) in target: ... + let current_context = self.ctx - ParserCtxFlags::FOR_TARGET; + let saved_context = self.set_ctx(current_context); + + lhs = loop { lhs = match self.current_token_kind() { TokenKind::Lpar => Expr::Call(self.parse_call_expression(lhs, start)), TokenKind::Lsqb => Expr::Subscript(self.parse_subscript_expression(lhs, start)), TokenKind::Dot => Expr::Attribute(self.parse_attribute_expression(lhs, start)), _ => break lhs, }; - } + }; + + self.restore_ctx(current_context, saved_context); + lhs } /// Parse a call expression. @@ -1748,26 +1786,13 @@ impl<'src> Parser<'src> { .into(); } - // We need to unset the `FOR_TARGET` in the context when parsing a parenthesized expression - // otherwise a parenthesized comparison expression will not be parsed in a `for` target. - - // test_ok parenthesized_compare_expr_in_for - // for (x in y)[0] in iter: ... - // for (x in y).attr in iter: ... - - // test_err parenthesized_compare_expr_in_for - // for (x in y)() in iter: ... - // for (x in y) in iter: ... - let current_context = self.ctx - ParserCtxFlags::FOR_TARGET; - let saved_context = self.set_ctx(current_context); - // Use the more general rule of the three to parse the first element // and limit it later. let mut parsed_expr = self.parse_yield_expression_or_else(|p| { p.parse_star_expression_or_higher(AllowNamedExpression::Yes) }); - let parsed_expr = match self.current_token_kind() { + match self.current_token_kind() { TokenKind::Comma => { // grammar: `tuple` let tuple = self.parse_tuple_expression( @@ -1812,11 +1837,7 @@ impl<'src> Parser<'src> { parsed_expr.is_parenthesized = true; parsed_expr } - }; - - self.restore_ctx(current_context, saved_context); - - parsed_expr + } } /// Parses multiple items separated by a comma into a tuple expression. diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@for_in_target_postfix_expr.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@for_in_target_postfix_expr.py.snap new file mode 100644 index 0000000000..c74d11d107 --- /dev/null +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@for_in_target_postfix_expr.py.snap @@ -0,0 +1,89 @@ +--- +source: crates/ruff_python_parser/tests/fixtures.rs +input_file: crates/ruff_python_parser/resources/inline/err/for_in_target_postfix_expr.py +--- +## AST + +``` +Module( + ModModule { + range: 0..29, + body: [ + For( + StmtFor { + range: 0..28, + is_async: false, + target: Call( + ExprCall { + range: 4..13, + func: Name( + ExprName { + range: 4..5, + id: "d", + ctx: Load, + }, + ), + arguments: Arguments { + range: 5..13, + args: [ + Compare( + ExprCompare { + range: 6..12, + left: Name( + ExprName { + range: 6..7, + id: "x", + ctx: Load, + }, + ), + ops: [ + In, + ], + comparators: [ + Name( + ExprName { + range: 11..12, + id: "y", + ctx: Load, + }, + ), + ], + }, + ), + ], + keywords: [], + }, + }, + ), + iter: Name( + ExprName { + range: 17..23, + id: "target", + ctx: Load, + }, + ), + body: [ + Expr( + StmtExpr { + range: 25..28, + value: EllipsisLiteral( + ExprEllipsisLiteral { + range: 25..28, + }, + ), + }, + ), + ], + orelse: [], + }, + ), + ], + }, +) +``` +## Errors + + | +1 | for d(x in y) in target: ... + | ^^^^^^^^^ Syntax Error: Invalid assignment target + | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@parenthesized_compare_expr_in_for.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@parenthesized_compare_expr_in_for.py.snap index 12f380eece..53cde3a0c9 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@parenthesized_compare_expr_in_for.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@parenthesized_compare_expr_in_for.py.snap @@ -7,7 +7,7 @@ input_file: crates/ruff_python_parser/resources/inline/err/parenthesized_compare ``` Module( ModModule { - range: 0..54, + range: 0..141, body: [ For( StmtFor { @@ -119,6 +119,201 @@ Module( orelse: [], }, ), + For( + StmtFor { + range: 54..82, + is_async: false, + target: Tuple( + ExprTuple { + range: 58..69, + elts: [ + Compare( + ExprCompare { + range: 59..65, + left: Name( + ExprName { + range: 59..60, + id: "x", + ctx: Load, + }, + ), + ops: [ + In, + ], + comparators: [ + Name( + ExprName { + range: 64..65, + id: "y", + ctx: Load, + }, + ), + ], + }, + ), + Name( + ExprName { + range: 67..68, + id: "z", + ctx: Store, + }, + ), + ], + ctx: Store, + parenthesized: true, + }, + ), + iter: Name( + ExprName { + range: 73..77, + id: "iter", + ctx: Load, + }, + ), + body: [ + Expr( + StmtExpr { + range: 79..82, + value: EllipsisLiteral( + ExprEllipsisLiteral { + range: 79..82, + }, + ), + }, + ), + ], + orelse: [], + }, + ), + For( + StmtFor { + range: 83..111, + is_async: false, + target: List( + ExprList { + range: 87..98, + elts: [ + Compare( + ExprCompare { + range: 88..94, + left: Name( + ExprName { + range: 88..89, + id: "x", + ctx: Load, + }, + ), + ops: [ + In, + ], + comparators: [ + Name( + ExprName { + range: 93..94, + id: "y", + ctx: Load, + }, + ), + ], + }, + ), + Name( + ExprName { + range: 96..97, + id: "z", + ctx: Store, + }, + ), + ], + ctx: Store, + }, + ), + iter: Name( + ExprName { + range: 102..106, + id: "iter", + ctx: Load, + }, + ), + body: [ + Expr( + StmtExpr { + range: 108..111, + value: EllipsisLiteral( + ExprEllipsisLiteral { + range: 108..111, + }, + ), + }, + ), + ], + orelse: [], + }, + ), + For( + StmtFor { + range: 112..140, + is_async: false, + target: Set( + ExprSet { + range: 116..127, + elts: [ + Compare( + ExprCompare { + range: 117..123, + left: Name( + ExprName { + range: 117..118, + id: "x", + ctx: Load, + }, + ), + ops: [ + In, + ], + comparators: [ + Name( + ExprName { + range: 122..123, + id: "y", + ctx: Load, + }, + ), + ], + }, + ), + Name( + ExprName { + range: 125..126, + id: "z", + ctx: Load, + }, + ), + ], + }, + ), + iter: Name( + ExprName { + range: 131..135, + id: "iter", + ctx: Load, + }, + ), + body: [ + Expr( + StmtExpr { + range: 137..140, + value: EllipsisLiteral( + ExprEllipsisLiteral { + range: 137..140, + }, + ), + }, + ), + ], + orelse: [], + }, + ), ], }, ) @@ -129,6 +324,7 @@ Module( 1 | for (x in y)() in iter: ... | ^^^^^^^^^^ Syntax Error: Invalid assignment target 2 | for (x in y) in iter: ... +3 | for (x in y, z) in iter: ... | @@ -136,4 +332,33 @@ Module( 1 | for (x in y)() in iter: ... 2 | for (x in y) in iter: ... | ^^^^^^ Syntax Error: Invalid assignment target +3 | for (x in y, z) in iter: ... +4 | for [x in y, z] in iter: ... + | + + + | +1 | for (x in y)() in iter: ... +2 | for (x in y) in iter: ... +3 | for (x in y, z) in iter: ... + | ^^^^^^ Syntax Error: Invalid assignment target +4 | for [x in y, z] in iter: ... +5 | for {x in y, z} in iter: ... + | + + + | +2 | for (x in y) in iter: ... +3 | for (x in y, z) in iter: ... +4 | for [x in y, z] in iter: ... + | ^^^^^^ Syntax Error: Invalid assignment target +5 | for {x in y, z} in iter: ... + | + + + | +3 | for (x in y, z) in iter: ... +4 | for [x in y, z] in iter: ... +5 | for {x in y, z} in iter: ... + | ^^^^^^^^^^^ Syntax Error: Invalid assignment target | diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@for_in_target_postfix_expr.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@for_in_target_postfix_expr.py.snap new file mode 100644 index 0000000000..13637f642d --- /dev/null +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@for_in_target_postfix_expr.py.snap @@ -0,0 +1,78 @@ +--- +source: crates/ruff_python_parser/tests/fixtures.rs +input_file: crates/ruff_python_parser/resources/inline/ok/for_in_target_postfix_expr.py +--- +## AST + +``` +Module( + ModModule { + range: 0..29, + body: [ + For( + StmtFor { + range: 0..28, + is_async: false, + target: Subscript( + ExprSubscript { + range: 4..13, + value: Name( + ExprName { + range: 4..5, + id: "d", + ctx: Load, + }, + ), + slice: Compare( + ExprCompare { + range: 6..12, + left: Name( + ExprName { + range: 6..7, + id: "x", + ctx: Load, + }, + ), + ops: [ + In, + ], + comparators: [ + Name( + ExprName { + range: 11..12, + id: "y", + ctx: Load, + }, + ), + ], + }, + ), + ctx: Store, + }, + ), + iter: Name( + ExprName { + range: 17..23, + id: "target", + ctx: Load, + }, + ), + body: [ + Expr( + StmtExpr { + range: 25..28, + value: EllipsisLiteral( + ExprEllipsisLiteral { + range: 25..28, + }, + ), + }, + ), + ], + orelse: [], + }, + ), + ], + }, +) +```