From d0623888b3231fdda09bfa7079d67f8162faa10c Mon Sep 17 00:00:00 2001 From: Brent Westbrook <36778786+ntBre@users.noreply.github.com> Date: Wed, 5 Mar 2025 08:46:43 -0500 Subject: [PATCH] [syntax-errors] Positional-only parameters before Python 3.8 (#16481) Summary -- Detect positional-only parameters before Python 3.8, as marked by the `/` separator in a parameter list. Test Plan -- Inline tests. --- .../resources/inline/err/pos_only_py37.py | 5 + .../resources/inline/ok/pos_only_py38.py | 2 + crates/ruff_python_parser/src/error.rs | 30 ++ .../src/parser/statement.rs | 15 + .../invalid_syntax@pos_only_py37.py.snap | 291 ++++++++++++++++++ .../valid_syntax@pos_only_py38.py.snap | 61 ++++ 6 files changed, 404 insertions(+) create mode 100644 crates/ruff_python_parser/resources/inline/err/pos_only_py37.py create mode 100644 crates/ruff_python_parser/resources/inline/ok/pos_only_py38.py create mode 100644 crates/ruff_python_parser/tests/snapshots/invalid_syntax@pos_only_py37.py.snap create mode 100644 crates/ruff_python_parser/tests/snapshots/valid_syntax@pos_only_py38.py.snap diff --git a/crates/ruff_python_parser/resources/inline/err/pos_only_py37.py b/crates/ruff_python_parser/resources/inline/err/pos_only_py37.py new file mode 100644 index 0000000000..cd47c569ac --- /dev/null +++ b/crates/ruff_python_parser/resources/inline/err/pos_only_py37.py @@ -0,0 +1,5 @@ +# parse_options: {"target-version": "3.7"} +def foo(a, /): ... +def foo(a, /, b, /): ... +def foo(a, *args, /, b): ... +def foo(a, //): ... diff --git a/crates/ruff_python_parser/resources/inline/ok/pos_only_py38.py b/crates/ruff_python_parser/resources/inline/ok/pos_only_py38.py new file mode 100644 index 0000000000..fb4afdb425 --- /dev/null +++ b/crates/ruff_python_parser/resources/inline/ok/pos_only_py38.py @@ -0,0 +1,2 @@ +# parse_options: {"target-version": "3.8"} +def foo(a, /): ... diff --git a/crates/ruff_python_parser/src/error.rs b/crates/ruff_python_parser/src/error.rs index c975698869..8a953a61a9 100644 --- a/crates/ruff_python_parser/src/error.rs +++ b/crates/ruff_python_parser/src/error.rs @@ -449,6 +449,32 @@ pub enum UnsupportedSyntaxErrorKind { Match, Walrus, ExceptStar, + /// Represents the use of a [PEP 570] positional-only parameter before Python 3.8. + /// + /// ## Examples + /// + /// Python 3.8 added the `/` syntax for marking preceding parameters as positional-only: + /// + /// ```python + /// def foo(a, b, /, c): ... + /// ``` + /// + /// This means `a` and `b` in this case can only be provided by position, not by name. In other + /// words, this code results in a `TypeError` at runtime: + /// + /// ```pycon + /// >>> def foo(a, b, /, c): ... + /// ... + /// >>> foo(a=1, b=2, c=3) + /// Traceback (most recent call last): + /// File "", line 1, in + /// foo(a=1, b=2, c=3) + /// ~~~^^^^^^^^^^^^^^^ + /// TypeError: foo() got some positional-only arguments passed as keyword arguments: 'a, b' + /// ``` + /// + /// [PEP 570]: https://peps.python.org/pep-0570/ + PositionalOnlyParameter, /// Represents the use of a [type parameter list] before Python 3.12. /// /// ## Examples @@ -487,6 +513,9 @@ impl Display for UnsupportedSyntaxError { UnsupportedSyntaxErrorKind::Match => "Cannot use `match` statement", UnsupportedSyntaxErrorKind::Walrus => "Cannot use named assignment expression (`:=`)", UnsupportedSyntaxErrorKind::ExceptStar => "Cannot use `except*`", + UnsupportedSyntaxErrorKind::PositionalOnlyParameter => { + "Cannot use positional-only parameter separator" + } UnsupportedSyntaxErrorKind::TypeParameterList => "Cannot use type parameter lists", UnsupportedSyntaxErrorKind::TypeAliasStatement => "Cannot use `type` alias statement", UnsupportedSyntaxErrorKind::TypeParamDefault => { @@ -509,6 +538,7 @@ impl UnsupportedSyntaxErrorKind { UnsupportedSyntaxErrorKind::Match => PythonVersion::PY310, UnsupportedSyntaxErrorKind::Walrus => PythonVersion::PY38, UnsupportedSyntaxErrorKind::ExceptStar => PythonVersion::PY311, + UnsupportedSyntaxErrorKind::PositionalOnlyParameter => PythonVersion::PY38, 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 c793297342..f340fd3bb0 100644 --- a/crates/ruff_python_parser/src/parser/statement.rs +++ b/crates/ruff_python_parser/src/parser/statement.rs @@ -3050,6 +3050,21 @@ impl<'src> Parser<'src> { // first time, otherwise it's a user error. std::mem::swap(&mut parameters.args, &mut parameters.posonlyargs); seen_positional_only_separator = true; + + // test_ok pos_only_py38 + // # parse_options: {"target-version": "3.8"} + // def foo(a, /): ... + + // test_err pos_only_py37 + // # parse_options: {"target-version": "3.7"} + // def foo(a, /): ... + // def foo(a, /, b, /): ... + // def foo(a, *args, /, b): ... + // def foo(a, //): ... + parser.add_unsupported_syntax_error( + UnsupportedSyntaxErrorKind::PositionalOnlyParameter, + slash_range, + ); } last_keyword_only_separator_range = None; diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@pos_only_py37.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@pos_only_py37.py.snap new file mode 100644 index 0000000000..adb8b30bc1 --- /dev/null +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@pos_only_py37.py.snap @@ -0,0 +1,291 @@ +--- +source: crates/ruff_python_parser/tests/fixtures.rs +input_file: crates/ruff_python_parser/resources/inline/err/pos_only_py37.py +--- +## AST + +``` +Module( + ModModule { + range: 0..136, + body: [ + FunctionDef( + StmtFunctionDef { + range: 43..61, + is_async: false, + decorator_list: [], + name: Identifier { + id: Name("foo"), + range: 47..50, + }, + type_params: None, + parameters: Parameters { + range: 50..56, + posonlyargs: [ + ParameterWithDefault { + range: 51..52, + parameter: Parameter { + range: 51..52, + name: Identifier { + id: Name("a"), + range: 51..52, + }, + annotation: None, + }, + default: None, + }, + ], + args: [], + vararg: None, + kwonlyargs: [], + kwarg: None, + }, + returns: None, + body: [ + Expr( + StmtExpr { + range: 58..61, + value: EllipsisLiteral( + ExprEllipsisLiteral { + range: 58..61, + }, + ), + }, + ), + ], + }, + ), + FunctionDef( + StmtFunctionDef { + range: 62..86, + is_async: false, + decorator_list: [], + name: Identifier { + id: Name("foo"), + range: 66..69, + }, + type_params: None, + parameters: Parameters { + range: 69..81, + posonlyargs: [ + ParameterWithDefault { + range: 70..71, + parameter: Parameter { + range: 70..71, + name: Identifier { + id: Name("a"), + range: 70..71, + }, + annotation: None, + }, + default: None, + }, + ], + args: [ + ParameterWithDefault { + range: 76..77, + parameter: Parameter { + range: 76..77, + name: Identifier { + id: Name("b"), + range: 76..77, + }, + annotation: None, + }, + default: None, + }, + ], + vararg: None, + kwonlyargs: [], + kwarg: None, + }, + returns: None, + body: [ + Expr( + StmtExpr { + range: 83..86, + value: EllipsisLiteral( + ExprEllipsisLiteral { + range: 83..86, + }, + ), + }, + ), + ], + }, + ), + FunctionDef( + StmtFunctionDef { + range: 87..115, + is_async: false, + decorator_list: [], + name: Identifier { + id: Name("foo"), + range: 91..94, + }, + type_params: None, + parameters: Parameters { + range: 94..110, + posonlyargs: [ + ParameterWithDefault { + range: 95..96, + parameter: Parameter { + range: 95..96, + name: Identifier { + id: Name("a"), + range: 95..96, + }, + annotation: None, + }, + default: None, + }, + ], + args: [], + vararg: Some( + Parameter { + range: 98..103, + name: Identifier { + id: Name("args"), + range: 99..103, + }, + annotation: None, + }, + ), + kwonlyargs: [ + ParameterWithDefault { + range: 108..109, + parameter: Parameter { + range: 108..109, + name: Identifier { + id: Name("b"), + range: 108..109, + }, + annotation: None, + }, + default: None, + }, + ], + kwarg: None, + }, + returns: None, + body: [ + Expr( + StmtExpr { + range: 112..115, + value: EllipsisLiteral( + ExprEllipsisLiteral { + range: 112..115, + }, + ), + }, + ), + ], + }, + ), + FunctionDef( + StmtFunctionDef { + range: 116..135, + is_async: false, + decorator_list: [], + name: Identifier { + id: Name("foo"), + range: 120..123, + }, + type_params: None, + parameters: Parameters { + range: 123..130, + posonlyargs: [], + args: [ + ParameterWithDefault { + range: 124..125, + parameter: Parameter { + range: 124..125, + name: Identifier { + id: Name("a"), + range: 124..125, + }, + annotation: None, + }, + default: None, + }, + ], + vararg: None, + kwonlyargs: [], + kwarg: None, + }, + returns: None, + body: [ + Expr( + StmtExpr { + range: 132..135, + value: EllipsisLiteral( + ExprEllipsisLiteral { + range: 132..135, + }, + ), + }, + ), + ], + }, + ), + ], + }, +) +``` +## Errors + + | +1 | # parse_options: {"target-version": "3.7"} +2 | def foo(a, /): ... +3 | def foo(a, /, b, /): ... + | ^ Syntax Error: Only one '/' separator allowed +4 | def foo(a, *args, /, b): ... +5 | def foo(a, //): ... + | + + + | +2 | def foo(a, /): ... +3 | def foo(a, /, b, /): ... +4 | def foo(a, *args, /, b): ... + | ^ Syntax Error: '/' parameter must appear before '*' parameter +5 | def foo(a, //): ... + | + + + | +3 | def foo(a, /, b, /): ... +4 | def foo(a, *args, /, b): ... +5 | def foo(a, //): ... + | ^^ Syntax Error: Expected ',', found '//' + | + + +## Unsupported Syntax Errors + + | +1 | # parse_options: {"target-version": "3.7"} +2 | def foo(a, /): ... + | ^ Syntax Error: Cannot use positional-only parameter separator on Python 3.7 (syntax was added in Python 3.8) +3 | def foo(a, /, b, /): ... +4 | def foo(a, *args, /, b): ... + | + + + | +1 | # parse_options: {"target-version": "3.7"} +2 | def foo(a, /): ... +3 | def foo(a, /, b, /): ... + | ^ Syntax Error: Cannot use positional-only parameter separator on Python 3.7 (syntax was added in Python 3.8) +4 | def foo(a, *args, /, b): ... +5 | def foo(a, //): ... + | + + + | +2 | def foo(a, /): ... +3 | def foo(a, /, b, /): ... +4 | def foo(a, *args, /, b): ... + | ^ Syntax Error: Cannot use positional-only parameter separator on Python 3.7 (syntax was added in Python 3.8) +5 | def foo(a, //): ... + | diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@pos_only_py38.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@pos_only_py38.py.snap new file mode 100644 index 0000000000..2e5f4b9e98 --- /dev/null +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@pos_only_py38.py.snap @@ -0,0 +1,61 @@ +--- +source: crates/ruff_python_parser/tests/fixtures.rs +input_file: crates/ruff_python_parser/resources/inline/ok/pos_only_py38.py +--- +## AST + +``` +Module( + ModModule { + range: 0..62, + body: [ + FunctionDef( + StmtFunctionDef { + range: 43..61, + is_async: false, + decorator_list: [], + name: Identifier { + id: Name("foo"), + range: 47..50, + }, + type_params: None, + parameters: Parameters { + range: 50..56, + posonlyargs: [ + ParameterWithDefault { + range: 51..52, + parameter: Parameter { + range: 51..52, + name: Identifier { + id: Name("a"), + range: 51..52, + }, + annotation: None, + }, + default: None, + }, + ], + args: [], + vararg: None, + kwonlyargs: [], + kwarg: None, + }, + returns: None, + body: [ + Expr( + StmtExpr { + range: 58..61, + value: EllipsisLiteral( + ExprEllipsisLiteral { + range: 58..61, + }, + ), + }, + ), + ], + }, + ), + ], + }, +) +```