diff --git a/crates/ruff_python_parser/resources/inline/err/decorator_await_expression_py38.py b/crates/ruff_python_parser/resources/inline/err/decorator_await_expression_py38.py new file mode 100644 index 0000000000000..a84e92d8dff3e --- /dev/null +++ b/crates/ruff_python_parser/resources/inline/err/decorator_await_expression_py38.py @@ -0,0 +1,4 @@ +# parse_options: { "target-version": "3.8" } +async def foo(): + @await bar + def baz(): ... diff --git a/crates/ruff_python_parser/resources/inline/err/decorator_dict_literal_py38.py b/crates/ruff_python_parser/resources/inline/err/decorator_dict_literal_py38.py new file mode 100644 index 0000000000000..ab802e9754b5f --- /dev/null +++ b/crates/ruff_python_parser/resources/inline/err/decorator_dict_literal_py38.py @@ -0,0 +1,3 @@ +# parse_options: { "target-version": "3.8" } +@{3: 3} +def bar(): ... diff --git a/crates/ruff_python_parser/resources/inline/err/decorator_float_literal_py38.py b/crates/ruff_python_parser/resources/inline/err/decorator_float_literal_py38.py new file mode 100644 index 0000000000000..64f444d6eb564 --- /dev/null +++ b/crates/ruff_python_parser/resources/inline/err/decorator_float_literal_py38.py @@ -0,0 +1,3 @@ +# parse_options: { "target-version": "3.8" } +@3.14 +def bar(): ... diff --git a/crates/ruff_python_parser/resources/inline/err/decorator_non_toplevel_call_expression_py38.py b/crates/ruff_python_parser/resources/inline/err/decorator_non_toplevel_call_expression_py38.py new file mode 100644 index 0000000000000..16ecd93239002 --- /dev/null +++ b/crates/ruff_python_parser/resources/inline/err/decorator_non_toplevel_call_expression_py38.py @@ -0,0 +1,3 @@ +# parse_options: { "target-version": "3.8" } +@foo().bar() +def baz(): ... diff --git a/crates/ruff_python_parser/resources/inline/ok/decorator_await_expression_py39.py b/crates/ruff_python_parser/resources/inline/ok/decorator_await_expression_py39.py new file mode 100644 index 0000000000000..dcd8ed69b17c5 --- /dev/null +++ b/crates/ruff_python_parser/resources/inline/ok/decorator_await_expression_py39.py @@ -0,0 +1,4 @@ +# parse_options: { "target-version": "3.9" } +async def foo(): + @await bar + def baz(): ... diff --git a/crates/ruff_python_parser/src/error.rs b/crates/ruff_python_parser/src/error.rs index 6d0f6c38452c8..a19088ff60ea7 100644 --- a/crates/ruff_python_parser/src/error.rs +++ b/crates/ruff_python_parser/src/error.rs @@ -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. /// @@ -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" } @@ -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), @@ -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) } diff --git a/crates/ruff_python_parser/src/parser/helpers.rs b/crates/ruff_python_parser/src/parser/helpers.rs index b9c374eb71a38..c3e84cf69c96d 100644 --- a/crates/ruff_python_parser/src/parser/helpers.rs +++ b/crates/ruff_python_parser/src/parser/helpers.rs @@ -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`, @@ -47,11 +48,56 @@ pub(super) const fn token_kind_to_cmp_op(tokens: [TokenKind; 2]) -> Option 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())) } diff --git a/crates/ruff_python_parser/src/parser/statement.rs b/crates/ruff_python_parser/src/parser/statement.rs index f9bde0aa0407c..279ba8996de1e 100644 --- a/crates/ruff_python_parser/src/parser/statement.rs +++ b/crates/ruff_python_parser/src/parser/statement.rs @@ -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, ); } } diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@decorator_await_expression_py38.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@decorator_await_expression_py38.py.snap new file mode 100644 index 0000000000000..e86f1ea1f1d86 --- /dev/null +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@decorator_await_expression_py38.py.snap @@ -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(): ... + | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@decorator_dict_literal_py38.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@decorator_dict_literal_py38.py.snap new file mode 100644 index 0000000000000..e42d100e1f14f --- /dev/null +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@decorator_dict_literal_py38.py.snap @@ -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(): ... + | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@decorator_expression_py38.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@decorator_expression_py38.py.snap index 0caec0493648e..d530df39de0cf 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@decorator_expression_py38.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@decorator_expression_py38.py.snap @@ -96,6 +96,6 @@ Module( | 1 | # parse_options: { "target-version": "3.8" } 2 | @buttons[0].clicked.connect - | ^^^^^^^^^^^^^^^^^^^^^^^^^^ Syntax Error: Unsupported expression in decorators on Python 3.8 (syntax was added in Python 3.9) + | ^^^^^^^^^^ Syntax Error: Cannot use subscript expression outside function call arguments in a decorator on Python 3.8 (syntax was added in Python 3.9) 3 | def spam(): ... | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@decorator_float_literal_py38.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@decorator_float_literal_py38.py.snap new file mode 100644 index 0000000000000..8f2af04e27591 --- /dev/null +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@decorator_float_literal_py38.py.snap @@ -0,0 +1,68 @@ +--- +source: crates/ruff_python_parser/tests/fixtures.rs +input_file: crates/ruff_python_parser/resources/inline/err/decorator_float_literal_py38.py +--- +## AST + +``` +Module( + ModModule { + range: 0..66, + body: [ + FunctionDef( + StmtFunctionDef { + range: 45..65, + is_async: false, + decorator_list: [ + Decorator { + range: 45..50, + expression: NumberLiteral( + ExprNumberLiteral { + range: 46..50, + value: Float( + 3.14, + ), + }, + ), + }, + ], + name: Identifier { + id: Name("bar"), + range: 55..58, + }, + type_params: None, + parameters: Parameters { + range: 58..60, + posonlyargs: [], + args: [], + vararg: None, + kwonlyargs: [], + kwarg: None, + }, + returns: None, + body: [ + Expr( + StmtExpr { + range: 62..65, + value: EllipsisLiteral( + ExprEllipsisLiteral { + range: 62..65, + }, + ), + }, + ), + ], + }, + ), + ], + }, +) +``` +## Unsupported Syntax Errors + + | +1 | # parse_options: { "target-version": "3.8" } +2 | @3.14 + | ^^^^ Syntax Error: Cannot use a float literal outside function call arguments in a decorator on Python 3.8 (syntax was added in Python 3.9) +3 | def bar(): ... + | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@decorator_named_expression_py37.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@decorator_named_expression_py37.py.snap index 3a27ec1f1b842..fbbef25495456 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@decorator_named_expression_py37.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@decorator_named_expression_py37.py.snap @@ -128,6 +128,6 @@ Module( | 1 | # parse_options: { "target-version": "3.7" } 2 | @(x := lambda x: x)(foo) - | ^^^^^^^^^^^^^^^^^^^^^^^ Syntax Error: Unsupported expression in decorators on Python 3.7 (syntax was added in Python 3.9) + | ^^^^^^^^^^^^^^^^ Syntax Error: Cannot use assignment expression outside function call arguments in a decorator on Python 3.7 (syntax was added in Python 3.9) 3 | def bar(): ... | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@decorator_non_toplevel_call_expression_py38.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@decorator_non_toplevel_call_expression_py38.py.snap new file mode 100644 index 0000000000000..2374561e1678d --- /dev/null +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@decorator_non_toplevel_call_expression_py38.py.snap @@ -0,0 +1,97 @@ +--- +source: crates/ruff_python_parser/tests/fixtures.rs +input_file: crates/ruff_python_parser/resources/inline/err/decorator_non_toplevel_call_expression_py38.py +--- +## AST + +``` +Module( + ModModule { + range: 0..73, + body: [ + FunctionDef( + StmtFunctionDef { + range: 45..72, + is_async: false, + decorator_list: [ + Decorator { + range: 45..57, + expression: Call( + ExprCall { + range: 46..57, + func: Attribute( + ExprAttribute { + range: 46..55, + value: Call( + ExprCall { + range: 46..51, + func: Name( + ExprName { + range: 46..49, + id: Name("foo"), + ctx: Load, + }, + ), + arguments: Arguments { + range: 49..51, + args: [], + keywords: [], + }, + }, + ), + attr: Identifier { + id: Name("bar"), + range: 52..55, + }, + ctx: Load, + }, + ), + arguments: Arguments { + range: 55..57, + args: [], + keywords: [], + }, + }, + ), + }, + ], + name: Identifier { + id: Name("baz"), + range: 62..65, + }, + type_params: None, + parameters: Parameters { + range: 65..67, + posonlyargs: [], + args: [], + vararg: None, + kwonlyargs: [], + kwarg: None, + }, + returns: None, + body: [ + Expr( + StmtExpr { + range: 69..72, + value: EllipsisLiteral( + ExprEllipsisLiteral { + range: 69..72, + }, + ), + }, + ), + ], + }, + ), + ], + }, +) +``` +## Unsupported Syntax Errors + + | +1 | # parse_options: { "target-version": "3.8" } +2 | @foo().bar() + | ^^^^^ Syntax Error: Cannot use a call expression in a decorator on Python 3.8 unless it is the top-level expression or it occurs in the argument list of a top-level call expression (relaxed decorator syntax was added in Python 3.9) +3 | def baz(): ... + | diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@decorator_await_expression_py39.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@decorator_await_expression_py39.py.snap new file mode 100644 index 0000000000000..20b6b6d6dfb47 --- /dev/null +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@decorator_await_expression_py39.py.snap @@ -0,0 +1,87 @@ +--- +source: crates/ruff_python_parser/tests/fixtures.rs +input_file: crates/ruff_python_parser/resources/inline/ok/decorator_await_expression_py39.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, + }, + ), + }, + ), + ], + }, + ), + ], + }, + ), + ], + }, +) +```