Skip to content

Commit 08badbb

Browse files
committed
Improve error message and range for pre-PEP-614 decorator syntax errors
1 parent c970b79 commit 08badbb

13 files changed

+446
-18
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# parse_options: { "target-version": "3.8" }
2+
async def foo():
3+
@await bar
4+
def baz(): ...
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# parse_options: { "target-version": "3.8" }
2+
@{3: 3}
3+
def bar(): ...
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# parse_options: { "target-version": "3.8" }
2+
@3.14
3+
def bar(): ...
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# parse_options: { "target-version": "3.9" }
2+
async def foo():
3+
@await bar
4+
def baz(): ...

crates/ruff_python_parser/src/error.rs

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -557,7 +557,9 @@ pub enum UnsupportedSyntaxErrorKind {
557557
/// [PEP 614]: https://peps.python.org/pep-0614/
558558
/// [`dotted_name`]: https://docs.python.org/3.8/reference/compound_stmts.html#grammar-token-dotted-name
559559
/// [decorator grammar]: https://docs.python.org/3/reference/compound_stmts.html#grammar-token-python-grammar-decorator
560-
RelaxedDecorator,
560+
RelaxedDecorator {
561+
invalid_node_name: &'static str,
562+
},
561563

562564
/// Represents the use of a [PEP 570] positional-only parameter before Python 3.8.
563565
///
@@ -633,7 +635,12 @@ impl Display for UnsupportedSyntaxError {
633635
UnsupportedSyntaxErrorKind::StarTuple(StarTupleKind::Yield) => {
634636
"Cannot use iterable unpacking in yield expressions"
635637
}
636-
UnsupportedSyntaxErrorKind::RelaxedDecorator => "Unsupported expression in decorators",
638+
UnsupportedSyntaxErrorKind::RelaxedDecorator { invalid_node_name } => return write!(
639+
f,
640+
"Cannot use {invalid_node_name} outside function-call arguments in a decorator \
641+
on Python {target_version} (syntax was added in Python 3.9)",
642+
target_version = self.target_version,
643+
),
637644
UnsupportedSyntaxErrorKind::PositionalOnlyParameter => {
638645
"Cannot use positional-only parameter separator"
639646
}
@@ -677,7 +684,9 @@ impl UnsupportedSyntaxErrorKind {
677684
UnsupportedSyntaxErrorKind::Walrus => Change::Added(PythonVersion::PY38),
678685
UnsupportedSyntaxErrorKind::ExceptStar => Change::Added(PythonVersion::PY311),
679686
UnsupportedSyntaxErrorKind::StarTuple(_) => Change::Added(PythonVersion::PY38),
680-
UnsupportedSyntaxErrorKind::RelaxedDecorator => Change::Added(PythonVersion::PY39),
687+
UnsupportedSyntaxErrorKind::RelaxedDecorator { .. } => {
688+
Change::Added(PythonVersion::PY39)
689+
}
681690
UnsupportedSyntaxErrorKind::PositionalOnlyParameter => {
682691
Change::Added(PythonVersion::PY38)
683692
}

crates/ruff_python_parser/src/parser/helpers.rs

Lines changed: 49 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
use ruff_python_ast::{self as ast, CmpOp, Expr, ExprContext};
1+
use ruff_python_ast::{self as ast, CmpOp, Expr, ExprContext, Number};
2+
use ruff_text_size::{Ranged, TextRange};
23

34
use crate::TokenKind;
45

@@ -47,11 +48,52 @@ pub(super) const fn token_kind_to_cmp_op(tokens: [TokenKind; 2]) -> Option<CmpOp
4748
/// Helper for `parse_decorators` to determine if `expr` is a [`dotted_name`] from the decorator
4849
/// grammar before Python 3.9.
4950
///
51+
/// Returns `None` if `expr` is a `dotted_name`. Returns `Some((description, range))` if it is not,
52+
/// where `description` is a string describing the invalid node and `range` is the node's range.
53+
///
5054
/// [`dotted_name`]: https://docs.python.org/3.8/reference/compound_stmts.html#grammar-token-dotted-name
51-
pub(super) fn is_name_or_attribute_expression(expr: &Expr) -> bool {
52-
match expr {
53-
Expr::Attribute(attr) => is_name_or_attribute_expression(&attr.value),
54-
Expr::Name(_) => true,
55-
_ => false,
56-
}
55+
pub(super) fn invalid_pre_py39_decorator_node(expr: &Expr) -> Option<(&'static str, TextRange)> {
56+
let description = match expr {
57+
Expr::Attribute(attr) => return invalid_pre_py39_decorator_node(&attr.value),
58+
59+
Expr::Name(_) => None,
60+
61+
Expr::NumberLiteral(number) => match &number.value {
62+
Number::Int(_) => Some("an int literal"),
63+
Number::Float(_) => Some("a float literal"),
64+
Number::Complex { .. } => Some("a complex literal"),
65+
},
66+
67+
Expr::BoolOp(_) => Some("boolean expression"),
68+
Expr::BinOp(_) => Some("binary-operation expression"),
69+
Expr::UnaryOp(_) => Some("unary-operation expression"),
70+
Expr::Await(_) => Some("`await` expression"),
71+
Expr::Lambda(_) => Some("lambda expression"),
72+
Expr::If(_) => Some("conditional expression"),
73+
Expr::Dict(_) => Some("a dict literal"),
74+
Expr::Set(_) => Some("a set literal"),
75+
Expr::List(_) => Some("a list literal"),
76+
Expr::Tuple(_) => Some("a tuple literal"),
77+
Expr::Starred(_) => Some("starred expression"),
78+
Expr::Slice(_) => Some("slice expression"),
79+
Expr::BytesLiteral(_) => Some("bytes literal"),
80+
Expr::StringLiteral(_) => Some("string literal"),
81+
Expr::EllipsisLiteral(_) => Some("ellipsis literal"),
82+
Expr::NoneLiteral(_) => Some("`None` literal"),
83+
Expr::BooleanLiteral(_) => Some("boolean literal"),
84+
Expr::ListComp(_) => Some("list comprehension"),
85+
Expr::SetComp(_) => Some("set comprehension"),
86+
Expr::DictComp(_) => Some("dict comprehension"),
87+
Expr::Generator(_) => Some("generator expression"),
88+
Expr::Yield(_) => Some("`yield` expression"),
89+
Expr::YieldFrom(_) => Some("`yield from` expression"),
90+
Expr::Compare(_) => Some("comparison expression"),
91+
Expr::Call(_) => Some("function call"),
92+
Expr::FString(_) => Some("f-string"),
93+
Expr::Named(_) => Some("assignment expression"),
94+
Expr::Subscript(_) => Some("subscript expression"),
95+
Expr::IpyEscapeCommand(_) => Some("IPython escape command"),
96+
};
97+
98+
description.map(|description| (description, expr.range()))
5799
}

crates/ruff_python_parser/src/parser/statement.rs

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2678,17 +2678,42 @@ impl<'src> Parser<'src> {
26782678
// # parse_options: { "target-version": "3.7" }
26792679
// @(x := lambda x: x)(foo)
26802680
// def bar(): ...
2681-
let allowed_decorator = match &parsed_expr.expr {
2681+
2682+
// test_err decorator_dict_literal_py38
2683+
// # parse_options: { "target-version": "3.8" }
2684+
// @{3: 3}
2685+
// def bar(): ...
2686+
2687+
// test_err decorator_float_literal_py38
2688+
// # parse_options: { "target-version": "3.8" }
2689+
// @3.14
2690+
// def bar(): ...
2691+
2692+
// test_ok decorator_await_expression_py39
2693+
// # parse_options: { "target-version": "3.9" }
2694+
// async def foo():
2695+
// @await bar
2696+
// def baz(): ...
2697+
2698+
// test_err decorator_await_expression_py38
2699+
// # parse_options: { "target-version": "3.8" }
2700+
// async def foo():
2701+
// @await bar
2702+
// def baz(): ...
2703+
2704+
let disallowed_expression = match &parsed_expr.expr {
26822705
Expr::Call(expr_call) => {
2683-
helpers::is_name_or_attribute_expression(&expr_call.func)
2706+
helpers::invalid_pre_py39_decorator_node(&expr_call.func)
26842707
}
2685-
expr => helpers::is_name_or_attribute_expression(expr),
2708+
expr => helpers::invalid_pre_py39_decorator_node(expr),
26862709
};
26872710

2688-
if !allowed_decorator {
2711+
if let Some((description, range)) = disallowed_expression {
26892712
self.add_unsupported_syntax_error(
2690-
UnsupportedSyntaxErrorKind::RelaxedDecorator,
2691-
parsed_expr.range(),
2713+
UnsupportedSyntaxErrorKind::RelaxedDecorator {
2714+
invalid_node_name: description,
2715+
},
2716+
range,
26922717
);
26932718
}
26942719
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
---
2+
source: crates/ruff_python_parser/tests/fixtures.rs
3+
input_file: crates/ruff_python_parser/resources/inline/err/decorator_await_expression_py38.py
4+
---
5+
## AST
6+
7+
```
8+
Module(
9+
ModModule {
10+
range: 0..96,
11+
body: [
12+
FunctionDef(
13+
StmtFunctionDef {
14+
range: 45..95,
15+
is_async: true,
16+
decorator_list: [],
17+
name: Identifier {
18+
id: Name("foo"),
19+
range: 55..58,
20+
},
21+
type_params: None,
22+
parameters: Parameters {
23+
range: 58..60,
24+
posonlyargs: [],
25+
args: [],
26+
vararg: None,
27+
kwonlyargs: [],
28+
kwarg: None,
29+
},
30+
returns: None,
31+
body: [
32+
FunctionDef(
33+
StmtFunctionDef {
34+
range: 66..95,
35+
is_async: false,
36+
decorator_list: [
37+
Decorator {
38+
range: 66..76,
39+
expression: Await(
40+
ExprAwait {
41+
range: 67..76,
42+
value: Name(
43+
ExprName {
44+
range: 73..76,
45+
id: Name("bar"),
46+
ctx: Load,
47+
},
48+
),
49+
},
50+
),
51+
},
52+
],
53+
name: Identifier {
54+
id: Name("baz"),
55+
range: 85..88,
56+
},
57+
type_params: None,
58+
parameters: Parameters {
59+
range: 88..90,
60+
posonlyargs: [],
61+
args: [],
62+
vararg: None,
63+
kwonlyargs: [],
64+
kwarg: None,
65+
},
66+
returns: None,
67+
body: [
68+
Expr(
69+
StmtExpr {
70+
range: 92..95,
71+
value: EllipsisLiteral(
72+
ExprEllipsisLiteral {
73+
range: 92..95,
74+
},
75+
),
76+
},
77+
),
78+
],
79+
},
80+
),
81+
],
82+
},
83+
),
84+
],
85+
},
86+
)
87+
```
88+
## Unsupported Syntax Errors
89+
90+
|
91+
1 | # parse_options: { "target-version": "3.8" }
92+
2 | async def foo():
93+
3 | @await bar
94+
| ^^^^^^^^^ Syntax Error: Cannot use `await` expression outside function-call arguments in a decorator on Python 3.8 (syntax was added in Python 3.9)
95+
4 | def baz(): ...
96+
|
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
---
2+
source: crates/ruff_python_parser/tests/fixtures.rs
3+
input_file: crates/ruff_python_parser/resources/inline/err/decorator_dict_literal_py38.py
4+
---
5+
## AST
6+
7+
```
8+
Module(
9+
ModModule {
10+
range: 0..68,
11+
body: [
12+
FunctionDef(
13+
StmtFunctionDef {
14+
range: 45..67,
15+
is_async: false,
16+
decorator_list: [
17+
Decorator {
18+
range: 45..52,
19+
expression: Dict(
20+
ExprDict {
21+
range: 46..52,
22+
items: [
23+
DictItem {
24+
key: Some(
25+
NumberLiteral(
26+
ExprNumberLiteral {
27+
range: 47..48,
28+
value: Int(
29+
3,
30+
),
31+
},
32+
),
33+
),
34+
value: NumberLiteral(
35+
ExprNumberLiteral {
36+
range: 50..51,
37+
value: Int(
38+
3,
39+
),
40+
},
41+
),
42+
},
43+
],
44+
},
45+
),
46+
},
47+
],
48+
name: Identifier {
49+
id: Name("bar"),
50+
range: 57..60,
51+
},
52+
type_params: None,
53+
parameters: Parameters {
54+
range: 60..62,
55+
posonlyargs: [],
56+
args: [],
57+
vararg: None,
58+
kwonlyargs: [],
59+
kwarg: None,
60+
},
61+
returns: None,
62+
body: [
63+
Expr(
64+
StmtExpr {
65+
range: 64..67,
66+
value: EllipsisLiteral(
67+
ExprEllipsisLiteral {
68+
range: 64..67,
69+
},
70+
),
71+
},
72+
),
73+
],
74+
},
75+
),
76+
],
77+
},
78+
)
79+
```
80+
## Unsupported Syntax Errors
81+
82+
|
83+
1 | # parse_options: { "target-version": "3.8" }
84+
2 | @{3: 3}
85+
| ^^^^^^ Syntax Error: Cannot use a dict literal outside function-call arguments in a decorator on Python 3.8 (syntax was added in Python 3.9)
86+
3 | def bar(): ...
87+
|

crates/ruff_python_parser/tests/snapshots/invalid_syntax@decorator_expression_py38.py.snap

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,6 @@ Module(
9696
|
9797
1 | # parse_options: { "target-version": "3.8" }
9898
2 | @buttons[0].clicked.connect
99-
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ Syntax Error: Unsupported expression in decorators on Python 3.8 (syntax was added in Python 3.9)
99+
| ^^^^^^^^^^ Syntax Error: Cannot use subscript expression outside function-call arguments in a decorator on Python 3.8 (syntax was added in Python 3.9)
100100
3 | def spam(): ...
101101
|

0 commit comments

Comments
 (0)