From 3a32e564451aabbc810596f29fa7f049a2ed7994 Mon Sep 17 00:00:00 2001 From: Brent Westbrook <36778786+ntBre@users.noreply.github.com> Date: Fri, 14 Mar 2025 15:06:42 -0400 Subject: [PATCH] [syntax-errors] Unparenthesized assignment expressions in sets and indexes (#16404) ## Summary This PR detects unparenthesized assignment expressions used in set literals and comprehensions and in sequence indexes. The link to the release notes in https://github.com/astral-sh/ruff/issues/6591 just has this entry: > * Assignment expressions can now be used unparenthesized within set literals and set comprehensions, as well as in sequence indexes (but not slices). with no other information, so hopefully the test cases I came up with cover all of the changes. I also tested these out in the Python REPL and they actually worked in Python 3.9 too. I'm guessing this may be another case that was "formally made part of the language spec in Python 3.10, but usable -- and commonly used -- in Python >=3.9" as @AlexWaygood added to the body of #6591 for context managers. So we may want to change the version cutoff, but I've gone along with the release notes for now. ## Test Plan New inline parser tests and linter CLI tests. --- .../resources/inline/err/named_expr_slice.py | 4 + .../err/named_expr_slice_parse_error.py | 3 + .../unparenthesized_named_expr_index_py38.py | 2 + ...nparenthesized_named_expr_set_comp_py38.py | 2 + ...renthesized_named_expr_set_literal_py38.py | 4 + .../ok/parenthesized_named_expr_index_py38.py | 2 + .../ok/parenthesized_named_expr_py38.py | 3 + .../unparenthesized_named_expr_index_py39.py | 2 + .../ok/unparenthesized_named_expr_py39.py | 3 + crates/ruff_python_parser/src/error.rs | 56 ++++ .../src/parser/expression.rs | 99 ++++++- .../invalid_syntax@named_expr_slice.py.snap | 258 ++++++++++++++++++ ...yntax@named_expr_slice_parse_error.py.snap | 85 ++++++ ...arenthesized_named_expr_index_py38.py.snap | 60 ++++ ...nthesized_named_expr_set_comp_py38.py.snap | 91 ++++++ ...esized_named_expr_set_literal_py38.py.snap | 185 +++++++++++++ ...arenthesized_named_expr_index_py38.py.snap | 53 ++++ ...ntax@parenthesized_named_expr_py38.py.snap | 132 +++++++++ ...arenthesized_named_expr_index_py39.py.snap | 53 ++++ ...ax@unparenthesized_named_expr_py39.py.snap | 132 +++++++++ 20 files changed, 1220 insertions(+), 9 deletions(-) create mode 100644 crates/ruff_python_parser/resources/inline/err/named_expr_slice.py create mode 100644 crates/ruff_python_parser/resources/inline/err/named_expr_slice_parse_error.py create mode 100644 crates/ruff_python_parser/resources/inline/err/unparenthesized_named_expr_index_py38.py create mode 100644 crates/ruff_python_parser/resources/inline/err/unparenthesized_named_expr_set_comp_py38.py create mode 100644 crates/ruff_python_parser/resources/inline/err/unparenthesized_named_expr_set_literal_py38.py create mode 100644 crates/ruff_python_parser/resources/inline/ok/parenthesized_named_expr_index_py38.py create mode 100644 crates/ruff_python_parser/resources/inline/ok/parenthesized_named_expr_py38.py create mode 100644 crates/ruff_python_parser/resources/inline/ok/unparenthesized_named_expr_index_py39.py create mode 100644 crates/ruff_python_parser/resources/inline/ok/unparenthesized_named_expr_py39.py create mode 100644 crates/ruff_python_parser/tests/snapshots/invalid_syntax@named_expr_slice.py.snap create mode 100644 crates/ruff_python_parser/tests/snapshots/invalid_syntax@named_expr_slice_parse_error.py.snap create mode 100644 crates/ruff_python_parser/tests/snapshots/invalid_syntax@unparenthesized_named_expr_index_py38.py.snap create mode 100644 crates/ruff_python_parser/tests/snapshots/invalid_syntax@unparenthesized_named_expr_set_comp_py38.py.snap create mode 100644 crates/ruff_python_parser/tests/snapshots/invalid_syntax@unparenthesized_named_expr_set_literal_py38.py.snap create mode 100644 crates/ruff_python_parser/tests/snapshots/valid_syntax@parenthesized_named_expr_index_py38.py.snap create mode 100644 crates/ruff_python_parser/tests/snapshots/valid_syntax@parenthesized_named_expr_py38.py.snap create mode 100644 crates/ruff_python_parser/tests/snapshots/valid_syntax@unparenthesized_named_expr_index_py39.py.snap create mode 100644 crates/ruff_python_parser/tests/snapshots/valid_syntax@unparenthesized_named_expr_py39.py.snap diff --git a/crates/ruff_python_parser/resources/inline/err/named_expr_slice.py b/crates/ruff_python_parser/resources/inline/err/named_expr_slice.py new file mode 100644 index 0000000000..c0afba07f6 --- /dev/null +++ b/crates/ruff_python_parser/resources/inline/err/named_expr_slice.py @@ -0,0 +1,4 @@ +# even after 3.9, an unparenthesized named expression is not allowed in a slice +lst[x:=1:-1] +lst[1:x:=1] +lst[1:3:x:=1] diff --git a/crates/ruff_python_parser/resources/inline/err/named_expr_slice_parse_error.py b/crates/ruff_python_parser/resources/inline/err/named_expr_slice_parse_error.py new file mode 100644 index 0000000000..275c6a18d1 --- /dev/null +++ b/crates/ruff_python_parser/resources/inline/err/named_expr_slice_parse_error.py @@ -0,0 +1,3 @@ +# parse_options: {"target-version": "3.8"} +# before 3.9, only emit the parse error, not the unsupported syntax error +lst[x:=1:-1] diff --git a/crates/ruff_python_parser/resources/inline/err/unparenthesized_named_expr_index_py38.py b/crates/ruff_python_parser/resources/inline/err/unparenthesized_named_expr_index_py38.py new file mode 100644 index 0000000000..6ea69e2ba2 --- /dev/null +++ b/crates/ruff_python_parser/resources/inline/err/unparenthesized_named_expr_index_py38.py @@ -0,0 +1,2 @@ +# parse_options: {"target-version": "3.8"} +lst[x:=1] diff --git a/crates/ruff_python_parser/resources/inline/err/unparenthesized_named_expr_set_comp_py38.py b/crates/ruff_python_parser/resources/inline/err/unparenthesized_named_expr_set_comp_py38.py new file mode 100644 index 0000000000..b18e0db1c0 --- /dev/null +++ b/crates/ruff_python_parser/resources/inline/err/unparenthesized_named_expr_set_comp_py38.py @@ -0,0 +1,2 @@ +# parse_options: {"target-version": "3.8"} +{last := x for x in range(3)} diff --git a/crates/ruff_python_parser/resources/inline/err/unparenthesized_named_expr_set_literal_py38.py b/crates/ruff_python_parser/resources/inline/err/unparenthesized_named_expr_set_literal_py38.py new file mode 100644 index 0000000000..373ed6210f --- /dev/null +++ b/crates/ruff_python_parser/resources/inline/err/unparenthesized_named_expr_set_literal_py38.py @@ -0,0 +1,4 @@ +# parse_options: {"target-version": "3.8"} +{x := 1, 2, 3} +{1, x := 2, 3} +{1, 2, x := 3} diff --git a/crates/ruff_python_parser/resources/inline/ok/parenthesized_named_expr_index_py38.py b/crates/ruff_python_parser/resources/inline/ok/parenthesized_named_expr_index_py38.py new file mode 100644 index 0000000000..2d4b3d8fcd --- /dev/null +++ b/crates/ruff_python_parser/resources/inline/ok/parenthesized_named_expr_index_py38.py @@ -0,0 +1,2 @@ +# parse_options: {"target-version": "3.8"} +lst[(x:=1)] diff --git a/crates/ruff_python_parser/resources/inline/ok/parenthesized_named_expr_py38.py b/crates/ruff_python_parser/resources/inline/ok/parenthesized_named_expr_py38.py new file mode 100644 index 0000000000..ff189e9941 --- /dev/null +++ b/crates/ruff_python_parser/resources/inline/ok/parenthesized_named_expr_py38.py @@ -0,0 +1,3 @@ +# parse_options: {"target-version": "3.8"} +{(x := 1), 2, 3} +{(last := x) for x in range(3)} diff --git a/crates/ruff_python_parser/resources/inline/ok/unparenthesized_named_expr_index_py39.py b/crates/ruff_python_parser/resources/inline/ok/unparenthesized_named_expr_index_py39.py new file mode 100644 index 0000000000..813f1eb15c --- /dev/null +++ b/crates/ruff_python_parser/resources/inline/ok/unparenthesized_named_expr_index_py39.py @@ -0,0 +1,2 @@ +# parse_options: {"target-version": "3.9"} +lst[x:=1] diff --git a/crates/ruff_python_parser/resources/inline/ok/unparenthesized_named_expr_py39.py b/crates/ruff_python_parser/resources/inline/ok/unparenthesized_named_expr_py39.py new file mode 100644 index 0000000000..1b1bf189dc --- /dev/null +++ b/crates/ruff_python_parser/resources/inline/ok/unparenthesized_named_expr_py39.py @@ -0,0 +1,3 @@ +# parse_options: {"target-version": "3.9"} +{x := 1, 2, 3} +{last := x for x in range(3)} diff --git a/crates/ruff_python_parser/src/error.rs b/crates/ruff_python_parser/src/error.rs index 59a91c5da9..defc9f6280 100644 --- a/crates/ruff_python_parser/src/error.rs +++ b/crates/ruff_python_parser/src/error.rs @@ -452,11 +452,55 @@ pub enum StarTupleKind { Yield, } +#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)] +pub enum UnparenthesizedNamedExprKind { + SequenceIndex, + SetLiteral, + SetComprehension, +} + #[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)] pub enum UnsupportedSyntaxErrorKind { Match, Walrus, ExceptStar, + /// Represents the use of an unparenthesized named expression (`:=`) in a set literal, set + /// comprehension, or sequence index before Python 3.10. + /// + /// ## Examples + /// + /// These are allowed on Python 3.10: + /// + /// ```python + /// {x := 1, 2, 3} # set literal + /// {last := x for x in range(3)} # set comprehension + /// lst[x := 1] # sequence index + /// ``` + /// + /// But on Python 3.9 the named expression needs to be parenthesized: + /// + /// ```python + /// {(x := 1), 2, 3} # set literal + /// {(last := x) for x in range(3)} # set comprehension + /// lst[(x := 1)] # sequence index + /// ``` + /// + /// However, unparenthesized named expressions are never allowed in slices: + /// + /// ```python + /// lst[x:=1:-1] # syntax error + /// lst[1:x:=1] # syntax error + /// lst[1:3:x:=1] # syntax error + /// + /// lst[(x:=1):-1] # ok + /// lst[1:(x:=1)] # ok + /// lst[1:3:(x:=1)] # ok + /// ``` + /// + /// ## References + /// + /// - [Python 3.10 Other Language Changes](https://docs.python.org/3/whatsnew/3.10.html#other-language-changes) + UnparenthesizedNamedExpr(UnparenthesizedNamedExprKind), /// Represents the use of a parenthesized keyword argument name after Python 3.8. /// @@ -706,6 +750,15 @@ impl Display for UnsupportedSyntaxError { UnsupportedSyntaxErrorKind::Match => "Cannot use `match` statement", UnsupportedSyntaxErrorKind::Walrus => "Cannot use named assignment expression (`:=`)", UnsupportedSyntaxErrorKind::ExceptStar => "Cannot use `except*`", + UnsupportedSyntaxErrorKind::UnparenthesizedNamedExpr( + UnparenthesizedNamedExprKind::SequenceIndex, + ) => "Cannot use unparenthesized assignment expression in a sequence index", + UnsupportedSyntaxErrorKind::UnparenthesizedNamedExpr( + UnparenthesizedNamedExprKind::SetLiteral, + ) => "Cannot use unparenthesized assignment expression as an element in a set literal", + UnsupportedSyntaxErrorKind::UnparenthesizedNamedExpr( + UnparenthesizedNamedExprKind::SetComprehension, + ) => "Cannot use unparenthesized assignment expression as an element in a set comprehension", UnsupportedSyntaxErrorKind::ParenthesizedKeywordArgumentName => { "Cannot use parenthesized keyword argument name" } @@ -765,6 +818,9 @@ impl UnsupportedSyntaxErrorKind { UnsupportedSyntaxErrorKind::Match => Change::Added(PythonVersion::PY310), UnsupportedSyntaxErrorKind::Walrus => Change::Added(PythonVersion::PY38), UnsupportedSyntaxErrorKind::ExceptStar => Change::Added(PythonVersion::PY311), + UnsupportedSyntaxErrorKind::UnparenthesizedNamedExpr(_) => { + Change::Added(PythonVersion::PY39) + } UnsupportedSyntaxErrorKind::StarTuple(_) => Change::Added(PythonVersion::PY38), UnsupportedSyntaxErrorKind::RelaxedDecorator => Change::Added(PythonVersion::PY39), UnsupportedSyntaxErrorKind::PositionalOnlyParameter => { diff --git a/crates/ruff_python_parser/src/parser/expression.rs b/crates/ruff_python_parser/src/parser/expression.rs index f9f764e2ff..43ff8881af 100644 --- a/crates/ruff_python_parser/src/parser/expression.rs +++ b/crates/ruff_python_parser/src/parser/expression.rs @@ -11,7 +11,7 @@ use ruff_python_ast::{ }; use ruff_text_size::{Ranged, TextLen, TextRange, TextSize}; -use crate::error::StarTupleKind; +use crate::error::{StarTupleKind, UnparenthesizedNamedExprKind}; use crate::parser::progress::ParserProgress; use crate::parser::{helpers, FunctionKind, Parser}; use crate::string::{parse_fstring_literal_element, parse_string_literal, StringType}; @@ -900,15 +900,48 @@ impl<'src> Parser<'src> { const STEP_END_SET: TokenSet = TokenSet::new([TokenKind::Comma, TokenKind::Rsqb]).union(NEWLINE_EOF_SET); + // test_err named_expr_slice + // # even after 3.9, an unparenthesized named expression is not allowed in a slice + // lst[x:=1:-1] + // lst[1:x:=1] + // lst[1:3:x:=1] + + // test_err named_expr_slice_parse_error + // # parse_options: {"target-version": "3.8"} + // # before 3.9, only emit the parse error, not the unsupported syntax error + // lst[x:=1:-1] + let start = self.node_start(); let lower = if self.at_expr() { let lower = self.parse_named_expression_or_higher(ExpressionContext::starred_conditional()); + + // This means we're in a subscript. if self.at_ts(NEWLINE_EOF_SET.union([TokenKind::Rsqb, TokenKind::Comma].into())) { + // test_ok parenthesized_named_expr_index_py38 + // # parse_options: {"target-version": "3.8"} + // lst[(x:=1)] + + // test_ok unparenthesized_named_expr_index_py39 + // # parse_options: {"target-version": "3.9"} + // lst[x:=1] + + // test_err unparenthesized_named_expr_index_py38 + // # parse_options: {"target-version": "3.8"} + // lst[x:=1] + if lower.is_unparenthesized_named_expr() { + self.add_unsupported_syntax_error( + UnsupportedSyntaxErrorKind::UnparenthesizedNamedExpr( + UnparenthesizedNamedExprKind::SequenceIndex, + ), + lower.range(), + ); + } return lower.expr; } + // Now we know we're in a slice. if !lower.is_parenthesized { match lower.expr { Expr::Starred(_) => { @@ -1659,6 +1692,26 @@ impl<'src> Parser<'src> { ParseErrorType::IterableUnpackingInComprehension, &key_or_element, ); + } else if key_or_element.is_unparenthesized_named_expr() { + // test_ok parenthesized_named_expr_py38 + // # parse_options: {"target-version": "3.8"} + // {(x := 1), 2, 3} + // {(last := x) for x in range(3)} + + // test_ok unparenthesized_named_expr_py39 + // # parse_options: {"target-version": "3.9"} + // {x := 1, 2, 3} + // {last := x for x in range(3)} + + // test_err unparenthesized_named_expr_set_comp_py38 + // # parse_options: {"target-version": "3.8"} + // {last := x for x in range(3)} + self.add_unsupported_syntax_error( + UnsupportedSyntaxErrorKind::UnparenthesizedNamedExpr( + UnparenthesizedNamedExprKind::SetComprehension, + ), + key_or_element.range(), + ); } Expr::SetComp(self.parse_set_comprehension_expression(key_or_element.expr, start)) @@ -1697,7 +1750,7 @@ impl<'src> Parser<'src> { )) } } - _ => Expr::Set(self.parse_set_expression(key_or_element.expr, start)), + _ => Expr::Set(self.parse_set_expression(key_or_element, start)), } } @@ -1847,19 +1900,42 @@ impl<'src> Parser<'src> { /// Parses a set expression. /// /// See: - fn parse_set_expression(&mut self, first_element: Expr, start: TextSize) -> ast::ExprSet { + fn parse_set_expression(&mut self, first_element: ParsedExpr, start: TextSize) -> ast::ExprSet { if !self.at_sequence_end() { self.expect(TokenKind::Comma); } - let mut elts = vec![first_element]; + // test_err unparenthesized_named_expr_set_literal_py38 + // # parse_options: {"target-version": "3.8"} + // {x := 1, 2, 3} + // {1, x := 2, 3} + // {1, 2, x := 3} + + if first_element.is_unparenthesized_named_expr() { + self.add_unsupported_syntax_error( + UnsupportedSyntaxErrorKind::UnparenthesizedNamedExpr( + UnparenthesizedNamedExprKind::SetLiteral, + ), + first_element.range(), + ); + } + + let mut elts = vec![first_element.expr]; self.parse_comma_separated_list(RecoveryContextKind::SetElements, |parser| { - elts.push( - parser - .parse_named_expression_or_higher(ExpressionContext::starred_bitwise_or()) - .expr, - ); + let parsed_expr = + parser.parse_named_expression_or_higher(ExpressionContext::starred_bitwise_or()); + + if parsed_expr.is_unparenthesized_named_expr() { + parser.add_unsupported_syntax_error( + UnsupportedSyntaxErrorKind::UnparenthesizedNamedExpr( + UnparenthesizedNamedExprKind::SetLiteral, + ), + parsed_expr.range(), + ); + } + + elts.push(parsed_expr.expr); }); self.expect(TokenKind::Rbrace); @@ -2410,6 +2486,11 @@ impl ParsedExpr { pub(super) const fn is_unparenthesized_starred_expr(&self) -> bool { !self.is_parenthesized && self.expr.is_starred_expr() } + + #[inline] + pub(super) const fn is_unparenthesized_named_expr(&self) -> bool { + !self.is_parenthesized && self.expr.is_named_expr() + } } impl From for ParsedExpr { diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@named_expr_slice.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@named_expr_slice.py.snap new file mode 100644 index 0000000000..815b74f88c --- /dev/null +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@named_expr_slice.py.snap @@ -0,0 +1,258 @@ +--- +source: crates/ruff_python_parser/tests/fixtures.rs +input_file: crates/ruff_python_parser/resources/inline/err/named_expr_slice.py +--- +## AST + +``` +Module( + ModModule { + range: 0..119, + body: [ + Expr( + StmtExpr { + range: 80..92, + value: Subscript( + ExprSubscript { + range: 80..92, + value: Name( + ExprName { + range: 80..83, + id: Name("lst"), + ctx: Load, + }, + ), + slice: Slice( + ExprSlice { + range: 84..91, + lower: Some( + Named( + ExprNamed { + range: 84..88, + target: Name( + ExprName { + range: 84..85, + id: Name("x"), + ctx: Store, + }, + ), + value: NumberLiteral( + ExprNumberLiteral { + range: 87..88, + value: Int( + 1, + ), + }, + ), + }, + ), + ), + upper: Some( + UnaryOp( + ExprUnaryOp { + range: 89..91, + op: USub, + operand: NumberLiteral( + ExprNumberLiteral { + range: 90..91, + value: Int( + 1, + ), + }, + ), + }, + ), + ), + step: None, + }, + ), + ctx: Load, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 93..100, + value: Subscript( + ExprSubscript { + range: 93..100, + value: Name( + ExprName { + range: 93..96, + id: Name("lst"), + ctx: Load, + }, + ), + slice: Slice( + ExprSlice { + range: 97..100, + lower: Some( + NumberLiteral( + ExprNumberLiteral { + range: 97..98, + value: Int( + 1, + ), + }, + ), + ), + upper: Some( + Name( + ExprName { + range: 99..100, + id: Name("x"), + ctx: Load, + }, + ), + ), + step: None, + }, + ), + ctx: Load, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 102..103, + value: NumberLiteral( + ExprNumberLiteral { + range: 102..103, + value: Int( + 1, + ), + }, + ), + }, + ), + Expr( + StmtExpr { + range: 105..114, + value: Subscript( + ExprSubscript { + range: 105..114, + value: Name( + ExprName { + range: 105..108, + id: Name("lst"), + ctx: Load, + }, + ), + slice: Slice( + ExprSlice { + range: 109..114, + lower: Some( + NumberLiteral( + ExprNumberLiteral { + range: 109..110, + value: Int( + 1, + ), + }, + ), + ), + upper: Some( + NumberLiteral( + ExprNumberLiteral { + range: 111..112, + value: Int( + 3, + ), + }, + ), + ), + step: Some( + Name( + ExprName { + range: 113..114, + id: Name("x"), + ctx: Load, + }, + ), + ), + }, + ), + ctx: Load, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 116..117, + value: NumberLiteral( + ExprNumberLiteral { + range: 116..117, + value: Int( + 1, + ), + }, + ), + }, + ), + ], + }, +) +``` +## Errors + + | +1 | # even after 3.9, an unparenthesized named expression is not allowed in a slice +2 | lst[x:=1:-1] + | ^^^^ Syntax Error: Unparenthesized named expression cannot be used here +3 | lst[1:x:=1] +4 | lst[1:3:x:=1] + | + + + | +1 | # even after 3.9, an unparenthesized named expression is not allowed in a slice +2 | lst[x:=1:-1] +3 | lst[1:x:=1] + | ^^ Syntax Error: Expected ']', found ':=' +4 | lst[1:3:x:=1] + | + + + | +1 | # even after 3.9, an unparenthesized named expression is not allowed in a slice +2 | lst[x:=1:-1] +3 | lst[1:x:=1] + | ^ Syntax Error: Expected a statement +4 | lst[1:3:x:=1] + | + + + | +1 | # even after 3.9, an unparenthesized named expression is not allowed in a slice +2 | lst[x:=1:-1] +3 | lst[1:x:=1] + | ^ Syntax Error: Expected a statement +4 | lst[1:3:x:=1] + | + + + | +2 | lst[x:=1:-1] +3 | lst[1:x:=1] +4 | lst[1:3:x:=1] + | ^^ Syntax Error: Expected ']', found ':=' + | + + + | +2 | lst[x:=1:-1] +3 | lst[1:x:=1] +4 | lst[1:3:x:=1] + | ^ Syntax Error: Expected a statement + | + + + | +2 | lst[x:=1:-1] +3 | lst[1:x:=1] +4 | lst[1:3:x:=1] + | ^ Syntax Error: Expected a statement + | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@named_expr_slice_parse_error.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@named_expr_slice_parse_error.py.snap new file mode 100644 index 0000000000..d0166af2f4 --- /dev/null +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@named_expr_slice_parse_error.py.snap @@ -0,0 +1,85 @@ +--- +source: crates/ruff_python_parser/tests/fixtures.rs +input_file: crates/ruff_python_parser/resources/inline/err/named_expr_slice_parse_error.py +--- +## AST + +``` +Module( + ModModule { + range: 0..130, + body: [ + Expr( + StmtExpr { + range: 117..129, + value: Subscript( + ExprSubscript { + range: 117..129, + value: Name( + ExprName { + range: 117..120, + id: Name("lst"), + ctx: Load, + }, + ), + slice: Slice( + ExprSlice { + range: 121..128, + lower: Some( + Named( + ExprNamed { + range: 121..125, + target: Name( + ExprName { + range: 121..122, + id: Name("x"), + ctx: Store, + }, + ), + value: NumberLiteral( + ExprNumberLiteral { + range: 124..125, + value: Int( + 1, + ), + }, + ), + }, + ), + ), + upper: Some( + UnaryOp( + ExprUnaryOp { + range: 126..128, + op: USub, + operand: NumberLiteral( + ExprNumberLiteral { + range: 127..128, + value: Int( + 1, + ), + }, + ), + }, + ), + ), + step: None, + }, + ), + ctx: Load, + }, + ), + }, + ), + ], + }, +) +``` +## Errors + + | +1 | # parse_options: {"target-version": "3.8"} +2 | # before 3.9, only emit the parse error, not the unsupported syntax error +3 | lst[x:=1:-1] + | ^^^^ Syntax Error: Unparenthesized named expression cannot be used here + | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@unparenthesized_named_expr_index_py38.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@unparenthesized_named_expr_index_py38.py.snap new file mode 100644 index 0000000000..55d81b949d --- /dev/null +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@unparenthesized_named_expr_index_py38.py.snap @@ -0,0 +1,60 @@ +--- +source: crates/ruff_python_parser/tests/fixtures.rs +input_file: crates/ruff_python_parser/resources/inline/err/unparenthesized_named_expr_index_py38.py +--- +## AST + +``` +Module( + ModModule { + range: 0..53, + body: [ + Expr( + StmtExpr { + range: 43..52, + value: Subscript( + ExprSubscript { + range: 43..52, + value: Name( + ExprName { + range: 43..46, + id: Name("lst"), + ctx: Load, + }, + ), + slice: Named( + ExprNamed { + range: 47..51, + target: Name( + ExprName { + range: 47..48, + id: Name("x"), + ctx: Store, + }, + ), + value: NumberLiteral( + ExprNumberLiteral { + range: 50..51, + value: Int( + 1, + ), + }, + ), + }, + ), + ctx: Load, + }, + ), + }, + ), + ], + }, +) +``` +## Unsupported Syntax Errors + + | +1 | # parse_options: {"target-version": "3.8"} +2 | lst[x:=1] + | ^^^^ Syntax Error: Cannot use unparenthesized assignment expression in a sequence index on Python 3.8 (syntax was added in Python 3.9) + | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@unparenthesized_named_expr_set_comp_py38.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@unparenthesized_named_expr_set_comp_py38.py.snap new file mode 100644 index 0000000000..a608f55121 --- /dev/null +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@unparenthesized_named_expr_set_comp_py38.py.snap @@ -0,0 +1,91 @@ +--- +source: crates/ruff_python_parser/tests/fixtures.rs +input_file: crates/ruff_python_parser/resources/inline/err/unparenthesized_named_expr_set_comp_py38.py +--- +## AST + +``` +Module( + ModModule { + range: 0..73, + body: [ + Expr( + StmtExpr { + range: 43..72, + value: SetComp( + ExprSetComp { + range: 43..72, + elt: Named( + ExprNamed { + range: 44..53, + target: Name( + ExprName { + range: 44..48, + id: Name("last"), + ctx: Store, + }, + ), + value: Name( + ExprName { + range: 52..53, + id: Name("x"), + ctx: Load, + }, + ), + }, + ), + generators: [ + Comprehension { + range: 54..71, + target: Name( + ExprName { + range: 58..59, + id: Name("x"), + ctx: Store, + }, + ), + iter: Call( + ExprCall { + range: 63..71, + func: Name( + ExprName { + range: 63..68, + id: Name("range"), + ctx: Load, + }, + ), + arguments: Arguments { + range: 68..71, + args: [ + NumberLiteral( + ExprNumberLiteral { + range: 69..70, + value: Int( + 3, + ), + }, + ), + ], + keywords: [], + }, + }, + ), + ifs: [], + is_async: false, + }, + ], + }, + ), + }, + ), + ], + }, +) +``` +## Unsupported Syntax Errors + + | +1 | # parse_options: {"target-version": "3.8"} +2 | {last := x for x in range(3)} + | ^^^^^^^^^ Syntax Error: Cannot use unparenthesized assignment expression as an element in a set comprehension on Python 3.8 (syntax was added in Python 3.9) + | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@unparenthesized_named_expr_set_literal_py38.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@unparenthesized_named_expr_set_literal_py38.py.snap new file mode 100644 index 0000000000..d03f0ebe48 --- /dev/null +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@unparenthesized_named_expr_set_literal_py38.py.snap @@ -0,0 +1,185 @@ +--- +source: crates/ruff_python_parser/tests/fixtures.rs +input_file: crates/ruff_python_parser/resources/inline/err/unparenthesized_named_expr_set_literal_py38.py +--- +## AST + +``` +Module( + ModModule { + range: 0..88, + body: [ + Expr( + StmtExpr { + range: 43..57, + value: Set( + ExprSet { + range: 43..57, + elts: [ + Named( + ExprNamed { + range: 44..50, + target: Name( + ExprName { + range: 44..45, + id: Name("x"), + ctx: Store, + }, + ), + value: NumberLiteral( + ExprNumberLiteral { + range: 49..50, + value: Int( + 1, + ), + }, + ), + }, + ), + NumberLiteral( + ExprNumberLiteral { + range: 52..53, + value: Int( + 2, + ), + }, + ), + NumberLiteral( + ExprNumberLiteral { + range: 55..56, + value: Int( + 3, + ), + }, + ), + ], + }, + ), + }, + ), + Expr( + StmtExpr { + range: 58..72, + value: Set( + ExprSet { + range: 58..72, + elts: [ + NumberLiteral( + ExprNumberLiteral { + range: 59..60, + value: Int( + 1, + ), + }, + ), + Named( + ExprNamed { + range: 62..68, + target: Name( + ExprName { + range: 62..63, + id: Name("x"), + ctx: Store, + }, + ), + value: NumberLiteral( + ExprNumberLiteral { + range: 67..68, + value: Int( + 2, + ), + }, + ), + }, + ), + NumberLiteral( + ExprNumberLiteral { + range: 70..71, + value: Int( + 3, + ), + }, + ), + ], + }, + ), + }, + ), + Expr( + StmtExpr { + range: 73..87, + value: Set( + ExprSet { + range: 73..87, + elts: [ + NumberLiteral( + ExprNumberLiteral { + range: 74..75, + value: Int( + 1, + ), + }, + ), + NumberLiteral( + ExprNumberLiteral { + range: 77..78, + value: Int( + 2, + ), + }, + ), + Named( + ExprNamed { + range: 80..86, + target: Name( + ExprName { + range: 80..81, + id: Name("x"), + ctx: Store, + }, + ), + value: NumberLiteral( + ExprNumberLiteral { + range: 85..86, + value: Int( + 3, + ), + }, + ), + }, + ), + ], + }, + ), + }, + ), + ], + }, +) +``` +## Unsupported Syntax Errors + + | +1 | # parse_options: {"target-version": "3.8"} +2 | {x := 1, 2, 3} + | ^^^^^^ Syntax Error: Cannot use unparenthesized assignment expression as an element in a set literal on Python 3.8 (syntax was added in Python 3.9) +3 | {1, x := 2, 3} +4 | {1, 2, x := 3} + | + + + | +1 | # parse_options: {"target-version": "3.8"} +2 | {x := 1, 2, 3} +3 | {1, x := 2, 3} + | ^^^^^^ Syntax Error: Cannot use unparenthesized assignment expression as an element in a set literal on Python 3.8 (syntax was added in Python 3.9) +4 | {1, 2, x := 3} + | + + + | +2 | {x := 1, 2, 3} +3 | {1, x := 2, 3} +4 | {1, 2, x := 3} + | ^^^^^^ Syntax Error: Cannot use unparenthesized assignment expression as an element in a set literal on Python 3.8 (syntax was added in Python 3.9) + | diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@parenthesized_named_expr_index_py38.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@parenthesized_named_expr_index_py38.py.snap new file mode 100644 index 0000000000..c8e09fd54a --- /dev/null +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@parenthesized_named_expr_index_py38.py.snap @@ -0,0 +1,53 @@ +--- +source: crates/ruff_python_parser/tests/fixtures.rs +input_file: crates/ruff_python_parser/resources/inline/ok/parenthesized_named_expr_index_py38.py +--- +## AST + +``` +Module( + ModModule { + range: 0..55, + body: [ + Expr( + StmtExpr { + range: 43..54, + value: Subscript( + ExprSubscript { + range: 43..54, + value: Name( + ExprName { + range: 43..46, + id: Name("lst"), + ctx: Load, + }, + ), + slice: Named( + ExprNamed { + range: 48..52, + target: Name( + ExprName { + range: 48..49, + id: Name("x"), + ctx: Store, + }, + ), + value: NumberLiteral( + ExprNumberLiteral { + range: 51..52, + value: Int( + 1, + ), + }, + ), + }, + ), + ctx: Load, + }, + ), + }, + ), + ], + }, +) +``` diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@parenthesized_named_expr_py38.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@parenthesized_named_expr_py38.py.snap new file mode 100644 index 0000000000..18a6f2406c --- /dev/null +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@parenthesized_named_expr_py38.py.snap @@ -0,0 +1,132 @@ +--- +source: crates/ruff_python_parser/tests/fixtures.rs +input_file: crates/ruff_python_parser/resources/inline/ok/parenthesized_named_expr_py38.py +--- +## AST + +``` +Module( + ModModule { + range: 0..92, + body: [ + Expr( + StmtExpr { + range: 43..59, + value: Set( + ExprSet { + range: 43..59, + elts: [ + Named( + ExprNamed { + range: 45..51, + target: Name( + ExprName { + range: 45..46, + id: Name("x"), + ctx: Store, + }, + ), + value: NumberLiteral( + ExprNumberLiteral { + range: 50..51, + value: Int( + 1, + ), + }, + ), + }, + ), + NumberLiteral( + ExprNumberLiteral { + range: 54..55, + value: Int( + 2, + ), + }, + ), + NumberLiteral( + ExprNumberLiteral { + range: 57..58, + value: Int( + 3, + ), + }, + ), + ], + }, + ), + }, + ), + Expr( + StmtExpr { + range: 60..91, + value: SetComp( + ExprSetComp { + range: 60..91, + elt: Named( + ExprNamed { + range: 62..71, + target: Name( + ExprName { + range: 62..66, + id: Name("last"), + ctx: Store, + }, + ), + value: Name( + ExprName { + range: 70..71, + id: Name("x"), + ctx: Load, + }, + ), + }, + ), + generators: [ + Comprehension { + range: 73..90, + target: Name( + ExprName { + range: 77..78, + id: Name("x"), + ctx: Store, + }, + ), + iter: Call( + ExprCall { + range: 82..90, + func: Name( + ExprName { + range: 82..87, + id: Name("range"), + ctx: Load, + }, + ), + arguments: Arguments { + range: 87..90, + args: [ + NumberLiteral( + ExprNumberLiteral { + range: 88..89, + value: Int( + 3, + ), + }, + ), + ], + keywords: [], + }, + }, + ), + ifs: [], + is_async: false, + }, + ], + }, + ), + }, + ), + ], + }, +) +``` diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@unparenthesized_named_expr_index_py39.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@unparenthesized_named_expr_index_py39.py.snap new file mode 100644 index 0000000000..e3329425de --- /dev/null +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@unparenthesized_named_expr_index_py39.py.snap @@ -0,0 +1,53 @@ +--- +source: crates/ruff_python_parser/tests/fixtures.rs +input_file: crates/ruff_python_parser/resources/inline/ok/unparenthesized_named_expr_index_py39.py +--- +## AST + +``` +Module( + ModModule { + range: 0..53, + body: [ + Expr( + StmtExpr { + range: 43..52, + value: Subscript( + ExprSubscript { + range: 43..52, + value: Name( + ExprName { + range: 43..46, + id: Name("lst"), + ctx: Load, + }, + ), + slice: Named( + ExprNamed { + range: 47..51, + target: Name( + ExprName { + range: 47..48, + id: Name("x"), + ctx: Store, + }, + ), + value: NumberLiteral( + ExprNumberLiteral { + range: 50..51, + value: Int( + 1, + ), + }, + ), + }, + ), + ctx: Load, + }, + ), + }, + ), + ], + }, +) +``` diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@unparenthesized_named_expr_py39.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@unparenthesized_named_expr_py39.py.snap new file mode 100644 index 0000000000..33d284c9b5 --- /dev/null +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@unparenthesized_named_expr_py39.py.snap @@ -0,0 +1,132 @@ +--- +source: crates/ruff_python_parser/tests/fixtures.rs +input_file: crates/ruff_python_parser/resources/inline/ok/unparenthesized_named_expr_py39.py +--- +## AST + +``` +Module( + ModModule { + range: 0..88, + body: [ + Expr( + StmtExpr { + range: 43..57, + value: Set( + ExprSet { + range: 43..57, + elts: [ + Named( + ExprNamed { + range: 44..50, + target: Name( + ExprName { + range: 44..45, + id: Name("x"), + ctx: Store, + }, + ), + value: NumberLiteral( + ExprNumberLiteral { + range: 49..50, + value: Int( + 1, + ), + }, + ), + }, + ), + NumberLiteral( + ExprNumberLiteral { + range: 52..53, + value: Int( + 2, + ), + }, + ), + NumberLiteral( + ExprNumberLiteral { + range: 55..56, + value: Int( + 3, + ), + }, + ), + ], + }, + ), + }, + ), + Expr( + StmtExpr { + range: 58..87, + value: SetComp( + ExprSetComp { + range: 58..87, + elt: Named( + ExprNamed { + range: 59..68, + target: Name( + ExprName { + range: 59..63, + id: Name("last"), + ctx: Store, + }, + ), + value: Name( + ExprName { + range: 67..68, + id: Name("x"), + ctx: Load, + }, + ), + }, + ), + generators: [ + Comprehension { + range: 69..86, + target: Name( + ExprName { + range: 73..74, + id: Name("x"), + ctx: Store, + }, + ), + iter: Call( + ExprCall { + range: 78..86, + func: Name( + ExprName { + range: 78..83, + id: Name("range"), + ctx: Load, + }, + ), + arguments: Arguments { + range: 83..86, + args: [ + NumberLiteral( + ExprNumberLiteral { + range: 84..85, + value: Int( + 3, + ), + }, + ), + ], + keywords: [], + }, + }, + ), + ifs: [], + is_async: false, + }, + ], + }, + ), + }, + ), + ], + }, +) +```