From e92ca5fbbf9880a0806999b1d8c85cc56e3e3b8c Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Mon, 3 Mar 2025 15:18:37 -0500 Subject: [PATCH 01/14] [syntax-errors] Parenthesized keyword arguments after Python 3.8 Summary -- Unlike the other syntax errors detected so far, parenthesized keyword arguments are only allowed *before* 3.8. It sounds like they were only accidentally allowed before that [^1]. I wanted to mark the whole parenthesized range instead of just the kwarg name, so I moved the `ParsedExpr::is_parenthesized` bool to an `Option` and replaced it with an `is_parenthesized` method. Test Plan -- Inline tests. [^1]: https://github.com/python/cpython/issues/78822 --- .../inline/err/parenthesized_kwarg_py38.py | 2 + .../inline/ok/parenthesized_kwarg_py37.py | 2 + crates/ruff_python_parser/src/error.rs | 11 +++- .../src/parser/expression.rs | 62 ++++++++++++++----- .../src/parser/statement.rs | 6 +- ...id_syntax@parenthesized_kwarg_py38.py.snap | 62 +++++++++++++++++++ ...id_syntax@parenthesized_kwarg_py37.py.snap | 55 ++++++++++++++++ 7 files changed, 182 insertions(+), 18 deletions(-) create mode 100644 crates/ruff_python_parser/resources/inline/err/parenthesized_kwarg_py38.py create mode 100644 crates/ruff_python_parser/resources/inline/ok/parenthesized_kwarg_py37.py create mode 100644 crates/ruff_python_parser/tests/snapshots/invalid_syntax@parenthesized_kwarg_py38.py.snap create mode 100644 crates/ruff_python_parser/tests/snapshots/valid_syntax@parenthesized_kwarg_py37.py.snap diff --git a/crates/ruff_python_parser/resources/inline/err/parenthesized_kwarg_py38.py b/crates/ruff_python_parser/resources/inline/err/parenthesized_kwarg_py38.py new file mode 100644 index 0000000000000..00a1f65c3f3bb --- /dev/null +++ b/crates/ruff_python_parser/resources/inline/err/parenthesized_kwarg_py38.py @@ -0,0 +1,2 @@ +# parse_options: {"target-version": "3.8"} +f((a)=1) diff --git a/crates/ruff_python_parser/resources/inline/ok/parenthesized_kwarg_py37.py b/crates/ruff_python_parser/resources/inline/ok/parenthesized_kwarg_py37.py new file mode 100644 index 0000000000000..6b3d964f7832e --- /dev/null +++ b/crates/ruff_python_parser/resources/inline/ok/parenthesized_kwarg_py37.py @@ -0,0 +1,2 @@ +# parse_options: {"target-version": "3.7"} +f((a)=1) diff --git a/crates/ruff_python_parser/src/error.rs b/crates/ruff_python_parser/src/error.rs index 191068c2a1128..b3d4f8d0f0d01 100644 --- a/crates/ruff_python_parser/src/error.rs +++ b/crates/ruff_python_parser/src/error.rs @@ -449,6 +449,7 @@ pub enum UnsupportedSyntaxErrorKind { Match, Walrus, ExceptStar, + ParenKwargName, } impl Display for UnsupportedSyntaxError { @@ -457,10 +458,16 @@ impl Display for UnsupportedSyntaxError { UnsupportedSyntaxErrorKind::Match => "`match` statement", UnsupportedSyntaxErrorKind::Walrus => "named assignment expression (`:=`)", UnsupportedSyntaxErrorKind::ExceptStar => "`except*`", + UnsupportedSyntaxErrorKind::ParenKwargName => "parenthesized keyword argument name", }; + let changed = match self.kind { + UnsupportedSyntaxErrorKind::ParenKwargName => "removed", + _ => "added", + }; + write!( f, - "Cannot use {kind} on Python {} (syntax was added in Python {})", + "Cannot use {kind} on Python {} (syntax was {changed} in Python {})", self.target_version, self.kind.minimum_version(), ) @@ -474,6 +481,8 @@ impl UnsupportedSyntaxErrorKind { UnsupportedSyntaxErrorKind::Match => PythonVersion::PY310, UnsupportedSyntaxErrorKind::Walrus => PythonVersion::PY38, UnsupportedSyntaxErrorKind::ExceptStar => PythonVersion::PY311, + // This is actually a *maximum* version in this case + UnsupportedSyntaxErrorKind::ParenKwargName => PythonVersion::PY38, } } } diff --git a/crates/ruff_python_parser/src/parser/expression.rs b/crates/ruff_python_parser/src/parser/expression.rs index e37fb835d766a..639bbb68336b0 100644 --- a/crates/ruff_python_parser/src/parser/expression.rs +++ b/crates/ruff_python_parser/src/parser/expression.rs @@ -7,7 +7,7 @@ use rustc_hash::{FxBuildHasher, FxHashSet}; use ruff_python_ast::name::Name; use ruff_python_ast::{ self as ast, BoolOp, CmpOp, ConversionFlag, Expr, ExprContext, FStringElement, FStringElements, - IpyEscapeKind, Number, Operator, StringFlags, UnaryOp, + IpyEscapeKind, Number, Operator, PythonVersion, StringFlags, UnaryOp, }; use ruff_text_size::{Ranged, TextLen, TextRange, TextSize}; @@ -16,7 +16,9 @@ use crate::parser::{helpers, FunctionKind, Parser}; use crate::string::{parse_fstring_literal_element, parse_string_literal, StringType}; use crate::token::{TokenKind, TokenValue}; use crate::token_set::TokenSet; -use crate::{FStringErrorType, Mode, ParseErrorType, UnsupportedSyntaxErrorKind}; +use crate::{ + FStringErrorType, Mode, ParseErrorType, UnsupportedSyntaxError, UnsupportedSyntaxErrorKind, +}; use super::{FStringElementsKind, Parenthesized, RecoveryContextKind}; @@ -405,7 +407,7 @@ impl<'src> Parser<'src> { ParsedExpr { expr: self.parse_postfix_expression(lhs.expr, start), - is_parenthesized: lhs.is_parenthesized, + parenthesized_range: lhs.parenthesized_range, } } @@ -419,7 +421,7 @@ impl<'src> Parser<'src> { fn parse_expression_with_bitwise_or_precedence(&mut self) -> ParsedExpr { let parsed_expr = self.parse_conditional_expression_or_higher(); - if parsed_expr.is_parenthesized { + if parsed_expr.is_parenthesized() { // Parentheses resets the precedence, so we don't need to validate it. return parsed_expr; } @@ -703,7 +705,34 @@ impl<'src> Parser<'src> { if parser.eat(TokenKind::Equal) { seen_keyword_argument = true; - let arg = if let Expr::Name(ident_expr) = parsed_expr.expr { + let arg = if let ParsedExpr { + expr: Expr::Name(ident_expr), + parenthesized_range, + } = parsed_expr + { + // test_ok parenthesized_kwarg_py37 + // # parse_options: {"target-version": "3.7"} + // f((a)=1) + + // test_err parenthesized_kwarg_py38 + // # parse_options: {"target-version": "3.8"} + // f((a)=1) + + // Note that this is a greater than check unlike inside + // `Parser::add_unsupported_syntax_error`. Parenthesized kwarg names were no + // longer allowed in 3.8 + if let Some(range) = parenthesized_range { + if parser.options.target_version > PythonVersion::PY37 { + parser + .unsupported_syntax_errors + .push(UnsupportedSyntaxError { + kind: UnsupportedSyntaxErrorKind::ParenKwargName, + range, + target_version: parser.options.target_version, + }); + } + } + ast::Identifier { id: ident_expr.id, range: ident_expr.range, @@ -857,7 +886,7 @@ impl<'src> Parser<'src> { return lower.expr; } - if !lower.is_parenthesized { + if !lower.is_parenthesized() { match lower.expr { Expr::Starred(_) => { self.add_error(ParseErrorType::InvalidStarredExpressionUsage, &lower); @@ -1403,7 +1432,7 @@ impl<'src> Parser<'src> { // f"{*yield x}" let value = self.parse_expression_list(ExpressionContext::yield_or_starred_bitwise_or()); - if !value.is_parenthesized && value.expr.is_lambda_expr() { + if !value.is_parenthesized() && value.expr.is_lambda_expr() { // TODO(dhruvmanila): This requires making some changes in lambda expression // parsing logic to handle the emitted `FStringMiddle` token in case the // lambda expression is not parenthesized. @@ -1614,7 +1643,7 @@ impl<'src> Parser<'src> { TokenKind::Colon => { // Now, we know that it's either a dictionary expression or a dictionary comprehension. // In either case, the key is limited to an `expression`. - if !key_or_element.is_parenthesized { + if !key_or_element.is_parenthesized() { match key_or_element.expr { Expr::Starred(_) => self.add_error( ParseErrorType::InvalidStarredExpressionUsage, @@ -1693,7 +1722,7 @@ impl<'src> Parser<'src> { ParsedExpr { expr: tuple.into(), - is_parenthesized: false, + parenthesized_range: None, } } TokenKind::Async | TokenKind::For => { @@ -1713,7 +1742,7 @@ impl<'src> Parser<'src> { ParsedExpr { expr: generator, - is_parenthesized: false, + parenthesized_range: None, } } _ => { @@ -1724,7 +1753,7 @@ impl<'src> Parser<'src> { self.expect(TokenKind::Rpar); - parsed_expr.is_parenthesized = true; + parsed_expr.parenthesized_range = Some(self.node_range(start)); parsed_expr } } @@ -2330,13 +2359,18 @@ impl<'src> Parser<'src> { #[derive(Debug)] pub(super) struct ParsedExpr { pub(super) expr: Expr, - pub(super) is_parenthesized: bool, + pub(super) parenthesized_range: Option, } impl ParsedExpr { #[inline] pub(super) const fn is_unparenthesized_starred_expr(&self) -> bool { - !self.is_parenthesized && self.expr.is_starred_expr() + !self.is_parenthesized() && self.expr.is_starred_expr() + } + + #[inline] + pub(super) const fn is_parenthesized(&self) -> bool { + self.parenthesized_range.is_some() } } @@ -2345,7 +2379,7 @@ impl From for ParsedExpr { fn from(expr: Expr) -> Self { ParsedExpr { expr, - is_parenthesized: false, + parenthesized_range: None, } } } diff --git a/crates/ruff_python_parser/src/parser/statement.rs b/crates/ruff_python_parser/src/parser/statement.rs index 5f7f870a00c2f..9bf79e59ad125 100644 --- a/crates/ruff_python_parser/src/parser/statement.rs +++ b/crates/ruff_python_parser/src/parser/statement.rs @@ -1005,7 +1005,7 @@ impl<'src> Parser<'src> { IpyEscapeKind::Help }; - if parsed_expr.is_parenthesized { + if parsed_expr.is_parenthesized() { let token_range = self.node_range(start); self.add_error( ParseErrorType::OtherError( @@ -1136,7 +1136,7 @@ impl<'src> Parser<'src> { // (a): int // a.b: int // a[0]: int - let simple = target.is_name_expr() && !target.is_parenthesized; + let simple = target.is_name_expr() && !target.is_parenthesized(); // test_err ann_assign_stmt_invalid_annotation // x: *int = 1 @@ -2155,7 +2155,7 @@ impl<'src> Parser<'src> { .then(|| Box::new(self.parse_with_item_optional_vars().expr)); ParsedWithItem { - is_parenthesized: context_expr.is_parenthesized, + is_parenthesized: context_expr.is_parenthesized(), item: ast::WithItem { range: self.node_range(start), context_expr: context_expr.expr, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@parenthesized_kwarg_py38.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@parenthesized_kwarg_py38.py.snap new file mode 100644 index 0000000000000..2088153085d06 --- /dev/null +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@parenthesized_kwarg_py38.py.snap @@ -0,0 +1,62 @@ +--- +source: crates/ruff_python_parser/tests/fixtures.rs +input_file: crates/ruff_python_parser/resources/inline/err/parenthesized_kwarg_py38.py +--- +## AST + +``` +Module( + ModModule { + range: 0..52, + body: [ + Expr( + StmtExpr { + range: 43..51, + value: Call( + ExprCall { + range: 43..51, + func: Name( + ExprName { + range: 43..44, + id: Name("f"), + ctx: Load, + }, + ), + arguments: Arguments { + range: 44..51, + args: [], + keywords: [ + Keyword { + range: 45..50, + arg: Some( + Identifier { + id: Name("a"), + range: 46..47, + }, + ), + value: NumberLiteral( + ExprNumberLiteral { + range: 49..50, + value: Int( + 1, + ), + }, + ), + }, + ], + }, + }, + ), + }, + ), + ], + }, +) +``` +## Unsupported Syntax Errors + + | +1 | # parse_options: {"target-version": "3.8"} +2 | f((a)=1) + | ^^^ Syntax Error: Cannot use parenthesized keyword argument name on Python 3.8 (syntax was removed in Python 3.8) + | diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@parenthesized_kwarg_py37.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@parenthesized_kwarg_py37.py.snap new file mode 100644 index 0000000000000..bb42e78d8c5a9 --- /dev/null +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@parenthesized_kwarg_py37.py.snap @@ -0,0 +1,55 @@ +--- +source: crates/ruff_python_parser/tests/fixtures.rs +input_file: crates/ruff_python_parser/resources/inline/ok/parenthesized_kwarg_py37.py +--- +## AST + +``` +Module( + ModModule { + range: 0..52, + body: [ + Expr( + StmtExpr { + range: 43..51, + value: Call( + ExprCall { + range: 43..51, + func: Name( + ExprName { + range: 43..44, + id: Name("f"), + ctx: Load, + }, + ), + arguments: Arguments { + range: 44..51, + args: [], + keywords: [ + Keyword { + range: 45..50, + arg: Some( + Identifier { + id: Name("a"), + range: 46..47, + }, + ), + value: NumberLiteral( + ExprNumberLiteral { + range: 49..50, + value: Int( + 1, + ), + }, + ), + }, + ], + }, + }, + ), + }, + ), + ], + }, +) +``` From 19fc13ad9cb8bb2f7edfee2ebcfd26f02c10fae9 Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Tue, 4 Mar 2025 15:09:38 -0500 Subject: [PATCH 02/14] ParenKwargName -> ParenthesizedKeywordArgumentName --- crates/ruff_python_parser/src/error.rs | 10 ++++++---- crates/ruff_python_parser/src/parser/expression.rs | 2 +- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/crates/ruff_python_parser/src/error.rs b/crates/ruff_python_parser/src/error.rs index b3d4f8d0f0d01..abcfa7aca5916 100644 --- a/crates/ruff_python_parser/src/error.rs +++ b/crates/ruff_python_parser/src/error.rs @@ -449,7 +449,7 @@ pub enum UnsupportedSyntaxErrorKind { Match, Walrus, ExceptStar, - ParenKwargName, + ParenthesizedKeywordArgumentName, } impl Display for UnsupportedSyntaxError { @@ -458,10 +458,12 @@ impl Display for UnsupportedSyntaxError { UnsupportedSyntaxErrorKind::Match => "`match` statement", UnsupportedSyntaxErrorKind::Walrus => "named assignment expression (`:=`)", UnsupportedSyntaxErrorKind::ExceptStar => "`except*`", - UnsupportedSyntaxErrorKind::ParenKwargName => "parenthesized keyword argument name", + UnsupportedSyntaxErrorKind::ParenthesizedKeywordArgumentName => { + "parenthesized keyword argument name" + } }; let changed = match self.kind { - UnsupportedSyntaxErrorKind::ParenKwargName => "removed", + UnsupportedSyntaxErrorKind::ParenthesizedKeywordArgumentName => "removed", _ => "added", }; @@ -482,7 +484,7 @@ impl UnsupportedSyntaxErrorKind { UnsupportedSyntaxErrorKind::Walrus => PythonVersion::PY38, UnsupportedSyntaxErrorKind::ExceptStar => PythonVersion::PY311, // This is actually a *maximum* version in this case - UnsupportedSyntaxErrorKind::ParenKwargName => PythonVersion::PY38, + UnsupportedSyntaxErrorKind::ParenthesizedKeywordArgumentName => PythonVersion::PY38, } } } diff --git a/crates/ruff_python_parser/src/parser/expression.rs b/crates/ruff_python_parser/src/parser/expression.rs index 639bbb68336b0..ccae025708d23 100644 --- a/crates/ruff_python_parser/src/parser/expression.rs +++ b/crates/ruff_python_parser/src/parser/expression.rs @@ -726,7 +726,7 @@ impl<'src> Parser<'src> { parser .unsupported_syntax_errors .push(UnsupportedSyntaxError { - kind: UnsupportedSyntaxErrorKind::ParenKwargName, + kind: UnsupportedSyntaxErrorKind::ParenthesizedKeywordArgumentName, range, target_version: parser.options.target_version, }); From b668be7e967d49f333f39433fcbf4e14936ac14f Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Tue, 4 Mar 2025 15:42:10 -0500 Subject: [PATCH 03/14] try is_unsupported approach --- crates/ruff_python_parser/src/error.rs | 65 +++++++++++++++---- .../src/parser/expression.rs | 19 ++---- crates/ruff_python_parser/src/parser/mod.rs | 2 +- 3 files changed, 59 insertions(+), 27 deletions(-) diff --git a/crates/ruff_python_parser/src/error.rs b/crates/ruff_python_parser/src/error.rs index abcfa7aca5916..c7f6695239d96 100644 --- a/crates/ruff_python_parser/src/error.rs +++ b/crates/ruff_python_parser/src/error.rs @@ -462,29 +462,68 @@ impl Display for UnsupportedSyntaxError { "parenthesized keyword argument name" } }; - let changed = match self.kind { - UnsupportedSyntaxErrorKind::ParenthesizedKeywordArgumentName => "removed", - _ => "added", - }; + + let (changed, changed_version) = self.kind.changed_version(); write!( f, - "Cannot use {kind} on Python {} (syntax was {changed} in Python {})", + "Cannot use {kind} on Python {} (syntax was {changed} in Python {changed_version})", self.target_version, - self.kind.minimum_version(), ) } } +/// Represents the kind of change in Python syntax between versions. +/// +/// Most changes so far have been additions (e.g. `match` and `type` statements), but others are +/// removals (e.g. parenthesized keyword argument names). +enum Change { + Added, + Removed, +} + +impl Display for Change { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Change::Added => f.write_str("added"), + Change::Removed => f.write_str("removed"), + } + } +} + impl UnsupportedSyntaxErrorKind { - /// The earliest allowed version for the syntax associated with this error. - pub const fn minimum_version(&self) -> PythonVersion { + /// Returns the Python version when the syntax associated with this error was changed, and the + /// type of [`Change`] (added or removed). + const fn changed_version(&self) -> (Change, PythonVersion) { match self { - UnsupportedSyntaxErrorKind::Match => PythonVersion::PY310, - UnsupportedSyntaxErrorKind::Walrus => PythonVersion::PY38, - UnsupportedSyntaxErrorKind::ExceptStar => PythonVersion::PY311, - // This is actually a *maximum* version in this case - UnsupportedSyntaxErrorKind::ParenthesizedKeywordArgumentName => PythonVersion::PY38, + UnsupportedSyntaxErrorKind::Match => (Change::Added, PythonVersion::PY310), + UnsupportedSyntaxErrorKind::Walrus => (Change::Added, PythonVersion::PY38), + UnsupportedSyntaxErrorKind::ExceptStar => (Change::Added, PythonVersion::PY311), + UnsupportedSyntaxErrorKind::ParenthesizedKeywordArgumentName => { + (Change::Removed, PythonVersion::PY38) + } + } + } + + /// Returns whether or not this kind of syntax is unsupported on `target_version`. + /// + /// ## Examples + /// + /// ``` + /// assert!(UnsupportedSyntaxError::Match.is_unsupported(PythonVersion::PY39)); + /// assert!(!UnsupportedSyntaxError::Match.is_unsupported(PythonVersion::PY310)); + /// assert!(UnsupportedSyntaxError::ParenthesizedKeywordArgumentName.is_unsupported(PythonVersion::PY38)); + /// assert!(!UnsupportedSyntaxError::ParenthesizedKeywordArgumentName.is_unsupported(PythonVersion::PY37)); + /// ``` + pub(crate) fn is_unsupported(&self, target_version: PythonVersion) -> bool { + let (_, version) = self.changed_version(); + match self { + UnsupportedSyntaxErrorKind::Match + | UnsupportedSyntaxErrorKind::Walrus + | UnsupportedSyntaxErrorKind::ExceptStar => target_version < version, + UnsupportedSyntaxErrorKind::ParenthesizedKeywordArgumentName => { + target_version >= version + } } } } diff --git a/crates/ruff_python_parser/src/parser/expression.rs b/crates/ruff_python_parser/src/parser/expression.rs index ccae025708d23..76b1385edd1c1 100644 --- a/crates/ruff_python_parser/src/parser/expression.rs +++ b/crates/ruff_python_parser/src/parser/expression.rs @@ -7,7 +7,7 @@ use rustc_hash::{FxBuildHasher, FxHashSet}; use ruff_python_ast::name::Name; use ruff_python_ast::{ self as ast, BoolOp, CmpOp, ConversionFlag, Expr, ExprContext, FStringElement, FStringElements, - IpyEscapeKind, Number, Operator, PythonVersion, StringFlags, UnaryOp, + IpyEscapeKind, Number, Operator, StringFlags, UnaryOp, }; use ruff_text_size::{Ranged, TextLen, TextRange, TextSize}; @@ -16,9 +16,7 @@ use crate::parser::{helpers, FunctionKind, Parser}; use crate::string::{parse_fstring_literal_element, parse_string_literal, StringType}; use crate::token::{TokenKind, TokenValue}; use crate::token_set::TokenSet; -use crate::{ - FStringErrorType, Mode, ParseErrorType, UnsupportedSyntaxError, UnsupportedSyntaxErrorKind, -}; +use crate::{FStringErrorType, Mode, ParseErrorType, UnsupportedSyntaxErrorKind}; use super::{FStringElementsKind, Parenthesized, RecoveryContextKind}; @@ -722,15 +720,10 @@ impl<'src> Parser<'src> { // `Parser::add_unsupported_syntax_error`. Parenthesized kwarg names were no // longer allowed in 3.8 if let Some(range) = parenthesized_range { - if parser.options.target_version > PythonVersion::PY37 { - parser - .unsupported_syntax_errors - .push(UnsupportedSyntaxError { - kind: UnsupportedSyntaxErrorKind::ParenthesizedKeywordArgumentName, - range, - target_version: parser.options.target_version, - }); - } + parser.add_unsupported_syntax_error( + UnsupportedSyntaxErrorKind::ParenthesizedKeywordArgumentName, + range, + ); } ast::Identifier { diff --git a/crates/ruff_python_parser/src/parser/mod.rs b/crates/ruff_python_parser/src/parser/mod.rs index 78bf047d463e5..5ad6557d92098 100644 --- a/crates/ruff_python_parser/src/parser/mod.rs +++ b/crates/ruff_python_parser/src/parser/mod.rs @@ -441,7 +441,7 @@ impl<'src> Parser<'src> { /// Add an [`UnsupportedSyntaxError`] with the given [`UnsupportedSyntaxErrorKind`] and /// [`TextRange`] if its minimum version is less than [`Parser::target_version`]. fn add_unsupported_syntax_error(&mut self, kind: UnsupportedSyntaxErrorKind, range: TextRange) { - if self.options.target_version < kind.minimum_version() { + if kind.is_unsupported(self.options.target_version) { self.unsupported_syntax_errors.push(UnsupportedSyntaxError { kind, range, From 9052e4b7f755f00c1a97517a5196750cf0c7f578 Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Tue, 4 Mar 2025 15:54:00 -0500 Subject: [PATCH 04/14] clippy --- crates/ruff_python_parser/src/error.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/ruff_python_parser/src/error.rs b/crates/ruff_python_parser/src/error.rs index ead078ebda127..04caf0cf0d844 100644 --- a/crates/ruff_python_parser/src/error.rs +++ b/crates/ruff_python_parser/src/error.rs @@ -500,7 +500,7 @@ impl Display for Change { impl UnsupportedSyntaxErrorKind { /// Returns the Python version when the syntax associated with this error was changed, and the /// type of [`Change`] (added or removed). - const fn changed_version(&self) -> (Change, PythonVersion) { + const fn changed_version(self) -> (Change, PythonVersion) { match self { UnsupportedSyntaxErrorKind::Match => (Change::Added, PythonVersion::PY310), UnsupportedSyntaxErrorKind::Walrus => (Change::Added, PythonVersion::PY38), @@ -523,7 +523,7 @@ impl UnsupportedSyntaxErrorKind { /// assert!(UnsupportedSyntaxError::ParenthesizedKeywordArgumentName.is_unsupported(PythonVersion::PY38)); /// assert!(!UnsupportedSyntaxError::ParenthesizedKeywordArgumentName.is_unsupported(PythonVersion::PY37)); /// ``` - pub(crate) fn is_unsupported(&self, target_version: PythonVersion) -> bool { + pub(crate) fn is_unsupported(self, target_version: PythonVersion) -> bool { let (_, version) = self.changed_version(); match self { UnsupportedSyntaxErrorKind::Match From 9e04d15c32a97fb0269fa909a18dc50909ee3c0f Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Tue, 4 Mar 2025 16:15:33 -0500 Subject: [PATCH 05/14] remove docs that break tests --- crates/ruff_python_parser/src/error.rs | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/crates/ruff_python_parser/src/error.rs b/crates/ruff_python_parser/src/error.rs index 04caf0cf0d844..18f31c7c9758a 100644 --- a/crates/ruff_python_parser/src/error.rs +++ b/crates/ruff_python_parser/src/error.rs @@ -436,11 +436,6 @@ pub struct UnsupportedSyntaxError { pub kind: UnsupportedSyntaxErrorKind, pub range: TextRange, /// The target [`PythonVersion`] for which this error was detected. - /// - /// This is different from the version reported by the - /// [`minimum_version`](UnsupportedSyntaxErrorKind::minimum_version) method, which is the - /// earliest allowed version for this piece of syntax. The `target_version` is primarily used - /// for user-facing error messages. pub target_version: PythonVersion, } @@ -514,15 +509,6 @@ impl UnsupportedSyntaxErrorKind { } /// Returns whether or not this kind of syntax is unsupported on `target_version`. - /// - /// ## Examples - /// - /// ``` - /// assert!(UnsupportedSyntaxError::Match.is_unsupported(PythonVersion::PY39)); - /// assert!(!UnsupportedSyntaxError::Match.is_unsupported(PythonVersion::PY310)); - /// assert!(UnsupportedSyntaxError::ParenthesizedKeywordArgumentName.is_unsupported(PythonVersion::PY38)); - /// assert!(!UnsupportedSyntaxError::ParenthesizedKeywordArgumentName.is_unsupported(PythonVersion::PY37)); - /// ``` pub(crate) fn is_unsupported(self, target_version: PythonVersion) -> bool { let (_, version) = self.changed_version(); match self { From e717a25b93082ed14653161f6e68b0f8f6230d75 Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Tue, 4 Mar 2025 16:28:59 -0500 Subject: [PATCH 06/14] delete outdated comment --- crates/ruff_python_parser/src/parser/expression.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/crates/ruff_python_parser/src/parser/expression.rs b/crates/ruff_python_parser/src/parser/expression.rs index 76b1385edd1c1..feb9a13d8b89e 100644 --- a/crates/ruff_python_parser/src/parser/expression.rs +++ b/crates/ruff_python_parser/src/parser/expression.rs @@ -716,9 +716,6 @@ impl<'src> Parser<'src> { // # parse_options: {"target-version": "3.8"} // f((a)=1) - // Note that this is a greater than check unlike inside - // `Parser::add_unsupported_syntax_error`. Parenthesized kwarg names were no - // longer allowed in 3.8 if let Some(range) = parenthesized_range { parser.add_unsupported_syntax_error( UnsupportedSyntaxErrorKind::ParenthesizedKeywordArgumentName, From 866ce592f44ba6a44309ca6274e1cc8dcd209994 Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Tue, 4 Mar 2025 16:29:42 -0500 Subject: [PATCH 07/14] add test cases with whitespace --- .../inline/err/parenthesized_kwarg_py38.py | 2 + .../src/parser/expression.rs | 2 + ...id_syntax@parenthesized_kwarg_py38.py.snap | 101 +++++++++++++++++- 3 files changed, 104 insertions(+), 1 deletion(-) diff --git a/crates/ruff_python_parser/resources/inline/err/parenthesized_kwarg_py38.py b/crates/ruff_python_parser/resources/inline/err/parenthesized_kwarg_py38.py index 00a1f65c3f3bb..bb74eb0b7b15b 100644 --- a/crates/ruff_python_parser/resources/inline/err/parenthesized_kwarg_py38.py +++ b/crates/ruff_python_parser/resources/inline/err/parenthesized_kwarg_py38.py @@ -1,2 +1,4 @@ # parse_options: {"target-version": "3.8"} f((a)=1) +f((a) = 1) +f( ( a ) = 1) diff --git a/crates/ruff_python_parser/src/parser/expression.rs b/crates/ruff_python_parser/src/parser/expression.rs index feb9a13d8b89e..8e5e3ea66d325 100644 --- a/crates/ruff_python_parser/src/parser/expression.rs +++ b/crates/ruff_python_parser/src/parser/expression.rs @@ -715,6 +715,8 @@ impl<'src> Parser<'src> { // test_err parenthesized_kwarg_py38 // # parse_options: {"target-version": "3.8"} // f((a)=1) + // f((a) = 1) + // f( ( a ) = 1) if let Some(range) = parenthesized_range { parser.add_unsupported_syntax_error( diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@parenthesized_kwarg_py38.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@parenthesized_kwarg_py38.py.snap index 2088153085d06..4b55ae1a8a6ff 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@parenthesized_kwarg_py38.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@parenthesized_kwarg_py38.py.snap @@ -7,7 +7,7 @@ input_file: crates/ruff_python_parser/resources/inline/err/parenthesized_kwarg_p ``` Module( ModModule { - range: 0..52, + range: 0..77, body: [ Expr( StmtExpr { @@ -49,6 +49,86 @@ Module( ), }, ), + Expr( + StmtExpr { + range: 52..62, + value: Call( + ExprCall { + range: 52..62, + func: Name( + ExprName { + range: 52..53, + id: Name("f"), + ctx: Load, + }, + ), + arguments: Arguments { + range: 53..62, + args: [], + keywords: [ + Keyword { + range: 54..61, + arg: Some( + Identifier { + id: Name("a"), + range: 55..56, + }, + ), + value: NumberLiteral( + ExprNumberLiteral { + range: 60..61, + value: Int( + 1, + ), + }, + ), + }, + ], + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 63..76, + value: Call( + ExprCall { + range: 63..76, + func: Name( + ExprName { + range: 63..64, + id: Name("f"), + ctx: Load, + }, + ), + arguments: Arguments { + range: 64..76, + args: [], + keywords: [ + Keyword { + range: 66..75, + arg: Some( + Identifier { + id: Name("a"), + range: 68..69, + }, + ), + value: NumberLiteral( + ExprNumberLiteral { + range: 74..75, + value: Int( + 1, + ), + }, + ), + }, + ], + }, + }, + ), + }, + ), ], }, ) @@ -59,4 +139,23 @@ Module( 1 | # parse_options: {"target-version": "3.8"} 2 | f((a)=1) | ^^^ Syntax Error: Cannot use parenthesized keyword argument name on Python 3.8 (syntax was removed in Python 3.8) +3 | f((a) = 1) +4 | f( ( a ) = 1) + | + + + | +1 | # parse_options: {"target-version": "3.8"} +2 | f((a)=1) +3 | f((a) = 1) + | ^^^ Syntax Error: Cannot use parenthesized keyword argument name on Python 3.8 (syntax was removed in Python 3.8) +4 | f( ( a ) = 1) + | + + + | +2 | f((a)=1) +3 | f((a) = 1) +4 | f( ( a ) = 1) + | ^^^^^ Syntax Error: Cannot use parenthesized keyword argument name on Python 3.8 (syntax was removed in Python 3.8) | From 329652f7e07f674350c64b67a8a7ff6f797c8352 Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Tue, 4 Mar 2025 16:42:27 -0500 Subject: [PATCH 08/14] only store the end of the parenthesized range --- .../src/parser/expression.rs | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/crates/ruff_python_parser/src/parser/expression.rs b/crates/ruff_python_parser/src/parser/expression.rs index 8e5e3ea66d325..9408b66ffaa2b 100644 --- a/crates/ruff_python_parser/src/parser/expression.rs +++ b/crates/ruff_python_parser/src/parser/expression.rs @@ -405,7 +405,7 @@ impl<'src> Parser<'src> { ParsedExpr { expr: self.parse_postfix_expression(lhs.expr, start), - parenthesized_range: lhs.parenthesized_range, + end_parenthesis: lhs.end_parenthesis, } } @@ -705,7 +705,7 @@ impl<'src> Parser<'src> { seen_keyword_argument = true; let arg = if let ParsedExpr { expr: Expr::Name(ident_expr), - parenthesized_range, + end_parenthesis, } = parsed_expr { // test_ok parenthesized_kwarg_py37 @@ -718,10 +718,10 @@ impl<'src> Parser<'src> { // f((a) = 1) // f( ( a ) = 1) - if let Some(range) = parenthesized_range { + if let Some(end) = end_parenthesis { parser.add_unsupported_syntax_error( UnsupportedSyntaxErrorKind::ParenthesizedKeywordArgumentName, - range, + TextRange::new(start, end), ); } @@ -1714,7 +1714,7 @@ impl<'src> Parser<'src> { ParsedExpr { expr: tuple.into(), - parenthesized_range: None, + end_parenthesis: None, } } TokenKind::Async | TokenKind::For => { @@ -1734,7 +1734,7 @@ impl<'src> Parser<'src> { ParsedExpr { expr: generator, - parenthesized_range: None, + end_parenthesis: None, } } _ => { @@ -1745,7 +1745,7 @@ impl<'src> Parser<'src> { self.expect(TokenKind::Rpar); - parsed_expr.parenthesized_range = Some(self.node_range(start)); + parsed_expr.end_parenthesis = Some(self.node_range(start).end()); parsed_expr } } @@ -2351,7 +2351,8 @@ impl<'src> Parser<'src> { #[derive(Debug)] pub(super) struct ParsedExpr { pub(super) expr: Expr, - pub(super) parenthesized_range: Option, + /// Contains the location of the closing parenthesis, if the expression is parenthesized + pub(super) end_parenthesis: Option, } impl ParsedExpr { @@ -2362,7 +2363,7 @@ impl ParsedExpr { #[inline] pub(super) const fn is_parenthesized(&self) -> bool { - self.parenthesized_range.is_some() + self.end_parenthesis.is_some() } } @@ -2371,7 +2372,7 @@ impl From for ParsedExpr { fn from(expr: Expr) -> Self { ParsedExpr { expr, - parenthesized_range: None, + end_parenthesis: None, } } } From 0e0d37551640416810d24b90f9556023f779367b Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Wed, 5 Mar 2025 08:20:29 -0500 Subject: [PATCH 09/14] implement is_unsupported via changed_version --- crates/ruff_python_parser/src/error.rs | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/crates/ruff_python_parser/src/error.rs b/crates/ruff_python_parser/src/error.rs index 18f31c7c9758a..92b43ce671cd4 100644 --- a/crates/ruff_python_parser/src/error.rs +++ b/crates/ruff_python_parser/src/error.rs @@ -510,16 +510,10 @@ impl UnsupportedSyntaxErrorKind { /// Returns whether or not this kind of syntax is unsupported on `target_version`. pub(crate) fn is_unsupported(self, target_version: PythonVersion) -> bool { - let (_, version) = self.changed_version(); - match self { - UnsupportedSyntaxErrorKind::Match - | UnsupportedSyntaxErrorKind::Walrus - | UnsupportedSyntaxErrorKind::ExceptStar - | UnsupportedSyntaxErrorKind::TypeAliasStatement - | UnsupportedSyntaxErrorKind::TypeParamDefault => target_version < version, - UnsupportedSyntaxErrorKind::ParenthesizedKeywordArgumentName => { - target_version >= version - } + let (change, version) = self.changed_version(); + match change { + Change::Added => target_version < version, + Change::Removed => target_version >= version, } } } From bbe8eacc5fe6ad342fa0fbcb058a04610735bbf9 Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Wed, 5 Mar 2025 08:26:51 -0500 Subject: [PATCH 10/14] store the PythonVersion in Change --- crates/ruff_python_parser/src/error.rs | 34 ++++++++++++-------------- 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/crates/ruff_python_parser/src/error.rs b/crates/ruff_python_parser/src/error.rs index 92b43ce671cd4..503f8941f1729 100644 --- a/crates/ruff_python_parser/src/error.rs +++ b/crates/ruff_python_parser/src/error.rs @@ -464,12 +464,11 @@ impl Display for UnsupportedSyntaxError { } }; - let (changed, changed_version) = self.kind.changed_version(); - write!( f, - "{kind} on Python {} (syntax was {changed} in Python {changed_version})", + "{kind} on Python {} (syntax was {changed})", self.target_version, + changed = self.kind.changed_version(), ) } } @@ -479,15 +478,15 @@ impl Display for UnsupportedSyntaxError { /// Most changes so far have been additions (e.g. `match` and `type` statements), but others are /// removals (e.g. parenthesized keyword argument names). enum Change { - Added, - Removed, + Added(PythonVersion), + Removed(PythonVersion), } impl Display for Change { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - Change::Added => f.write_str("added"), - Change::Removed => f.write_str("removed"), + Change::Added(version) => write!(f, "added in Python {version}"), + Change::Removed(version) => write!(f, "removed in Python {version}"), } } } @@ -495,25 +494,24 @@ impl Display for Change { impl UnsupportedSyntaxErrorKind { /// Returns the Python version when the syntax associated with this error was changed, and the /// type of [`Change`] (added or removed). - const fn changed_version(self) -> (Change, PythonVersion) { + const fn changed_version(self) -> Change { match self { - UnsupportedSyntaxErrorKind::Match => (Change::Added, PythonVersion::PY310), - UnsupportedSyntaxErrorKind::Walrus => (Change::Added, PythonVersion::PY38), - UnsupportedSyntaxErrorKind::ExceptStar => (Change::Added, PythonVersion::PY311), + UnsupportedSyntaxErrorKind::Match => Change::Added(PythonVersion::PY310), + UnsupportedSyntaxErrorKind::Walrus => Change::Added(PythonVersion::PY38), + UnsupportedSyntaxErrorKind::ExceptStar => Change::Added(PythonVersion::PY311), UnsupportedSyntaxErrorKind::ParenthesizedKeywordArgumentName => { - (Change::Removed, PythonVersion::PY38) + Change::Removed(PythonVersion::PY38) } - UnsupportedSyntaxErrorKind::TypeAliasStatement => (Change::Added, PythonVersion::PY312), - UnsupportedSyntaxErrorKind::TypeParamDefault => (Change::Added, PythonVersion::PY313), + UnsupportedSyntaxErrorKind::TypeAliasStatement => Change::Added(PythonVersion::PY312), + UnsupportedSyntaxErrorKind::TypeParamDefault => Change::Added(PythonVersion::PY313), } } /// Returns whether or not this kind of syntax is unsupported on `target_version`. pub(crate) fn is_unsupported(self, target_version: PythonVersion) -> bool { - let (change, version) = self.changed_version(); - match change { - Change::Added => target_version < version, - Change::Removed => target_version >= version, + match self.changed_version() { + Change::Added(version) => target_version < version, + Change::Removed(version) => target_version >= version, } } } From 7537816432b22e3521b7a056e6941c03945b291d Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Thu, 6 Mar 2025 08:05:24 -0500 Subject: [PATCH 11/14] delete doc --- crates/ruff_python_parser/src/error.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/crates/ruff_python_parser/src/error.rs b/crates/ruff_python_parser/src/error.rs index cb33a732c3462..a837fc8aa194d 100644 --- a/crates/ruff_python_parser/src/error.rs +++ b/crates/ruff_python_parser/src/error.rs @@ -564,9 +564,6 @@ impl Display for UnsupportedSyntaxError { } /// Represents the kind of change in Python syntax between versions. -/// -/// Most changes so far have been additions (e.g. `match` and `type` statements), but others are -/// removals (e.g. parenthesized keyword argument names). enum Change { Added(PythonVersion), Removed(PythonVersion), From ad72ffdba851125c2206b758d419a2f546466eec Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Thu, 6 Mar 2025 08:12:17 -0500 Subject: [PATCH 12/14] just use node_range --- crates/ruff_python_parser/src/parser/expression.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/crates/ruff_python_parser/src/parser/expression.rs b/crates/ruff_python_parser/src/parser/expression.rs index f6e005aebff2e..a0a5a64e70ad5 100644 --- a/crates/ruff_python_parser/src/parser/expression.rs +++ b/crates/ruff_python_parser/src/parser/expression.rs @@ -701,6 +701,7 @@ impl<'src> Parser<'src> { } } + let arg_range = parser.node_range(start); if parser.eat(TokenKind::Equal) { seen_keyword_argument = true; let arg = if let ParsedExpr { @@ -718,10 +719,10 @@ impl<'src> Parser<'src> { // f((a) = 1) // f( ( a ) = 1) - if let Some(end) = end_parenthesis { + if end_parenthesis.is_some() { parser.add_unsupported_syntax_error( UnsupportedSyntaxErrorKind::ParenthesizedKeywordArgumentName, - TextRange::new(start, end), + arg_range, ); } From e995a385885263647e78e12410dfc7054ce87702 Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Thu, 6 Mar 2025 08:22:07 -0500 Subject: [PATCH 13/14] restore ParsedExpr::is_parenthesized field --- .../src/parser/expression.rs | 32 ++++++++----------- .../src/parser/statement.rs | 6 ++-- 2 files changed, 16 insertions(+), 22 deletions(-) diff --git a/crates/ruff_python_parser/src/parser/expression.rs b/crates/ruff_python_parser/src/parser/expression.rs index a0a5a64e70ad5..bcd0393a676fd 100644 --- a/crates/ruff_python_parser/src/parser/expression.rs +++ b/crates/ruff_python_parser/src/parser/expression.rs @@ -405,7 +405,7 @@ impl<'src> Parser<'src> { ParsedExpr { expr: self.parse_postfix_expression(lhs.expr, start), - end_parenthesis: lhs.end_parenthesis, + is_parenthesized: lhs.is_parenthesized, } } @@ -419,7 +419,7 @@ impl<'src> Parser<'src> { fn parse_expression_with_bitwise_or_precedence(&mut self) -> ParsedExpr { let parsed_expr = self.parse_conditional_expression_or_higher(); - if parsed_expr.is_parenthesized() { + if parsed_expr.is_parenthesized { // Parentheses resets the precedence, so we don't need to validate it. return parsed_expr; } @@ -706,7 +706,7 @@ impl<'src> Parser<'src> { seen_keyword_argument = true; let arg = if let ParsedExpr { expr: Expr::Name(ident_expr), - end_parenthesis, + is_parenthesized, } = parsed_expr { // test_ok parenthesized_kwarg_py37 @@ -719,7 +719,7 @@ impl<'src> Parser<'src> { // f((a) = 1) // f( ( a ) = 1) - if end_parenthesis.is_some() { + if is_parenthesized { parser.add_unsupported_syntax_error( UnsupportedSyntaxErrorKind::ParenthesizedKeywordArgumentName, arg_range, @@ -879,7 +879,7 @@ impl<'src> Parser<'src> { return lower.expr; } - if !lower.is_parenthesized() { + if !lower.is_parenthesized { match lower.expr { Expr::Starred(_) => { self.add_error(ParseErrorType::InvalidStarredExpressionUsage, &lower); @@ -1425,7 +1425,7 @@ impl<'src> Parser<'src> { // f"{*yield x}" let value = self.parse_expression_list(ExpressionContext::yield_or_starred_bitwise_or()); - if !value.is_parenthesized() && value.expr.is_lambda_expr() { + if !value.is_parenthesized && value.expr.is_lambda_expr() { // TODO(dhruvmanila): This requires making some changes in lambda expression // parsing logic to handle the emitted `FStringMiddle` token in case the // lambda expression is not parenthesized. @@ -1636,7 +1636,7 @@ impl<'src> Parser<'src> { TokenKind::Colon => { // Now, we know that it's either a dictionary expression or a dictionary comprehension. // In either case, the key is limited to an `expression`. - if !key_or_element.is_parenthesized() { + if !key_or_element.is_parenthesized { match key_or_element.expr { Expr::Starred(_) => self.add_error( ParseErrorType::InvalidStarredExpressionUsage, @@ -1715,7 +1715,7 @@ impl<'src> Parser<'src> { ParsedExpr { expr: tuple.into(), - end_parenthesis: None, + is_parenthesized: false, } } TokenKind::Async | TokenKind::For => { @@ -1735,7 +1735,7 @@ impl<'src> Parser<'src> { ParsedExpr { expr: generator, - end_parenthesis: None, + is_parenthesized: false, } } _ => { @@ -1746,7 +1746,7 @@ impl<'src> Parser<'src> { self.expect(TokenKind::Rpar); - parsed_expr.end_parenthesis = Some(self.node_range(start).end()); + parsed_expr.is_parenthesized = true; parsed_expr } } @@ -2352,19 +2352,13 @@ impl<'src> Parser<'src> { #[derive(Debug)] pub(super) struct ParsedExpr { pub(super) expr: Expr, - /// Contains the location of the closing parenthesis, if the expression is parenthesized - pub(super) end_parenthesis: Option, + pub(super) is_parenthesized: bool, } impl ParsedExpr { #[inline] pub(super) const fn is_unparenthesized_starred_expr(&self) -> bool { - !self.is_parenthesized() && self.expr.is_starred_expr() - } - - #[inline] - pub(super) const fn is_parenthesized(&self) -> bool { - self.end_parenthesis.is_some() + !self.is_parenthesized && self.expr.is_starred_expr() } } @@ -2373,7 +2367,7 @@ impl From for ParsedExpr { fn from(expr: Expr) -> Self { ParsedExpr { expr, - end_parenthesis: None, + is_parenthesized: false, } } } diff --git a/crates/ruff_python_parser/src/parser/statement.rs b/crates/ruff_python_parser/src/parser/statement.rs index 36a75f4d50b7e..bd569f613dbab 100644 --- a/crates/ruff_python_parser/src/parser/statement.rs +++ b/crates/ruff_python_parser/src/parser/statement.rs @@ -1020,7 +1020,7 @@ impl<'src> Parser<'src> { IpyEscapeKind::Help }; - if parsed_expr.is_parenthesized() { + if parsed_expr.is_parenthesized { let token_range = self.node_range(start); self.add_error( ParseErrorType::OtherError( @@ -1151,7 +1151,7 @@ impl<'src> Parser<'src> { // (a): int // a.b: int // a[0]: int - let simple = target.is_name_expr() && !target.is_parenthesized(); + let simple = target.is_name_expr() && !target.is_parenthesized; // test_err ann_assign_stmt_invalid_annotation // x: *int = 1 @@ -2201,7 +2201,7 @@ impl<'src> Parser<'src> { .then(|| Box::new(self.parse_with_item_optional_vars().expr)); ParsedWithItem { - is_parenthesized: context_expr.is_parenthesized(), + is_parenthesized: context_expr.is_parenthesized, item: ast::WithItem { range: self.node_range(start), context_expr: context_expr.expr, From abcb7486477a46c649d44619620496df9ffa8e0a Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Thu, 6 Mar 2025 08:36:10 -0500 Subject: [PATCH 14/14] add variant docs --- crates/ruff_python_parser/src/error.rs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/crates/ruff_python_parser/src/error.rs b/crates/ruff_python_parser/src/error.rs index a837fc8aa194d..bf9f6883e7340 100644 --- a/crates/ruff_python_parser/src/error.rs +++ b/crates/ruff_python_parser/src/error.rs @@ -444,6 +444,24 @@ pub enum UnsupportedSyntaxErrorKind { Match, Walrus, ExceptStar, + /// Represents the use of a parenthesized keyword argument name after Python 3.8. + /// + /// ## Example + /// + /// From [BPO 34641] it sounds like this was only accidentally supported and was removed when + /// noticed. Code like this used to be valid: + /// + /// ```python + /// f((a)=1) + /// ``` + /// + /// After Python 3.8, you have to omit the parentheses around `a`: + /// + /// ```python + /// f(a=1) + /// ``` + /// + /// [BPO 34641]: https://github.com/python/cpython/issues/78822 ParenthesizedKeywordArgumentName, /// Represents the use of a "relaxed" [PEP 614] decorator before Python 3.9. ///