diff --git a/crates/ruff_python_parser/resources/invalid/expressions/pep798_boolean_then_error.py b/crates/ruff_python_parser/resources/invalid/expressions/pep798_boolean_then_error.py new file mode 100644 index 0000000000000..206a96324116b --- /dev/null +++ b/crates/ruff_python_parser/resources/invalid/expressions/pep798_boolean_then_error.py @@ -0,0 +1,2 @@ +# parse_options: {"target-version": "3.15"} +(*x or y for x in z); a = diff --git a/crates/ruff_python_parser/resources/invalid/expressions/pep798_call_conditional_then_error.py b/crates/ruff_python_parser/resources/invalid/expressions/pep798_call_conditional_then_error.py new file mode 100644 index 0000000000000..ce10033963e37 --- /dev/null +++ b/crates/ruff_python_parser/resources/invalid/expressions/pep798_call_conditional_then_error.py @@ -0,0 +1,2 @@ +# parse_options: {"target-version": "3.15"} +f(*x if c else y for x in z); a = diff --git a/crates/ruff_python_parser/resources/invalid/expressions/pep798_comparison_then_error.py b/crates/ruff_python_parser/resources/invalid/expressions/pep798_comparison_then_error.py new file mode 100644 index 0000000000000..a2e143184dd8e --- /dev/null +++ b/crates/ruff_python_parser/resources/invalid/expressions/pep798_comparison_then_error.py @@ -0,0 +1,2 @@ +# parse_options: {"target-version": "3.15"} +(*x < y for x in z); a = diff --git a/crates/ruff_python_parser/resources/invalid/expressions/pep798_conditional_display.py b/crates/ruff_python_parser/resources/invalid/expressions/pep798_conditional_display.py new file mode 100644 index 0000000000000..f4ec8092b101b --- /dev/null +++ b/crates/ruff_python_parser/resources/invalid/expressions/pep798_conditional_display.py @@ -0,0 +1,5 @@ +# parse_options: {"target-version": "3.15"} +[*x if x else y] +{**x if x else y} +[x if x else *y] +{x if x else **y} diff --git a/crates/ruff_python_parser/resources/invalid/expressions/pep798_conditional_then_error.py b/crates/ruff_python_parser/resources/invalid/expressions/pep798_conditional_then_error.py new file mode 100644 index 0000000000000..0d6f6a4f79b2f --- /dev/null +++ b/crates/ruff_python_parser/resources/invalid/expressions/pep798_conditional_then_error.py @@ -0,0 +1,2 @@ +# parse_options: {"target-version": "3.15"} +(*x if c else y for x in z); a = diff --git a/crates/ruff_python_parser/resources/invalid/expressions/pep798_dict_positions.py b/crates/ruff_python_parser/resources/invalid/expressions/pep798_dict_positions.py new file mode 100644 index 0000000000000..3be40013ad192 --- /dev/null +++ b/crates/ruff_python_parser/resources/invalid/expressions/pep798_dict_positions.py @@ -0,0 +1,8 @@ +# parse_options: {"target-version": "3.15"} +[**x for x in xs] +(**x for x in xs) +f(**x for x in xs) +{*x: y for x in xs} +{**x: y for x in xs} +{x: *y for x in xs} +{x: **y for x in xs} diff --git a/crates/ruff_python_parser/resources/invalid/expressions/pep798_lambda_then_error.py b/crates/ruff_python_parser/resources/invalid/expressions/pep798_lambda_then_error.py new file mode 100644 index 0000000000000..205b65bcaa0a7 --- /dev/null +++ b/crates/ruff_python_parser/resources/invalid/expressions/pep798_lambda_then_error.py @@ -0,0 +1,2 @@ +# parse_options: {"target-version": "3.15"} +(*lambda: x for x in z); a = diff --git a/crates/ruff_python_parser/resources/invalid/expressions/pep798_missing_operand.py b/crates/ruff_python_parser/resources/invalid/expressions/pep798_missing_operand.py new file mode 100644 index 0000000000000..594562e9855f4 --- /dev/null +++ b/crates/ruff_python_parser/resources/invalid/expressions/pep798_missing_operand.py @@ -0,0 +1,5 @@ +# parse_options: {"target-version": "3.15"} +[**] +(**) +[**, x] +[** for x in z] diff --git a/crates/ruff_python_parser/resources/invalid/expressions/pep798_nested_conditional_then_error.py b/crates/ruff_python_parser/resources/invalid/expressions/pep798_nested_conditional_then_error.py new file mode 100644 index 0000000000000..4a2eefb51e8b1 --- /dev/null +++ b/crates/ruff_python_parser/resources/invalid/expressions/pep798_nested_conditional_then_error.py @@ -0,0 +1,2 @@ +# parse_options: {"target-version": "3.15"} +(*(*x if c else y for x in xs) if d else z for z in zs); a = diff --git a/crates/ruff_python_parser/resources/invalid/expressions/pep798_nested_operand.py b/crates/ruff_python_parser/resources/invalid/expressions/pep798_nested_operand.py new file mode 100644 index 0000000000000..f22c317d340ba --- /dev/null +++ b/crates/ruff_python_parser/resources/invalid/expressions/pep798_nested_operand.py @@ -0,0 +1,3 @@ +# parse_options: {"target-version": "3.15"} +[**(x if x else *y) for x in z] +(**(x if x else **y) for x in z) diff --git a/crates/ruff_python_parser/resources/invalid/expressions/pep798_nested_then_error.py b/crates/ruff_python_parser/resources/invalid/expressions/pep798_nested_then_error.py new file mode 100644 index 0000000000000..365882d87f3bb --- /dev/null +++ b/crates/ruff_python_parser/resources/invalid/expressions/pep798_nested_then_error.py @@ -0,0 +1,2 @@ +# parse_options: {"target-version": "3.15"} +(*(*x or y for x in z) for z in zs); a = diff --git a/crates/ruff_python_parser/resources/invalid/expressions/pep798_soft_keyword_operand.py b/crates/ruff_python_parser/resources/invalid/expressions/pep798_soft_keyword_operand.py new file mode 100644 index 0000000000000..d7b42bab4e0aa --- /dev/null +++ b/crates/ruff_python_parser/resources/invalid/expressions/pep798_soft_keyword_operand.py @@ -0,0 +1,4 @@ +# parse_options: {"target-version": "3.15"} +[**match] +[**case] +[**type] diff --git a/crates/ruff_python_parser/resources/valid/expressions/pep798_unpacking_operands.py b/crates/ruff_python_parser/resources/valid/expressions/pep798_unpacking_operands.py new file mode 100644 index 0000000000000..f122216af6e04 --- /dev/null +++ b/crates/ruff_python_parser/resources/valid/expressions/pep798_unpacking_operands.py @@ -0,0 +1,16 @@ +# parse_options: {"target-version": "3.15"} +{**x if c else y for x in xs} +{**x or y for x in xs} +{**x < y for x in xs} +{**lambda: x for x in xs} +(*x if c else y for x in xs) +(*x or y for x in xs) +(*x < y for x in xs) +(*lambda: x for x in xs) +f(*x if c else y for x in xs) +f(*x if c else y) +(*(*x or y for x in z) for z in zs) +async def unpack(xs): + return {**x if c else y async for x in xs} +async def iterate(xs): + return (*x if c else y async for x in xs) diff --git a/crates/ruff_python_parser/src/error.rs b/crates/ruff_python_parser/src/error.rs index 1c5cdf39ed558..9da76b5d6eb2c 100644 --- a/crates/ruff_python_parser/src/error.rs +++ b/crates/ruff_python_parser/src/error.rs @@ -224,6 +224,18 @@ pub enum ParseErrorType { InvalidYieldExpressionUsage, /// An invalid usage of a starred expression was found. InvalidStarredExpressionUsage, + /// An unpacked conditional expression is missing parentheses. + InvalidConditionalUnpacking { double_starred: bool }, + /// Only the else branch of a conditional expression is unpacked. + InvalidConditionalBranchUnpacking { double_starred: bool }, + /// Dictionary unpacking is used in a list or generator comprehension. + InvalidComprehensionDictUnpacking { generator: bool }, + /// An unpacked expression is used as a dictionary key. + InvalidDictKeyUnpacking { double_starred: bool }, + /// An unpacked expression is used as a dictionary value. + InvalidDictValueUnpacking { double_starred: bool }, + /// Dictionary unpacking is used outside a dictionary or call argument. + InvalidDictUnpacking, /// A star pattern was found outside a sequence pattern. InvalidStarPatternUsage, /// An underscore was used as a binding target in a match pattern. @@ -538,6 +550,49 @@ impl std::fmt::Display for ParseErrorType { ParseErrorType::InvalidLambdaExpressionUsage => { f.write_str("lambda expression cannot be used here") } + ParseErrorType::InvalidConditionalUnpacking { double_starred } => { + let kind = if *double_starred { + "double starred" + } else { + "starred" + }; + write!( + f, + "invalid {kind} expression. Did you forget to wrap the conditional expression in parentheses?" + ) + } + ParseErrorType::InvalidConditionalBranchUnpacking { double_starred } => { + if *double_starred { + f.write_str( + "cannot use dict unpacking on only part of a conditional expression", + ) + } else { + f.write_str("cannot unpack only part of a conditional expression") + } + } + ParseErrorType::InvalidComprehensionDictUnpacking { generator } => { + let kind = if *generator { + "generator expression" + } else { + "list comprehension" + }; + write!(f, "cannot use dict unpacking in {kind}") + } + ParseErrorType::InvalidDictKeyUnpacking { double_starred } + | ParseErrorType::InvalidDictValueUnpacking { double_starred } => { + let kind = if *double_starred { + "dict unpacking" + } else { + "a starred expression" + }; + let position = if matches!(self, ParseErrorType::InvalidDictKeyUnpacking { .. }) { + "key" + } else { + "value" + }; + write!(f, "cannot use {kind} in a dictionary {position}") + } + ParseErrorType::InvalidDictUnpacking => f.write_str("cannot use dict unpacking here"), ParseErrorType::InvalidStarredExpressionUsage => f.write_str("invalid syntax"), ParseErrorType::PositionalAfterKeywordArgument => { f.write_str("positional argument follows keyword argument") diff --git a/crates/ruff_python_parser/src/parser/expression.rs b/crates/ruff_python_parser/src/parser/expression.rs index 595f906b1f913..062b51566591b 100644 --- a/crates/ruff_python_parser/src/parser/expression.rs +++ b/crates/ruff_python_parser/src/parser/expression.rs @@ -687,22 +687,33 @@ impl<'src> Parser<'src> { } } - /// Parses an expression with a minimum precedence of bitwise `or`. - /// - /// This methods actually parses the expression using the `expression` rule - /// of the [Python grammar] and then validates the parsed expression. In a - /// sense, it matches the `bitwise_or` rule of the [Python grammar]. - /// - /// [Python grammar]: https://docs.python.org/3/reference/grammar.html - fn parse_expression_with_bitwise_or_precedence(&mut self) -> ParsedExpr { - let parsed_expr = - self.parse_conditional_expression_without_comma_check(ExpressionContext::default()); - + /// Checks the bitwise-or operand required by sequence and dictionary displays. + /// The caller can defer this check until a dictionary comprehension is ruled out. + fn validate_unpacking_precedence( + &mut self, + parsed_expr: &ParsedExpr, + start: TextSize, + double_starred: bool, + ) { if parsed_expr.is_parenthesized { // Parentheses resets the precedence, so we don't need to validate it. - return parsed_expr; + return; + } + if parsed_expr.expr.is_if_expr() { + self.add_error( + ParseErrorType::InvalidConditionalUnpacking { double_starred }, + TextRange::new(start, parsed_expr.end()), + ); + return; } + self.validate_bitwise_or_precedence(parsed_expr); + } + + fn validate_bitwise_or_precedence(&mut self, parsed_expr: &ParsedExpr) { + if parsed_expr.is_parenthesized { + return; + } let expr_name = match parsed_expr.expr { Expr::Compare(_) => "comparison", Expr::BoolOp(_) @@ -712,15 +723,13 @@ impl<'src> Parser<'src> { }) => "boolean", Expr::If(_) => "conditional", Expr::Lambda(_) => "lambda", - _ => return parsed_expr, + _ => return, }; self.add_error( ParseErrorType::OtherError(format!("{expr_name} expression cannot be used here")), - &parsed_expr, + parsed_expr, ); - - parsed_expr } /// Parses a name. @@ -1002,6 +1011,7 @@ impl<'src> Parser<'src> { let keywords_snapshot = self.keyword_scratch.snapshot(); let mut seen_keyword_argument = false; // foo = 1 let mut seen_keyword_unpacking = false; // **foo + let mut first_argument = true; // The first positional argument after a keyword argument, reported where the arguments // end, and the number of errors before it. let mut positional_after_keyword: Option<(ParseErrorType, usize)> = None; @@ -1009,8 +1019,43 @@ impl<'src> Parser<'src> { let has_trailing_comma = self.parse_comma_separated_list(RecoveryContextKind::Arguments, |parser| { let argument_start = parser.node_start(); + let is_first_argument = first_argument; + first_argument = false; if parser.eat(TokenKind::DoubleStar) { let mut value = parser.parse_conditional_expression_or_higher(); + if is_first_argument + && matches!(context, ArgumentsContext::Call) + && matches!( + parser.current_token_kind(), + TokenKind::Async | TokenKind::For + ) + { + let for_range = parser.current_token_range(); + let for_kind = parser.current_token_kind(); + let errors_start = parser.errors.len(); + let generators = parser.parse_generators(); + let (error, location) = if parser + .has_valid_first_comprehension_clause(&generators, errors_start) + { + ( + ParseErrorType::InvalidComprehensionDictUnpacking { + generator: true, + }, + TextRange::new(argument_start, value.end()), + ) + } else { + ( + ParseErrorType::ExpectedToken { + expected: TokenKind::Comma, + found: for_kind, + }, + for_range, + ) + }; + parser + .errors + .insert(errors_start, crate::ParseError { error, location }); + } if parser.eat(TokenKind::Equal) { // test_err args_keyword_unpacking_assignment @@ -2623,6 +2668,103 @@ impl<'src> Parser<'src> { } } + /// An invalid comprehension only needs one complete target/iterable clause. + /// Later incomplete filters or clauses do not undo that first successful clause. + fn has_valid_first_comprehension_clause( + &self, + generators: &[ast::Comprehension], + errors_start: usize, + ) -> bool { + generators.first().is_some_and(|clause| { + self.errors[errors_start..] + .iter() + .all(|error| error.location.start() > clause.iter.end()) + }) + } + + /// Recovers a dictionary unpack in a sequence, using the same parse for diagnostics. + fn parse_invalid_sequence_dict_unpacking(&mut self, start: TextSize, generator: bool) -> Expr { + let stars = self.current_token_range(); + let errors_start = self.errors.len(); + self.bump(TokenKind::DoubleStar); + let has_operand = self.at_expr(); + let has_bitwise_operand = has_operand + && !matches!( + self.current_token_kind(), + TokenKind::Lambda + | TokenKind::Not + | TokenKind::Yield + | TokenKind::Star + | TokenKind::DoubleStar + ); + let value = self.parse_conditional_expression_or_higher(); + let value_end = value.end(); + let operand_is_valid = self.errors.len() == errors_start; + let is_group = generator && self.at(TokenKind::Rpar); + let clause_errors_start = self.errors.len(); + let (expression, valid_comprehension) = + if matches!(self.current_token_kind(), TokenKind::Async | TokenKind::For) { + if generator { + let expression = + self.parse_generator_expression(value.expr, start, Parenthesized::Yes); + let valid = self.has_valid_first_comprehension_clause( + &expression.generators, + clause_errors_start, + ); + (Expr::Generator(expression), valid) + } else { + let expression = self.parse_list_comprehension_expression(value.expr, start); + let valid = self.has_valid_first_comprehension_clause( + &expression.generators, + clause_errors_start, + ); + (Expr::ListComp(expression), valid) + } + } else if !generator { + ( + Expr::List(self.parse_list_expression(value.expr, start)), + false, + ) + } else if self.at(TokenKind::Comma) { + let tuple = + self.parse_tuple_expression(value.expr, start, Parenthesized::Yes, |p| { + p.parse_named_expression_or_higher(ExpressionContext::starred_bitwise_or()) + }); + (Expr::Tuple(tuple), false) + } else { + self.expect(TokenKind::Rpar); + (value.expr, false) + }; + let invalid_target = self.errors[clause_errors_start..] + .iter() + .any(|error| matches!(error.error, ParseErrorType::InvalidAssignmentTarget { .. })); + let diagnostic = if invalid_target || (!operand_is_valid && has_operand) { + None + } else if operand_is_valid && valid_comprehension { + Some(( + ParseErrorType::InvalidComprehensionDictUnpacking { generator }, + TextRange::new(stars.start(), value_end), + )) + } else if operand_is_valid && has_bitwise_operand { + Some((ParseErrorType::InvalidDictUnpacking, stars)) + } else if operand_is_valid && is_group { + Some(( + ParseErrorType::OtherError("cannot use double starred expression here".to_string()), + stars, + )) + } else if !operand_is_valid || !has_bitwise_operand { + Some((ParseErrorType::ExpectedExpression, stars)) + } else { + None + }; + if let Some((error, location)) = diagnostic { + // The invalid production starts before any clause-recovery errors. + self.errors + .insert(errors_start, crate::ParseError { error, location }); + } + expression + } + /// Parses a list or a list comprehension expression. /// /// # Panics @@ -2655,6 +2797,9 @@ impl<'src> Parser<'src> { } // Parse the first element with a more general rule and limit it later. + if self.at(TokenKind::DoubleStar) { + return self.parse_invalid_sequence_dict_unpacking(start, false); + } let first_element = self.parse_named_expression_or_higher( ExpressionContext::starred_bitwise_or().with_for_excluded(), ); @@ -2751,7 +2896,8 @@ impl<'src> Parser<'src> { if self.eat(TokenKind::DoubleStar) { // Handle dictionary unpacking. Here, the grammar is `'**' bitwise_or` // which requires limiting the expression. - let value = self.parse_expression_with_bitwise_or_precedence(); + let value = + self.parse_conditional_expression_without_comma_check(ExpressionContext::default()); let unpack_range = TextRange::new(after_brace, value.range().end()); if matches!(self.current_token_kind(), TokenKind::Async | TokenKind::For) { @@ -2762,13 +2908,23 @@ impl<'src> Parser<'src> { unpack_range, ); - return Expr::DictComp( - self.parse_dictionary_comprehension_expression(None, value.expr, start), - ); + return Expr::DictComp(self.parse_dictionary_comprehension_expression( + None, + value.expr, + start, + Some(TextRange::at(after_brace, TextSize::new(2))), + )); } + self.validate_unpacking_precedence(&value, after_brace, true); + if self.at(TokenKind::Colon) { - self.add_error(ParseErrorType::InvalidStarredExpressionUsage, unpack_range); + self.add_error( + ParseErrorType::InvalidDictKeyUnpacking { + double_starred: true, + }, + unpack_range, + ); self.bump(TokenKind::Colon); let dict_value = self.parse_conditional_expression_or_higher(); @@ -2778,6 +2934,7 @@ impl<'src> Parser<'src> { Some(value.expr), dict_value.expr, start, + None, )); } @@ -2837,7 +2994,9 @@ impl<'src> Parser<'src> { if !key_or_element.is_parenthesized { match key_or_element.expr { Expr::Starred(_) => self.add_error( - ParseErrorType::InvalidStarredExpressionUsage, + ParseErrorType::InvalidDictKeyUnpacking { + double_starred: false, + }, &key_or_element.expr, ), Expr::Named(_) => self.add_error( @@ -2850,24 +3009,14 @@ impl<'src> Parser<'src> { let colon_range = self.current_token_range(); self.bump(TokenKind::Colon); - let value = if self.at(TokenKind::DoubleStar) { - let unpack_start = self.node_start(); - self.bump(TokenKind::DoubleStar); - let value = self.parse_expression_with_bitwise_or_precedence(); - self.add_error( - ParseErrorType::InvalidStarredExpressionUsage, - TextRange::new(unpack_start, value.range().end()), - ); - value - } else { - self.parse_dictionary_value(colon_range) - }; + let value = self.parse_dictionary_value(colon_range); if matches!(self.current_token_kind(), TokenKind::Async | TokenKind::For) { Expr::DictComp(self.parse_dictionary_comprehension_expression( Some(key_or_element.expr), value.expr, start, + None, )) } else { Expr::Dict(self.parse_dictionary_expression( @@ -2912,32 +3061,20 @@ impl<'src> Parser<'src> { .into(); } - if self.at(TokenKind::DoubleStar) { - let double_star_range = self.current_token_range(); - let checkpoint = self.checkpoint(); - self.bump(TokenKind::DoubleStar); - if self.at_expr() { - let mut parsed_expr = self.parse_conditional_expression_or_higher(); - if self.eat(TokenKind::Rpar) { - // test_err parenthesized_double_starred_expr - // (**x) - self.add_error( - ParseErrorType::OtherError( - "cannot use double starred expression here".to_string(), - ), - double_star_range, - ); - parsed_expr.is_parenthesized = true; - return parsed_expr; - } - } - self.rewind(checkpoint); - } - // Use the more general rule of the three to parse the first element // and limit it later. + if self.at(TokenKind::DoubleStar) { + // test_err parenthesized_double_starred_expr + // (**x) + return ParsedExpr { + expr: self.parse_invalid_sequence_dict_unpacking(start, true), + is_parenthesized: true, + }; + } let mut parsed_expr = self.parse_named_expression_or_higher( - ExpressionContext::yield_or_starred_bitwise_or().with_for_excluded(), + ExpressionContext::yield_or_starred_bitwise_or() + .with_for_excluded() + .with_starred_generator_allowed(), ); match self.current_token_kind() { @@ -3161,17 +3298,27 @@ impl<'src> Parser<'src> { self.parse_comma_separated_list(RecoveryContextKind::DictElements, |parser| { if parser.eat(TokenKind::DoubleStar) { - // Handle dictionary unpacking. Here, the grammar is `'**' bitwise_or` - // which requires limiting the expression. + // Subsequent dictionary spreads retain the ordinary display grammar. + let value = parser + .parse_conditional_expression_without_comma_check(ExpressionContext::default()); + parser.validate_bitwise_or_precedence(&value); items.push(ast::DictItem { key: None, - value: parser.parse_expression_with_bitwise_or_precedence().expr, + value: value.expr, }); } else { // A missing comma is not reported in a key after the first item. - let key = parser - .parse_conditional_expression_without_comma_check(ExpressionContext::default()) - .expr; + let key = parser.parse_conditional_expression_without_comma_check( + ExpressionContext::starred_bitwise_or(), + ); + if key.is_unparenthesized_starred_expr() { + parser.add_error( + ParseErrorType::InvalidDictKeyUnpacking { + double_starred: false, + }, + &key, + ); + } if !parser.at(TokenKind::Colon) { // test_err dict_missing_colon_after_key // {a: 1, b} @@ -3189,7 +3336,7 @@ impl<'src> Parser<'src> { parser.expect(TokenKind::Colon); items.push(ast::DictItem { - key: Some(key), + key: Some(key.expr), value: parser.parse_dictionary_value(colon_range).expr, }); } @@ -3219,18 +3366,23 @@ impl<'src> Parser<'src> { ), colon_range, ); - } else if self.at(TokenKind::Star) { + } else if matches!( + self.current_token_kind(), + TokenKind::Star | TokenKind::DoubleStar + ) { // test_err dict_starred_value // {a: *b} // {a: 1, b: *c} - let star_start = self.node_start(); - self.bump(TokenKind::Star); - let value = self.parse_expression_with_bitwise_or_precedence(); + let start = self.node_start(); + let double_starred = self.at(TokenKind::DoubleStar); + self.bump_any(); + let value = self.parse_binary_expression_or_higher( + OperatorPrecedence::ComparisonsMembershipIdentity, + ExpressionContext::default(), + ); self.add_error( - ParseErrorType::OtherError( - "cannot use a starred expression in a dictionary value".to_string(), - ), - TextRange::new(star_start, value.end()), + ParseErrorType::InvalidDictValueUnpacking { double_starred }, + TextRange::new(start, value.end()), ); return value; } @@ -3395,9 +3547,15 @@ impl<'src> Parser<'src> { key: Option, value: Expr, start: TextSize, + unpacking: Option, ) -> ast::ExprDictComp { let generators = self.parse_generators(); + if self.at(TokenKind::Colon) + && let Some(stars) = unpacking + { + self.add_error(ParseErrorType::InvalidDictUnpacking, stars); + } self.expect(TokenKind::Rbrace); ast::ExprDictComp { @@ -3475,6 +3633,7 @@ impl<'src> Parser<'src> { ); } + let errors_start = self.errors.len(); let parsed_expr = match context.starred_expression_precedence() { StarredExpressionPrecedence::Conditional => self .parse_conditional_expression_or_higher_impl( @@ -3485,9 +3644,32 @@ impl<'src> Parser<'src> { context.disallow_starred_expressions(), ), StarredExpressionPrecedence::BitwiseOr => { - self.parse_expression_with_bitwise_or_precedence() + let value = self + .parse_conditional_expression_without_comma_check(ExpressionContext::default()); + let is_generator = self.errors.len() == errors_start + && context.is_starred_generator_allowed() + && matches!(self.current_token_kind(), TokenKind::Async | TokenKind::For); + if !is_generator { + self.validate_unpacking_precedence(&value, start, false); + } + value } }; + // CPython retries invalid_starred_expression_unpacking only after a + // failed parse. Its conditional rule also applies to call arguments. + if errors_start == 0 + && self.errors.is_empty() + && self.deferred_starred_unpacking_error.is_none() + && !parsed_expr.is_parenthesized + && parsed_expr.expr.is_if_expr() + { + self.deferred_starred_unpacking_error = Some(crate::ParseError { + error: ParseErrorType::InvalidConditionalUnpacking { + double_starred: false, + }, + location: TextRange::new(start, parsed_expr.end()), + }); + } ast::ExprStarred { value: Box::new(parsed_expr.expr), @@ -3730,13 +3912,25 @@ impl<'src> Parser<'src> { let test = self.parse_simple_expression(ExpressionContext::default()); - if self.expect(TokenKind::Else) - && (!self.at_expr() - || matches!( - self.current_token_kind(), - TokenKind::Star | TokenKind::DoubleStar | TokenKind::Yield - )) + let has_else = self.expect(TokenKind::Else); + if has_else + && matches!( + self.current_token_kind(), + TokenKind::Star | TokenKind::DoubleStar + ) { + self.add_error( + ParseErrorType::InvalidConditionalBranchUnpacking { + double_starred: self.at(TokenKind::DoubleStar), + }, + self.current_token_range(), + ); + // Recover the operand rather than treating `**` as a binary power + // with an empty left-hand side outside its parent range. + if self.at(TokenKind::DoubleStar) { + self.bump(TokenKind::DoubleStar); + } + } else if has_else && (!self.at_expr() || self.at(TokenKind::Yield)) { // test_err if_expr_statement_after_else // x = 1 if 1 else pass // x = 1 if 1 else @@ -3975,6 +4169,10 @@ bitflags! { /// This flag is set when the `for` keyword, or `async` starting `async for`, should be /// excluded from an expression. const EXCLUDE_FOR = 1 << 4; + + /// The first element after `(` may become a generator expression. Its starred + /// operand can use full expression precedence if a comprehension clause follows. + const ALLOW_STARRED_GENERATOR = 1 << 5; } } @@ -4047,6 +4245,16 @@ impl ExpressionContext { ExpressionContext(self.0 | ExpressionContextFlags::EXCLUDE_FOR) } + /// Allow the first parenthesized starred element to become a generator element. + fn with_starred_generator_allowed(self) -> Self { + ExpressionContext(self.0 | ExpressionContextFlags::ALLOW_STARRED_GENERATOR) + } + + const fn is_starred_generator_allowed(self) -> bool { + self.0 + .contains(ExpressionContextFlags::ALLOW_STARRED_GENERATOR) + } + /// Returns `true` if the `in` keyword should be excluded from a comparison expression. const fn is_in_excluded(self) -> bool { self.0.contains(ExpressionContextFlags::EXCLUDE_IN) diff --git a/crates/ruff_python_parser/src/parser/mod.rs b/crates/ruff_python_parser/src/parser/mod.rs index fdaab748df0ff..ce2c4371c25d6 100644 --- a/crates/ruff_python_parser/src/parser/mod.rs +++ b/crates/ruff_python_parser/src/parser/mod.rs @@ -82,6 +82,10 @@ pub(crate) struct Parser<'src> { /// Stores all the syntax errors found during the parsing. errors: Vec, + /// The first conditional unpacking error from the invalid-grammar fallback, + /// used only when the overall parse fails. + deferred_starred_unpacking_error: Option, + /// Stores non-fatal syntax errors found during parsing, such as version-related errors. unsupported_syntax_errors: Vec, @@ -163,6 +167,7 @@ impl<'src> Parser<'src> { options, source, errors: Vec::new(), + deferred_starred_unpacking_error: None, unsupported_syntax_errors: Vec::new(), tokens, name_interner: NameInterner::default(), @@ -283,6 +288,14 @@ impl<'src> Parser<'src> { ); // TODO consider re-integrating lexical error handling into the parser? let mut parse_errors = self.errors; + if !parse_errors.is_empty() + && let Some(error) = self.deferred_starred_unpacking_error + { + // CPython retries its invalid grammar rules only after a parse failure. + // Preserve encounter order, including nested operands whose ranges begin + // after the outer expression's later error. Semantic errors occur later. + parse_errors.insert(0, error); + } let (tokens, lex_errors) = self.tokens.finish(); // Fast path for when there are no lex errors. @@ -1193,6 +1206,7 @@ impl<'src> Parser<'src> { ParserCheckpoint { tokens: self.tokens.checkpoint(), errors_position: self.errors.len(), + had_deferred_starred_unpacking_error: self.deferred_starred_unpacking_error.is_some(), unsupported_syntax_errors_position: self.unsupported_syntax_errors.len(), current_token_id: self.current_token_id, prev_token_end: self.prev_token_end, @@ -1205,6 +1219,7 @@ impl<'src> Parser<'src> { let ParserCheckpoint { tokens, errors_position, + had_deferred_starred_unpacking_error, unsupported_syntax_errors_position, current_token_id, prev_token_end, @@ -1213,6 +1228,9 @@ impl<'src> Parser<'src> { self.tokens.rewind(tokens); self.errors.truncate(errors_position); + if !had_deferred_starred_unpacking_error { + self.deferred_starred_unpacking_error = None; + } self.unsupported_syntax_errors .truncate(unsupported_syntax_errors_position); self.current_token_id = current_token_id; @@ -1244,6 +1262,7 @@ impl IpyEscapeContext { struct ParserCheckpoint { tokens: TokenSourceCheckpoint, errors_position: usize, + had_deferred_starred_unpacking_error: bool, unsupported_syntax_errors_position: usize, current_token_id: TokenId, prev_token_end: TextSize, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__dict__double_star_comprehension.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__dict__double_star_comprehension.py.snap index f97bc98dee241..be2ebf3b15fae 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__dict__double_star_comprehension.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__dict__double_star_comprehension.py.snap @@ -96,7 +96,7 @@ Module( 2 | # it's actually a comprehension. 3 | 4 | {**x: y for x, y in data} - | ^ Syntax Error: invalid syntax + | ^^^ Syntax Error: cannot use dict unpacking in a dictionary key 5 | 6 | # TODO(dhruvmanila): This test case fails because there's no way to represent `**y` | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__dict__recover.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__dict__recover.py.snap index 796fb7a196f4f..9dbbe3646d288 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__dict__recover.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__dict__recover.py.snap @@ -554,7 +554,7 @@ Module( | 22 | # Star expression not allowed here 23 | {*x: y, z: a, *b: c} - | ^ Syntax Error: invalid syntax + | ^^ Syntax Error: cannot use a starred expression in a dictionary key 24 | {x: *y, z: *a} | @@ -562,7 +562,7 @@ Module( | 22 | # Star expression not allowed here 23 | {*x: y, z: a, *b: c} - | ^ Syntax Error: invalid syntax + | ^^ Syntax Error: cannot use a starred expression in a dictionary key 24 | {x: *y, z: *a} | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__if__recover.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__if__recover.py.snap index 9121efdb7195d..722a421d11a2f 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__if__recover.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__if__recover.py.snap @@ -388,7 +388,7 @@ Module( | 7 | # Invalid orelse expression 8 | x if expr else *orelse - | ^ Syntax Error: expected expression after 'else', but statement is given + | ^ Syntax Error: cannot unpack only part of a conditional expression 9 | x if expr else yield y 10 | x if expr else yield from y | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__list__comprehension.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__list__comprehension.py.snap index 91fe18e3f438a..f6fbe8d17b047 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__list__comprehension.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__list__comprehension.py.snap @@ -488,27 +488,12 @@ Module( ExprListComp { node_index: NodeIndex(None), range: 193..216, - elt: BinOp( - ExprBinOp { + elt: Name( + ExprName { node_index: NodeIndex(None), - range: 194..197, - left: Name( - ExprName { - node_index: NodeIndex(None), - range: 194..194, - id: Name(""), - ctx: Invalid, - }, - ), - op: Pow, - right: Name( - ExprName { - node_index: NodeIndex(None), - range: 196..197, - id: Name("x"), - ctx: Load, - }, - ), + range: 196..197, + id: Name("x"), + ctx: Load, }, ), generators: [ @@ -1224,7 +1209,7 @@ Module( 10 | [x for x in yield from y] 11 | [x for x in lambda y: y] 12 | [**x for x in [{1: 2}]] - | ^^ Syntax Error: invalid syntax + | ^^^ Syntax Error: cannot use dict unpacking in list comprehension 13 | [*x, for x in y] 14 | [*x, *y for x in y] | @@ -1352,7 +1337,7 @@ Module( 19 | [x for x in data if yield from y] 20 | [x for x in data if lambda y: y] 21 | [*x if x else y for x in z] - | ^^^^^^^^^^^^^ Syntax Error: conditional expression cannot be used here + | ^^^^^^^^^^^^^^ Syntax Error: invalid starred expression. Did you forget to wrap the conditional expression in parentheses? 22 | [x if x else *y for x in z] | @@ -1361,7 +1346,7 @@ Module( 20 | [x for x in data if lambda y: y] 21 | [*x if x else y for x in z] 22 | [x if x else *y for x in z] - | ^ Syntax Error: expected expression after 'else', but statement is given + | ^ Syntax Error: cannot unpack only part of a conditional expression ## Unsupported Syntax Errors diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__list__star_expression_precedence.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__list__star_expression_precedence.py.snap index c77c0e1f75344..67acf0aa7364a 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__list__star_expression_precedence.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__list__star_expression_precedence.py.snap @@ -517,7 +517,7 @@ Module( 6 | [*x and y, z] 7 | [*x or y, z] 8 | [*x if True else y, z] - | ^^^^^^^^^^^^^^^^ Syntax Error: conditional expression cannot be used here + | ^^^^^^^^^^^^^^^^^ Syntax Error: invalid starred expression. Did you forget to wrap the conditional expression in parentheses? 9 | [*lambda x: x, z] 10 | [*x := 2, z] | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__parenthesized__tuple_starred_expr.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__parenthesized__tuple_starred_expr.py.snap index 1b60f04ed2209..9dd5376bf8399 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__parenthesized__tuple_starred_expr.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__parenthesized__tuple_starred_expr.py.snap @@ -1403,7 +1403,7 @@ Module( 6 | (*x and y, z, *x and y) 7 | (*x or y, z, *x or y) 8 | (*x if True else y, z, *x if True else y) - | ^^^^^^^^^^^^^^^^ Syntax Error: conditional expression cannot be used here + | ^^^^^^^^^^^^^^^^^ Syntax Error: invalid starred expression. Did you forget to wrap the conditional expression in parentheses? 9 | (*lambda x: x, z, *lambda x: x) 10 | (*x := 2, z, *x := 2) | @@ -1413,7 +1413,7 @@ Module( 6 | (*x and y, z, *x and y) 7 | (*x or y, z, *x or y) 8 | (*x if True else y, z, *x if True else y) - | ^^^^^^^^^^^^^^^^ Syntax Error: conditional expression cannot be used here + | ^^^^^^^^^^^^^^^^^ Syntax Error: invalid starred expression. Did you forget to wrap the conditional expression in parentheses? 9 | (*lambda x: x, z, *lambda x: x) 10 | (*x := 2, z, *x := 2) | @@ -1533,7 +1533,7 @@ Module( 16 | *x and y, z, *x and y 17 | *x or y, z, *x or y 18 | *x if True else y, z, *x if True else y - | ^^^^^^^^^^^^^^^^ Syntax Error: conditional expression cannot be used here + | ^^^^^^^^^^^^^^^^^ Syntax Error: invalid starred expression. Did you forget to wrap the conditional expression in parentheses? 19 | *lambda x: x, z, *lambda x: x 20 | *x := 2, z, *x := 2 | @@ -1543,7 +1543,7 @@ Module( 16 | *x and y, z, *x and y 17 | *x or y, z, *x or y 18 | *x if True else y, z, *x if True else y - | ^^^^^^^^^^^^^^^^ Syntax Error: conditional expression cannot be used here + | ^^^^^^^^^^^^^^^^^ Syntax Error: invalid starred expression. Did you forget to wrap the conditional expression in parentheses? 19 | *lambda x: x, z, *lambda x: x 20 | *x := 2, z, *x := 2 | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__pep798_boolean_then_error.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__pep798_boolean_then_error.py.snap new file mode 100644 index 0000000000000..bb35a1a7e4357 --- /dev/null +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__pep798_boolean_then_error.py.snap @@ -0,0 +1,122 @@ +--- +source: crates/ruff_python_parser/tests/fixtures.rs +input_file: crates/ruff_python_parser/resources/invalid/expressions/pep798_boolean_then_error.py +--- +## AST + +``` +Module( + ModModule { + node_index: NodeIndex(None), + range: 0..70, + body: [ + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 44..64, + value: Generator( + ExprGenerator { + node_index: NodeIndex(None), + range: 44..64, + elt: Starred( + ExprStarred { + node_index: NodeIndex(None), + range: 45..52, + value: BoolOp( + ExprBoolOp { + node_index: NodeIndex(None), + range: 46..52, + op: Or, + values: [ + Name( + ExprName { + node_index: NodeIndex(None), + range: 46..47, + id: Name("x"), + ctx: Load, + }, + ), + Name( + ExprName { + node_index: NodeIndex(None), + range: 51..52, + id: Name("y"), + ctx: Load, + }, + ), + ], + runtime_values: None, + }, + ), + ctx: Load, + }, + ), + generators: [ + Comprehension { + range: 53..63, + node_index: NodeIndex(None), + target: Name( + ExprName { + node_index: NodeIndex(None), + range: 57..58, + id: Name("x"), + ctx: Store, + }, + ), + iter: Name( + ExprName { + node_index: NodeIndex(None), + range: 62..63, + id: Name("z"), + ctx: Load, + }, + ), + ifs: [], + is_async: false, + runtime_ifs: None, + runtime_is_async: None, + }, + ], + parenthesized: true, + }, + ), + }, + ), + Assign( + StmtAssign { + node_index: NodeIndex(None), + range: 66..69, + targets: [ + Name( + ExprName { + node_index: NodeIndex(None), + range: 66..67, + id: Name("a"), + ctx: Store, + }, + ), + ], + value: Name( + ExprName { + node_index: NodeIndex(None), + range: 69..69, + id: Name(""), + ctx: Invalid, + }, + ), + runtime_targets: None, + runtime_type_comment: None, + runtime_type_comment_bytes: None, + }, + ), + ], + runtime_body: None, + }, +) +``` +## Errors + + | +1 | # parse_options: {"target-version": "3.15"} +2 | (*x or y for x in z); a = + | ^ Syntax Error: invalid syntax diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__pep798_call_conditional_then_error.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__pep798_call_conditional_then_error.py.snap new file mode 100644 index 0000000000000..43339b5bb9121 --- /dev/null +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__pep798_call_conditional_then_error.py.snap @@ -0,0 +1,155 @@ +--- +source: crates/ruff_python_parser/tests/fixtures.rs +input_file: crates/ruff_python_parser/resources/invalid/expressions/pep798_call_conditional_then_error.py +--- +## AST + +``` +Module( + ModModule { + node_index: NodeIndex(None), + range: 0..78, + body: [ + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 44..72, + value: Call( + ExprCall { + node_index: NodeIndex(None), + range: 44..72, + func: Name( + ExprName { + node_index: NodeIndex(None), + range: 44..45, + id: Name("f"), + ctx: Load, + }, + ), + arguments: Arguments { + range: 45..72, + node_index: NodeIndex(None), + args: [ + Generator( + ExprGenerator { + node_index: NodeIndex(None), + range: 46..71, + elt: Starred( + ExprStarred { + node_index: NodeIndex(None), + range: 46..60, + value: If( + ExprIf { + node_index: NodeIndex(None), + range: 47..60, + test: Name( + ExprName { + node_index: NodeIndex(None), + range: 52..53, + id: Name("c"), + ctx: Load, + }, + ), + body: Name( + ExprName { + node_index: NodeIndex(None), + range: 47..48, + id: Name("x"), + ctx: Load, + }, + ), + orelse: Name( + ExprName { + node_index: NodeIndex(None), + range: 59..60, + id: Name("y"), + ctx: Load, + }, + ), + }, + ), + ctx: Load, + }, + ), + generators: [ + Comprehension { + range: 61..71, + node_index: NodeIndex(None), + target: Name( + ExprName { + node_index: NodeIndex(None), + range: 65..66, + id: Name("x"), + ctx: Store, + }, + ), + iter: Name( + ExprName { + node_index: NodeIndex(None), + range: 70..71, + id: Name("z"), + ctx: Load, + }, + ), + ifs: [], + is_async: false, + runtime_ifs: None, + runtime_is_async: None, + }, + ], + parenthesized: false, + }, + ), + ], + keywords: [], + runtime_args: None, + runtime_bases: None, + }, + }, + ), + }, + ), + Assign( + StmtAssign { + node_index: NodeIndex(None), + range: 74..77, + targets: [ + Name( + ExprName { + node_index: NodeIndex(None), + range: 74..75, + id: Name("a"), + ctx: Store, + }, + ), + ], + value: Name( + ExprName { + node_index: NodeIndex(None), + range: 77..77, + id: Name(""), + ctx: Invalid, + }, + ), + runtime_targets: None, + runtime_type_comment: None, + runtime_type_comment_bytes: None, + }, + ), + ], + runtime_body: None, + }, +) +``` +## Errors + + | +1 | # parse_options: {"target-version": "3.15"} +2 | f(*x if c else y for x in z); a = + | ^^^^^^^^^^^^^^ Syntax Error: invalid starred expression. Did you forget to wrap the conditional expression in parentheses? + + + | +1 | # parse_options: {"target-version": "3.15"} +2 | f(*x if c else y for x in z); a = + | ^ Syntax Error: invalid syntax diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__pep798_comparison_then_error.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__pep798_comparison_then_error.py.snap new file mode 100644 index 0000000000000..1e96cd4e16378 --- /dev/null +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__pep798_comparison_then_error.py.snap @@ -0,0 +1,124 @@ +--- +source: crates/ruff_python_parser/tests/fixtures.rs +input_file: crates/ruff_python_parser/resources/invalid/expressions/pep798_comparison_then_error.py +--- +## AST + +``` +Module( + ModModule { + node_index: NodeIndex(None), + range: 0..69, + body: [ + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 44..63, + value: Generator( + ExprGenerator { + node_index: NodeIndex(None), + range: 44..63, + elt: Starred( + ExprStarred { + node_index: NodeIndex(None), + range: 45..51, + value: Compare( + ExprCompare { + node_index: NodeIndex(None), + range: 46..51, + ops: [ + Lt, + ], + operands: [ + Name( + ExprName { + node_index: NodeIndex(None), + range: 46..47, + id: Name("x"), + ctx: Load, + }, + ), + Name( + ExprName { + node_index: NodeIndex(None), + range: 50..51, + id: Name("y"), + ctx: Load, + }, + ), + ], + runtime_comparators: None, + }, + ), + ctx: Load, + }, + ), + generators: [ + Comprehension { + range: 52..62, + node_index: NodeIndex(None), + target: Name( + ExprName { + node_index: NodeIndex(None), + range: 56..57, + id: Name("x"), + ctx: Store, + }, + ), + iter: Name( + ExprName { + node_index: NodeIndex(None), + range: 61..62, + id: Name("z"), + ctx: Load, + }, + ), + ifs: [], + is_async: false, + runtime_ifs: None, + runtime_is_async: None, + }, + ], + parenthesized: true, + }, + ), + }, + ), + Assign( + StmtAssign { + node_index: NodeIndex(None), + range: 65..68, + targets: [ + Name( + ExprName { + node_index: NodeIndex(None), + range: 65..66, + id: Name("a"), + ctx: Store, + }, + ), + ], + value: Name( + ExprName { + node_index: NodeIndex(None), + range: 68..68, + id: Name(""), + ctx: Invalid, + }, + ), + runtime_targets: None, + runtime_type_comment: None, + runtime_type_comment_bytes: None, + }, + ), + ], + runtime_body: None, + }, +) +``` +## Errors + + | +1 | # parse_options: {"target-version": "3.15"} +2 | (*x < y for x in z); a = + | ^ Syntax Error: invalid syntax diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__pep798_conditional_display.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__pep798_conditional_display.py.snap new file mode 100644 index 0000000000000..98831a11fecf2 --- /dev/null +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__pep798_conditional_display.py.snap @@ -0,0 +1,251 @@ +--- +source: crates/ruff_python_parser/tests/fixtures.rs +input_file: crates/ruff_python_parser/resources/invalid/expressions/pep798_conditional_display.py +--- +## AST + +``` +Module( + ModModule { + node_index: NodeIndex(None), + range: 0..114, + body: [ + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 44..60, + value: List( + ExprList { + node_index: NodeIndex(None), + range: 44..60, + elts: [ + Starred( + ExprStarred { + node_index: NodeIndex(None), + range: 45..59, + value: If( + ExprIf { + node_index: NodeIndex(None), + range: 46..59, + test: Name( + ExprName { + node_index: NodeIndex(None), + range: 51..52, + id: Name("x"), + ctx: Load, + }, + ), + body: Name( + ExprName { + node_index: NodeIndex(None), + range: 46..47, + id: Name("x"), + ctx: Load, + }, + ), + orelse: Name( + ExprName { + node_index: NodeIndex(None), + range: 58..59, + id: Name("y"), + ctx: Load, + }, + ), + }, + ), + ctx: Load, + }, + ), + ], + ctx: Load, + runtime_elts: None, + }, + ), + }, + ), + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 61..78, + value: Dict( + ExprDict { + node_index: NodeIndex(None), + range: 61..78, + items: [ + DictItem { + key: None, + value: If( + ExprIf { + node_index: NodeIndex(None), + range: 64..77, + test: Name( + ExprName { + node_index: NodeIndex(None), + range: 69..70, + id: Name("x"), + ctx: Load, + }, + ), + body: Name( + ExprName { + node_index: NodeIndex(None), + range: 64..65, + id: Name("x"), + ctx: Load, + }, + ), + orelse: Name( + ExprName { + node_index: NodeIndex(None), + range: 76..77, + id: Name("y"), + ctx: Load, + }, + ), + }, + ), + }, + ], + runtime_values: None, + }, + ), + }, + ), + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 79..95, + value: List( + ExprList { + node_index: NodeIndex(None), + range: 79..95, + elts: [ + If( + ExprIf { + node_index: NodeIndex(None), + range: 80..94, + test: Name( + ExprName { + node_index: NodeIndex(None), + range: 85..86, + id: Name("x"), + ctx: Load, + }, + ), + body: Name( + ExprName { + node_index: NodeIndex(None), + range: 80..81, + id: Name("x"), + ctx: Load, + }, + ), + orelse: Starred( + ExprStarred { + node_index: NodeIndex(None), + range: 92..94, + value: Name( + ExprName { + node_index: NodeIndex(None), + range: 93..94, + id: Name("y"), + ctx: Load, + }, + ), + ctx: Load, + }, + ), + }, + ), + ], + ctx: Load, + runtime_elts: None, + }, + ), + }, + ), + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 96..113, + value: Set( + ExprSet { + node_index: NodeIndex(None), + range: 96..113, + elts: [ + If( + ExprIf { + node_index: NodeIndex(None), + range: 97..112, + test: Name( + ExprName { + node_index: NodeIndex(None), + range: 102..103, + id: Name("x"), + ctx: Load, + }, + ), + body: Name( + ExprName { + node_index: NodeIndex(None), + range: 97..98, + id: Name("x"), + ctx: Load, + }, + ), + orelse: Name( + ExprName { + node_index: NodeIndex(None), + range: 111..112, + id: Name("y"), + ctx: Load, + }, + ), + }, + ), + ], + runtime_elts: None, + }, + ), + }, + ), + ], + runtime_body: None, + }, +) +``` +## Errors + + | +1 | # parse_options: {"target-version": "3.15"} +2 | [*x if x else y] + | ^^^^^^^^^^^^^^ Syntax Error: invalid starred expression. Did you forget to wrap the conditional expression in parentheses? +3 | {**x if x else y} +4 | [x if x else *y] + | + + + | +1 | # parse_options: {"target-version": "3.15"} +2 | [*x if x else y] +3 | {**x if x else y} + | ^^^^^^^^^^^^^^^ Syntax Error: invalid double starred expression. Did you forget to wrap the conditional expression in parentheses? +4 | [x if x else *y] +5 | {x if x else **y} + | + + + | +2 | [*x if x else y] +3 | {**x if x else y} +4 | [x if x else *y] + | ^ Syntax Error: cannot unpack only part of a conditional expression +5 | {x if x else **y} + | + + + | +3 | {**x if x else y} +4 | [x if x else *y] +5 | {x if x else **y} + | ^^ Syntax Error: cannot use dict unpacking on only part of a conditional expression diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__pep798_conditional_then_error.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__pep798_conditional_then_error.py.snap new file mode 100644 index 0000000000000..7430b5f7a92b2 --- /dev/null +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__pep798_conditional_then_error.py.snap @@ -0,0 +1,132 @@ +--- +source: crates/ruff_python_parser/tests/fixtures.rs +input_file: crates/ruff_python_parser/resources/invalid/expressions/pep798_conditional_then_error.py +--- +## AST + +``` +Module( + ModModule { + node_index: NodeIndex(None), + range: 0..77, + body: [ + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 44..71, + value: Generator( + ExprGenerator { + node_index: NodeIndex(None), + range: 44..71, + elt: Starred( + ExprStarred { + node_index: NodeIndex(None), + range: 45..59, + value: If( + ExprIf { + node_index: NodeIndex(None), + range: 46..59, + test: Name( + ExprName { + node_index: NodeIndex(None), + range: 51..52, + id: Name("c"), + ctx: Load, + }, + ), + body: Name( + ExprName { + node_index: NodeIndex(None), + range: 46..47, + id: Name("x"), + ctx: Load, + }, + ), + orelse: Name( + ExprName { + node_index: NodeIndex(None), + range: 58..59, + id: Name("y"), + ctx: Load, + }, + ), + }, + ), + ctx: Load, + }, + ), + generators: [ + Comprehension { + range: 60..70, + node_index: NodeIndex(None), + target: Name( + ExprName { + node_index: NodeIndex(None), + range: 64..65, + id: Name("x"), + ctx: Store, + }, + ), + iter: Name( + ExprName { + node_index: NodeIndex(None), + range: 69..70, + id: Name("z"), + ctx: Load, + }, + ), + ifs: [], + is_async: false, + runtime_ifs: None, + runtime_is_async: None, + }, + ], + parenthesized: true, + }, + ), + }, + ), + Assign( + StmtAssign { + node_index: NodeIndex(None), + range: 73..76, + targets: [ + Name( + ExprName { + node_index: NodeIndex(None), + range: 73..74, + id: Name("a"), + ctx: Store, + }, + ), + ], + value: Name( + ExprName { + node_index: NodeIndex(None), + range: 76..76, + id: Name(""), + ctx: Invalid, + }, + ), + runtime_targets: None, + runtime_type_comment: None, + runtime_type_comment_bytes: None, + }, + ), + ], + runtime_body: None, + }, +) +``` +## Errors + + | +1 | # parse_options: {"target-version": "3.15"} +2 | (*x if c else y for x in z); a = + | ^^^^^^^^^^^^^^ Syntax Error: invalid starred expression. Did you forget to wrap the conditional expression in parentheses? + + + | +1 | # parse_options: {"target-version": "3.15"} +2 | (*x if c else y for x in z); a = + | ^ Syntax Error: invalid syntax diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__pep798_dict_positions.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__pep798_dict_positions.py.snap new file mode 100644 index 0000000000000..fe24b94791923 --- /dev/null +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__pep798_dict_positions.py.snap @@ -0,0 +1,448 @@ +--- +source: crates/ruff_python_parser/tests/fixtures.rs +input_file: crates/ruff_python_parser/resources/invalid/expressions/pep798_dict_positions.py +--- +## AST + +``` +Module( + ModModule { + node_index: NodeIndex(None), + range: 0..181, + body: [ + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 44..61, + value: ListComp( + ExprListComp { + node_index: NodeIndex(None), + range: 44..61, + elt: Name( + ExprName { + node_index: NodeIndex(None), + range: 47..48, + id: Name("x"), + ctx: Load, + }, + ), + generators: [ + Comprehension { + range: 49..60, + node_index: NodeIndex(None), + target: Name( + ExprName { + node_index: NodeIndex(None), + range: 53..54, + id: Name("x"), + ctx: Store, + }, + ), + iter: Name( + ExprName { + node_index: NodeIndex(None), + range: 58..60, + id: Name("xs"), + ctx: Load, + }, + ), + ifs: [], + is_async: false, + runtime_ifs: None, + runtime_is_async: None, + }, + ], + }, + ), + }, + ), + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 62..79, + value: Generator( + ExprGenerator { + node_index: NodeIndex(None), + range: 62..79, + elt: Name( + ExprName { + node_index: NodeIndex(None), + range: 65..66, + id: Name("x"), + ctx: Load, + }, + ), + generators: [ + Comprehension { + range: 67..78, + node_index: NodeIndex(None), + target: Name( + ExprName { + node_index: NodeIndex(None), + range: 71..72, + id: Name("x"), + ctx: Store, + }, + ), + iter: Name( + ExprName { + node_index: NodeIndex(None), + range: 76..78, + id: Name("xs"), + ctx: Load, + }, + ), + ifs: [], + is_async: false, + runtime_ifs: None, + runtime_is_async: None, + }, + ], + parenthesized: true, + }, + ), + }, + ), + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 80..98, + value: Call( + ExprCall { + node_index: NodeIndex(None), + range: 80..98, + func: Name( + ExprName { + node_index: NodeIndex(None), + range: 80..81, + id: Name("f"), + ctx: Load, + }, + ), + arguments: Arguments { + range: 81..98, + node_index: NodeIndex(None), + args: [], + keywords: [ + Keyword { + range: 82..97, + node_index: NodeIndex(None), + arg: None, + value: Name( + ExprName { + node_index: NodeIndex(None), + range: 84..85, + id: Name("x"), + ctx: Load, + }, + ), + }, + ], + runtime_args: None, + runtime_bases: None, + }, + }, + ), + }, + ), + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 99..118, + value: DictComp( + ExprDictComp { + node_index: NodeIndex(None), + range: 99..118, + key: Some( + Starred( + ExprStarred { + node_index: NodeIndex(None), + range: 100..102, + value: Name( + ExprName { + node_index: NodeIndex(None), + range: 101..102, + id: Name("x"), + ctx: Load, + }, + ), + ctx: Load, + }, + ), + ), + value: Name( + ExprName { + node_index: NodeIndex(None), + range: 104..105, + id: Name("y"), + ctx: Load, + }, + ), + generators: [ + Comprehension { + range: 106..117, + node_index: NodeIndex(None), + target: Name( + ExprName { + node_index: NodeIndex(None), + range: 110..111, + id: Name("x"), + ctx: Store, + }, + ), + iter: Name( + ExprName { + node_index: NodeIndex(None), + range: 115..117, + id: Name("xs"), + ctx: Load, + }, + ), + ifs: [], + is_async: false, + runtime_ifs: None, + runtime_is_async: None, + }, + ], + }, + ), + }, + ), + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 119..139, + value: DictComp( + ExprDictComp { + node_index: NodeIndex(None), + range: 119..139, + key: Some( + Name( + ExprName { + node_index: NodeIndex(None), + range: 122..123, + id: Name("x"), + ctx: Load, + }, + ), + ), + value: Name( + ExprName { + node_index: NodeIndex(None), + range: 125..126, + id: Name("y"), + ctx: Load, + }, + ), + generators: [ + Comprehension { + range: 127..138, + node_index: NodeIndex(None), + target: Name( + ExprName { + node_index: NodeIndex(None), + range: 131..132, + id: Name("x"), + ctx: Store, + }, + ), + iter: Name( + ExprName { + node_index: NodeIndex(None), + range: 136..138, + id: Name("xs"), + ctx: Load, + }, + ), + ifs: [], + is_async: false, + runtime_ifs: None, + runtime_is_async: None, + }, + ], + }, + ), + }, + ), + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 140..159, + value: DictComp( + ExprDictComp { + node_index: NodeIndex(None), + range: 140..159, + key: Some( + Name( + ExprName { + node_index: NodeIndex(None), + range: 141..142, + id: Name("x"), + ctx: Load, + }, + ), + ), + value: Name( + ExprName { + node_index: NodeIndex(None), + range: 145..146, + id: Name("y"), + ctx: Load, + }, + ), + generators: [ + Comprehension { + range: 147..158, + node_index: NodeIndex(None), + target: Name( + ExprName { + node_index: NodeIndex(None), + range: 151..152, + id: Name("x"), + ctx: Store, + }, + ), + iter: Name( + ExprName { + node_index: NodeIndex(None), + range: 156..158, + id: Name("xs"), + ctx: Load, + }, + ), + ifs: [], + is_async: false, + runtime_ifs: None, + runtime_is_async: None, + }, + ], + }, + ), + }, + ), + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 160..180, + value: DictComp( + ExprDictComp { + node_index: NodeIndex(None), + range: 160..180, + key: Some( + Name( + ExprName { + node_index: NodeIndex(None), + range: 161..162, + id: Name("x"), + ctx: Load, + }, + ), + ), + value: Name( + ExprName { + node_index: NodeIndex(None), + range: 166..167, + id: Name("y"), + ctx: Load, + }, + ), + generators: [ + Comprehension { + range: 168..179, + node_index: NodeIndex(None), + target: Name( + ExprName { + node_index: NodeIndex(None), + range: 172..173, + id: Name("x"), + ctx: Store, + }, + ), + iter: Name( + ExprName { + node_index: NodeIndex(None), + range: 177..179, + id: Name("xs"), + ctx: Load, + }, + ), + ifs: [], + is_async: false, + runtime_ifs: None, + runtime_is_async: None, + }, + ], + }, + ), + }, + ), + ], + runtime_body: None, + }, +) +``` +## Errors + + | +1 | # parse_options: {"target-version": "3.15"} +2 | [**x for x in xs] + | ^^^ Syntax Error: cannot use dict unpacking in list comprehension +3 | (**x for x in xs) +4 | f(**x for x in xs) + | + + + | +1 | # parse_options: {"target-version": "3.15"} +2 | [**x for x in xs] +3 | (**x for x in xs) + | ^^^ Syntax Error: cannot use dict unpacking in generator expression +4 | f(**x for x in xs) +5 | {*x: y for x in xs} + | + + + | +2 | [**x for x in xs] +3 | (**x for x in xs) +4 | f(**x for x in xs) + | ^^^ Syntax Error: cannot use dict unpacking in generator expression +5 | {*x: y for x in xs} +6 | {**x: y for x in xs} + | + + + | +3 | (**x for x in xs) +4 | f(**x for x in xs) +5 | {*x: y for x in xs} + | ^^ Syntax Error: cannot use a starred expression in a dictionary key +6 | {**x: y for x in xs} +7 | {x: *y for x in xs} + | + + + | +4 | f(**x for x in xs) +5 | {*x: y for x in xs} +6 | {**x: y for x in xs} + | ^^^ Syntax Error: cannot use dict unpacking in a dictionary key +7 | {x: *y for x in xs} +8 | {x: **y for x in xs} + | + + + | +5 | {*x: y for x in xs} +6 | {**x: y for x in xs} +7 | {x: *y for x in xs} + | ^^ Syntax Error: cannot use a starred expression in a dictionary value +8 | {x: **y for x in xs} + | + + + | +6 | {**x: y for x in xs} +7 | {x: *y for x in xs} +8 | {x: **y for x in xs} + | ^^^ Syntax Error: cannot use dict unpacking in a dictionary value diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__pep798_lambda_then_error.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__pep798_lambda_then_error.py.snap new file mode 100644 index 0000000000000..1d544e50fc8c0 --- /dev/null +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__pep798_lambda_then_error.py.snap @@ -0,0 +1,111 @@ +--- +source: crates/ruff_python_parser/tests/fixtures.rs +input_file: crates/ruff_python_parser/resources/invalid/expressions/pep798_lambda_then_error.py +--- +## AST + +``` +Module( + ModModule { + node_index: NodeIndex(None), + range: 0..73, + body: [ + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 44..67, + value: Generator( + ExprGenerator { + node_index: NodeIndex(None), + range: 44..67, + elt: Starred( + ExprStarred { + node_index: NodeIndex(None), + range: 45..55, + value: Lambda( + ExprLambda { + node_index: NodeIndex(None), + range: 46..55, + parameters: None, + body: Name( + ExprName { + node_index: NodeIndex(None), + range: 54..55, + id: Name("x"), + ctx: Load, + }, + ), + }, + ), + ctx: Load, + }, + ), + generators: [ + Comprehension { + range: 56..66, + node_index: NodeIndex(None), + target: Name( + ExprName { + node_index: NodeIndex(None), + range: 60..61, + id: Name("x"), + ctx: Store, + }, + ), + iter: Name( + ExprName { + node_index: NodeIndex(None), + range: 65..66, + id: Name("z"), + ctx: Load, + }, + ), + ifs: [], + is_async: false, + runtime_ifs: None, + runtime_is_async: None, + }, + ], + parenthesized: true, + }, + ), + }, + ), + Assign( + StmtAssign { + node_index: NodeIndex(None), + range: 69..72, + targets: [ + Name( + ExprName { + node_index: NodeIndex(None), + range: 69..70, + id: Name("a"), + ctx: Store, + }, + ), + ], + value: Name( + ExprName { + node_index: NodeIndex(None), + range: 72..72, + id: Name(""), + ctx: Invalid, + }, + ), + runtime_targets: None, + runtime_type_comment: None, + runtime_type_comment_bytes: None, + }, + ), + ], + runtime_body: None, + }, +) +``` +## Errors + + | +1 | # parse_options: {"target-version": "3.15"} +2 | (*lambda: x for x in z); a = + | ^ Syntax Error: invalid syntax diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__pep798_missing_operand.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__pep798_missing_operand.py.snap new file mode 100644 index 0000000000000..87a0c16603090 --- /dev/null +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__pep798_missing_operand.py.snap @@ -0,0 +1,216 @@ +--- +source: crates/ruff_python_parser/tests/fixtures.rs +input_file: crates/ruff_python_parser/resources/invalid/expressions/pep798_missing_operand.py +--- +## AST + +``` +Module( + ModModule { + node_index: NodeIndex(None), + range: 0..78, + body: [ + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 44..48, + value: List( + ExprList { + node_index: NodeIndex(None), + range: 44..48, + elts: [ + Name( + ExprName { + node_index: NodeIndex(None), + range: 47..47, + id: Name(""), + ctx: Invalid, + }, + ), + ], + ctx: Load, + runtime_elts: None, + }, + ), + }, + ), + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 49..53, + value: Name( + ExprName { + node_index: NodeIndex(None), + range: 52..52, + id: Name(""), + ctx: Invalid, + }, + ), + }, + ), + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 54..61, + value: List( + ExprList { + node_index: NodeIndex(None), + range: 54..61, + elts: [ + Name( + ExprName { + node_index: NodeIndex(None), + range: 57..57, + id: Name(""), + ctx: Invalid, + }, + ), + Name( + ExprName { + node_index: NodeIndex(None), + range: 59..60, + id: Name("x"), + ctx: Load, + }, + ), + ], + ctx: Load, + runtime_elts: None, + }, + ), + }, + ), + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 62..77, + value: List( + ExprList { + node_index: NodeIndex(None), + range: 62..77, + elts: [ + Name( + ExprName { + node_index: NodeIndex(None), + range: 66..69, + id: Name("for"), + ctx: Load, + }, + ), + Compare( + ExprCompare { + node_index: NodeIndex(None), + range: 70..76, + ops: [ + In, + ], + operands: [ + Name( + ExprName { + node_index: NodeIndex(None), + range: 70..71, + id: Name("x"), + ctx: Load, + }, + ), + Name( + ExprName { + node_index: NodeIndex(None), + range: 75..76, + id: Name("z"), + ctx: Load, + }, + ), + ], + runtime_comparators: None, + }, + ), + ], + ctx: Load, + runtime_elts: None, + }, + ), + }, + ), + ], + runtime_body: None, + }, +) +``` +## Errors + + | +1 | # parse_options: {"target-version": "3.15"} +2 | [**] + | ^^ Syntax Error: invalid syntax +3 | (**) +4 | [**, x] + | + + + | +1 | # parse_options: {"target-version": "3.15"} +2 | [**] + | ^ Syntax Error: invalid syntax +3 | (**) +4 | [**, x] + | + + + | +1 | # parse_options: {"target-version": "3.15"} +2 | [**] +3 | (**) + | ^^ Syntax Error: invalid syntax +4 | [**, x] +5 | [** for x in z] + | + + + | +1 | # parse_options: {"target-version": "3.15"} +2 | [**] +3 | (**) + | ^ Syntax Error: invalid syntax +4 | [**, x] +5 | [** for x in z] + | + + + | +2 | [**] +3 | (**) +4 | [**, x] + | ^^ Syntax Error: invalid syntax +5 | [** for x in z] + | + + + | +2 | [**] +3 | (**) +4 | [**, x] + | ^ Syntax Error: invalid syntax +5 | [** for x in z] + | + + + | +3 | (**) +4 | [**, x] +5 | [** for x in z] + | ^^ Syntax Error: invalid syntax + + + | +3 | (**) +4 | [**, x] +5 | [** for x in z] + | ^^^ Syntax Error: invalid syntax + + + | +3 | (**) +4 | [**, x] +5 | [** for x in z] + | ^ Syntax Error: invalid syntax diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__pep798_nested_conditional_then_error.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__pep798_nested_conditional_then_error.py.snap new file mode 100644 index 0000000000000..1d24aa2cad436 --- /dev/null +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__pep798_nested_conditional_then_error.py.snap @@ -0,0 +1,194 @@ +--- +source: crates/ruff_python_parser/tests/fixtures.rs +input_file: crates/ruff_python_parser/resources/invalid/expressions/pep798_nested_conditional_then_error.py +--- +## AST + +``` +Module( + ModModule { + node_index: NodeIndex(None), + range: 0..105, + body: [ + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 44..99, + value: Generator( + ExprGenerator { + node_index: NodeIndex(None), + range: 44..99, + elt: Starred( + ExprStarred { + node_index: NodeIndex(None), + range: 45..86, + value: If( + ExprIf { + node_index: NodeIndex(None), + range: 46..86, + test: Name( + ExprName { + node_index: NodeIndex(None), + range: 78..79, + id: Name("d"), + ctx: Load, + }, + ), + body: Generator( + ExprGenerator { + node_index: NodeIndex(None), + range: 46..74, + elt: Starred( + ExprStarred { + node_index: NodeIndex(None), + range: 47..61, + value: If( + ExprIf { + node_index: NodeIndex(None), + range: 48..61, + test: Name( + ExprName { + node_index: NodeIndex(None), + range: 53..54, + id: Name("c"), + ctx: Load, + }, + ), + body: Name( + ExprName { + node_index: NodeIndex(None), + range: 48..49, + id: Name("x"), + ctx: Load, + }, + ), + orelse: Name( + ExprName { + node_index: NodeIndex(None), + range: 60..61, + id: Name("y"), + ctx: Load, + }, + ), + }, + ), + ctx: Load, + }, + ), + generators: [ + Comprehension { + range: 62..73, + node_index: NodeIndex(None), + target: Name( + ExprName { + node_index: NodeIndex(None), + range: 66..67, + id: Name("x"), + ctx: Store, + }, + ), + iter: Name( + ExprName { + node_index: NodeIndex(None), + range: 71..73, + id: Name("xs"), + ctx: Load, + }, + ), + ifs: [], + is_async: false, + runtime_ifs: None, + runtime_is_async: None, + }, + ], + parenthesized: true, + }, + ), + orelse: Name( + ExprName { + node_index: NodeIndex(None), + range: 85..86, + id: Name("z"), + ctx: Load, + }, + ), + }, + ), + ctx: Load, + }, + ), + generators: [ + Comprehension { + range: 87..98, + node_index: NodeIndex(None), + target: Name( + ExprName { + node_index: NodeIndex(None), + range: 91..92, + id: Name("z"), + ctx: Store, + }, + ), + iter: Name( + ExprName { + node_index: NodeIndex(None), + range: 96..98, + id: Name("zs"), + ctx: Load, + }, + ), + ifs: [], + is_async: false, + runtime_ifs: None, + runtime_is_async: None, + }, + ], + parenthesized: true, + }, + ), + }, + ), + Assign( + StmtAssign { + node_index: NodeIndex(None), + range: 101..104, + targets: [ + Name( + ExprName { + node_index: NodeIndex(None), + range: 101..102, + id: Name("a"), + ctx: Store, + }, + ), + ], + value: Name( + ExprName { + node_index: NodeIndex(None), + range: 104..104, + id: Name(""), + ctx: Invalid, + }, + ), + runtime_targets: None, + runtime_type_comment: None, + runtime_type_comment_bytes: None, + }, + ), + ], + runtime_body: None, + }, +) +``` +## Errors + + | +1 | # parse_options: {"target-version": "3.15"} +2 | (*(*x if c else y for x in xs) if d else z for z in zs); a = + | ^^^^^^^^^^^^^^ Syntax Error: invalid starred expression. Did you forget to wrap the conditional expression in parentheses? + + + | +1 | # parse_options: {"target-version": "3.15"} +2 | (*(*x if c else y for x in xs) if d else z for z in zs); a = + | ^ Syntax Error: invalid syntax diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__pep798_nested_operand.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__pep798_nested_operand.py.snap new file mode 100644 index 0000000000000..c629d4a60a159 --- /dev/null +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__pep798_nested_operand.py.snap @@ -0,0 +1,176 @@ +--- +source: crates/ruff_python_parser/tests/fixtures.rs +input_file: crates/ruff_python_parser/resources/invalid/expressions/pep798_nested_operand.py +--- +## AST + +``` +Module( + ModModule { + node_index: NodeIndex(None), + range: 0..109, + body: [ + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 44..75, + value: ListComp( + ExprListComp { + node_index: NodeIndex(None), + range: 44..75, + elt: If( + ExprIf { + node_index: NodeIndex(None), + range: 48..62, + test: Name( + ExprName { + node_index: NodeIndex(None), + range: 53..54, + id: Name("x"), + ctx: Load, + }, + ), + body: Name( + ExprName { + node_index: NodeIndex(None), + range: 48..49, + id: Name("x"), + ctx: Load, + }, + ), + orelse: Starred( + ExprStarred { + node_index: NodeIndex(None), + range: 60..62, + value: Name( + ExprName { + node_index: NodeIndex(None), + range: 61..62, + id: Name("y"), + ctx: Load, + }, + ), + ctx: Load, + }, + ), + }, + ), + generators: [ + Comprehension { + range: 64..74, + node_index: NodeIndex(None), + target: Name( + ExprName { + node_index: NodeIndex(None), + range: 68..69, + id: Name("x"), + ctx: Store, + }, + ), + iter: Name( + ExprName { + node_index: NodeIndex(None), + range: 73..74, + id: Name("z"), + ctx: Load, + }, + ), + ifs: [], + is_async: false, + runtime_ifs: None, + runtime_is_async: None, + }, + ], + }, + ), + }, + ), + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 76..108, + value: Generator( + ExprGenerator { + node_index: NodeIndex(None), + range: 76..108, + elt: If( + ExprIf { + node_index: NodeIndex(None), + range: 80..95, + test: Name( + ExprName { + node_index: NodeIndex(None), + range: 85..86, + id: Name("x"), + ctx: Load, + }, + ), + body: Name( + ExprName { + node_index: NodeIndex(None), + range: 80..81, + id: Name("x"), + ctx: Load, + }, + ), + orelse: Name( + ExprName { + node_index: NodeIndex(None), + range: 94..95, + id: Name("y"), + ctx: Load, + }, + ), + }, + ), + generators: [ + Comprehension { + range: 97..107, + node_index: NodeIndex(None), + target: Name( + ExprName { + node_index: NodeIndex(None), + range: 101..102, + id: Name("x"), + ctx: Store, + }, + ), + iter: Name( + ExprName { + node_index: NodeIndex(None), + range: 106..107, + id: Name("z"), + ctx: Load, + }, + ), + ifs: [], + is_async: false, + runtime_ifs: None, + runtime_is_async: None, + }, + ], + parenthesized: true, + }, + ), + }, + ), + ], + runtime_body: None, + }, +) +``` +## Errors + + | +1 | # parse_options: {"target-version": "3.15"} +2 | [**(x if x else *y) for x in z] + | ^ Syntax Error: cannot unpack only part of a conditional expression +3 | (**(x if x else **y) for x in z) + | + + + | +1 | # parse_options: {"target-version": "3.15"} +2 | [**(x if x else *y) for x in z] +3 | (**(x if x else **y) for x in z) + | ^^ Syntax Error: cannot use dict unpacking on only part of a conditional expression diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__pep798_nested_then_error.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__pep798_nested_then_error.py.snap new file mode 100644 index 0000000000000..7f9f10ea14482 --- /dev/null +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__pep798_nested_then_error.py.snap @@ -0,0 +1,162 @@ +--- +source: crates/ruff_python_parser/tests/fixtures.rs +input_file: crates/ruff_python_parser/resources/invalid/expressions/pep798_nested_then_error.py +--- +## AST + +``` +Module( + ModModule { + node_index: NodeIndex(None), + range: 0..85, + body: [ + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 44..79, + value: Generator( + ExprGenerator { + node_index: NodeIndex(None), + range: 44..79, + elt: Starred( + ExprStarred { + node_index: NodeIndex(None), + range: 45..66, + value: Generator( + ExprGenerator { + node_index: NodeIndex(None), + range: 46..66, + elt: Starred( + ExprStarred { + node_index: NodeIndex(None), + range: 47..54, + value: BoolOp( + ExprBoolOp { + node_index: NodeIndex(None), + range: 48..54, + op: Or, + values: [ + Name( + ExprName { + node_index: NodeIndex(None), + range: 48..49, + id: Name("x"), + ctx: Load, + }, + ), + Name( + ExprName { + node_index: NodeIndex(None), + range: 53..54, + id: Name("y"), + ctx: Load, + }, + ), + ], + runtime_values: None, + }, + ), + ctx: Load, + }, + ), + generators: [ + Comprehension { + range: 55..65, + node_index: NodeIndex(None), + target: Name( + ExprName { + node_index: NodeIndex(None), + range: 59..60, + id: Name("x"), + ctx: Store, + }, + ), + iter: Name( + ExprName { + node_index: NodeIndex(None), + range: 64..65, + id: Name("z"), + ctx: Load, + }, + ), + ifs: [], + is_async: false, + runtime_ifs: None, + runtime_is_async: None, + }, + ], + parenthesized: true, + }, + ), + ctx: Load, + }, + ), + generators: [ + Comprehension { + range: 67..78, + node_index: NodeIndex(None), + target: Name( + ExprName { + node_index: NodeIndex(None), + range: 71..72, + id: Name("z"), + ctx: Store, + }, + ), + iter: Name( + ExprName { + node_index: NodeIndex(None), + range: 76..78, + id: Name("zs"), + ctx: Load, + }, + ), + ifs: [], + is_async: false, + runtime_ifs: None, + runtime_is_async: None, + }, + ], + parenthesized: true, + }, + ), + }, + ), + Assign( + StmtAssign { + node_index: NodeIndex(None), + range: 81..84, + targets: [ + Name( + ExprName { + node_index: NodeIndex(None), + range: 81..82, + id: Name("a"), + ctx: Store, + }, + ), + ], + value: Name( + ExprName { + node_index: NodeIndex(None), + range: 84..84, + id: Name(""), + ctx: Invalid, + }, + ), + runtime_targets: None, + runtime_type_comment: None, + runtime_type_comment_bytes: None, + }, + ), + ], + runtime_body: None, + }, +) +``` +## Errors + + | +1 | # parse_options: {"target-version": "3.15"} +2 | (*(*x or y for x in z) for z in zs); a = + | ^ Syntax Error: invalid syntax diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__pep798_soft_keyword_operand.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__pep798_soft_keyword_operand.py.snap new file mode 100644 index 0000000000000..c73c34b324de0 --- /dev/null +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__pep798_soft_keyword_operand.py.snap @@ -0,0 +1,114 @@ +--- +source: crates/ruff_python_parser/tests/fixtures.rs +input_file: crates/ruff_python_parser/resources/invalid/expressions/pep798_soft_keyword_operand.py +--- +## AST + +``` +Module( + ModModule { + node_index: NodeIndex(None), + range: 0..72, + body: [ + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 44..53, + value: List( + ExprList { + node_index: NodeIndex(None), + range: 44..53, + elts: [ + Name( + ExprName { + node_index: NodeIndex(None), + range: 47..52, + id: Name("match"), + ctx: Load, + }, + ), + ], + ctx: Load, + runtime_elts: None, + }, + ), + }, + ), + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 54..62, + value: List( + ExprList { + node_index: NodeIndex(None), + range: 54..62, + elts: [ + Name( + ExprName { + node_index: NodeIndex(None), + range: 57..61, + id: Name("case"), + ctx: Load, + }, + ), + ], + ctx: Load, + runtime_elts: None, + }, + ), + }, + ), + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 63..71, + value: List( + ExprList { + node_index: NodeIndex(None), + range: 63..71, + elts: [ + Name( + ExprName { + node_index: NodeIndex(None), + range: 66..70, + id: Name("type"), + ctx: Load, + }, + ), + ], + ctx: Load, + runtime_elts: None, + }, + ), + }, + ), + ], + runtime_body: None, + }, +) +``` +## Errors + + | +1 | # parse_options: {"target-version": "3.15"} +2 | [**match] + | ^^ Syntax Error: cannot use dict unpacking here +3 | [**case] +4 | [**type] + | + + + | +1 | # parse_options: {"target-version": "3.15"} +2 | [**match] +3 | [**case] + | ^^ Syntax Error: cannot use dict unpacking here +4 | [**type] + | + + + | +2 | [**match] +3 | [**case] +4 | [**type] + | ^^ Syntax Error: cannot use dict unpacking here diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__set__star_expression_precedence.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__set__star_expression_precedence.py.snap index 04dd3fcf4b519..f37ae52729f86 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__set__star_expression_precedence.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__set__star_expression_precedence.py.snap @@ -509,7 +509,7 @@ Module( 6 | {*x and y, z} 7 | {*x or y, z} 8 | {*x if True else y, z} - | ^^^^^^^^^^^^^^^^ Syntax Error: conditional expression cannot be used here + | ^^^^^^^^^^^^^^^^^ Syntax Error: invalid starred expression. Did you forget to wrap the conditional expression in parentheses? 9 | {*lambda x: x, z} 10 | {*x := 2, z} | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@parenthesized_double_starred_expr.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@parenthesized_double_starred_expr.py.snap index bc0ad8f573c90..cc5ea2401ec41 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@parenthesized_double_starred_expr.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@parenthesized_double_starred_expr.py.snap @@ -33,4 +33,4 @@ Module( | 1 | (**x) - | ^^ Syntax Error: cannot use double starred expression here + | ^^ Syntax Error: cannot use dict unpacking here diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@pep_798_invalid_dict_unpacking_comprehensions_py315.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@pep_798_invalid_dict_unpacking_comprehensions_py315.py.snap index 5a2c48211dcd0..780087a3fcb04 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@pep_798_invalid_dict_unpacking_comprehensions_py315.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@pep_798_invalid_dict_unpacking_comprehensions_py315.py.snap @@ -327,7 +327,7 @@ Module( | 1 | # parse_options: {"target-version": "3.15"} 2 | {*k: v for k, v in items} - | ^ Syntax Error: invalid syntax + | ^^ Syntax Error: cannot use a starred expression in a dictionary key 3 | {k: *v for k, v in items} 4 | {**k: v for k, v in items} | @@ -347,7 +347,7 @@ Module( 2 | {*k: v for k, v in items} 3 | {k: *v for k, v in items} 4 | {**k: v for k, v in items} - | ^ Syntax Error: invalid syntax + | ^^^ Syntax Error: cannot use dict unpacking in a dictionary key 5 | {k: **v for k, v in items} | @@ -356,4 +356,4 @@ Module( 3 | {k: *v for k, v in items} 4 | {**k: v for k, v in items} 5 | {k: **v for k, v in items} - | ^ Syntax Error: invalid syntax + | ^^^ Syntax Error: cannot use dict unpacking in a dictionary value diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__pep798_unpacking_operands.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__pep798_unpacking_operands.py.snap new file mode 100644 index 0000000000000..b0e9dcda14ae5 --- /dev/null +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__pep798_unpacking_operands.py.snap @@ -0,0 +1,1079 @@ +--- +source: crates/ruff_python_parser/tests/fixtures.rs +input_file: crates/ruff_python_parser/resources/valid/expressions/pep798_unpacking_operands.py +--- +## AST + +``` +Module( + ModModule { + node_index: NodeIndex(None), + range: 0..464, + body: [ + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 44..73, + value: DictComp( + ExprDictComp { + node_index: NodeIndex(None), + range: 44..73, + key: None, + value: If( + ExprIf { + node_index: NodeIndex(None), + range: 47..60, + test: Name( + ExprName { + node_index: NodeIndex(None), + range: 52..53, + id: Name("c"), + ctx: Load, + }, + ), + body: Name( + ExprName { + node_index: NodeIndex(None), + range: 47..48, + id: Name("x"), + ctx: Load, + }, + ), + orelse: Name( + ExprName { + node_index: NodeIndex(None), + range: 59..60, + id: Name("y"), + ctx: Load, + }, + ), + }, + ), + generators: [ + Comprehension { + range: 61..72, + node_index: NodeIndex(None), + target: Name( + ExprName { + node_index: NodeIndex(None), + range: 65..66, + id: Name("x"), + ctx: Store, + }, + ), + iter: Name( + ExprName { + node_index: NodeIndex(None), + range: 70..72, + id: Name("xs"), + ctx: Load, + }, + ), + ifs: [], + is_async: false, + runtime_ifs: None, + runtime_is_async: None, + }, + ], + }, + ), + }, + ), + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 74..96, + value: DictComp( + ExprDictComp { + node_index: NodeIndex(None), + range: 74..96, + key: None, + value: BoolOp( + ExprBoolOp { + node_index: NodeIndex(None), + range: 77..83, + op: Or, + values: [ + Name( + ExprName { + node_index: NodeIndex(None), + range: 77..78, + id: Name("x"), + ctx: Load, + }, + ), + Name( + ExprName { + node_index: NodeIndex(None), + range: 82..83, + id: Name("y"), + ctx: Load, + }, + ), + ], + runtime_values: None, + }, + ), + generators: [ + Comprehension { + range: 84..95, + node_index: NodeIndex(None), + target: Name( + ExprName { + node_index: NodeIndex(None), + range: 88..89, + id: Name("x"), + ctx: Store, + }, + ), + iter: Name( + ExprName { + node_index: NodeIndex(None), + range: 93..95, + id: Name("xs"), + ctx: Load, + }, + ), + ifs: [], + is_async: false, + runtime_ifs: None, + runtime_is_async: None, + }, + ], + }, + ), + }, + ), + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 97..118, + value: DictComp( + ExprDictComp { + node_index: NodeIndex(None), + range: 97..118, + key: None, + value: Compare( + ExprCompare { + node_index: NodeIndex(None), + range: 100..105, + ops: [ + Lt, + ], + operands: [ + Name( + ExprName { + node_index: NodeIndex(None), + range: 100..101, + id: Name("x"), + ctx: Load, + }, + ), + Name( + ExprName { + node_index: NodeIndex(None), + range: 104..105, + id: Name("y"), + ctx: Load, + }, + ), + ], + runtime_comparators: None, + }, + ), + generators: [ + Comprehension { + range: 106..117, + node_index: NodeIndex(None), + target: Name( + ExprName { + node_index: NodeIndex(None), + range: 110..111, + id: Name("x"), + ctx: Store, + }, + ), + iter: Name( + ExprName { + node_index: NodeIndex(None), + range: 115..117, + id: Name("xs"), + ctx: Load, + }, + ), + ifs: [], + is_async: false, + runtime_ifs: None, + runtime_is_async: None, + }, + ], + }, + ), + }, + ), + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 119..144, + value: DictComp( + ExprDictComp { + node_index: NodeIndex(None), + range: 119..144, + key: None, + value: Lambda( + ExprLambda { + node_index: NodeIndex(None), + range: 122..131, + parameters: None, + body: Name( + ExprName { + node_index: NodeIndex(None), + range: 130..131, + id: Name("x"), + ctx: Load, + }, + ), + }, + ), + generators: [ + Comprehension { + range: 132..143, + node_index: NodeIndex(None), + target: Name( + ExprName { + node_index: NodeIndex(None), + range: 136..137, + id: Name("x"), + ctx: Store, + }, + ), + iter: Name( + ExprName { + node_index: NodeIndex(None), + range: 141..143, + id: Name("xs"), + ctx: Load, + }, + ), + ifs: [], + is_async: false, + runtime_ifs: None, + runtime_is_async: None, + }, + ], + }, + ), + }, + ), + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 145..173, + value: Generator( + ExprGenerator { + node_index: NodeIndex(None), + range: 145..173, + elt: Starred( + ExprStarred { + node_index: NodeIndex(None), + range: 146..160, + value: If( + ExprIf { + node_index: NodeIndex(None), + range: 147..160, + test: Name( + ExprName { + node_index: NodeIndex(None), + range: 152..153, + id: Name("c"), + ctx: Load, + }, + ), + body: Name( + ExprName { + node_index: NodeIndex(None), + range: 147..148, + id: Name("x"), + ctx: Load, + }, + ), + orelse: Name( + ExprName { + node_index: NodeIndex(None), + range: 159..160, + id: Name("y"), + ctx: Load, + }, + ), + }, + ), + ctx: Load, + }, + ), + generators: [ + Comprehension { + range: 161..172, + node_index: NodeIndex(None), + target: Name( + ExprName { + node_index: NodeIndex(None), + range: 165..166, + id: Name("x"), + ctx: Store, + }, + ), + iter: Name( + ExprName { + node_index: NodeIndex(None), + range: 170..172, + id: Name("xs"), + ctx: Load, + }, + ), + ifs: [], + is_async: false, + runtime_ifs: None, + runtime_is_async: None, + }, + ], + parenthesized: true, + }, + ), + }, + ), + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 174..195, + value: Generator( + ExprGenerator { + node_index: NodeIndex(None), + range: 174..195, + elt: Starred( + ExprStarred { + node_index: NodeIndex(None), + range: 175..182, + value: BoolOp( + ExprBoolOp { + node_index: NodeIndex(None), + range: 176..182, + op: Or, + values: [ + Name( + ExprName { + node_index: NodeIndex(None), + range: 176..177, + id: Name("x"), + ctx: Load, + }, + ), + Name( + ExprName { + node_index: NodeIndex(None), + range: 181..182, + id: Name("y"), + ctx: Load, + }, + ), + ], + runtime_values: None, + }, + ), + ctx: Load, + }, + ), + generators: [ + Comprehension { + range: 183..194, + node_index: NodeIndex(None), + target: Name( + ExprName { + node_index: NodeIndex(None), + range: 187..188, + id: Name("x"), + ctx: Store, + }, + ), + iter: Name( + ExprName { + node_index: NodeIndex(None), + range: 192..194, + id: Name("xs"), + ctx: Load, + }, + ), + ifs: [], + is_async: false, + runtime_ifs: None, + runtime_is_async: None, + }, + ], + parenthesized: true, + }, + ), + }, + ), + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 196..216, + value: Generator( + ExprGenerator { + node_index: NodeIndex(None), + range: 196..216, + elt: Starred( + ExprStarred { + node_index: NodeIndex(None), + range: 197..203, + value: Compare( + ExprCompare { + node_index: NodeIndex(None), + range: 198..203, + ops: [ + Lt, + ], + operands: [ + Name( + ExprName { + node_index: NodeIndex(None), + range: 198..199, + id: Name("x"), + ctx: Load, + }, + ), + Name( + ExprName { + node_index: NodeIndex(None), + range: 202..203, + id: Name("y"), + ctx: Load, + }, + ), + ], + runtime_comparators: None, + }, + ), + ctx: Load, + }, + ), + generators: [ + Comprehension { + range: 204..215, + node_index: NodeIndex(None), + target: Name( + ExprName { + node_index: NodeIndex(None), + range: 208..209, + id: Name("x"), + ctx: Store, + }, + ), + iter: Name( + ExprName { + node_index: NodeIndex(None), + range: 213..215, + id: Name("xs"), + ctx: Load, + }, + ), + ifs: [], + is_async: false, + runtime_ifs: None, + runtime_is_async: None, + }, + ], + parenthesized: true, + }, + ), + }, + ), + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 217..241, + value: Generator( + ExprGenerator { + node_index: NodeIndex(None), + range: 217..241, + elt: Starred( + ExprStarred { + node_index: NodeIndex(None), + range: 218..228, + value: Lambda( + ExprLambda { + node_index: NodeIndex(None), + range: 219..228, + parameters: None, + body: Name( + ExprName { + node_index: NodeIndex(None), + range: 227..228, + id: Name("x"), + ctx: Load, + }, + ), + }, + ), + ctx: Load, + }, + ), + generators: [ + Comprehension { + range: 229..240, + node_index: NodeIndex(None), + target: Name( + ExprName { + node_index: NodeIndex(None), + range: 233..234, + id: Name("x"), + ctx: Store, + }, + ), + iter: Name( + ExprName { + node_index: NodeIndex(None), + range: 238..240, + id: Name("xs"), + ctx: Load, + }, + ), + ifs: [], + is_async: false, + runtime_ifs: None, + runtime_is_async: None, + }, + ], + parenthesized: true, + }, + ), + }, + ), + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 242..271, + value: Call( + ExprCall { + node_index: NodeIndex(None), + range: 242..271, + func: Name( + ExprName { + node_index: NodeIndex(None), + range: 242..243, + id: Name("f"), + ctx: Load, + }, + ), + arguments: Arguments { + range: 243..271, + node_index: NodeIndex(None), + args: [ + Generator( + ExprGenerator { + node_index: NodeIndex(None), + range: 244..270, + elt: Starred( + ExprStarred { + node_index: NodeIndex(None), + range: 244..258, + value: If( + ExprIf { + node_index: NodeIndex(None), + range: 245..258, + test: Name( + ExprName { + node_index: NodeIndex(None), + range: 250..251, + id: Name("c"), + ctx: Load, + }, + ), + body: Name( + ExprName { + node_index: NodeIndex(None), + range: 245..246, + id: Name("x"), + ctx: Load, + }, + ), + orelse: Name( + ExprName { + node_index: NodeIndex(None), + range: 257..258, + id: Name("y"), + ctx: Load, + }, + ), + }, + ), + ctx: Load, + }, + ), + generators: [ + Comprehension { + range: 259..270, + node_index: NodeIndex(None), + target: Name( + ExprName { + node_index: NodeIndex(None), + range: 263..264, + id: Name("x"), + ctx: Store, + }, + ), + iter: Name( + ExprName { + node_index: NodeIndex(None), + range: 268..270, + id: Name("xs"), + ctx: Load, + }, + ), + ifs: [], + is_async: false, + runtime_ifs: None, + runtime_is_async: None, + }, + ], + parenthesized: false, + }, + ), + ], + keywords: [], + runtime_args: None, + runtime_bases: None, + }, + }, + ), + }, + ), + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 272..289, + value: Call( + ExprCall { + node_index: NodeIndex(None), + range: 272..289, + func: Name( + ExprName { + node_index: NodeIndex(None), + range: 272..273, + id: Name("f"), + ctx: Load, + }, + ), + arguments: Arguments { + range: 273..289, + node_index: NodeIndex(None), + args: [ + Starred( + ExprStarred { + node_index: NodeIndex(None), + range: 274..288, + value: If( + ExprIf { + node_index: NodeIndex(None), + range: 275..288, + test: Name( + ExprName { + node_index: NodeIndex(None), + range: 280..281, + id: Name("c"), + ctx: Load, + }, + ), + body: Name( + ExprName { + node_index: NodeIndex(None), + range: 275..276, + id: Name("x"), + ctx: Load, + }, + ), + orelse: Name( + ExprName { + node_index: NodeIndex(None), + range: 287..288, + id: Name("y"), + ctx: Load, + }, + ), + }, + ), + ctx: Load, + }, + ), + ], + keywords: [], + runtime_args: None, + runtime_bases: None, + }, + }, + ), + }, + ), + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 290..325, + value: Generator( + ExprGenerator { + node_index: NodeIndex(None), + range: 290..325, + elt: Starred( + ExprStarred { + node_index: NodeIndex(None), + range: 291..312, + value: Generator( + ExprGenerator { + node_index: NodeIndex(None), + range: 292..312, + elt: Starred( + ExprStarred { + node_index: NodeIndex(None), + range: 293..300, + value: BoolOp( + ExprBoolOp { + node_index: NodeIndex(None), + range: 294..300, + op: Or, + values: [ + Name( + ExprName { + node_index: NodeIndex(None), + range: 294..295, + id: Name("x"), + ctx: Load, + }, + ), + Name( + ExprName { + node_index: NodeIndex(None), + range: 299..300, + id: Name("y"), + ctx: Load, + }, + ), + ], + runtime_values: None, + }, + ), + ctx: Load, + }, + ), + generators: [ + Comprehension { + range: 301..311, + node_index: NodeIndex(None), + target: Name( + ExprName { + node_index: NodeIndex(None), + range: 305..306, + id: Name("x"), + ctx: Store, + }, + ), + iter: Name( + ExprName { + node_index: NodeIndex(None), + range: 310..311, + id: Name("z"), + ctx: Load, + }, + ), + ifs: [], + is_async: false, + runtime_ifs: None, + runtime_is_async: None, + }, + ], + parenthesized: true, + }, + ), + ctx: Load, + }, + ), + generators: [ + Comprehension { + range: 313..324, + node_index: NodeIndex(None), + target: Name( + ExprName { + node_index: NodeIndex(None), + range: 317..318, + id: Name("z"), + ctx: Store, + }, + ), + iter: Name( + ExprName { + node_index: NodeIndex(None), + range: 322..324, + id: Name("zs"), + ctx: Load, + }, + ), + ifs: [], + is_async: false, + runtime_ifs: None, + runtime_is_async: None, + }, + ], + parenthesized: true, + }, + ), + }, + ), + FunctionDef( + StmtFunctionDef { + node_index: NodeIndex(None), + range: 326..394, + is_async: true, + decorator_list: [], + name: Identifier { + id: Name("unpack"), + range: 336..342, + node_index: NodeIndex(None), + }, + type_params: None, + parameters: Parameters { + range: 342..346, + node_index: NodeIndex(None), + posonlyargs: [], + args: [ + ParameterWithDefault { + range: 343..345, + node_index: NodeIndex(None), + parameter: Parameter { + range: 343..345, + node_index: NodeIndex(None), + name: Identifier { + id: Name("xs"), + range: 343..345, + node_index: NodeIndex(None), + }, + annotation: None, + runtime_type_comment: None, + runtime_type_comment_bytes: None, + }, + default: None, + }, + ], + vararg: None, + kwonlyargs: [], + kwarg: None, + runtime_defaults: None, + }, + returns: None, + body: [ + Return( + StmtReturn { + node_index: NodeIndex(None), + range: 352..394, + value: Some( + DictComp( + ExprDictComp { + node_index: NodeIndex(None), + range: 359..394, + key: None, + value: If( + ExprIf { + node_index: NodeIndex(None), + range: 362..375, + test: Name( + ExprName { + node_index: NodeIndex(None), + range: 367..368, + id: Name("c"), + ctx: Load, + }, + ), + body: Name( + ExprName { + node_index: NodeIndex(None), + range: 362..363, + id: Name("x"), + ctx: Load, + }, + ), + orelse: Name( + ExprName { + node_index: NodeIndex(None), + range: 374..375, + id: Name("y"), + ctx: Load, + }, + ), + }, + ), + generators: [ + Comprehension { + range: 376..393, + node_index: NodeIndex(None), + target: Name( + ExprName { + node_index: NodeIndex(None), + range: 386..387, + id: Name("x"), + ctx: Store, + }, + ), + iter: Name( + ExprName { + node_index: NodeIndex(None), + range: 391..393, + id: Name("xs"), + ctx: Load, + }, + ), + ifs: [], + is_async: true, + runtime_ifs: None, + runtime_is_async: None, + }, + ], + }, + ), + ), + }, + ), + ], + runtime_decorator_list: None, + runtime_type_comment: None, + runtime_type_comment_bytes: None, + runtime_body: None, + }, + ), + FunctionDef( + StmtFunctionDef { + node_index: NodeIndex(None), + range: 395..463, + is_async: true, + decorator_list: [], + name: Identifier { + id: Name("iterate"), + range: 405..412, + node_index: NodeIndex(None), + }, + type_params: None, + parameters: Parameters { + range: 412..416, + node_index: NodeIndex(None), + posonlyargs: [], + args: [ + ParameterWithDefault { + range: 413..415, + node_index: NodeIndex(None), + parameter: Parameter { + range: 413..415, + node_index: NodeIndex(None), + name: Identifier { + id: Name("xs"), + range: 413..415, + node_index: NodeIndex(None), + }, + annotation: None, + runtime_type_comment: None, + runtime_type_comment_bytes: None, + }, + default: None, + }, + ], + vararg: None, + kwonlyargs: [], + kwarg: None, + runtime_defaults: None, + }, + returns: None, + body: [ + Return( + StmtReturn { + node_index: NodeIndex(None), + range: 422..463, + value: Some( + Generator( + ExprGenerator { + node_index: NodeIndex(None), + range: 429..463, + elt: Starred( + ExprStarred { + node_index: NodeIndex(None), + range: 430..444, + value: If( + ExprIf { + node_index: NodeIndex(None), + range: 431..444, + test: Name( + ExprName { + node_index: NodeIndex(None), + range: 436..437, + id: Name("c"), + ctx: Load, + }, + ), + body: Name( + ExprName { + node_index: NodeIndex(None), + range: 431..432, + id: Name("x"), + ctx: Load, + }, + ), + orelse: Name( + ExprName { + node_index: NodeIndex(None), + range: 443..444, + id: Name("y"), + ctx: Load, + }, + ), + }, + ), + ctx: Load, + }, + ), + generators: [ + Comprehension { + range: 445..462, + node_index: NodeIndex(None), + target: Name( + ExprName { + node_index: NodeIndex(None), + range: 455..456, + id: Name("x"), + ctx: Store, + }, + ), + iter: Name( + ExprName { + node_index: NodeIndex(None), + range: 460..462, + id: Name("xs"), + ctx: Load, + }, + ), + ifs: [], + is_async: true, + runtime_ifs: None, + runtime_is_async: None, + }, + ], + parenthesized: true, + }, + ), + ), + }, + ), + ], + runtime_decorator_list: None, + runtime_type_comment: None, + runtime_type_comment_bytes: None, + runtime_body: None, + }, + ), + ], + runtime_body: None, + }, +) +```