Skip to content

Commit 38bfda9

Browse files
authored
[syntax-errors] Improve error message and range for pre-PEP-614 decorator syntax errors (#16581)
## Summary A small followup to #16386. We now tell the user exactly what it was about their decorator that constituted invalid syntax on Python <3.9, and the range now highlights the specific sub-expression that is invalid rather than highlighting the whole decorator ## Test Plan Inline snapshots are updated, and new ones are added.
1 parent 4da6936 commit 38bfda9

15 files changed

+574
-19
lines changed
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(): ...
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(): ...
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(): ...
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# parse_options: { "target-version": "3.8" }
2+
@foo().bar()
3+
def baz(): ...
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

+32-3
Original file line numberDiff line numberDiff line change
@@ -601,7 +601,7 @@ pub enum UnsupportedSyntaxErrorKind {
601601
/// [PEP 614]: https://peps.python.org/pep-0614/
602602
/// [`dotted_name`]: https://docs.python.org/3.8/reference/compound_stmts.html#grammar-token-dotted-name
603603
/// [decorator grammar]: https://docs.python.org/3/reference/compound_stmts.html#grammar-token-python-grammar-decorator
604-
RelaxedDecorator,
604+
RelaxedDecorator(RelaxedDecoratorError),
605605

606606
/// Represents the use of a [PEP 570] positional-only parameter before Python 3.8.
607607
///
@@ -768,7 +768,28 @@ impl Display for UnsupportedSyntaxError {
768768
UnsupportedSyntaxErrorKind::StarTuple(StarTupleKind::Yield) => {
769769
"Cannot use iterable unpacking in yield expressions"
770770
}
771-
UnsupportedSyntaxErrorKind::RelaxedDecorator => "Unsupported expression in decorators",
771+
UnsupportedSyntaxErrorKind::RelaxedDecorator(relaxed_decorator_error) => {
772+
return match relaxed_decorator_error {
773+
RelaxedDecoratorError::CallExpression => {
774+
write!(
775+
f,
776+
"Cannot use a call expression in a decorator on Python {} \
777+
unless it is the top-level expression or it occurs \
778+
in the argument list of a top-level call expression \
779+
(relaxed decorator syntax was {changed})",
780+
self.target_version,
781+
changed = self.kind.changed_version(),
782+
)
783+
}
784+
RelaxedDecoratorError::Other(description) => write!(
785+
f,
786+
"Cannot use {description} outside function call arguments in a decorator on Python {} \
787+
(syntax was {changed})",
788+
self.target_version,
789+
changed = self.kind.changed_version(),
790+
),
791+
}
792+
}
772793
UnsupportedSyntaxErrorKind::PositionalOnlyParameter => {
773794
"Cannot use positional-only parameter separator"
774795
}
@@ -795,6 +816,12 @@ impl Display for UnsupportedSyntaxError {
795816
}
796817
}
797818

819+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
820+
pub enum RelaxedDecoratorError {
821+
CallExpression,
822+
Other(&'static str),
823+
}
824+
798825
/// Represents the kind of change in Python syntax between versions.
799826
enum Change {
800827
Added(PythonVersion),
@@ -822,7 +849,9 @@ impl UnsupportedSyntaxErrorKind {
822849
Change::Added(PythonVersion::PY39)
823850
}
824851
UnsupportedSyntaxErrorKind::StarTuple(_) => Change::Added(PythonVersion::PY38),
825-
UnsupportedSyntaxErrorKind::RelaxedDecorator => Change::Added(PythonVersion::PY39),
852+
UnsupportedSyntaxErrorKind::RelaxedDecorator { .. } => {
853+
Change::Added(PythonVersion::PY39)
854+
}
826855
UnsupportedSyntaxErrorKind::PositionalOnlyParameter => {
827856
Change::Added(PythonVersion::PY38)
828857
}

crates/ruff_python_parser/src/parser/helpers.rs

+54-8
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
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

3-
use crate::TokenKind;
4+
use crate::{error::RelaxedDecoratorError, TokenKind};
45

56
/// Set the `ctx` for `Expr::Id`, `Expr::Attribute`, `Expr::Subscript`, `Expr::Starred`,
67
/// `Expr::Tuple` and `Expr::List`. If `expr` is either `Expr::Tuple` or `Expr::List`,
@@ -47,11 +48,56 @@ 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 `Some((error, range))` if `expr` is not a `dotted_name`, or `None` if it is a `dotted_name`.
52+
///
5053
/// [`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-
}
54+
pub(super) fn detect_invalid_pre_py39_decorator_node(
55+
expr: &Expr,
56+
) -> Option<(RelaxedDecoratorError, TextRange)> {
57+
let description = match expr {
58+
Expr::Name(_) => return None,
59+
60+
Expr::Attribute(attribute) => {
61+
return detect_invalid_pre_py39_decorator_node(&attribute.value)
62+
}
63+
64+
Expr::Call(_) => return Some((RelaxedDecoratorError::CallExpression, expr.range())),
65+
66+
Expr::NumberLiteral(number) => match &number.value {
67+
Number::Int(_) => "an int literal",
68+
Number::Float(_) => "a float literal",
69+
Number::Complex { .. } => "a complex literal",
70+
},
71+
72+
Expr::BoolOp(_) => "boolean expression",
73+
Expr::BinOp(_) => "binary-operation expression",
74+
Expr::UnaryOp(_) => "unary-operation expression",
75+
Expr::Await(_) => "`await` expression",
76+
Expr::Lambda(_) => "lambda expression",
77+
Expr::If(_) => "conditional expression",
78+
Expr::Dict(_) => "a dict literal",
79+
Expr::Set(_) => "a set literal",
80+
Expr::List(_) => "a list literal",
81+
Expr::Tuple(_) => "a tuple literal",
82+
Expr::Starred(_) => "starred expression",
83+
Expr::Slice(_) => "slice expression",
84+
Expr::BytesLiteral(_) => "a bytes literal",
85+
Expr::StringLiteral(_) => "a string literal",
86+
Expr::EllipsisLiteral(_) => "an ellipsis literal",
87+
Expr::NoneLiteral(_) => "a `None` literal",
88+
Expr::BooleanLiteral(_) => "a boolean literal",
89+
Expr::ListComp(_) => "a list comprehension",
90+
Expr::SetComp(_) => "a set comprehension",
91+
Expr::DictComp(_) => "a dict comprehension",
92+
Expr::Generator(_) => "generator expression",
93+
Expr::Yield(_) => "`yield` expression",
94+
Expr::YieldFrom(_) => "`yield from` expression",
95+
Expr::Compare(_) => "comparison expression",
96+
Expr::FString(_) => "f-string",
97+
Expr::Named(_) => "assignment expression",
98+
Expr::Subscript(_) => "subscript expression",
99+
Expr::IpyEscapeCommand(_) => "IPython escape command",
100+
};
101+
102+
Some((RelaxedDecoratorError::Other(description), expr.range()))
57103
}

crates/ruff_python_parser/src/parser/statement.rs

+34-6
Original file line numberDiff line numberDiff line change
@@ -2705,17 +2705,45 @@ impl<'src> Parser<'src> {
27052705
// # parse_options: { "target-version": "3.7" }
27062706
// @(x := lambda x: x)(foo)
27072707
// def bar(): ...
2708-
let allowed_decorator = match &parsed_expr.expr {
2708+
2709+
// test_err decorator_dict_literal_py38
2710+
// # parse_options: { "target-version": "3.8" }
2711+
// @{3: 3}
2712+
// def bar(): ...
2713+
2714+
// test_err decorator_float_literal_py38
2715+
// # parse_options: { "target-version": "3.8" }
2716+
// @3.14
2717+
// def bar(): ...
2718+
2719+
// test_ok decorator_await_expression_py39
2720+
// # parse_options: { "target-version": "3.9" }
2721+
// async def foo():
2722+
// @await bar
2723+
// def baz(): ...
2724+
2725+
// test_err decorator_await_expression_py38
2726+
// # parse_options: { "target-version": "3.8" }
2727+
// async def foo():
2728+
// @await bar
2729+
// def baz(): ...
2730+
2731+
// test_err decorator_non_toplevel_call_expression_py38
2732+
// # parse_options: { "target-version": "3.8" }
2733+
// @foo().bar()
2734+
// def baz(): ...
2735+
2736+
let relaxed_decorator_error = match &parsed_expr.expr {
27092737
Expr::Call(expr_call) => {
2710-
helpers::is_name_or_attribute_expression(&expr_call.func)
2738+
helpers::detect_invalid_pre_py39_decorator_node(&expr_call.func)
27112739
}
2712-
expr => helpers::is_name_or_attribute_expression(expr),
2740+
expr => helpers::detect_invalid_pre_py39_decorator_node(expr),
27132741
};
27142742

2715-
if !allowed_decorator {
2743+
if let Some((error, range)) = relaxed_decorator_error {
27162744
self.add_unsupported_syntax_error(
2717-
UnsupportedSyntaxErrorKind::RelaxedDecorator,
2718-
parsed_expr.range(),
2745+
UnsupportedSyntaxErrorKind::RelaxedDecorator(error),
2746+
range,
27192747
);
27202748
}
27212749
}
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+
|
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+
|

0 commit comments

Comments
 (0)