diff --git a/crates/ruff_python_parser/resources/inline/err/class_type_params_py311.py b/crates/ruff_python_parser/resources/inline/err/class_type_params_py311.py new file mode 100644 index 0000000000..31cf933a6e --- /dev/null +++ b/crates/ruff_python_parser/resources/inline/err/class_type_params_py311.py @@ -0,0 +1,3 @@ +# parse_options: {"target-version": "3.11"} +class Foo[S: (str, bytes), T: float, *Ts, **P]: ... +class Foo[]: ... diff --git a/crates/ruff_python_parser/resources/inline/err/function_type_params_py311.py b/crates/ruff_python_parser/resources/inline/err/function_type_params_py311.py new file mode 100644 index 0000000000..2d9604058c --- /dev/null +++ b/crates/ruff_python_parser/resources/inline/err/function_type_params_py311.py @@ -0,0 +1,3 @@ +# parse_options: {"target-version": "3.11"} +def foo[T](): ... +def foo[](): ... diff --git a/crates/ruff_python_parser/resources/inline/ok/class_type_params_py312.py b/crates/ruff_python_parser/resources/inline/ok/class_type_params_py312.py new file mode 100644 index 0000000000..c167ad6d56 --- /dev/null +++ b/crates/ruff_python_parser/resources/inline/ok/class_type_params_py312.py @@ -0,0 +1,2 @@ +# parse_options: {"target-version": "3.12"} +class Foo[S: (str, bytes), T: float, *Ts, **P]: ... diff --git a/crates/ruff_python_parser/resources/inline/ok/function_type_params_py312.py b/crates/ruff_python_parser/resources/inline/ok/function_type_params_py312.py new file mode 100644 index 0000000000..32f36b0be0 --- /dev/null +++ b/crates/ruff_python_parser/resources/inline/ok/function_type_params_py312.py @@ -0,0 +1,2 @@ +# parse_options: {"target-version": "3.12"} +def foo[T](): ... diff --git a/crates/ruff_python_parser/src/error.rs b/crates/ruff_python_parser/src/error.rs index 97af7a20a8..c975698869 100644 --- a/crates/ruff_python_parser/src/error.rs +++ b/crates/ruff_python_parser/src/error.rs @@ -449,6 +449,34 @@ pub enum UnsupportedSyntaxErrorKind { Match, Walrus, ExceptStar, + /// Represents the use of a [type parameter list] before Python 3.12. + /// + /// ## Examples + /// + /// Before Python 3.12, generic parameters had to be declared separately using a class like + /// [`typing.TypeVar`], which could then be used in a function or class definition: + /// + /// ```python + /// from typing import Generic, TypeVar + /// + /// T = TypeVar("T") + /// + /// def f(t: T): ... + /// class C(Generic[T]): ... + /// ``` + /// + /// [PEP 695], included in Python 3.12, introduced the new type parameter syntax, which allows + /// these to be written more compactly and without a separate type variable: + /// + /// ```python + /// def f[T](t: T): ... + /// class C[T]: ... + /// ``` + /// + /// [type parameter list]: https://docs.python.org/3/reference/compound_stmts.html#type-parameter-lists + /// [PEP 695]: https://peps.python.org/pep-0695/ + /// [`typing.TypeVar`]: https://docs.python.org/3/library/typing.html#typevar + TypeParameterList, TypeAliasStatement, TypeParamDefault, } @@ -459,6 +487,7 @@ impl Display for UnsupportedSyntaxError { UnsupportedSyntaxErrorKind::Match => "Cannot use `match` statement", UnsupportedSyntaxErrorKind::Walrus => "Cannot use named assignment expression (`:=`)", UnsupportedSyntaxErrorKind::ExceptStar => "Cannot use `except*`", + UnsupportedSyntaxErrorKind::TypeParameterList => "Cannot use type parameter lists", UnsupportedSyntaxErrorKind::TypeAliasStatement => "Cannot use `type` alias statement", UnsupportedSyntaxErrorKind::TypeParamDefault => { "Cannot set default type for a type parameter" @@ -480,6 +509,7 @@ impl UnsupportedSyntaxErrorKind { UnsupportedSyntaxErrorKind::Match => PythonVersion::PY310, UnsupportedSyntaxErrorKind::Walrus => PythonVersion::PY38, UnsupportedSyntaxErrorKind::ExceptStar => PythonVersion::PY311, + UnsupportedSyntaxErrorKind::TypeParameterList => PythonVersion::PY312, UnsupportedSyntaxErrorKind::TypeAliasStatement => PythonVersion::PY312, 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 deabb8fe1e..c793297342 100644 --- a/crates/ruff_python_parser/src/parser/statement.rs +++ b/crates/ruff_python_parser/src/parser/statement.rs @@ -1786,6 +1786,21 @@ impl<'src> Parser<'src> { // x = 10 let type_params = self.try_parse_type_params(); + // test_ok function_type_params_py312 + // # parse_options: {"target-version": "3.12"} + // def foo[T](): ... + + // test_err function_type_params_py311 + // # parse_options: {"target-version": "3.11"} + // def foo[T](): ... + // def foo[](): ... + if let Some(ast::TypeParams { range, .. }) = &type_params { + self.add_unsupported_syntax_error( + UnsupportedSyntaxErrorKind::TypeParameterList, + *range, + ); + } + // test_ok function_def_parameter_range // def foo( // first: int, @@ -1900,6 +1915,21 @@ impl<'src> Parser<'src> { // x = 10 let type_params = self.try_parse_type_params(); + // test_ok class_type_params_py312 + // # parse_options: {"target-version": "3.12"} + // class Foo[S: (str, bytes), T: float, *Ts, **P]: ... + + // test_err class_type_params_py311 + // # parse_options: {"target-version": "3.11"} + // class Foo[S: (str, bytes), T: float, *Ts, **P]: ... + // class Foo[]: ... + if let Some(ast::TypeParams { range, .. }) = &type_params { + self.add_unsupported_syntax_error( + UnsupportedSyntaxErrorKind::TypeParameterList, + *range, + ); + } + // test_ok class_def_arguments // class Foo: ... // class Foo(): ... diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@class_type_params_py311.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@class_type_params_py311.py.snap new file mode 100644 index 0000000000..60c7fd4974 --- /dev/null +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@class_type_params_py311.py.snap @@ -0,0 +1,174 @@ +--- +source: crates/ruff_python_parser/tests/fixtures.rs +input_file: crates/ruff_python_parser/resources/inline/err/class_type_params_py311.py +--- +## AST + +``` +Module( + ModModule { + range: 0..113, + body: [ + ClassDef( + StmtClassDef { + range: 44..95, + decorator_list: [], + name: Identifier { + id: Name("Foo"), + range: 50..53, + }, + type_params: Some( + TypeParams { + range: 53..90, + type_params: [ + TypeVar( + TypeParamTypeVar { + range: 54..69, + name: Identifier { + id: Name("S"), + range: 54..55, + }, + bound: Some( + Tuple( + ExprTuple { + range: 57..69, + elts: [ + Name( + ExprName { + range: 58..61, + id: Name("str"), + ctx: Load, + }, + ), + Name( + ExprName { + range: 63..68, + id: Name("bytes"), + ctx: Load, + }, + ), + ], + ctx: Load, + parenthesized: true, + }, + ), + ), + default: None, + }, + ), + TypeVar( + TypeParamTypeVar { + range: 71..79, + name: Identifier { + id: Name("T"), + range: 71..72, + }, + bound: Some( + Name( + ExprName { + range: 74..79, + id: Name("float"), + ctx: Load, + }, + ), + ), + default: None, + }, + ), + TypeVarTuple( + TypeParamTypeVarTuple { + range: 81..84, + name: Identifier { + id: Name("Ts"), + range: 82..84, + }, + default: None, + }, + ), + ParamSpec( + TypeParamParamSpec { + range: 86..89, + name: Identifier { + id: Name("P"), + range: 88..89, + }, + default: None, + }, + ), + ], + }, + ), + arguments: None, + body: [ + Expr( + StmtExpr { + range: 92..95, + value: EllipsisLiteral( + ExprEllipsisLiteral { + range: 92..95, + }, + ), + }, + ), + ], + }, + ), + ClassDef( + StmtClassDef { + range: 96..112, + decorator_list: [], + name: Identifier { + id: Name("Foo"), + range: 102..105, + }, + type_params: Some( + TypeParams { + range: 105..107, + type_params: [], + }, + ), + arguments: None, + body: [ + Expr( + StmtExpr { + range: 109..112, + value: EllipsisLiteral( + ExprEllipsisLiteral { + range: 109..112, + }, + ), + }, + ), + ], + }, + ), + ], + }, +) +``` +## Errors + + | +1 | # parse_options: {"target-version": "3.11"} +2 | class Foo[S: (str, bytes), T: float, *Ts, **P]: ... +3 | class Foo[]: ... + | ^ Syntax Error: Type parameter list cannot be empty + | + + +## Unsupported Syntax Errors + + | +1 | # parse_options: {"target-version": "3.11"} +2 | class Foo[S: (str, bytes), T: float, *Ts, **P]: ... + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Syntax Error: Cannot use type parameter lists on Python 3.11 (syntax was added in Python 3.12) +3 | class Foo[]: ... + | + + + | +1 | # parse_options: {"target-version": "3.11"} +2 | class Foo[S: (str, bytes), T: float, *Ts, **P]: ... +3 | class Foo[]: ... + | ^^ Syntax Error: Cannot use type parameter lists on Python 3.11 (syntax was added in Python 3.12) + | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@function_type_params_py311.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@function_type_params_py311.py.snap new file mode 100644 index 0000000000..80edd73d05 --- /dev/null +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@function_type_params_py311.py.snap @@ -0,0 +1,129 @@ +--- +source: crates/ruff_python_parser/tests/fixtures.rs +input_file: crates/ruff_python_parser/resources/inline/err/function_type_params_py311.py +--- +## AST + +``` +Module( + ModModule { + range: 0..79, + body: [ + FunctionDef( + StmtFunctionDef { + range: 44..61, + is_async: false, + decorator_list: [], + name: Identifier { + id: Name("foo"), + range: 48..51, + }, + type_params: Some( + TypeParams { + range: 51..54, + type_params: [ + TypeVar( + TypeParamTypeVar { + range: 52..53, + name: Identifier { + id: Name("T"), + range: 52..53, + }, + bound: None, + default: None, + }, + ), + ], + }, + ), + parameters: Parameters { + range: 54..56, + posonlyargs: [], + args: [], + vararg: None, + kwonlyargs: [], + kwarg: None, + }, + returns: None, + body: [ + Expr( + StmtExpr { + range: 58..61, + value: EllipsisLiteral( + ExprEllipsisLiteral { + range: 58..61, + }, + ), + }, + ), + ], + }, + ), + FunctionDef( + StmtFunctionDef { + range: 62..78, + is_async: false, + decorator_list: [], + name: Identifier { + id: Name("foo"), + range: 66..69, + }, + type_params: Some( + TypeParams { + range: 69..71, + type_params: [], + }, + ), + parameters: Parameters { + range: 71..73, + posonlyargs: [], + args: [], + vararg: None, + kwonlyargs: [], + kwarg: None, + }, + returns: None, + body: [ + Expr( + StmtExpr { + range: 75..78, + value: EllipsisLiteral( + ExprEllipsisLiteral { + range: 75..78, + }, + ), + }, + ), + ], + }, + ), + ], + }, +) +``` +## Errors + + | +1 | # parse_options: {"target-version": "3.11"} +2 | def foo[T](): ... +3 | def foo[](): ... + | ^ Syntax Error: Type parameter list cannot be empty + | + + +## Unsupported Syntax Errors + + | +1 | # parse_options: {"target-version": "3.11"} +2 | def foo[T](): ... + | ^^^ Syntax Error: Cannot use type parameter lists on Python 3.11 (syntax was added in Python 3.12) +3 | def foo[](): ... + | + + + | +1 | # parse_options: {"target-version": "3.11"} +2 | def foo[T](): ... +3 | def foo[](): ... + | ^^ Syntax Error: Cannot use type parameter lists on Python 3.11 (syntax was added in Python 3.12) + | diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@class_type_params_py312.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@class_type_params_py312.py.snap new file mode 100644 index 0000000000..8ca2d42037 --- /dev/null +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@class_type_params_py312.py.snap @@ -0,0 +1,119 @@ +--- +source: crates/ruff_python_parser/tests/fixtures.rs +input_file: crates/ruff_python_parser/resources/inline/ok/class_type_params_py312.py +--- +## AST + +``` +Module( + ModModule { + range: 0..96, + body: [ + ClassDef( + StmtClassDef { + range: 44..95, + decorator_list: [], + name: Identifier { + id: Name("Foo"), + range: 50..53, + }, + type_params: Some( + TypeParams { + range: 53..90, + type_params: [ + TypeVar( + TypeParamTypeVar { + range: 54..69, + name: Identifier { + id: Name("S"), + range: 54..55, + }, + bound: Some( + Tuple( + ExprTuple { + range: 57..69, + elts: [ + Name( + ExprName { + range: 58..61, + id: Name("str"), + ctx: Load, + }, + ), + Name( + ExprName { + range: 63..68, + id: Name("bytes"), + ctx: Load, + }, + ), + ], + ctx: Load, + parenthesized: true, + }, + ), + ), + default: None, + }, + ), + TypeVar( + TypeParamTypeVar { + range: 71..79, + name: Identifier { + id: Name("T"), + range: 71..72, + }, + bound: Some( + Name( + ExprName { + range: 74..79, + id: Name("float"), + ctx: Load, + }, + ), + ), + default: None, + }, + ), + TypeVarTuple( + TypeParamTypeVarTuple { + range: 81..84, + name: Identifier { + id: Name("Ts"), + range: 82..84, + }, + default: None, + }, + ), + ParamSpec( + TypeParamParamSpec { + range: 86..89, + name: Identifier { + id: Name("P"), + range: 88..89, + }, + default: None, + }, + ), + ], + }, + ), + arguments: None, + body: [ + Expr( + StmtExpr { + range: 92..95, + value: EllipsisLiteral( + ExprEllipsisLiteral { + range: 92..95, + }, + ), + }, + ), + ], + }, + ), + ], + }, +) +``` diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@function_type_params_py312.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@function_type_params_py312.py.snap new file mode 100644 index 0000000000..ed6c0528a6 --- /dev/null +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@function_type_params_py312.py.snap @@ -0,0 +1,65 @@ +--- +source: crates/ruff_python_parser/tests/fixtures.rs +input_file: crates/ruff_python_parser/resources/inline/ok/function_type_params_py312.py +--- +## AST + +``` +Module( + ModModule { + range: 0..62, + body: [ + FunctionDef( + StmtFunctionDef { + range: 44..61, + is_async: false, + decorator_list: [], + name: Identifier { + id: Name("foo"), + range: 48..51, + }, + type_params: Some( + TypeParams { + range: 51..54, + type_params: [ + TypeVar( + TypeParamTypeVar { + range: 52..53, + name: Identifier { + id: Name("T"), + range: 52..53, + }, + bound: None, + default: None, + }, + ), + ], + }, + ), + parameters: Parameters { + range: 54..56, + posonlyargs: [], + args: [], + vararg: None, + kwonlyargs: [], + kwarg: None, + }, + returns: None, + body: [ + Expr( + StmtExpr { + range: 58..61, + value: EllipsisLiteral( + ExprEllipsisLiteral { + range: 58..61, + }, + ), + }, + ), + ], + }, + ), + ], + }, +) +```