From e7b93f93efe1da9402e3e886cbdd4ec248d2785a Mon Sep 17 00:00:00 2001 From: Brent Westbrook <36778786+ntBre@users.noreply.github.com> Date: Tue, 4 Mar 2025 11:53:38 -0500 Subject: [PATCH] [syntax-errors] Type parameter defaults before Python 3.13 (#16447) Summary -- Detects the presence of a [PEP 696] type parameter default before Python 3.13. Test Plan -- New inline parser tests for type aliases, generic functions and generic classes. [PEP 696]: https://peps.python.org/pep-0696/#grammar-changes --- .../inline/err/type_param_default_py312.py | 5 + .../inline/ok/type_param_default_py313.py | 4 + crates/ruff_python_parser/src/error.rs | 13 +- .../src/parser/statement.rs | 21 ++ ...id_syntax@type_param_default_py312.py.snap | 302 ++++++++++++++++++ ...id_syntax@type_param_default_py313.py.snap | 173 ++++++++++ 6 files changed, 514 insertions(+), 4 deletions(-) create mode 100644 crates/ruff_python_parser/resources/inline/err/type_param_default_py312.py create mode 100644 crates/ruff_python_parser/resources/inline/ok/type_param_default_py313.py create mode 100644 crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_default_py312.py.snap create mode 100644 crates/ruff_python_parser/tests/snapshots/valid_syntax@type_param_default_py313.py.snap diff --git a/crates/ruff_python_parser/resources/inline/err/type_param_default_py312.py b/crates/ruff_python_parser/resources/inline/err/type_param_default_py312.py new file mode 100644 index 0000000000..c30f994329 --- /dev/null +++ b/crates/ruff_python_parser/resources/inline/err/type_param_default_py312.py @@ -0,0 +1,5 @@ +# parse_options: {"target-version": "3.12"} +type X[T = int] = int +def f[T = int](): ... +class C[T = int](): ... +class D[S, T = int, U = uint](): ... diff --git a/crates/ruff_python_parser/resources/inline/ok/type_param_default_py313.py b/crates/ruff_python_parser/resources/inline/ok/type_param_default_py313.py new file mode 100644 index 0000000000..d7c174eaca --- /dev/null +++ b/crates/ruff_python_parser/resources/inline/ok/type_param_default_py313.py @@ -0,0 +1,4 @@ +# parse_options: {"target-version": "3.13"} +type X[T = int] = int +def f[T = int](): ... +class C[T = int](): ... diff --git a/crates/ruff_python_parser/src/error.rs b/crates/ruff_python_parser/src/error.rs index 191068c2a1..bc58dccd44 100644 --- a/crates/ruff_python_parser/src/error.rs +++ b/crates/ruff_python_parser/src/error.rs @@ -449,18 +449,22 @@ pub enum UnsupportedSyntaxErrorKind { Match, Walrus, ExceptStar, + TypeParamDefault, } impl Display for UnsupportedSyntaxError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let kind = match self.kind { - UnsupportedSyntaxErrorKind::Match => "`match` statement", - UnsupportedSyntaxErrorKind::Walrus => "named assignment expression (`:=`)", - UnsupportedSyntaxErrorKind::ExceptStar => "`except*`", + UnsupportedSyntaxErrorKind::Match => "Cannot use `match` statement", + UnsupportedSyntaxErrorKind::Walrus => "Cannot use named assignment expression (`:=`)", + UnsupportedSyntaxErrorKind::ExceptStar => "Cannot use `except*`", + UnsupportedSyntaxErrorKind::TypeParamDefault => { + "Cannot set default type for a type parameter" + } }; write!( f, - "Cannot use {kind} on Python {} (syntax was added in Python {})", + "{kind} on Python {} (syntax was added in Python {})", self.target_version, self.kind.minimum_version(), ) @@ -474,6 +478,7 @@ impl UnsupportedSyntaxErrorKind { UnsupportedSyntaxErrorKind::Match => PythonVersion::PY310, UnsupportedSyntaxErrorKind::Walrus => PythonVersion::PY38, UnsupportedSyntaxErrorKind::ExceptStar => PythonVersion::PY311, + UnsupportedSyntaxErrorKind::TypeParamDefault => PythonVersion::PY313, } } } diff --git a/crates/ruff_python_parser/src/parser/statement.rs b/crates/ruff_python_parser/src/parser/statement.rs index bfe4678acc..62aebfe4bb 100644 --- a/crates/ruff_python_parser/src/parser/statement.rs +++ b/crates/ruff_python_parser/src/parser/statement.rs @@ -3243,6 +3243,7 @@ impl<'src> Parser<'src> { None }; + let equal_token_start = self.node_start(); let default = if self.eat(TokenKind::Equal) { if self.at_expr() { // test_err type_param_type_var_invalid_default_expr @@ -3268,6 +3269,26 @@ impl<'src> Parser<'src> { None }; + // test_ok type_param_default_py313 + // # parse_options: {"target-version": "3.13"} + // type X[T = int] = int + // def f[T = int](): ... + // class C[T = int](): ... + + // test_err type_param_default_py312 + // # parse_options: {"target-version": "3.12"} + // type X[T = int] = int + // def f[T = int](): ... + // class C[T = int](): ... + // class D[S, T = int, U = uint](): ... + + if default.is_some() { + self.add_unsupported_syntax_error( + UnsupportedSyntaxErrorKind::TypeParamDefault, + self.node_range(equal_token_start), + ); + } + ast::TypeParam::TypeVar(ast::TypeParamTypeVar { range: self.node_range(start), name, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_default_py312.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_default_py312.py.snap new file mode 100644 index 0000000000..68d3ff0524 --- /dev/null +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_default_py312.py.snap @@ -0,0 +1,302 @@ +--- +source: crates/ruff_python_parser/tests/fixtures.rs +input_file: crates/ruff_python_parser/resources/inline/err/type_param_default_py312.py +--- +## AST + +``` +Module( + ModModule { + range: 0..149, + body: [ + TypeAlias( + StmtTypeAlias { + range: 44..65, + name: Name( + ExprName { + range: 49..50, + id: Name("X"), + ctx: Store, + }, + ), + type_params: Some( + TypeParams { + range: 50..59, + type_params: [ + TypeVar( + TypeParamTypeVar { + range: 51..58, + name: Identifier { + id: Name("T"), + range: 51..52, + }, + bound: None, + default: Some( + Name( + ExprName { + range: 55..58, + id: Name("int"), + ctx: Load, + }, + ), + ), + }, + ), + ], + }, + ), + value: Name( + ExprName { + range: 62..65, + id: Name("int"), + ctx: Load, + }, + ), + }, + ), + FunctionDef( + StmtFunctionDef { + range: 66..87, + is_async: false, + decorator_list: [], + name: Identifier { + id: Name("f"), + range: 70..71, + }, + type_params: Some( + TypeParams { + range: 71..80, + type_params: [ + TypeVar( + TypeParamTypeVar { + range: 72..79, + name: Identifier { + id: Name("T"), + range: 72..73, + }, + bound: None, + default: Some( + Name( + ExprName { + range: 76..79, + id: Name("int"), + ctx: Load, + }, + ), + ), + }, + ), + ], + }, + ), + parameters: Parameters { + range: 80..82, + posonlyargs: [], + args: [], + vararg: None, + kwonlyargs: [], + kwarg: None, + }, + returns: None, + body: [ + Expr( + StmtExpr { + range: 84..87, + value: EllipsisLiteral( + ExprEllipsisLiteral { + range: 84..87, + }, + ), + }, + ), + ], + }, + ), + ClassDef( + StmtClassDef { + range: 88..111, + decorator_list: [], + name: Identifier { + id: Name("C"), + range: 94..95, + }, + type_params: Some( + TypeParams { + range: 95..104, + type_params: [ + TypeVar( + TypeParamTypeVar { + range: 96..103, + name: Identifier { + id: Name("T"), + range: 96..97, + }, + bound: None, + default: Some( + Name( + ExprName { + range: 100..103, + id: Name("int"), + ctx: Load, + }, + ), + ), + }, + ), + ], + }, + ), + arguments: Some( + Arguments { + range: 104..106, + args: [], + keywords: [], + }, + ), + body: [ + Expr( + StmtExpr { + range: 108..111, + value: EllipsisLiteral( + ExprEllipsisLiteral { + range: 108..111, + }, + ), + }, + ), + ], + }, + ), + ClassDef( + StmtClassDef { + range: 112..148, + decorator_list: [], + name: Identifier { + id: Name("D"), + range: 118..119, + }, + type_params: Some( + TypeParams { + range: 119..141, + type_params: [ + TypeVar( + TypeParamTypeVar { + range: 120..121, + name: Identifier { + id: Name("S"), + range: 120..121, + }, + bound: None, + default: None, + }, + ), + TypeVar( + TypeParamTypeVar { + range: 123..130, + name: Identifier { + id: Name("T"), + range: 123..124, + }, + bound: None, + default: Some( + Name( + ExprName { + range: 127..130, + id: Name("int"), + ctx: Load, + }, + ), + ), + }, + ), + TypeVar( + TypeParamTypeVar { + range: 132..140, + name: Identifier { + id: Name("U"), + range: 132..133, + }, + bound: None, + default: Some( + Name( + ExprName { + range: 136..140, + id: Name("uint"), + ctx: Load, + }, + ), + ), + }, + ), + ], + }, + ), + arguments: Some( + Arguments { + range: 141..143, + args: [], + keywords: [], + }, + ), + body: [ + Expr( + StmtExpr { + range: 145..148, + value: EllipsisLiteral( + ExprEllipsisLiteral { + range: 145..148, + }, + ), + }, + ), + ], + }, + ), + ], + }, +) +``` +## Unsupported Syntax Errors + + | +1 | # parse_options: {"target-version": "3.12"} +2 | type X[T = int] = int + | ^^^^^ Syntax Error: Cannot set default type for a type parameter on Python 3.12 (syntax was added in Python 3.13) +3 | def f[T = int](): ... +4 | class C[T = int](): ... + | + + + | +1 | # parse_options: {"target-version": "3.12"} +2 | type X[T = int] = int +3 | def f[T = int](): ... + | ^^^^^ Syntax Error: Cannot set default type for a type parameter on Python 3.12 (syntax was added in Python 3.13) +4 | class C[T = int](): ... +5 | class D[S, T = int, U = uint](): ... + | + + + | +2 | type X[T = int] = int +3 | def f[T = int](): ... +4 | class C[T = int](): ... + | ^^^^^ Syntax Error: Cannot set default type for a type parameter on Python 3.12 (syntax was added in Python 3.13) +5 | class D[S, T = int, U = uint](): ... + | + + + | +3 | def f[T = int](): ... +4 | class C[T = int](): ... +5 | class D[S, T = int, U = uint](): ... + | ^^^^^ Syntax Error: Cannot set default type for a type parameter on Python 3.12 (syntax was added in Python 3.13) + | + + + | +3 | def f[T = int](): ... +4 | class C[T = int](): ... +5 | class D[S, T = int, U = uint](): ... + | ^^^^^^ Syntax Error: Cannot set default type for a type parameter on Python 3.12 (syntax was added in Python 3.13) + | diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@type_param_default_py313.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@type_param_default_py313.py.snap new file mode 100644 index 0000000000..010da9ab55 --- /dev/null +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@type_param_default_py313.py.snap @@ -0,0 +1,173 @@ +--- +source: crates/ruff_python_parser/tests/fixtures.rs +input_file: crates/ruff_python_parser/resources/inline/ok/type_param_default_py313.py +--- +## AST + +``` +Module( + ModModule { + range: 0..112, + body: [ + TypeAlias( + StmtTypeAlias { + range: 44..65, + name: Name( + ExprName { + range: 49..50, + id: Name("X"), + ctx: Store, + }, + ), + type_params: Some( + TypeParams { + range: 50..59, + type_params: [ + TypeVar( + TypeParamTypeVar { + range: 51..58, + name: Identifier { + id: Name("T"), + range: 51..52, + }, + bound: None, + default: Some( + Name( + ExprName { + range: 55..58, + id: Name("int"), + ctx: Load, + }, + ), + ), + }, + ), + ], + }, + ), + value: Name( + ExprName { + range: 62..65, + id: Name("int"), + ctx: Load, + }, + ), + }, + ), + FunctionDef( + StmtFunctionDef { + range: 66..87, + is_async: false, + decorator_list: [], + name: Identifier { + id: Name("f"), + range: 70..71, + }, + type_params: Some( + TypeParams { + range: 71..80, + type_params: [ + TypeVar( + TypeParamTypeVar { + range: 72..79, + name: Identifier { + id: Name("T"), + range: 72..73, + }, + bound: None, + default: Some( + Name( + ExprName { + range: 76..79, + id: Name("int"), + ctx: Load, + }, + ), + ), + }, + ), + ], + }, + ), + parameters: Parameters { + range: 80..82, + posonlyargs: [], + args: [], + vararg: None, + kwonlyargs: [], + kwarg: None, + }, + returns: None, + body: [ + Expr( + StmtExpr { + range: 84..87, + value: EllipsisLiteral( + ExprEllipsisLiteral { + range: 84..87, + }, + ), + }, + ), + ], + }, + ), + ClassDef( + StmtClassDef { + range: 88..111, + decorator_list: [], + name: Identifier { + id: Name("C"), + range: 94..95, + }, + type_params: Some( + TypeParams { + range: 95..104, + type_params: [ + TypeVar( + TypeParamTypeVar { + range: 96..103, + name: Identifier { + id: Name("T"), + range: 96..97, + }, + bound: None, + default: Some( + Name( + ExprName { + range: 100..103, + id: Name("int"), + ctx: Load, + }, + ), + ), + }, + ), + ], + }, + ), + arguments: Some( + Arguments { + range: 104..106, + args: [], + keywords: [], + }, + ), + body: [ + Expr( + StmtExpr { + range: 108..111, + value: EllipsisLiteral( + ExprEllipsisLiteral { + range: 108..111, + }, + ), + }, + ), + ], + }, + ), + ], + }, +) +```