diff --git a/crates/ruff_python_parser/resources/inline/err/decorator_expression_py38.py b/crates/ruff_python_parser/resources/inline/err/decorator_expression_py38.py new file mode 100644 index 0000000000..9889848e9f --- /dev/null +++ b/crates/ruff_python_parser/resources/inline/err/decorator_expression_py38.py @@ -0,0 +1,3 @@ +# parse_options: { "target-version": "3.8" } +@buttons[0].clicked.connect +def spam(): ... diff --git a/crates/ruff_python_parser/resources/inline/err/decorator_named_expression_py37.py b/crates/ruff_python_parser/resources/inline/err/decorator_named_expression_py37.py new file mode 100644 index 0000000000..cff401f921 --- /dev/null +++ b/crates/ruff_python_parser/resources/inline/err/decorator_named_expression_py37.py @@ -0,0 +1,3 @@ +# parse_options: { "target-version": "3.7" } +@(x := lambda x: x)(foo) +def bar(): ... diff --git a/crates/ruff_python_parser/resources/inline/ok/decorator_expression_dotted_ident_py38.py b/crates/ruff_python_parser/resources/inline/ok/decorator_expression_dotted_ident_py38.py new file mode 100644 index 0000000000..dc6bad99a7 --- /dev/null +++ b/crates/ruff_python_parser/resources/inline/ok/decorator_expression_dotted_ident_py38.py @@ -0,0 +1,3 @@ +# parse_options: { "target-version": "3.8" } +@buttons.clicked.connect +def spam(): ... diff --git a/crates/ruff_python_parser/resources/inline/ok/decorator_expression_eval_hack_py38.py b/crates/ruff_python_parser/resources/inline/ok/decorator_expression_eval_hack_py38.py new file mode 100644 index 0000000000..2cfeed2640 --- /dev/null +++ b/crates/ruff_python_parser/resources/inline/ok/decorator_expression_eval_hack_py38.py @@ -0,0 +1,3 @@ +# parse_options: { "target-version": "3.8" } +@eval("buttons[0].clicked.connect") +def spam(): ... diff --git a/crates/ruff_python_parser/resources/inline/ok/decorator_expression_identity_hack_py38.py b/crates/ruff_python_parser/resources/inline/ok/decorator_expression_identity_hack_py38.py new file mode 100644 index 0000000000..704c3156e9 --- /dev/null +++ b/crates/ruff_python_parser/resources/inline/ok/decorator_expression_identity_hack_py38.py @@ -0,0 +1,4 @@ +# parse_options: { "target-version": "3.8" } +def _(x): return x +@_(buttons[0].clicked.connect) +def spam(): ... diff --git a/crates/ruff_python_parser/resources/inline/ok/decorator_expression_py39.py b/crates/ruff_python_parser/resources/inline/ok/decorator_expression_py39.py new file mode 100644 index 0000000000..f4c93cf941 --- /dev/null +++ b/crates/ruff_python_parser/resources/inline/ok/decorator_expression_py39.py @@ -0,0 +1,5 @@ +# parse_options: { "target-version": "3.9" } +@buttons[0].clicked.connect +def spam(): ... +@(x := lambda x: x)(foo) +def bar(): ... diff --git a/crates/ruff_python_parser/src/error.rs b/crates/ruff_python_parser/src/error.rs index 8a953a61a9..4cdb4ab814 100644 --- a/crates/ruff_python_parser/src/error.rs +++ b/crates/ruff_python_parser/src/error.rs @@ -449,6 +449,37 @@ pub enum UnsupportedSyntaxErrorKind { Match, Walrus, ExceptStar, + /// Represents the use of a "relaxed" [PEP 614] decorator before Python 3.9. + /// + /// ## Examples + /// + /// Prior to Python 3.9, decorators were defined to be [`dotted_name`]s, optionally followed by + /// an argument list. For example: + /// + /// ```python + /// @buttons.clicked.connect + /// def foo(): ... + /// + /// @buttons.clicked.connect(1, 2, 3) + /// def foo(): ... + /// ``` + /// + /// As pointed out in the PEP, this prevented reasonable extensions like subscripts: + /// + /// ```python + /// buttons = [QPushButton(f'Button {i}') for i in range(10)] + /// + /// @buttons[0].clicked.connect + /// def spam(): ... + /// ``` + /// + /// Python 3.9 removed these restrictions and expanded the [decorator grammar] to include any + /// assignment expression and include cases like the example above. + /// + /// [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, /// Represents the use of a [PEP 570] positional-only parameter before Python 3.8. /// /// ## Examples @@ -513,6 +544,7 @@ impl Display for UnsupportedSyntaxError { UnsupportedSyntaxErrorKind::Match => "Cannot use `match` statement", UnsupportedSyntaxErrorKind::Walrus => "Cannot use named assignment expression (`:=`)", UnsupportedSyntaxErrorKind::ExceptStar => "Cannot use `except*`", + UnsupportedSyntaxErrorKind::RelaxedDecorator => "Unsupported expression in decorators", UnsupportedSyntaxErrorKind::PositionalOnlyParameter => { "Cannot use positional-only parameter separator" } @@ -538,6 +570,7 @@ impl UnsupportedSyntaxErrorKind { UnsupportedSyntaxErrorKind::Match => PythonVersion::PY310, UnsupportedSyntaxErrorKind::Walrus => PythonVersion::PY38, UnsupportedSyntaxErrorKind::ExceptStar => PythonVersion::PY311, + UnsupportedSyntaxErrorKind::RelaxedDecorator => PythonVersion::PY39, UnsupportedSyntaxErrorKind::PositionalOnlyParameter => PythonVersion::PY38, UnsupportedSyntaxErrorKind::TypeParameterList => PythonVersion::PY312, UnsupportedSyntaxErrorKind::TypeAliasStatement => PythonVersion::PY312, diff --git a/crates/ruff_python_parser/src/parser/expression.rs b/crates/ruff_python_parser/src/parser/expression.rs index e37fb835d7..957b1d277e 100644 --- a/crates/ruff_python_parser/src/parser/expression.rs +++ b/crates/ruff_python_parser/src/parser/expression.rs @@ -632,7 +632,7 @@ impl<'src> Parser<'src> { /// If the parser isn't position at a `(` token. /// /// See: - fn parse_call_expression(&mut self, func: Expr, start: TextSize) -> ast::ExprCall { + pub(super) fn parse_call_expression(&mut self, func: Expr, start: TextSize) -> ast::ExprCall { let arguments = self.parse_arguments(); ast::ExprCall { diff --git a/crates/ruff_python_parser/src/parser/helpers.rs b/crates/ruff_python_parser/src/parser/helpers.rs index d4846d9039..b9c374eb71 100644 --- a/crates/ruff_python_parser/src/parser/helpers.rs +++ b/crates/ruff_python_parser/src/parser/helpers.rs @@ -43,3 +43,15 @@ pub(super) const fn token_kind_to_cmp_op(tokens: [TokenKind; 2]) -> Option return None, }) } + +/// Helper for `parse_decorators` to determine if `expr` is a [`dotted_name`] from the decorator +/// grammar before Python 3.9. +/// +/// [`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, + } +} diff --git a/crates/ruff_python_parser/src/parser/statement.rs b/crates/ruff_python_parser/src/parser/statement.rs index f340fd3bb0..bd569f613d 100644 --- a/crates/ruff_python_parser/src/parser/statement.rs +++ b/crates/ruff_python_parser/src/parser/statement.rs @@ -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, TextRange, TextSize}; @@ -2599,6 +2600,56 @@ impl<'src> Parser<'src> { let decorator_start = self.node_start(); self.bump(TokenKind::At); + let parsed_expr = self.parse_named_expression_or_higher(ExpressionContext::default()); + + if self.options.target_version < PythonVersion::PY39 { + // 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(): ... + // @(x := lambda x: x)(foo) + // def bar(): ... + + // test_err decorator_expression_py38 + // # parse_options: { "target-version": "3.8" } + // @buttons[0].clicked.connect + // def spam(): ... + + // test_err decorator_named_expression_py37 + // # parse_options: { "target-version": "3.7" } + // @(x := lambda x: x)(foo) + // def bar(): ... + let allowed_decorator = match &parsed_expr.expr { + Expr::Call(expr_call) => { + helpers::is_name_or_attribute_expression(&expr_call.func) + } + expr => helpers::is_name_or_attribute_expression(expr), + }; + + if !allowed_decorator { + self.add_unsupported_syntax_error( + UnsupportedSyntaxErrorKind::RelaxedDecorator, + parsed_expr.range(), + ); + } + } + // test_err decorator_invalid_expression // @*x // @(*x) @@ -2606,7 +2657,6 @@ impl<'src> Parser<'src> { // @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, 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 new file mode 100644 index 0000000000..0caec04936 --- /dev/null +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@decorator_expression_py38.py.snap @@ -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: Unsupported expression in decorators 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_named_expression_py37.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@decorator_named_expression_py37.py.snap new file mode 100644 index 0000000000..3a27ec1f1b --- /dev/null +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@decorator_named_expression_py37.py.snap @@ -0,0 +1,133 @@ +--- +source: crates/ruff_python_parser/tests/fixtures.rs +input_file: crates/ruff_python_parser/resources/inline/err/decorator_named_expression_py37.py +--- +## AST + +``` +Module( + ModModule { + range: 0..85, + body: [ + FunctionDef( + StmtFunctionDef { + range: 45..84, + is_async: false, + decorator_list: [ + Decorator { + range: 45..69, + expression: Call( + ExprCall { + range: 46..69, + func: Named( + ExprNamed { + range: 47..63, + target: Name( + ExprName { + range: 47..48, + id: Name("x"), + ctx: Store, + }, + ), + value: Lambda( + ExprLambda { + range: 52..63, + parameters: Some( + Parameters { + range: 59..60, + posonlyargs: [], + args: [ + ParameterWithDefault { + range: 59..60, + parameter: Parameter { + range: 59..60, + name: Identifier { + id: Name("x"), + range: 59..60, + }, + annotation: None, + }, + default: None, + }, + ], + vararg: None, + kwonlyargs: [], + kwarg: None, + }, + ), + body: Name( + ExprName { + range: 62..63, + id: Name("x"), + ctx: Load, + }, + ), + }, + ), + }, + ), + arguments: Arguments { + range: 64..69, + args: [ + Name( + ExprName { + range: 65..68, + id: Name("foo"), + ctx: Load, + }, + ), + ], + keywords: [], + }, + }, + ), + }, + ], + name: Identifier { + id: Name("bar"), + range: 74..77, + }, + type_params: None, + parameters: Parameters { + range: 77..79, + posonlyargs: [], + args: [], + vararg: None, + kwonlyargs: [], + kwarg: None, + }, + returns: None, + body: [ + Expr( + StmtExpr { + range: 81..84, + value: EllipsisLiteral( + ExprEllipsisLiteral { + range: 81..84, + }, + ), + }, + ), + ], + }, + ), + ], + }, +) +``` +## Unsupported Syntax Errors + + | +1 | # parse_options: { "target-version": "3.7" } +2 | @(x := lambda x: x)(foo) + | ^^^^^^^^^^^^^^^^ Syntax Error: Cannot use named assignment expression (`:=`) on Python 3.7 (syntax was added in Python 3.8) +3 | def bar(): ... + | + + + | +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) +3 | def bar(): ... + | diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@decorator_expression_dotted_ident_py38.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@decorator_expression_dotted_ident_py38.py.snap new file mode 100644 index 0000000000..c18f2aa994 --- /dev/null +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@decorator_expression_dotted_ident_py38.py.snap @@ -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, + }, + ), + }, + ), + ], + }, + ), + ], + }, +) +``` diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@decorator_expression_eval_hack_py38.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@decorator_expression_eval_hack_py38.py.snap new file mode 100644 index 0000000000..ff8fce644f --- /dev/null +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@decorator_expression_eval_hack_py38.py.snap @@ -0,0 +1,88 @@ +--- +source: crates/ruff_python_parser/tests/fixtures.rs +input_file: crates/ruff_python_parser/resources/inline/ok/decorator_expression_eval_hack_py38.py +--- +## AST + +``` +Module( + ModModule { + range: 0..97, + body: [ + FunctionDef( + StmtFunctionDef { + range: 45..96, + is_async: false, + decorator_list: [ + Decorator { + range: 45..80, + expression: Call( + ExprCall { + range: 46..80, + func: Name( + ExprName { + range: 46..50, + id: Name("eval"), + ctx: Load, + }, + ), + arguments: Arguments { + range: 50..80, + args: [ + StringLiteral( + ExprStringLiteral { + range: 51..79, + value: StringLiteralValue { + inner: Single( + StringLiteral { + range: 51..79, + value: "buttons[0].clicked.connect", + flags: StringLiteralFlags { + quote_style: Double, + prefix: Empty, + triple_quoted: false, + }, + }, + ), + }, + }, + ), + ], + keywords: [], + }, + }, + ), + }, + ], + name: Identifier { + id: Name("spam"), + range: 85..89, + }, + type_params: None, + parameters: Parameters { + range: 89..91, + posonlyargs: [], + args: [], + vararg: None, + kwonlyargs: [], + kwarg: None, + }, + returns: None, + body: [ + Expr( + StmtExpr { + range: 93..96, + value: EllipsisLiteral( + ExprEllipsisLiteral { + range: 93..96, + }, + ), + }, + ), + ], + }, + ), + ], + }, +) +``` diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@decorator_expression_identity_hack_py38.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@decorator_expression_identity_hack_py38.py.snap new file mode 100644 index 0000000000..cedb9372d8 --- /dev/null +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@decorator_expression_identity_hack_py38.py.snap @@ -0,0 +1,161 @@ +--- +source: crates/ruff_python_parser/tests/fixtures.rs +input_file: crates/ruff_python_parser/resources/inline/ok/decorator_expression_identity_hack_py38.py +--- +## AST + +``` +Module( + ModModule { + range: 0..111, + body: [ + FunctionDef( + StmtFunctionDef { + range: 45..63, + is_async: false, + decorator_list: [], + name: Identifier { + id: Name("_"), + range: 49..50, + }, + type_params: None, + parameters: Parameters { + range: 50..53, + posonlyargs: [], + args: [ + ParameterWithDefault { + range: 51..52, + parameter: Parameter { + range: 51..52, + name: Identifier { + id: Name("x"), + range: 51..52, + }, + annotation: None, + }, + default: None, + }, + ], + vararg: None, + kwonlyargs: [], + kwarg: None, + }, + returns: None, + body: [ + Return( + StmtReturn { + range: 55..63, + value: Some( + Name( + ExprName { + range: 62..63, + id: Name("x"), + ctx: Load, + }, + ), + ), + }, + ), + ], + }, + ), + FunctionDef( + StmtFunctionDef { + range: 64..110, + is_async: false, + decorator_list: [ + Decorator { + range: 64..94, + expression: Call( + ExprCall { + range: 65..94, + func: Name( + ExprName { + range: 65..66, + id: Name("_"), + ctx: Load, + }, + ), + arguments: Arguments { + range: 66..94, + args: [ + Attribute( + ExprAttribute { + range: 67..93, + value: Attribute( + ExprAttribute { + range: 67..85, + value: Subscript( + ExprSubscript { + range: 67..77, + value: Name( + ExprName { + range: 67..74, + id: Name("buttons"), + ctx: Load, + }, + ), + slice: NumberLiteral( + ExprNumberLiteral { + range: 75..76, + value: Int( + 0, + ), + }, + ), + ctx: Load, + }, + ), + attr: Identifier { + id: Name("clicked"), + range: 78..85, + }, + ctx: Load, + }, + ), + attr: Identifier { + id: Name("connect"), + range: 86..93, + }, + ctx: Load, + }, + ), + ], + keywords: [], + }, + }, + ), + }, + ], + name: Identifier { + id: Name("spam"), + range: 99..103, + }, + type_params: None, + parameters: Parameters { + range: 103..105, + posonlyargs: [], + args: [], + vararg: None, + kwonlyargs: [], + kwarg: None, + }, + returns: None, + body: [ + Expr( + StmtExpr { + range: 107..110, + value: EllipsisLiteral( + ExprEllipsisLiteral { + range: 107..110, + }, + ), + }, + ), + ], + }, + ), + ], + }, +) +``` diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@decorator_expression_py39.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@decorator_expression_py39.py.snap new file mode 100644 index 0000000000..50c0502003 --- /dev/null +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@decorator_expression_py39.py.snap @@ -0,0 +1,195 @@ +--- +source: crates/ruff_python_parser/tests/fixtures.rs +input_file: crates/ruff_python_parser/resources/inline/ok/decorator_expression_py39.py +--- +## AST + +``` +Module( + ModModule { + range: 0..129, + 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, + }, + ), + }, + ), + ], + }, + ), + FunctionDef( + StmtFunctionDef { + range: 89..128, + is_async: false, + decorator_list: [ + Decorator { + range: 89..113, + expression: Call( + ExprCall { + range: 90..113, + func: Named( + ExprNamed { + range: 91..107, + target: Name( + ExprName { + range: 91..92, + id: Name("x"), + ctx: Store, + }, + ), + value: Lambda( + ExprLambda { + range: 96..107, + parameters: Some( + Parameters { + range: 103..104, + posonlyargs: [], + args: [ + ParameterWithDefault { + range: 103..104, + parameter: Parameter { + range: 103..104, + name: Identifier { + id: Name("x"), + range: 103..104, + }, + annotation: None, + }, + default: None, + }, + ], + vararg: None, + kwonlyargs: [], + kwarg: None, + }, + ), + body: Name( + ExprName { + range: 106..107, + id: Name("x"), + ctx: Load, + }, + ), + }, + ), + }, + ), + arguments: Arguments { + range: 108..113, + args: [ + Name( + ExprName { + range: 109..112, + id: Name("foo"), + ctx: Load, + }, + ), + ], + keywords: [], + }, + }, + ), + }, + ], + name: Identifier { + id: Name("bar"), + range: 118..121, + }, + type_params: None, + parameters: Parameters { + range: 121..123, + posonlyargs: [], + args: [], + vararg: None, + kwonlyargs: [], + kwarg: None, + }, + returns: None, + body: [ + Expr( + StmtExpr { + range: 125..128, + value: EllipsisLiteral( + ExprEllipsisLiteral { + range: 125..128, + }, + ), + }, + ), + ], + }, + ), + ], + }, +) +```