From 63114123735cc95d06cf2d6e31fe9667385271ee Mon Sep 17 00:00:00 2001 From: Brent Westbrook <36778786+ntBre@users.noreply.github.com> Date: Fri, 14 Mar 2025 11:20:44 -0400 Subject: [PATCH] [syntax-errors] Star annotations before Python 3.11 (#16545) Summary -- This is closely related to (and stacked on) https://github.com/astral-sh/ruff/pull/16544 and detects star annotations in function definitions. I initially called the variant `StarExpressionInAnnotation` to mirror `StarExpressionInIndex`, but I realized it's not really a "star expression" in this position and renamed it. `StarAnnotation` seems in line with the PEP. Test Plan -- Two new inline tests. It looked like there was pretty good existing coverage of this syntax, so I just added simple examples to test the version cutoff. --- .../err/param_with_star_annotation_py310.py | 2 + .../ok/param_with_star_annotation_py311.py | 2 + crates/ruff_python_parser/src/error.rs | 30 +++++++ .../src/parser/statement.rs | 18 ++++- ...x@param_with_star_annotation_py310.py.snap | 78 +++++++++++++++++++ ...x@param_with_star_annotation_py311.py.snap | 71 +++++++++++++++++ 6 files changed, 199 insertions(+), 2 deletions(-) create mode 100644 crates/ruff_python_parser/resources/inline/err/param_with_star_annotation_py310.py create mode 100644 crates/ruff_python_parser/resources/inline/ok/param_with_star_annotation_py311.py create mode 100644 crates/ruff_python_parser/tests/snapshots/invalid_syntax@param_with_star_annotation_py310.py.snap create mode 100644 crates/ruff_python_parser/tests/snapshots/valid_syntax@param_with_star_annotation_py311.py.snap diff --git a/crates/ruff_python_parser/resources/inline/err/param_with_star_annotation_py310.py b/crates/ruff_python_parser/resources/inline/err/param_with_star_annotation_py310.py new file mode 100644 index 0000000000..39cf903ce6 --- /dev/null +++ b/crates/ruff_python_parser/resources/inline/err/param_with_star_annotation_py310.py @@ -0,0 +1,2 @@ +# parse_options: {"target-version": "3.10"} +def foo(*args: *Ts): ... diff --git a/crates/ruff_python_parser/resources/inline/ok/param_with_star_annotation_py311.py b/crates/ruff_python_parser/resources/inline/ok/param_with_star_annotation_py311.py new file mode 100644 index 0000000000..4ba49a293a --- /dev/null +++ b/crates/ruff_python_parser/resources/inline/ok/param_with_star_annotation_py311.py @@ -0,0 +1,2 @@ +# parse_options: {"target-version": "3.11"} +def foo(*args: *Ts): ... diff --git a/crates/ruff_python_parser/src/error.rs b/crates/ruff_python_parser/src/error.rs index d8267fd311..59a91c5da9 100644 --- a/crates/ruff_python_parser/src/error.rs +++ b/crates/ruff_python_parser/src/error.rs @@ -644,6 +644,34 @@ pub enum UnsupportedSyntaxErrorKind { /// /// [PEP 646]: https://peps.python.org/pep-0646/#change-1-star-expressions-in-indexes StarExpressionInIndex, + + /// Represents the use of a [PEP 646] star annotations in a function definition. + /// + /// ## Examples + /// + /// Before Python 3.11, star annotations were not allowed in function definitions. This + /// restriction was lifted in [PEP 646] to allow type annotations for `typing.TypeVarTuple`, + /// also added in Python 3.11: + /// + /// ```python + /// from typing import TypeVarTuple + /// + /// Ts = TypeVarTuple('Ts') + /// + /// def foo(*args: *Ts): ... + /// ``` + /// + /// Unlike [`UnsupportedSyntaxErrorKind::StarExpressionInIndex`], this does not include any + /// other annotation positions: + /// + /// ```python + /// x: *Ts # Syntax error + /// def foo(x: *Ts): ... # Syntax error + /// ``` + /// + /// [PEP 646]: https://peps.python.org/pep-0646/#change-2-args-as-a-typevartuple + StarAnnotation, + /// Represents the use of tuple unpacking in a `for` statement iterator clause before Python /// 3.9. /// @@ -699,6 +727,7 @@ impl Display for UnsupportedSyntaxError { UnsupportedSyntaxErrorKind::StarExpressionInIndex => { "Cannot use star expression in index" } + UnsupportedSyntaxErrorKind::StarAnnotation => "Cannot use star annotation", UnsupportedSyntaxErrorKind::UnparenthesizedUnpackInFor => { "Cannot use iterable unpacking in `for` statements" } @@ -750,6 +779,7 @@ impl UnsupportedSyntaxErrorKind { UnsupportedSyntaxErrorKind::StarExpressionInIndex => { Change::Added(PythonVersion::PY311) } + UnsupportedSyntaxErrorKind::StarAnnotation => Change::Added(PythonVersion::PY311), UnsupportedSyntaxErrorKind::UnparenthesizedUnpackInFor => { Change::Added(PythonVersion::PY39) } diff --git a/crates/ruff_python_parser/src/parser/statement.rs b/crates/ruff_python_parser/src/parser/statement.rs index e234562c83..2450a3834c 100644 --- a/crates/ruff_python_parser/src/parser/statement.rs +++ b/crates/ruff_python_parser/src/parser/statement.rs @@ -2882,9 +2882,23 @@ impl<'src> Parser<'src> { // def foo(*args: *int or str): ... // def foo(*args: *yield x): ... // # def foo(*args: **int): ... - self.parse_conditional_expression_or_higher_impl( + let parsed_expr = self.parse_conditional_expression_or_higher_impl( ExpressionContext::starred_bitwise_or(), - ) + ); + + // test_ok param_with_star_annotation_py311 + // # parse_options: {"target-version": "3.11"} + // def foo(*args: *Ts): ... + + // test_err param_with_star_annotation_py310 + // # parse_options: {"target-version": "3.10"} + // def foo(*args: *Ts): ... + self.add_unsupported_syntax_error( + UnsupportedSyntaxErrorKind::StarAnnotation, + parsed_expr.range(), + ); + + parsed_expr } AllowStarAnnotation::No => { // test_ok param_with_annotation diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@param_with_star_annotation_py310.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@param_with_star_annotation_py310.py.snap new file mode 100644 index 0000000000..ef87ca461f --- /dev/null +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@param_with_star_annotation_py310.py.snap @@ -0,0 +1,78 @@ +--- +source: crates/ruff_python_parser/tests/fixtures.rs +input_file: crates/ruff_python_parser/resources/inline/err/param_with_star_annotation_py310.py +--- +## AST + +``` +Module( + ModModule { + range: 0..69, + body: [ + FunctionDef( + StmtFunctionDef { + range: 44..68, + is_async: false, + decorator_list: [], + name: Identifier { + id: Name("foo"), + range: 48..51, + }, + type_params: None, + parameters: Parameters { + range: 51..63, + posonlyargs: [], + args: [], + vararg: Some( + Parameter { + range: 52..62, + name: Identifier { + id: Name("args"), + range: 53..57, + }, + annotation: Some( + Starred( + ExprStarred { + range: 59..62, + value: Name( + ExprName { + range: 60..62, + id: Name("Ts"), + ctx: Load, + }, + ), + ctx: Load, + }, + ), + ), + }, + ), + kwonlyargs: [], + kwarg: None, + }, + returns: None, + body: [ + Expr( + StmtExpr { + range: 65..68, + value: EllipsisLiteral( + ExprEllipsisLiteral { + range: 65..68, + }, + ), + }, + ), + ], + }, + ), + ], + }, +) +``` +## Unsupported Syntax Errors + + | +1 | # parse_options: {"target-version": "3.10"} +2 | def foo(*args: *Ts): ... + | ^^^ Syntax Error: Cannot use star annotation on Python 3.10 (syntax was added in Python 3.11) + | diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@param_with_star_annotation_py311.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@param_with_star_annotation_py311.py.snap new file mode 100644 index 0000000000..7156b308c5 --- /dev/null +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@param_with_star_annotation_py311.py.snap @@ -0,0 +1,71 @@ +--- +source: crates/ruff_python_parser/tests/fixtures.rs +input_file: crates/ruff_python_parser/resources/inline/ok/param_with_star_annotation_py311.py +--- +## AST + +``` +Module( + ModModule { + range: 0..69, + body: [ + FunctionDef( + StmtFunctionDef { + range: 44..68, + is_async: false, + decorator_list: [], + name: Identifier { + id: Name("foo"), + range: 48..51, + }, + type_params: None, + parameters: Parameters { + range: 51..63, + posonlyargs: [], + args: [], + vararg: Some( + Parameter { + range: 52..62, + name: Identifier { + id: Name("args"), + range: 53..57, + }, + annotation: Some( + Starred( + ExprStarred { + range: 59..62, + value: Name( + ExprName { + range: 60..62, + id: Name("Ts"), + ctx: Load, + }, + ), + ctx: Load, + }, + ), + ), + }, + ), + kwonlyargs: [], + kwarg: None, + }, + returns: None, + body: [ + Expr( + StmtExpr { + range: 65..68, + value: EllipsisLiteral( + ExprEllipsisLiteral { + range: 65..68, + }, + ), + }, + ), + ], + }, + ), + ], + }, +) +```