From 0d973af827754382b70ea1052d283de44295221f Mon Sep 17 00:00:00 2001 From: Javier Garcia <93437997+IamYipi@users.noreply.github.com> Date: Wed, 30 Sep 2026 05:41:53 +0200 Subject: [PATCH] fix(parser): reject a comma after an object rest element in assignment patterns An ObjectAssignmentPattern ends with its AssignmentRestProperty: `{...rest,}` is not one. The cover grammar accepted it, since the object literal kept no trace of the comma after its spread property. Record it the way ArrayLiteral already does, and refuse to convert such a literal to a pattern, so `({...rest,} = {})` and the for-in/of heads are SyntaxErrors. --- core/ast/src/expression/literal/object.rs | 16 ++++- .../primary/object_initializer/mod.rs | 16 ++++- .../primary/object_initializer/tests.rs | 70 +++++++++++++++---- core/parser/src/parser/tests/mod.rs | 23 +++++- 4 files changed, 107 insertions(+), 18 deletions(-) diff --git a/core/ast/src/expression/literal/object.rs b/core/ast/src/expression/literal/object.rs index 700a3612991..8786354ef3b 100644 --- a/core/ast/src/expression/literal/object.rs +++ b/core/ast/src/expression/literal/object.rs @@ -41,6 +41,7 @@ use core::{fmt::Write as _, ops::ControlFlow}; #[derive(Clone, Debug, PartialEq)] pub struct ObjectLiteral { properties: Box<[PropertyDefinition]>, + has_trailing_comma_spread: bool, span: Span, } @@ -48,12 +49,13 @@ impl ObjectLiteral { /// Create a new [`ObjectLiteral`]. #[inline] #[must_use] - pub fn new(properties: T, span: Span) -> Self + pub fn new(properties: T, has_trailing_comma_spread: bool, span: Span) -> Self where T: Into>, { Self { properties: properties.into(), + has_trailing_comma_spread, span, } } @@ -65,9 +67,21 @@ impl ObjectLiteral { &self.properties } + /// Indicates if a spread property in the object literal is followed by a trailing comma. + /// This is a syntax error if the object literal is converted into a pattern. + #[inline] + #[must_use] + pub const fn has_trailing_comma_spread(&self) -> bool { + self.has_trailing_comma_spread + } + /// Converts the object literal into an [`ObjectPattern`]. #[must_use] pub fn to_pattern(&self, strict: bool) -> Option { + if self.has_trailing_comma_spread() { + return None; + } + let mut bindings = Vec::new(); for (i, property) in self.properties.iter().enumerate() { match property { diff --git a/core/parser/src/parser/expression/primary/object_initializer/mod.rs b/core/parser/src/parser/expression/primary/object_initializer/mod.rs index a7cc16a98e9..68e92068e5e 100644 --- a/core/parser/src/parser/expression/primary/object_initializer/mod.rs +++ b/core/parser/src/parser/expression/primary/object_initializer/mod.rs @@ -85,6 +85,7 @@ where let mut has_proto = false; let mut duplicate_proto_position = None; + let mut has_trailing_comma_spread = false; let end = loop { if let Some(token) = cursor.next_if(Punctuator::CloseBlock, interner)? { @@ -106,6 +107,7 @@ where } } + let is_spread = matches!(property, PropertyDefinitionNode::SpreadObject(_)); elements.push(property); if let Some(token) = cursor.next_if(Punctuator::CloseBlock, interner)? { @@ -121,6 +123,14 @@ where "object literal", )); } + + if is_spread + && cursor.peek(0, interner)?.is_some_and(|token| { + token.kind() == &TokenKind::Punctuator(Punctuator::CloseBlock) + }) + { + has_trailing_comma_spread = true; + } }; if let Some(position) = duplicate_proto_position @@ -136,7 +146,11 @@ where } let start = open_block_token.span().start(); - Ok(literal::ObjectLiteral::new(elements, Span::new(start, end))) + Ok(literal::ObjectLiteral::new( + elements, + has_trailing_comma_spread, + Span::new(start, end), + )) } } diff --git a/core/parser/src/parser/expression/primary/object_initializer/tests.rs b/core/parser/src/parser/expression/primary/object_initializer/tests.rs index f26ec4a0eff..c1cdf6807cd 100644 --- a/core/parser/src/parser/expression/primary/object_initializer/tests.rs +++ b/core/parser/src/parser/expression/primary/object_initializer/tests.rs @@ -53,7 +53,10 @@ fn check_object_literal() { interner.get_or_intern_static("x", utf16!("x")), Span::new((1, 7), (1, 8)), ), - Some(ObjectLiteral::new(object_properties, Span::new((1, 11), (4, 2))).into()), + Some( + ObjectLiteral::new(object_properties, false, Span::new((1, 11), (4, 2))) + .into(), + ), )] .try_into() .unwrap(), @@ -105,7 +108,10 @@ fn check_object_short_function() { interner.get_or_intern_static("x", utf16!("x")), Span::new((1, 7), (1, 8)), ), - Some(ObjectLiteral::new(object_properties, Span::new((1, 11), (4, 2))).into()), + Some( + ObjectLiteral::new(object_properties, false, Span::new((1, 11), (4, 2))) + .into(), + ), )] .try_into() .unwrap(), @@ -171,7 +177,10 @@ fn check_object_short_function_arguments() { interner.get_or_intern_static("x", utf16!("x")), Span::new((1, 7), (1, 8)), ), - Some(ObjectLiteral::new(object_properties, Span::new((1, 11), (4, 2))).into()), + Some( + ObjectLiteral::new(object_properties, false, Span::new((1, 11), (4, 2))) + .into(), + ), )] .try_into() .unwrap(), @@ -222,7 +231,10 @@ fn check_object_getter() { interner.get_or_intern_static("x", utf16!("x")), Span::new((1, 7), (1, 8)), ), - Some(ObjectLiteral::new(object_properties, Span::new((1, 11), (4, 2))).into()), + Some( + ObjectLiteral::new(object_properties, false, Span::new((1, 11), (4, 2))) + .into(), + ), )] .try_into() .unwrap(), @@ -287,7 +299,10 @@ fn check_object_setter() { interner.get_or_intern_static("x", utf16!("x")), Span::new((1, 7), (1, 8)), ), - Some(ObjectLiteral::new(object_properties, Span::new((1, 11), (4, 2))).into()), + Some( + ObjectLiteral::new(object_properties, false, Span::new((1, 11), (4, 2))) + .into(), + ), )] .try_into() .unwrap(), @@ -325,7 +340,10 @@ fn check_object_short_function_get() { interner.get_or_intern_static("x", utf16!("x")), Span::new((1, 7), (1, 8)), ), - Some(ObjectLiteral::new(object_properties, Span::new((1, 11), (3, 2))).into()), + Some( + ObjectLiteral::new(object_properties, false, Span::new((1, 11), (3, 2))) + .into(), + ), )] .try_into() .unwrap(), @@ -363,7 +381,10 @@ fn check_object_short_function_set() { interner.get_or_intern_static("x", utf16!("x")), Span::new((1, 7), (1, 8)), ), - Some(ObjectLiteral::new(object_properties, Span::new((1, 11), (3, 2))).into()), + Some( + ObjectLiteral::new(object_properties, false, Span::new((1, 11), (3, 2))) + .into(), + ), )] .try_into() .unwrap(), @@ -407,7 +428,10 @@ fn check_object_shorthand_property_names() { interner.get_or_intern_static("x", utf16!("x")), Span::new((2, 7), (2, 8)), ), - Some(ObjectLiteral::new(object_properties, Span::new((2, 11), (2, 16))).into()), + Some( + ObjectLiteral::new(object_properties, false, Span::new((2, 11), (2, 16))) + .into(), + ), )] .try_into() .unwrap(), @@ -470,7 +494,10 @@ fn check_object_shorthand_multiple_properties() { interner.get_or_intern_static("x", utf16!("x")), Span::new((3, 7), (3, 8)), ), - Some(ObjectLiteral::new(object_properties, Span::new((3, 11), (3, 20))).into()), + Some( + ObjectLiteral::new(object_properties, false, Span::new((3, 11), (3, 20))) + .into(), + ), )] .try_into() .unwrap(), @@ -512,7 +539,10 @@ fn check_object_spread() { interner.get_or_intern_static("x", utf16!("x")), Span::new((1, 7), (1, 8)), ), - Some(ObjectLiteral::new(object_properties, Span::new((1, 11), (1, 25))).into()), + Some( + ObjectLiteral::new(object_properties, false, Span::new((1, 11), (1, 25))) + .into(), + ), )] .try_into() .unwrap(), @@ -554,7 +584,10 @@ fn check_async_method() { interner.get_or_intern_static("x", utf16!("x")), Span::new((1, 7), (1, 8)), ), - Some(ObjectLiteral::new(object_properties, Span::new((1, 11), (3, 2))).into()), + Some( + ObjectLiteral::new(object_properties, false, Span::new((1, 11), (3, 2))) + .into(), + ), )] .try_into() .unwrap(), @@ -596,7 +629,10 @@ fn check_async_generator_method() { interner.get_or_intern_static("x", utf16!("x")), Span::new((1, 7), (1, 8)), ), - Some(ObjectLiteral::new(object_properties, Span::new((1, 11), (3, 2))).into()), + Some( + ObjectLiteral::new(object_properties, false, Span::new((1, 11), (3, 2))) + .into(), + ), )] .try_into() .unwrap(), @@ -660,7 +696,10 @@ fn check_async_ordinary_method() { interner.get_or_intern_static("x", utf16!("x")), Span::new((1, 7), (1, 8)), ), - Some(ObjectLiteral::new(object_properties, Span::new((1, 11), (3, 2))).into()), + Some( + ObjectLiteral::new(object_properties, false, Span::new((1, 11), (3, 2))) + .into(), + ), )] .try_into() .unwrap(), @@ -697,7 +736,10 @@ fn check_async_property() { interner.get_or_intern_static("x", utf16!("x")), Span::new((1, 7), (1, 8)), ), - Some(ObjectLiteral::new(object_properties, Span::new((1, 11), (3, 2))).into()), + Some( + ObjectLiteral::new(object_properties, false, Span::new((1, 11), (3, 2))) + .into(), + ), )] .try_into() .unwrap(), diff --git a/core/parser/src/parser/tests/mod.rs b/core/parser/src/parser/tests/mod.rs index d88c845f29b..deb2ee514e3 100644 --- a/core/parser/src/parser/tests/mod.rs +++ b/core/parser/src/parser/tests/mod.rs @@ -623,7 +623,10 @@ fn spread_in_object() { interner.get_or_intern_static("x", utf16!("x")), Span::new((1, 5), (1, 6)), ), - Some(ObjectLiteral::new(object_properties, Span::new((1, 9), (4, 2))).into()), + Some( + ObjectLiteral::new(object_properties, true, Span::new((1, 9), (4, 2))) + .into(), + ), )] .try_into() .unwrap(), @@ -762,7 +765,7 @@ fn hashbang_use_strict_with_with_statement() { "#}, vec![ Statement::With(With::new( - ObjectLiteral::new([], Span::new((2, 6), (2, 8))).into(), + ObjectLiteral::new([], false, Span::new((2, 6), (2, 8))).into(), Block::from(StatementList::new([], LinearPosition::new(27), false)).into(), )) .into(), @@ -825,6 +828,22 @@ fn debugger_statement() { check_invalid_script("debugger + debugger"); } +/// An object rest element cannot be followed by a comma in an assignment pattern. +#[test] +fn object_rest_trailing_comma_in_assignment_pattern() { + check_invalid_script("var rest; ({...rest,} = {});"); + check_invalid_script("var rest; [{...rest,}] = [{}];"); + check_invalid_script("var rest; for ({...rest,} in {});"); + check_invalid_script("var rest; for ({...rest,} of [{}]);"); + + // The comma is fine where the literal stays an object literal. + assert!( + Parser::new(Source::from_bytes("var rest; ({...rest,});")) + .parse_script(&Scope::new_global(), &mut Interner::default()) + .is_ok() + ); +} + #[test] fn invalid_arrow_function() { check_invalid_script(r#"(!()=>"#);