Skip to content

Commit d062388

Browse files
authored
[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.
1 parent 23fd492 commit d062388

File tree

6 files changed

+404
-0
lines changed

6 files changed

+404
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# parse_options: {"target-version": "3.7"}
2+
def foo(a, /): ...
3+
def foo(a, /, b, /): ...
4+
def foo(a, *args, /, b): ...
5+
def foo(a, //): ...
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# parse_options: {"target-version": "3.8"}
2+
def foo(a, /): ...

crates/ruff_python_parser/src/error.rs

+30
Original file line numberDiff line numberDiff line change
@@ -449,6 +449,32 @@ pub enum UnsupportedSyntaxErrorKind {
449449
Match,
450450
Walrus,
451451
ExceptStar,
452+
/// Represents the use of a [PEP 570] positional-only parameter before Python 3.8.
453+
///
454+
/// ## Examples
455+
///
456+
/// Python 3.8 added the `/` syntax for marking preceding parameters as positional-only:
457+
///
458+
/// ```python
459+
/// def foo(a, b, /, c): ...
460+
/// ```
461+
///
462+
/// This means `a` and `b` in this case can only be provided by position, not by name. In other
463+
/// words, this code results in a `TypeError` at runtime:
464+
///
465+
/// ```pycon
466+
/// >>> def foo(a, b, /, c): ...
467+
/// ...
468+
/// >>> foo(a=1, b=2, c=3)
469+
/// Traceback (most recent call last):
470+
/// File "<python-input-3>", line 1, in <module>
471+
/// foo(a=1, b=2, c=3)
472+
/// ~~~^^^^^^^^^^^^^^^
473+
/// TypeError: foo() got some positional-only arguments passed as keyword arguments: 'a, b'
474+
/// ```
475+
///
476+
/// [PEP 570]: https://peps.python.org/pep-0570/
477+
PositionalOnlyParameter,
452478
/// Represents the use of a [type parameter list] before Python 3.12.
453479
///
454480
/// ## Examples
@@ -487,6 +513,9 @@ impl Display for UnsupportedSyntaxError {
487513
UnsupportedSyntaxErrorKind::Match => "Cannot use `match` statement",
488514
UnsupportedSyntaxErrorKind::Walrus => "Cannot use named assignment expression (`:=`)",
489515
UnsupportedSyntaxErrorKind::ExceptStar => "Cannot use `except*`",
516+
UnsupportedSyntaxErrorKind::PositionalOnlyParameter => {
517+
"Cannot use positional-only parameter separator"
518+
}
490519
UnsupportedSyntaxErrorKind::TypeParameterList => "Cannot use type parameter lists",
491520
UnsupportedSyntaxErrorKind::TypeAliasStatement => "Cannot use `type` alias statement",
492521
UnsupportedSyntaxErrorKind::TypeParamDefault => {
@@ -509,6 +538,7 @@ impl UnsupportedSyntaxErrorKind {
509538
UnsupportedSyntaxErrorKind::Match => PythonVersion::PY310,
510539
UnsupportedSyntaxErrorKind::Walrus => PythonVersion::PY38,
511540
UnsupportedSyntaxErrorKind::ExceptStar => PythonVersion::PY311,
541+
UnsupportedSyntaxErrorKind::PositionalOnlyParameter => PythonVersion::PY38,
512542
UnsupportedSyntaxErrorKind::TypeParameterList => PythonVersion::PY312,
513543
UnsupportedSyntaxErrorKind::TypeAliasStatement => PythonVersion::PY312,
514544
UnsupportedSyntaxErrorKind::TypeParamDefault => PythonVersion::PY313,

crates/ruff_python_parser/src/parser/statement.rs

+15
Original file line numberDiff line numberDiff line change
@@ -3050,6 +3050,21 @@ impl<'src> Parser<'src> {
30503050
// first time, otherwise it's a user error.
30513051
std::mem::swap(&mut parameters.args, &mut parameters.posonlyargs);
30523052
seen_positional_only_separator = true;
3053+
3054+
// test_ok pos_only_py38
3055+
// # parse_options: {"target-version": "3.8"}
3056+
// def foo(a, /): ...
3057+
3058+
// test_err pos_only_py37
3059+
// # parse_options: {"target-version": "3.7"}
3060+
// def foo(a, /): ...
3061+
// def foo(a, /, b, /): ...
3062+
// def foo(a, *args, /, b): ...
3063+
// def foo(a, //): ...
3064+
parser.add_unsupported_syntax_error(
3065+
UnsupportedSyntaxErrorKind::PositionalOnlyParameter,
3066+
slash_range,
3067+
);
30533068
}
30543069

30553070
last_keyword_only_separator_range = None;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,291 @@
1+
---
2+
source: crates/ruff_python_parser/tests/fixtures.rs
3+
input_file: crates/ruff_python_parser/resources/inline/err/pos_only_py37.py
4+
---
5+
## AST
6+
7+
```
8+
Module(
9+
ModModule {
10+
range: 0..136,
11+
body: [
12+
FunctionDef(
13+
StmtFunctionDef {
14+
range: 43..61,
15+
is_async: false,
16+
decorator_list: [],
17+
name: Identifier {
18+
id: Name("foo"),
19+
range: 47..50,
20+
},
21+
type_params: None,
22+
parameters: Parameters {
23+
range: 50..56,
24+
posonlyargs: [
25+
ParameterWithDefault {
26+
range: 51..52,
27+
parameter: Parameter {
28+
range: 51..52,
29+
name: Identifier {
30+
id: Name("a"),
31+
range: 51..52,
32+
},
33+
annotation: None,
34+
},
35+
default: None,
36+
},
37+
],
38+
args: [],
39+
vararg: None,
40+
kwonlyargs: [],
41+
kwarg: None,
42+
},
43+
returns: None,
44+
body: [
45+
Expr(
46+
StmtExpr {
47+
range: 58..61,
48+
value: EllipsisLiteral(
49+
ExprEllipsisLiteral {
50+
range: 58..61,
51+
},
52+
),
53+
},
54+
),
55+
],
56+
},
57+
),
58+
FunctionDef(
59+
StmtFunctionDef {
60+
range: 62..86,
61+
is_async: false,
62+
decorator_list: [],
63+
name: Identifier {
64+
id: Name("foo"),
65+
range: 66..69,
66+
},
67+
type_params: None,
68+
parameters: Parameters {
69+
range: 69..81,
70+
posonlyargs: [
71+
ParameterWithDefault {
72+
range: 70..71,
73+
parameter: Parameter {
74+
range: 70..71,
75+
name: Identifier {
76+
id: Name("a"),
77+
range: 70..71,
78+
},
79+
annotation: None,
80+
},
81+
default: None,
82+
},
83+
],
84+
args: [
85+
ParameterWithDefault {
86+
range: 76..77,
87+
parameter: Parameter {
88+
range: 76..77,
89+
name: Identifier {
90+
id: Name("b"),
91+
range: 76..77,
92+
},
93+
annotation: None,
94+
},
95+
default: None,
96+
},
97+
],
98+
vararg: None,
99+
kwonlyargs: [],
100+
kwarg: None,
101+
},
102+
returns: None,
103+
body: [
104+
Expr(
105+
StmtExpr {
106+
range: 83..86,
107+
value: EllipsisLiteral(
108+
ExprEllipsisLiteral {
109+
range: 83..86,
110+
},
111+
),
112+
},
113+
),
114+
],
115+
},
116+
),
117+
FunctionDef(
118+
StmtFunctionDef {
119+
range: 87..115,
120+
is_async: false,
121+
decorator_list: [],
122+
name: Identifier {
123+
id: Name("foo"),
124+
range: 91..94,
125+
},
126+
type_params: None,
127+
parameters: Parameters {
128+
range: 94..110,
129+
posonlyargs: [
130+
ParameterWithDefault {
131+
range: 95..96,
132+
parameter: Parameter {
133+
range: 95..96,
134+
name: Identifier {
135+
id: Name("a"),
136+
range: 95..96,
137+
},
138+
annotation: None,
139+
},
140+
default: None,
141+
},
142+
],
143+
args: [],
144+
vararg: Some(
145+
Parameter {
146+
range: 98..103,
147+
name: Identifier {
148+
id: Name("args"),
149+
range: 99..103,
150+
},
151+
annotation: None,
152+
},
153+
),
154+
kwonlyargs: [
155+
ParameterWithDefault {
156+
range: 108..109,
157+
parameter: Parameter {
158+
range: 108..109,
159+
name: Identifier {
160+
id: Name("b"),
161+
range: 108..109,
162+
},
163+
annotation: None,
164+
},
165+
default: None,
166+
},
167+
],
168+
kwarg: None,
169+
},
170+
returns: None,
171+
body: [
172+
Expr(
173+
StmtExpr {
174+
range: 112..115,
175+
value: EllipsisLiteral(
176+
ExprEllipsisLiteral {
177+
range: 112..115,
178+
},
179+
),
180+
},
181+
),
182+
],
183+
},
184+
),
185+
FunctionDef(
186+
StmtFunctionDef {
187+
range: 116..135,
188+
is_async: false,
189+
decorator_list: [],
190+
name: Identifier {
191+
id: Name("foo"),
192+
range: 120..123,
193+
},
194+
type_params: None,
195+
parameters: Parameters {
196+
range: 123..130,
197+
posonlyargs: [],
198+
args: [
199+
ParameterWithDefault {
200+
range: 124..125,
201+
parameter: Parameter {
202+
range: 124..125,
203+
name: Identifier {
204+
id: Name("a"),
205+
range: 124..125,
206+
},
207+
annotation: None,
208+
},
209+
default: None,
210+
},
211+
],
212+
vararg: None,
213+
kwonlyargs: [],
214+
kwarg: None,
215+
},
216+
returns: None,
217+
body: [
218+
Expr(
219+
StmtExpr {
220+
range: 132..135,
221+
value: EllipsisLiteral(
222+
ExprEllipsisLiteral {
223+
range: 132..135,
224+
},
225+
),
226+
},
227+
),
228+
],
229+
},
230+
),
231+
],
232+
},
233+
)
234+
```
235+
## Errors
236+
237+
|
238+
1 | # parse_options: {"target-version": "3.7"}
239+
2 | def foo(a, /): ...
240+
3 | def foo(a, /, b, /): ...
241+
| ^ Syntax Error: Only one '/' separator allowed
242+
4 | def foo(a, *args, /, b): ...
243+
5 | def foo(a, //): ...
244+
|
245+
246+
247+
|
248+
2 | def foo(a, /): ...
249+
3 | def foo(a, /, b, /): ...
250+
4 | def foo(a, *args, /, b): ...
251+
| ^ Syntax Error: '/' parameter must appear before '*' parameter
252+
5 | def foo(a, //): ...
253+
|
254+
255+
256+
|
257+
3 | def foo(a, /, b, /): ...
258+
4 | def foo(a, *args, /, b): ...
259+
5 | def foo(a, //): ...
260+
| ^^ Syntax Error: Expected ',', found '//'
261+
|
262+
263+
264+
## Unsupported Syntax Errors
265+
266+
|
267+
1 | # parse_options: {"target-version": "3.7"}
268+
2 | def foo(a, /): ...
269+
| ^ Syntax Error: Cannot use positional-only parameter separator on Python 3.7 (syntax was added in Python 3.8)
270+
3 | def foo(a, /, b, /): ...
271+
4 | def foo(a, *args, /, b): ...
272+
|
273+
274+
275+
|
276+
1 | # parse_options: {"target-version": "3.7"}
277+
2 | def foo(a, /): ...
278+
3 | def foo(a, /, b, /): ...
279+
| ^ Syntax Error: Cannot use positional-only parameter separator on Python 3.7 (syntax was added in Python 3.8)
280+
4 | def foo(a, *args, /, b): ...
281+
5 | def foo(a, //): ...
282+
|
283+
284+
285+
|
286+
2 | def foo(a, /): ...
287+
3 | def foo(a, /, b, /): ...
288+
4 | def foo(a, *args, /, b): ...
289+
| ^ Syntax Error: Cannot use positional-only parameter separator on Python 3.7 (syntax was added in Python 3.8)
290+
5 | def foo(a, //): ...
291+
|

0 commit comments

Comments
 (0)