Skip to content

[syntax-errors] Improve error message and range for pre-PEP-614 decorator syntax errors #16581

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Mar 17, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# parse_options: { "target-version": "3.8" }
async def foo():
@await bar
def baz(): ...
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# parse_options: { "target-version": "3.8" }
@{3: 3}
def bar(): ...
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# parse_options: { "target-version": "3.8" }
@3.14
def bar(): ...
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# parse_options: { "target-version": "3.8" }
@foo().bar()
def baz(): ...
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# parse_options: { "target-version": "3.9" }
async def foo():
@await bar
def baz(): ...
35 changes: 32 additions & 3 deletions crates/ruff_python_parser/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -557,7 +557,7 @@ pub enum UnsupportedSyntaxErrorKind {
/// [PEP 614]: https://peps.python.org/pep-0614/
/// [`dotted_name`]: https://docs.python.org/3.8/reference/compound_stmts.html#grammar-token-dotted-name
/// [decorator grammar]: https://docs.python.org/3/reference/compound_stmts.html#grammar-token-python-grammar-decorator
RelaxedDecorator,
RelaxedDecorator(RelaxedDecoratorError),

/// Represents the use of a [PEP 570] positional-only parameter before Python 3.8.
///
Expand Down Expand Up @@ -633,7 +633,28 @@ impl Display for UnsupportedSyntaxError {
UnsupportedSyntaxErrorKind::StarTuple(StarTupleKind::Yield) => {
"Cannot use iterable unpacking in yield expressions"
}
UnsupportedSyntaxErrorKind::RelaxedDecorator => "Unsupported expression in decorators",
UnsupportedSyntaxErrorKind::RelaxedDecorator(relaxed_decorator_error) => {
return match relaxed_decorator_error {
RelaxedDecoratorError::CallExpression => {
write!(
f,
"Cannot use a call expression in a decorator on Python {} \
unless it is the top-level expression or it occurs \
in the argument list of a top-level call expression \
(relaxed decorator syntax was {changed})",
self.target_version,
changed = self.kind.changed_version(),
)
}
RelaxedDecoratorError::Other(description) => write!(
f,
"Cannot use {description} outside function call arguments in a decorator on Python {} \
(syntax was {changed})",
self.target_version,
changed = self.kind.changed_version(),
),
}
}
UnsupportedSyntaxErrorKind::PositionalOnlyParameter => {
"Cannot use positional-only parameter separator"
}
Expand All @@ -653,6 +674,12 @@ impl Display for UnsupportedSyntaxError {
}
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum RelaxedDecoratorError {
CallExpression,
Other(&'static str),
}

/// Represents the kind of change in Python syntax between versions.
enum Change {
Added(PythonVersion),
Expand All @@ -677,7 +704,9 @@ impl UnsupportedSyntaxErrorKind {
UnsupportedSyntaxErrorKind::Walrus => Change::Added(PythonVersion::PY38),
UnsupportedSyntaxErrorKind::ExceptStar => Change::Added(PythonVersion::PY311),
UnsupportedSyntaxErrorKind::StarTuple(_) => Change::Added(PythonVersion::PY38),
UnsupportedSyntaxErrorKind::RelaxedDecorator => Change::Added(PythonVersion::PY39),
UnsupportedSyntaxErrorKind::RelaxedDecorator { .. } => {
Change::Added(PythonVersion::PY39)
}
UnsupportedSyntaxErrorKind::PositionalOnlyParameter => {
Change::Added(PythonVersion::PY38)
}
Expand Down
62 changes: 54 additions & 8 deletions crates/ruff_python_parser/src/parser/helpers.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use ruff_python_ast::{self as ast, CmpOp, Expr, ExprContext};
use ruff_python_ast::{self as ast, CmpOp, Expr, ExprContext, Number};
use ruff_text_size::{Ranged, TextRange};

use crate::TokenKind;
use crate::{error::RelaxedDecoratorError, TokenKind};

/// Set the `ctx` for `Expr::Id`, `Expr::Attribute`, `Expr::Subscript`, `Expr::Starred`,
/// `Expr::Tuple` and `Expr::List`. If `expr` is either `Expr::Tuple` or `Expr::List`,
Expand Down Expand Up @@ -47,11 +48,56 @@ pub(super) const fn token_kind_to_cmp_op(tokens: [TokenKind; 2]) -> Option<CmpOp
/// Helper for `parse_decorators` to determine if `expr` is a [`dotted_name`] from the decorator
/// grammar before Python 3.9.
///
/// Returns `Some((error, range))` if `expr` is not a `dotted_name`, or `None` if it is a `dotted_name`.
///
/// [`dotted_name`]: https://docs.python.org/3.8/reference/compound_stmts.html#grammar-token-dotted-name
pub(super) fn is_name_or_attribute_expression(expr: &Expr) -> bool {
match expr {
Expr::Attribute(attr) => is_name_or_attribute_expression(&attr.value),
Expr::Name(_) => true,
_ => false,
}
pub(super) fn detect_invalid_pre_py39_decorator_node(
expr: &Expr,
) -> Option<(RelaxedDecoratorError, TextRange)> {
let description = match expr {
Expr::Name(_) => return None,

Expr::Attribute(attribute) => {
return detect_invalid_pre_py39_decorator_node(&attribute.value)
}

Expr::Call(_) => return Some((RelaxedDecoratorError::CallExpression, expr.range())),

Expr::NumberLiteral(number) => match &number.value {
Number::Int(_) => "an int literal",
Number::Float(_) => "a float literal",
Number::Complex { .. } => "a complex literal",
},

Expr::BoolOp(_) => "boolean expression",
Expr::BinOp(_) => "binary-operation expression",
Expr::UnaryOp(_) => "unary-operation expression",
Expr::Await(_) => "`await` expression",
Expr::Lambda(_) => "lambda expression",
Expr::If(_) => "conditional expression",
Expr::Dict(_) => "a dict literal",
Expr::Set(_) => "a set literal",
Expr::List(_) => "a list literal",
Expr::Tuple(_) => "a tuple literal",
Expr::Starred(_) => "starred expression",
Expr::Slice(_) => "slice expression",
Expr::BytesLiteral(_) => "a bytes literal",
Expr::StringLiteral(_) => "a string literal",
Expr::EllipsisLiteral(_) => "an ellipsis literal",
Expr::NoneLiteral(_) => "a `None` literal",
Expr::BooleanLiteral(_) => "a boolean literal",
Expr::ListComp(_) => "a list comprehension",
Expr::SetComp(_) => "a set comprehension",
Expr::DictComp(_) => "a dict comprehension",
Expr::Generator(_) => "generator expression",
Expr::Yield(_) => "`yield` expression",
Expr::YieldFrom(_) => "`yield from` expression",
Expr::Compare(_) => "comparison expression",
Expr::FString(_) => "f-string",
Expr::Named(_) => "assignment expression",
Expr::Subscript(_) => "subscript expression",
Expr::IpyEscapeCommand(_) => "IPython escape command",
};

Some((RelaxedDecoratorError::Other(description), expr.range()))
}
40 changes: 34 additions & 6 deletions crates/ruff_python_parser/src/parser/statement.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2678,17 +2678,45 @@ impl<'src> Parser<'src> {
// # parse_options: { "target-version": "3.7" }
// @(x := lambda x: x)(foo)
// def bar(): ...
let allowed_decorator = match &parsed_expr.expr {

// test_err decorator_dict_literal_py38
// # parse_options: { "target-version": "3.8" }
// @{3: 3}
// def bar(): ...

// test_err decorator_float_literal_py38
// # parse_options: { "target-version": "3.8" }
// @3.14
// def bar(): ...

// test_ok decorator_await_expression_py39
// # parse_options: { "target-version": "3.9" }
// async def foo():
// @await bar
// def baz(): ...

// test_err decorator_await_expression_py38
// # parse_options: { "target-version": "3.8" }
// async def foo():
// @await bar
// def baz(): ...

// test_err decorator_non_toplevel_call_expression_py38
// # parse_options: { "target-version": "3.8" }
// @foo().bar()
// def baz(): ...

let relaxed_decorator_error = match &parsed_expr.expr {
Expr::Call(expr_call) => {
helpers::is_name_or_attribute_expression(&expr_call.func)
helpers::detect_invalid_pre_py39_decorator_node(&expr_call.func)
}
expr => helpers::is_name_or_attribute_expression(expr),
expr => helpers::detect_invalid_pre_py39_decorator_node(expr),
};

if !allowed_decorator {
if let Some((error, range)) = relaxed_decorator_error {
self.add_unsupported_syntax_error(
UnsupportedSyntaxErrorKind::RelaxedDecorator,
parsed_expr.range(),
UnsupportedSyntaxErrorKind::RelaxedDecorator(error),
range,
);
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
---
source: crates/ruff_python_parser/tests/fixtures.rs
input_file: crates/ruff_python_parser/resources/inline/err/decorator_await_expression_py38.py
---
## AST

```
Module(
ModModule {
range: 0..96,
body: [
FunctionDef(
StmtFunctionDef {
range: 45..95,
is_async: true,
decorator_list: [],
name: Identifier {
id: Name("foo"),
range: 55..58,
},
type_params: None,
parameters: Parameters {
range: 58..60,
posonlyargs: [],
args: [],
vararg: None,
kwonlyargs: [],
kwarg: None,
},
returns: None,
body: [
FunctionDef(
StmtFunctionDef {
range: 66..95,
is_async: false,
decorator_list: [
Decorator {
range: 66..76,
expression: Await(
ExprAwait {
range: 67..76,
value: Name(
ExprName {
range: 73..76,
id: Name("bar"),
ctx: Load,
},
),
},
),
},
],
name: Identifier {
id: Name("baz"),
range: 85..88,
},
type_params: None,
parameters: Parameters {
range: 88..90,
posonlyargs: [],
args: [],
vararg: None,
kwonlyargs: [],
kwarg: None,
},
returns: None,
body: [
Expr(
StmtExpr {
range: 92..95,
value: EllipsisLiteral(
ExprEllipsisLiteral {
range: 92..95,
},
),
},
),
],
},
),
],
},
),
],
},
)
```
## Unsupported Syntax Errors

|
1 | # parse_options: { "target-version": "3.8" }
2 | async def foo():
3 | @await bar
| ^^^^^^^^^ Syntax Error: Cannot use `await` expression outside function call arguments in a decorator on Python 3.8 (syntax was added in Python 3.9)
4 | def baz(): ...
|
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
---
source: crates/ruff_python_parser/tests/fixtures.rs
input_file: crates/ruff_python_parser/resources/inline/err/decorator_dict_literal_py38.py
---
## AST

```
Module(
ModModule {
range: 0..68,
body: [
FunctionDef(
StmtFunctionDef {
range: 45..67,
is_async: false,
decorator_list: [
Decorator {
range: 45..52,
expression: Dict(
ExprDict {
range: 46..52,
items: [
DictItem {
key: Some(
NumberLiteral(
ExprNumberLiteral {
range: 47..48,
value: Int(
3,
),
},
),
),
value: NumberLiteral(
ExprNumberLiteral {
range: 50..51,
value: Int(
3,
),
},
),
},
],
},
),
},
],
name: Identifier {
id: Name("bar"),
range: 57..60,
},
type_params: None,
parameters: Parameters {
range: 60..62,
posonlyargs: [],
args: [],
vararg: None,
kwonlyargs: [],
kwarg: None,
},
returns: None,
body: [
Expr(
StmtExpr {
range: 64..67,
value: EllipsisLiteral(
ExprEllipsisLiteral {
range: 64..67,
},
),
},
),
],
},
),
],
},
)
```
## Unsupported Syntax Errors

|
1 | # parse_options: { "target-version": "3.8" }
2 | @{3: 3}
| ^^^^^^ 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)
3 | def bar(): ...
|
Loading
Loading