-
Notifications
You must be signed in to change notification settings - Fork 1.6k
[syntax-errors] Named expressions in decorators before Python 3.9 #16386
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
Changes from 4 commits
2b4b1a2
c7b36ef
7ccbc74
ca5b34f
e85248c
0d56c4c
ae2afcf
4b5a706
886db18
0585337
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| # parse_options: { "target-version": "3.8" } | ||
| @buttons[0].clicked.connect | ||
| def spam(): ... |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| # parse_options: { "target-version": "3.8" } | ||
| @buttons.clicked.connect | ||
| def spam(): ... |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| # parse_options: { "target-version": "3.8" } | ||
| @eval("buttons[0].clicked.connect") | ||
| def spam(): ... |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| # parse_options: { "target-version": "3.8" } | ||
| def _(x): return x | ||
| @_(buttons[0].clicked.connect) | ||
| def spam(): ... |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| # parse_options: { "target-version": "3.9" } | ||
| @buttons[0].clicked.connect | ||
| def spam(): ... |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,7 +5,8 @@ use rustc_hash::{FxBuildHasher, FxHashSet}; | |
|
|
||
| use ruff_python_ast::name::Name; | ||
| use ruff_python_ast::{ | ||
| self as ast, ExceptHandler, Expr, ExprContext, IpyEscapeKind, Operator, Stmt, WithItem, | ||
| self as ast, ExceptHandler, Expr, ExprContext, IpyEscapeKind, Operator, PythonVersion, Stmt, | ||
| WithItem, | ||
| }; | ||
| use ruff_text_size::{Ranged, TextSize}; | ||
|
|
||
|
|
@@ -2533,6 +2534,45 @@ impl<'src> Parser<'src> { | |
| } | ||
| } | ||
|
|
||
| /// Try to parse a decorator list from before Python 3.9. | ||
| /// | ||
| /// See: <https://docs.python.org/3.8/reference/compound_stmts.html#grammar-token-decorators> | ||
| fn try_parse_old_decorators(&mut self) -> Option<ParsedExpr> { | ||
| let errors = self.errors.len(); | ||
| let start = self.node_start(); | ||
| // initial identifier | ||
| let ident = self.parse_identifier(); | ||
| if !ident.is_valid() { | ||
| return None; | ||
| } | ||
| let mut name = Expr::from(ast::ExprName { | ||
| range: self.node_range(start), | ||
| id: ident.id, | ||
| ctx: ExprContext::Load, | ||
| }); | ||
| // ("." identifier)* | ||
| while self.at(TokenKind::Dot) { | ||
| let attr = self.parse_attribute_expression(name, start); | ||
| if !attr.attr.is_valid() { | ||
| return None; | ||
| } | ||
| name = Expr::from(attr); | ||
| } | ||
| // ["(" [argument_list [","]] ")"] NEWLINE | ||
| let parsed = match self.current_token_kind() { | ||
| TokenKind::Lpar => Some(Expr::Call(self.parse_call_expression(name, start)).into()), | ||
| TokenKind::Newline => Some(name.into()), | ||
| _ => None, | ||
| }; | ||
|
|
||
| // ignore version-related errors if there are more serious errors when parsing this way | ||
| if self.errors.len() > errors { | ||
| return None; | ||
| } | ||
|
|
||
| parsed | ||
| } | ||
|
|
||
| /// Parses a decorator list followed by a class, function or async function definition. | ||
| /// | ||
| /// See: <https://docs.python.org/3/reference/compound_stmts.html#grammar-token-python-grammar-decorators> | ||
|
|
@@ -2542,6 +2582,38 @@ impl<'src> Parser<'src> { | |
| let mut decorators = vec![]; | ||
| let mut progress = ParserProgress::default(); | ||
|
|
||
| // these examples are adapted from [PEP 614](https://peps.python.org/pep-0614/), which | ||
| // relaxed the restriction that "decorators consist of a dotted name, optionally followed by | ||
| // a single call" in Python 3.9. we want to catch decorators that don't meet these criteria | ||
| // before 3.9 but avoid false positives on examples like the `@_` "identity function hack" | ||
| // or the "eval hack" called out in the PEP. | ||
|
|
||
| // test_ok decorator_expression_dotted_ident_py38 | ||
| // # parse_options: { "target-version": "3.8" } | ||
| // @buttons.clicked.connect | ||
| // def spam(): ... | ||
|
|
||
| // test_ok decorator_expression_identity_hack_py38 | ||
| // # parse_options: { "target-version": "3.8" } | ||
| // def _(x): return x | ||
| // @_(buttons[0].clicked.connect) | ||
| // def spam(): ... | ||
|
|
||
| // test_ok decorator_expression_eval_hack_py38 | ||
| // # parse_options: { "target-version": "3.8" } | ||
| // @eval("buttons[0].clicked.connect") | ||
| // def spam(): ... | ||
|
|
||
| // test_ok decorator_expression_py39 | ||
| // # parse_options: { "target-version": "3.9" } | ||
| // @buttons[0].clicked.connect | ||
| // def spam(): ... | ||
|
|
||
| // test_err decorator_expression_py38 | ||
| // # parse_options: { "target-version": "3.8" } | ||
| // @buttons[0].clicked.connect | ||
| // def spam(): ... | ||
|
|
||
| // test_err decorator_missing_expression | ||
| // @def foo(): ... | ||
| // @ | ||
|
|
@@ -2554,14 +2626,34 @@ impl<'src> Parser<'src> { | |
| let decorator_start = self.node_start(); | ||
| self.bump(TokenKind::At); | ||
|
|
||
| let checkpoint = self.checkpoint(); | ||
|
||
| let parsed_expr = if self.options.target_version < PythonVersion::PY39 { | ||
| match self.try_parse_old_decorators() { | ||
| Some(parsed) => parsed, | ||
| None => { | ||
| self.rewind(checkpoint); | ||
| let parsed = | ||
| self.parse_named_expression_or_higher(ExpressionContext::default()); | ||
|
|
||
| self.add_unsupported_syntax_error( | ||
| UnsupportedSyntaxErrorKind::RelaxedDecorator, | ||
| self.node_range(decorator_start), | ||
| ); | ||
|
|
||
| parsed | ||
| } | ||
| } | ||
| } else { | ||
| self.parse_named_expression_or_higher(ExpressionContext::default()) | ||
| }; | ||
|
|
||
| // test_err decorator_invalid_expression | ||
| // @*x | ||
| // @(*x) | ||
| // @((*x)) | ||
| // @yield x | ||
| // @yield from x | ||
| // def foo(): ... | ||
| let parsed_expr = self.parse_named_expression_or_higher(ExpressionContext::default()); | ||
|
|
||
| decorators.push(ast::Decorator { | ||
| expression: parsed_expr.expr, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,101 @@ | ||
| --- | ||
| source: crates/ruff_python_parser/tests/fixtures.rs | ||
| input_file: crates/ruff_python_parser/resources/inline/err/decorator_expression_py38.py | ||
| --- | ||
| ## AST | ||
|
|
||
| ``` | ||
| Module( | ||
| ModModule { | ||
| range: 0..89, | ||
| body: [ | ||
| FunctionDef( | ||
| StmtFunctionDef { | ||
| range: 45..88, | ||
| is_async: false, | ||
| decorator_list: [ | ||
| Decorator { | ||
| range: 45..72, | ||
| expression: Attribute( | ||
| ExprAttribute { | ||
| range: 46..72, | ||
| value: Attribute( | ||
| ExprAttribute { | ||
| range: 46..64, | ||
| value: Subscript( | ||
| ExprSubscript { | ||
| range: 46..56, | ||
| value: Name( | ||
| ExprName { | ||
| range: 46..53, | ||
| id: Name("buttons"), | ||
| ctx: Load, | ||
| }, | ||
| ), | ||
| slice: NumberLiteral( | ||
| ExprNumberLiteral { | ||
| range: 54..55, | ||
| value: Int( | ||
| 0, | ||
| ), | ||
| }, | ||
| ), | ||
| ctx: Load, | ||
| }, | ||
| ), | ||
| attr: Identifier { | ||
| id: Name("clicked"), | ||
| range: 57..64, | ||
| }, | ||
| ctx: Load, | ||
| }, | ||
| ), | ||
| attr: Identifier { | ||
| id: Name("connect"), | ||
| range: 65..72, | ||
| }, | ||
| ctx: Load, | ||
| }, | ||
| ), | ||
| }, | ||
| ], | ||
| name: Identifier { | ||
| id: Name("spam"), | ||
| range: 77..81, | ||
| }, | ||
| type_params: None, | ||
| parameters: Parameters { | ||
| range: 81..83, | ||
| posonlyargs: [], | ||
| args: [], | ||
| vararg: None, | ||
| kwonlyargs: [], | ||
| kwarg: None, | ||
| }, | ||
| returns: None, | ||
| body: [ | ||
| Expr( | ||
| StmtExpr { | ||
| range: 85..88, | ||
| value: EllipsisLiteral( | ||
| ExprEllipsisLiteral { | ||
| range: 85..88, | ||
| }, | ||
| ), | ||
| }, | ||
| ), | ||
| ], | ||
| }, | ||
| ), | ||
| ], | ||
| }, | ||
| ) | ||
| ``` | ||
| ## Unsupported Syntax Errors | ||
|
|
||
| | | ||
| 1 | # parse_options: { "target-version": "3.8" } | ||
| 2 | @buttons[0].clicked.connect | ||
| | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ Syntax Error: Cannot use named expression in decorator on Python 3.8 (syntax was added in Python 3.9) | ||
| 3 | def spam(): ... | ||
| | |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,79 @@ | ||
| --- | ||
| source: crates/ruff_python_parser/tests/fixtures.rs | ||
| input_file: crates/ruff_python_parser/resources/inline/ok/decorator_expression_dotted_ident_py38.py | ||
| --- | ||
| ## AST | ||
|
|
||
| ``` | ||
| Module( | ||
| ModModule { | ||
| range: 0..86, | ||
| body: [ | ||
| FunctionDef( | ||
| StmtFunctionDef { | ||
| range: 45..85, | ||
| is_async: false, | ||
| decorator_list: [ | ||
| Decorator { | ||
| range: 45..69, | ||
| expression: Attribute( | ||
| ExprAttribute { | ||
| range: 46..69, | ||
| value: Attribute( | ||
| ExprAttribute { | ||
| range: 46..61, | ||
| value: Name( | ||
| ExprName { | ||
| range: 46..53, | ||
| id: Name("buttons"), | ||
| ctx: Load, | ||
| }, | ||
| ), | ||
| attr: Identifier { | ||
| id: Name("clicked"), | ||
| range: 54..61, | ||
| }, | ||
| ctx: Load, | ||
| }, | ||
| ), | ||
| attr: Identifier { | ||
| id: Name("connect"), | ||
| range: 62..69, | ||
| }, | ||
| ctx: Load, | ||
| }, | ||
| ), | ||
| }, | ||
| ], | ||
| name: Identifier { | ||
| id: Name("spam"), | ||
| range: 74..78, | ||
| }, | ||
| type_params: None, | ||
| parameters: Parameters { | ||
| range: 78..80, | ||
| posonlyargs: [], | ||
| args: [], | ||
| vararg: None, | ||
| kwonlyargs: [], | ||
| kwarg: None, | ||
| }, | ||
| returns: None, | ||
| body: [ | ||
| Expr( | ||
| StmtExpr { | ||
| range: 82..85, | ||
| value: EllipsisLiteral( | ||
| ExprEllipsisLiteral { | ||
| range: 82..85, | ||
| }, | ||
| ), | ||
| }, | ||
| ), | ||
| ], | ||
| }, | ||
| ), | ||
| ], | ||
| }, | ||
| ) | ||
| ``` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, I think "named expression" generally refers specifically to walrus expressions (https://peps.python.org/pep-0572 uses the term several times), so I'm not sure it's the best term to use here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah that makes sense, I was just following PEP 614 and the AST reference (which I see now actually calls them
assignment_expressions, but I think that's another synonym for the walrus operator?).Do you have any ideas for a replacement? I didn't think "relaxed decorator" was much more helpful, but that's closer to the PEP name and the "What's new" entry.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe something like this?
Then if it was e.g. a subscript expression, the error message would be
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I'll leave this nicer error reporting for a possible follow-up. It would complicate the code a fair amount to track the expression kind through the whole process just for the error message. Micha also pointed out on Discord that 3.9 is the oldest version receiving security updates, so this message will probably have a fairly limited audience.
I did remove the confusing "named expression" part and went for something closer to pyright's "Expression form not supported for decorator prior to Python 3.9":
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice, thanks. I still think there's room for improvement but this is definitely good enough for now!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Absolutely, I meant to say I think your suggestion would make for the ideal error message. Thanks!