diff --git a/Cargo.lock b/Cargo.lock index 3723cbe5..1885f51c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2775,7 +2775,7 @@ dependencies = [ [[package]] name = "ruff_annotate_snippets" version = "0.1.0" -source = "git+https://github.com/samuelcolvin/ruff.git?rev=6aaa91ac2b269df1414954ccd5134f0e6f5c6d30#6aaa91ac2b269df1414954ccd5134f0e6f5c6d30" +source = "git+https://github.com/samuelcolvin/ruff.git?rev=7ccad7a110233d0d53621039cf91425f0313bd71#7ccad7a110233d0d53621039cf91425f0313bd71" dependencies = [ "anstyle", "memchr", @@ -2785,7 +2785,7 @@ dependencies = [ [[package]] name = "ruff_db" version = "0.0.0" -source = "git+https://github.com/samuelcolvin/ruff.git?rev=6aaa91ac2b269df1414954ccd5134f0e6f5c6d30#6aaa91ac2b269df1414954ccd5134f0e6f5c6d30" +source = "git+https://github.com/samuelcolvin/ruff.git?rev=7ccad7a110233d0d53621039cf91425f0313bd71#7ccad7a110233d0d53621039cf91425f0313bd71" dependencies = [ "anstyle", "arc-swap", @@ -2822,7 +2822,7 @@ dependencies = [ [[package]] name = "ruff_diagnostics" version = "0.0.0" -source = "git+https://github.com/samuelcolvin/ruff.git?rev=6aaa91ac2b269df1414954ccd5134f0e6f5c6d30#6aaa91ac2b269df1414954ccd5134f0e6f5c6d30" +source = "git+https://github.com/samuelcolvin/ruff.git?rev=7ccad7a110233d0d53621039cf91425f0313bd71#7ccad7a110233d0d53621039cf91425f0313bd71" dependencies = [ "get-size2", "is-macro", @@ -2833,7 +2833,7 @@ dependencies = [ [[package]] name = "ruff_index" version = "0.0.0" -source = "git+https://github.com/samuelcolvin/ruff.git?rev=6aaa91ac2b269df1414954ccd5134f0e6f5c6d30#6aaa91ac2b269df1414954ccd5134f0e6f5c6d30" +source = "git+https://github.com/samuelcolvin/ruff.git?rev=7ccad7a110233d0d53621039cf91425f0313bd71#7ccad7a110233d0d53621039cf91425f0313bd71" dependencies = [ "get-size2", "ruff_macros", @@ -2843,7 +2843,7 @@ dependencies = [ [[package]] name = "ruff_macros" version = "0.0.0" -source = "git+https://github.com/samuelcolvin/ruff.git?rev=6aaa91ac2b269df1414954ccd5134f0e6f5c6d30#6aaa91ac2b269df1414954ccd5134f0e6f5c6d30" +source = "git+https://github.com/samuelcolvin/ruff.git?rev=7ccad7a110233d0d53621039cf91425f0313bd71#7ccad7a110233d0d53621039cf91425f0313bd71" dependencies = [ "heck", "itertools 0.14.0", @@ -2857,7 +2857,7 @@ dependencies = [ [[package]] name = "ruff_memory_usage" version = "0.0.0" -source = "git+https://github.com/samuelcolvin/ruff.git?rev=6aaa91ac2b269df1414954ccd5134f0e6f5c6d30#6aaa91ac2b269df1414954ccd5134f0e6f5c6d30" +source = "git+https://github.com/samuelcolvin/ruff.git?rev=7ccad7a110233d0d53621039cf91425f0313bd71#7ccad7a110233d0d53621039cf91425f0313bd71" dependencies = [ "get-size2", ] @@ -2865,7 +2865,7 @@ dependencies = [ [[package]] name = "ruff_notebook" version = "0.0.0" -source = "git+https://github.com/samuelcolvin/ruff.git?rev=6aaa91ac2b269df1414954ccd5134f0e6f5c6d30#6aaa91ac2b269df1414954ccd5134f0e6f5c6d30" +source = "git+https://github.com/samuelcolvin/ruff.git?rev=7ccad7a110233d0d53621039cf91425f0313bd71#7ccad7a110233d0d53621039cf91425f0313bd71" dependencies = [ "anyhow", "itertools 0.14.0", @@ -2883,7 +2883,7 @@ dependencies = [ [[package]] name = "ruff_python_ast" version = "0.0.0" -source = "git+https://github.com/samuelcolvin/ruff.git?rev=6aaa91ac2b269df1414954ccd5134f0e6f5c6d30#6aaa91ac2b269df1414954ccd5134f0e6f5c6d30" +source = "git+https://github.com/samuelcolvin/ruff.git?rev=7ccad7a110233d0d53621039cf91425f0313bd71#7ccad7a110233d0d53621039cf91425f0313bd71" dependencies = [ "aho-corasick", "bitflags 2.10.0", @@ -2903,7 +2903,7 @@ dependencies = [ [[package]] name = "ruff_python_literal" version = "0.0.0" -source = "git+https://github.com/samuelcolvin/ruff.git?rev=6aaa91ac2b269df1414954ccd5134f0e6f5c6d30#6aaa91ac2b269df1414954ccd5134f0e6f5c6d30" +source = "git+https://github.com/samuelcolvin/ruff.git?rev=7ccad7a110233d0d53621039cf91425f0313bd71#7ccad7a110233d0d53621039cf91425f0313bd71" dependencies = [ "bitflags 2.10.0", "icu_properties", @@ -2914,7 +2914,7 @@ dependencies = [ [[package]] name = "ruff_python_parser" version = "0.0.0" -source = "git+https://github.com/samuelcolvin/ruff.git?rev=6aaa91ac2b269df1414954ccd5134f0e6f5c6d30#6aaa91ac2b269df1414954ccd5134f0e6f5c6d30" +source = "git+https://github.com/samuelcolvin/ruff.git?rev=7ccad7a110233d0d53621039cf91425f0313bd71#7ccad7a110233d0d53621039cf91425f0313bd71" dependencies = [ "bitflags 2.10.0", "bstr", @@ -2935,7 +2935,7 @@ dependencies = [ [[package]] name = "ruff_python_stdlib" version = "0.0.0" -source = "git+https://github.com/samuelcolvin/ruff.git?rev=6aaa91ac2b269df1414954ccd5134f0e6f5c6d30#6aaa91ac2b269df1414954ccd5134f0e6f5c6d30" +source = "git+https://github.com/samuelcolvin/ruff.git?rev=7ccad7a110233d0d53621039cf91425f0313bd71#7ccad7a110233d0d53621039cf91425f0313bd71" dependencies = [ "bitflags 2.10.0", "unicode-ident", @@ -2944,7 +2944,7 @@ dependencies = [ [[package]] name = "ruff_python_trivia" version = "0.0.0" -source = "git+https://github.com/samuelcolvin/ruff.git?rev=6aaa91ac2b269df1414954ccd5134f0e6f5c6d30#6aaa91ac2b269df1414954ccd5134f0e6f5c6d30" +source = "git+https://github.com/samuelcolvin/ruff.git?rev=7ccad7a110233d0d53621039cf91425f0313bd71#7ccad7a110233d0d53621039cf91425f0313bd71" dependencies = [ "itertools 0.14.0", "ruff_source_file", @@ -2955,7 +2955,7 @@ dependencies = [ [[package]] name = "ruff_source_file" version = "0.0.0" -source = "git+https://github.com/samuelcolvin/ruff.git?rev=6aaa91ac2b269df1414954ccd5134f0e6f5c6d30#6aaa91ac2b269df1414954ccd5134f0e6f5c6d30" +source = "git+https://github.com/samuelcolvin/ruff.git?rev=7ccad7a110233d0d53621039cf91425f0313bd71#7ccad7a110233d0d53621039cf91425f0313bd71" dependencies = [ "get-size2", "memchr", @@ -2966,7 +2966,7 @@ dependencies = [ [[package]] name = "ruff_text_size" version = "0.0.0" -source = "git+https://github.com/samuelcolvin/ruff.git?rev=6aaa91ac2b269df1414954ccd5134f0e6f5c6d30#6aaa91ac2b269df1414954ccd5134f0e6f5c6d30" +source = "git+https://github.com/samuelcolvin/ruff.git?rev=7ccad7a110233d0d53621039cf91425f0313bd71#7ccad7a110233d0d53621039cf91425f0313bd71" dependencies = [ "get-size2", "serde", @@ -3556,7 +3556,7 @@ dependencies = [ [[package]] name = "ty_combine" version = "0.0.0" -source = "git+https://github.com/samuelcolvin/ruff.git?rev=6aaa91ac2b269df1414954ccd5134f0e6f5c6d30#6aaa91ac2b269df1414954ccd5134f0e6f5c6d30" +source = "git+https://github.com/samuelcolvin/ruff.git?rev=7ccad7a110233d0d53621039cf91425f0313bd71#7ccad7a110233d0d53621039cf91425f0313bd71" dependencies = [ "ordermap", "ruff_db", @@ -3566,7 +3566,7 @@ dependencies = [ [[package]] name = "ty_module_resolver" version = "0.0.0" -source = "git+https://github.com/samuelcolvin/ruff.git?rev=6aaa91ac2b269df1414954ccd5134f0e6f5c6d30#6aaa91ac2b269df1414954ccd5134f0e6f5c6d30" +source = "git+https://github.com/samuelcolvin/ruff.git?rev=7ccad7a110233d0d53621039cf91425f0313bd71#7ccad7a110233d0d53621039cf91425f0313bd71" dependencies = [ "anyhow", "camino", @@ -3589,7 +3589,7 @@ dependencies = [ [[package]] name = "ty_python_core" version = "0.0.0" -source = "git+https://github.com/samuelcolvin/ruff.git?rev=6aaa91ac2b269df1414954ccd5134f0e6f5c6d30#6aaa91ac2b269df1414954ccd5134f0e6f5c6d30" +source = "git+https://github.com/samuelcolvin/ruff.git?rev=7ccad7a110233d0d53621039cf91425f0313bd71#7ccad7a110233d0d53621039cf91425f0313bd71" dependencies = [ "bitflags 2.10.0", "bitvec", @@ -3619,7 +3619,7 @@ dependencies = [ [[package]] name = "ty_python_semantic" version = "0.0.0" -source = "git+https://github.com/samuelcolvin/ruff.git?rev=6aaa91ac2b269df1414954ccd5134f0e6f5c6d30#6aaa91ac2b269df1414954ccd5134f0e6f5c6d30" +source = "git+https://github.com/samuelcolvin/ruff.git?rev=7ccad7a110233d0d53621039cf91425f0313bd71#7ccad7a110233d0d53621039cf91425f0313bd71" dependencies = [ "bitflags 2.10.0", "compact_str", @@ -3658,7 +3658,7 @@ dependencies = [ [[package]] name = "ty_site_packages" version = "0.0.0" -source = "git+https://github.com/samuelcolvin/ruff.git?rev=6aaa91ac2b269df1414954ccd5134f0e6f5c6d30#6aaa91ac2b269df1414954ccd5134f0e6f5c6d30" +source = "git+https://github.com/samuelcolvin/ruff.git?rev=7ccad7a110233d0d53621039cf91425f0313bd71#7ccad7a110233d0d53621039cf91425f0313bd71" dependencies = [ "camino", "colored 3.0.0", @@ -3679,7 +3679,7 @@ dependencies = [ [[package]] name = "ty_static" version = "0.0.1" -source = "git+https://github.com/samuelcolvin/ruff.git?rev=6aaa91ac2b269df1414954ccd5134f0e6f5c6d30#6aaa91ac2b269df1414954ccd5134f0e6f5c6d30" +source = "git+https://github.com/samuelcolvin/ruff.git?rev=7ccad7a110233d0d53621039cf91425f0313bd71#7ccad7a110233d0d53621039cf91425f0313bd71" dependencies = [ "ruff_macros", ] @@ -3687,7 +3687,7 @@ dependencies = [ [[package]] name = "ty_vendored" version = "0.0.0" -source = "git+https://github.com/samuelcolvin/ruff.git?rev=6aaa91ac2b269df1414954ccd5134f0e6f5c6d30#6aaa91ac2b269df1414954ccd5134f0e6f5c6d30" +source = "git+https://github.com/samuelcolvin/ruff.git?rev=7ccad7a110233d0d53621039cf91425f0313bd71#7ccad7a110233d0d53621039cf91425f0313bd71" dependencies = [ "path-slash", "ruff_db", diff --git a/Cargo.toml b/Cargo.toml index 6e0d9e60..c6cb4c99 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,15 +40,15 @@ lto = false [workspace.dependencies] # ruff, ty and related crates -ruff_python_parser = { git = "https://github.com/samuelcolvin/ruff.git", package = "ruff_python_parser", rev = "6aaa91ac2b269df1414954ccd5134f0e6f5c6d30" } -ruff_python_stdlib = { git = "https://github.com/samuelcolvin/ruff.git", package = "ruff_python_stdlib", rev = "6aaa91ac2b269df1414954ccd5134f0e6f5c6d30" } -ruff_python_ast = { git = "https://github.com/samuelcolvin/ruff.git", package = "ruff_python_ast", rev = "6aaa91ac2b269df1414954ccd5134f0e6f5c6d30" } -ruff_text_size = { git = "https://github.com/samuelcolvin/ruff.git", package = "ruff_text_size", rev = "6aaa91ac2b269df1414954ccd5134f0e6f5c6d30" } -ruff_db = { git = "https://github.com/samuelcolvin/ruff.git", package = "ruff_db", rev = "6aaa91ac2b269df1414954ccd5134f0e6f5c6d30", features = ["serde"] } -ty_python_semantic = { git = "https://github.com/samuelcolvin/ruff.git", package = "ty_python_semantic", rev = "6aaa91ac2b269df1414954ccd5134f0e6f5c6d30" } -ty_python_core = { git = "https://github.com/samuelcolvin/ruff.git", package = "ty_python_core", rev = "6aaa91ac2b269df1414954ccd5134f0e6f5c6d30" } -ty_module_resolver = { git = "https://github.com/samuelcolvin/ruff.git", package = "ty_module_resolver", rev = "6aaa91ac2b269df1414954ccd5134f0e6f5c6d30" } -ty_vendored = { git = "https://github.com/samuelcolvin/ruff.git", package = "ty_vendored", rev = "6aaa91ac2b269df1414954ccd5134f0e6f5c6d30" } +ruff_python_parser = { git = "https://github.com/samuelcolvin/ruff.git", package = "ruff_python_parser", rev = "7ccad7a110233d0d53621039cf91425f0313bd71" } +ruff_python_stdlib = { git = "https://github.com/samuelcolvin/ruff.git", package = "ruff_python_stdlib", rev = "7ccad7a110233d0d53621039cf91425f0313bd71" } +ruff_python_ast = { git = "https://github.com/samuelcolvin/ruff.git", package = "ruff_python_ast", rev = "7ccad7a110233d0d53621039cf91425f0313bd71" } +ruff_text_size = { git = "https://github.com/samuelcolvin/ruff.git", package = "ruff_text_size", rev = "7ccad7a110233d0d53621039cf91425f0313bd71" } +ruff_db = { git = "https://github.com/samuelcolvin/ruff.git", package = "ruff_db", rev = "7ccad7a110233d0d53621039cf91425f0313bd71", features = ["serde"] } +ty_python_semantic = { git = "https://github.com/samuelcolvin/ruff.git", package = "ty_python_semantic", rev = "7ccad7a110233d0d53621039cf91425f0313bd71" } +ty_python_core = { git = "https://github.com/samuelcolvin/ruff.git", package = "ty_python_core", rev = "7ccad7a110233d0d53621039cf91425f0313bd71" } +ty_module_resolver = { git = "https://github.com/samuelcolvin/ruff.git", package = "ty_module_resolver", rev = "7ccad7a110233d0d53621039cf91425f0313bd71" } +ty_vendored = { git = "https://github.com/samuelcolvin/ruff.git", package = "ty_vendored", rev = "7ccad7a110233d0d53621039cf91425f0313bd71" } # salsa version matches current main of ruff salsa = { version="0.26.1", default-features = false, features = [ "compact_str", diff --git a/crates/monty/src/parse.rs b/crates/monty/src/parse.rs index 8d9b7813..9676fe7a 100644 --- a/crates/monty/src/parse.rs +++ b/crates/monty/src/parse.rs @@ -1,4 +1,4 @@ -use std::{borrow::Cow, fmt}; +use std::{borrow::Cow, fmt, mem}; use num_bigint::BigInt; use num_traits::Num; @@ -344,30 +344,26 @@ impl<'a> Parser<'a> { let op = convert_op(op); let value = self.parse_expression(*value)?; match *target { - AstExpr::Subscript(ast::ExprSubscript { - value: object, - slice, - range, - .. - }) => Ok(Node::SubscriptOpAssign { - target: self.parse_expression(*object)?, - index: self.parse_expression(*slice)?, - op, - value, - target_position: self.convert_range(range), - }), - AstExpr::Attribute(ast::ExprAttribute { - value: object, - attr, - range, - .. - }) => Ok(Node::AttrOpAssign { - object: self.parse_expression(*object)?, - attr: EitherStr::Interned(self.interner.intern(attr.id())), - op, - value, - target_position: self.convert_range(range), - }), + AstExpr::Subscript(subscript) => { + let (object, slice, range) = deconstruct_subscript(subscript); + Ok(Node::SubscriptOpAssign { + target: self.parse_expression(*object)?, + index: self.parse_expression(*slice)?, + op, + value, + target_position: self.convert_range(range), + }) + } + AstExpr::Attribute(attribute) => { + let (object, attr, range) = deconstruct_attribute(attribute); + Ok(Node::AttrOpAssign { + object: self.parse_expression(*object)?, + attr: EitherStr::Interned(self.interner.intern(attr.id())), + op, + value, + target_position: self.convert_range(range), + }) + } other => Ok(Node::OpAssign { target: self.parse_identifier(other)?, op, @@ -711,18 +707,22 @@ impl<'a> Parser<'a> { /// its downstream consumers (prepare and compiler). fn parse_assign_target(&mut self, lhs: AstExpr) -> Result { match lhs { - AstExpr::Subscript(ast::ExprSubscript { - value, slice, range, .. - }) => Ok(AssignTarget::Subscript { - target: self.parse_expression(*value)?, - index: self.parse_expression(*slice)?, - target_position: self.convert_range(range), - }), - AstExpr::Attribute(ast::ExprAttribute { value, attr, range, .. }) => Ok(AssignTarget::Attr { - object: self.parse_expression(*value)?, - attr: EitherStr::Interned(self.interner.intern(attr.id())), - target_position: self.convert_range(range), - }), + AstExpr::Subscript(subscript) => { + let (value, slice, range) = deconstruct_subscript(subscript); + Ok(AssignTarget::Subscript { + target: self.parse_expression(*value)?, + index: self.parse_expression(*slice)?, + target_position: self.convert_range(range), + }) + } + AstExpr::Attribute(attribute) => { + let (value, attr, range) = deconstruct_attribute(attribute); + Ok(AssignTarget::Attr { + object: self.parse_expression(*value)?, + attr: EitherStr::Interned(self.interner.intern(attr.id())), + target_position: self.convert_range(range), + }) + } AstExpr::Tuple(ast::ExprTuple { elts, range, .. }) => { let targets_position = self.convert_range(range); let targets = elts @@ -811,18 +811,19 @@ impl<'a> Parser<'a> { }, )) } - AstExpr::BinOp(ast::ExprBinOp { - left, op, right, range, .. - }) => { + AstExpr::BinOp(mut bin_op) => { + // `ExprBinOp` carries a manual `Drop` impl (see `placeholder_expr`), so its + // `left`/`right` edges must be vacated via `mem::replace` rather than moved out. + let op = convert_op(bin_op.op); + let range = bin_op.range; + let left = mem::replace(&mut bin_op.left, Box::new(placeholder_expr())); + let right = mem::replace(&mut bin_op.right, Box::new(placeholder_expr())); + let left = Box::new(self.parse_expression(*left)?); let right = Box::new(self.parse_expression(*right)?); Ok(ExprLoc { position: self.convert_range(range), - expr: Expr::Op { - left, - op: convert_op(op), - right, - }, + expr: Expr::Op { left, op, right }, }) } AstExpr::UnaryOp(ast::ExprUnaryOp { op, operand, range, .. }) => match op { @@ -1030,10 +1031,13 @@ impl<'a> Parser<'a> { // Chain comparison: transform to nested And expressions self.parse_chain_comparison(*left, ops_vec, comparators_vec, position) } - AstExpr::Call(ast::ExprCall { - func, arguments, range, .. - }) => { - let position = self.convert_range(range); + AstExpr::Call(mut call) => { + // `ExprCall` carries a manual `Drop` impl (see `placeholder_expr`), so its + // owned fields must be vacated via `mem::replace` rather than moved out. + let position = self.convert_range(call.range); + let func = mem::replace(&mut call.func, Box::new(placeholder_expr())); + let arguments = mem::replace(&mut call.arguments, placeholder_arguments()); + let ast::Arguments { args, keywords, .. } = arguments; let args_vec = args.into_vec(); let keywords_vec = keywords.into_vec(); @@ -1064,7 +1068,8 @@ impl<'a> Parser<'a> { }, )) } - AstExpr::Attribute(ast::ExprAttribute { value, attr, .. }) => { + AstExpr::Attribute(attribute) => { + let (value, attr, _) = deconstruct_attribute(attribute); let object = Box::new(self.parse_expression(*value)?); Ok(ExprLoc::new( position, @@ -1138,7 +1143,8 @@ impl<'a> Parser<'a> { self.convert_range(range), Expr::Literal(Literal::Ellipsis), )), - AstExpr::Attribute(ast::ExprAttribute { value, attr, range, .. }) => { + AstExpr::Attribute(attribute) => { + let (value, attr, range) = deconstruct_attribute(attribute); let object = Box::new(self.parse_expression(*value)?); let position = self.convert_range(range); Ok(ExprLoc::new( @@ -1149,9 +1155,8 @@ impl<'a> Parser<'a> { }, )) } - AstExpr::Subscript(ast::ExprSubscript { - value, slice, range, .. - }) => { + AstExpr::Subscript(subscript) => { + let (value, slice, range) = deconstruct_subscript(subscript); let object = Box::new(self.parse_expression(*value)?); let index = Box::new(self.parse_expression(*slice)?); Ok(ExprLoc::new( @@ -1674,6 +1679,38 @@ impl<'a> Parser<'a> { } } +/// Move the owned parts out of an [`ast::ExprSubscript`]; see [`placeholder_expr`]. +fn deconstruct_subscript(mut node: ast::ExprSubscript) -> (Box, Box, TextRange) { + let value = mem::replace(&mut node.value, Box::new(placeholder_expr())); + let slice = mem::replace(&mut node.slice, Box::new(placeholder_expr())); + (value, slice, node.range) +} + +/// Move the owned parts out of an [`ast::ExprAttribute`]; see [`placeholder_expr`]. +fn deconstruct_attribute(mut node: ast::ExprAttribute) -> (Box, ast::Identifier, TextRange) { + let value = mem::replace(&mut node.value, Box::new(placeholder_expr())); + let attr = mem::replace(&mut node.attr, ast::Identifier::new("", TextRange::default())); + (value, attr, node.range) +} + +/// A trivial owned expression used to sever an owned `Box` edge of a +/// "spine" node (`BinOp`/`Call`/`Subscript`/`Attribute`) before that node is +/// dropped. +fn placeholder_expr() -> AstExpr { + AstExpr::NoneLiteral(ast::ExprNoneLiteral::default()) +} + +/// Empty placeholder [`ast::Arguments`] used to vacate the `arguments` field of +/// an [`ast::ExprCall`] husk; see [`placeholder_expr`]. +fn placeholder_arguments() -> ast::Arguments { + ast::Arguments { + range: TextRange::default(), + node_index: ast::AtomicNodeIndex::default(), + args: Box::default(), + keywords: Box::default(), + } +} + fn convert_op(op: AstOperator) -> Operator { match op { AstOperator::Add => Operator::Add, diff --git a/crates/monty/tests/security.rs b/crates/monty/tests/security.rs index 15fb92c1..2e018d8f 100644 --- a/crates/monty/tests/security.rs +++ b/crates/monty/tests/security.rs @@ -33,3 +33,17 @@ fn deeply_nested_attribute_access_does_not_stack_overflow() { let err = result.expect_err("expected parse error for deeply nested attribute access"); assert_snapshot!(err.message().unwrap_or(""), @"Source is too deeply nested"); } + +#[test] +fn many_plus() { + // '1+' * 15_000 + '1' + let depth = 100_000; + let mut code = String::with_capacity(depth * 2 + 1); + for _ in 0..depth { + code.push_str("1+"); + } + code.push('1'); + let result = MontyRun::new(code, "test.py", vec![]); + let err = result.expect_err("expected parse error for deeply nested attribute access"); + assert_snapshot!(err.message().unwrap_or(""), @"Source is too deeply nested"); +}