diff --git a/.changeset/fix-svelte-each-nested-destructuring.md b/.changeset/fix-svelte-each-nested-destructuring.md new file mode 100644 index 000000000000..9b326d7b2e21 --- /dev/null +++ b/.changeset/fix-svelte-each-nested-destructuring.md @@ -0,0 +1,5 @@ +--- +"@biomejs/biome": patch +--- + +Fixed Svelte `#each` destructuring parsing and formatting for nested patterns such as `[key, { a, b }]`. diff --git a/crates/biome_html_formatter/src/svelte/any/binding_assignment_binding.rs b/crates/biome_html_formatter/src/svelte/any/binding_assignment_binding.rs index fe13b4c3c100..c69918fcea16 100644 --- a/crates/biome_html_formatter/src/svelte/any/binding_assignment_binding.rs +++ b/crates/biome_html_formatter/src/svelte/any/binding_assignment_binding.rs @@ -12,6 +12,9 @@ impl FormatRule for FormatAnySvelteBindingAss f: &mut HtmlFormatter, ) -> FormatResult<()> { match node { + AnySvelteBindingAssignmentBinding::AnySvelteDestructuredName(node) => { + node.format().fmt(f) + } AnySvelteBindingAssignmentBinding::SvelteName(node) => node.format().fmt(f), AnySvelteBindingAssignmentBinding::SvelteRestBinding(node) => node.format().fmt(f), } diff --git a/crates/biome_html_formatter/tests/specs/html/svelte/each_with_destructuring.svelte b/crates/biome_html_formatter/tests/specs/html/svelte/each_with_destructuring.svelte index 821ca50d6d29..6de4e41046f2 100644 --- a/crates/biome_html_formatter/tests/specs/html/svelte/each_with_destructuring.svelte +++ b/crates/biome_html_formatter/tests/specs/html/svelte/each_with_destructuring.svelte @@ -13,3 +13,7 @@ {#each items as { id, ...rest }}
{id}
{/each} + +{#each Object.entries(foo) as [key, { a, b }]} + {a} {b} +{/each} diff --git a/crates/biome_html_formatter/tests/specs/html/svelte/each_with_destructuring.svelte.snap b/crates/biome_html_formatter/tests/specs/html/svelte/each_with_destructuring.svelte.snap index 88709d00f4e9..d9038b5ec2f6 100644 --- a/crates/biome_html_formatter/tests/specs/html/svelte/each_with_destructuring.svelte.snap +++ b/crates/biome_html_formatter/tests/specs/html/svelte/each_with_destructuring.svelte.snap @@ -1,5 +1,6 @@ --- source: crates/biome_formatter_test/src/snapshot_builder.rs +assertion_line: 240 info: svelte/each_with_destructuring.svelte --- @@ -22,6 +23,10 @@ info: svelte/each_with_destructuring.svelte
{id}
{/each} +{#each Object.entries(foo) as [key, { a, b }]} + {a} {b} +{/each} + ``` @@ -44,4 +49,8 @@ info: svelte/each_with_destructuring.svelte
{id}
{/each} +{#each Object.entries(foo) as [ key, { a, b } ]} + {a} {b} +{/each} + ``` diff --git a/crates/biome_html_parser/src/syntax/svelte.rs b/crates/biome_html_parser/src/syntax/svelte.rs index 1c774e4f1fcb..68b0e9bcf0be 100644 --- a/crates/biome_html_parser/src/syntax/svelte.rs +++ b/crates/biome_html_parser/src/syntax/svelte.rs @@ -761,7 +761,7 @@ fn parse_square_destructured_name(p: &mut HtmlParser) -> ParsedSyntax { SvelteBindingAssignmentBindingList.parse_list(p); - p.expect(T![']']); + p.expect_with_context(T![']'], HtmlLexContext::Svelte); Present(m.complete(p, SVELTE_SQUARE_DESTRUCTURED_NAME)) } @@ -1072,6 +1072,14 @@ impl ParseSeparatedList for SvelteBindingAssignmentBindingList { fn parse_element(&mut self, p: &mut Self::Parser<'_>) -> ParsedSyntax { if p.at(T![...]) { parse_rest_name(p) + } else if p.at(T!['{']) { + let result = parse_curly_destructured_name(p); + p.re_lex(HtmlReLexContext::Svelte); + result + } else if p.at(T!['[']) { + let result = parse_square_destructured_name(p); + p.re_lex(HtmlReLexContext::Svelte); + result } else { parse_svelte_name(p) } diff --git a/crates/biome_html_parser/tests/html_specs/ok/svelte/each_with_destructuring.svelte b/crates/biome_html_parser/tests/html_specs/ok/svelte/each_with_destructuring.svelte index 821ca50d6d29..6de4e41046f2 100644 --- a/crates/biome_html_parser/tests/html_specs/ok/svelte/each_with_destructuring.svelte +++ b/crates/biome_html_parser/tests/html_specs/ok/svelte/each_with_destructuring.svelte @@ -13,3 +13,7 @@ {#each items as { id, ...rest }}
{id}
{/each} + +{#each Object.entries(foo) as [key, { a, b }]} + {a} {b} +{/each} diff --git a/crates/biome_html_parser/tests/html_specs/ok/svelte/each_with_destructuring.svelte.snap b/crates/biome_html_parser/tests/html_specs/ok/svelte/each_with_destructuring.svelte.snap index 8799472d00ec..20a4b510e0c6 100644 --- a/crates/biome_html_parser/tests/html_specs/ok/svelte/each_with_destructuring.svelte.snap +++ b/crates/biome_html_parser/tests/html_specs/ok/svelte/each_with_destructuring.svelte.snap @@ -1,5 +1,6 @@ --- source: crates/biome_html_parser/tests/spec_test.rs +assertion_line: 145 expression: snapshot --- @@ -22,6 +23,10 @@ expression: snapshot
{id}
{/each} +{#each Object.entries(foo) as [key, { a, b }]} + {a} {b} +{/each} + ``` @@ -336,19 +341,79 @@ HtmlRoot { r_curly_token: R_CURLY@270..271 "}" [] [], }, }, + SvelteEachBlock { + opening_block: SvelteEachOpeningBlock { + sv_curly_hash_token: SV_CURLY_HASH@271..275 "{#" [Newline("\n"), Newline("\n")] [], + each_token: EACH_KW@275..279 "each" [] [], + list: HtmlTextExpression { + html_literal_token: HTML_LITERAL@279..300 " Object.entries(foo) " [] [], + }, + item: SvelteEachAsKeyedItem { + as_token: AS_KW@300..303 "as" [] [Whitespace(" ")], + name: SvelteSquareDestructuredName { + l_brack_token: L_BRACKET@303..304 "[" [] [], + names: SvelteBindingAssignmentBindingList [ + SvelteName { + ident_token: IDENT@304..307 "key" [] [], + }, + COMMA@307..309 "," [] [Whitespace(" ")], + SvelteCurlyDestructuredName { + l_curly_token: L_CURLY@309..311 "{" [] [Whitespace(" ")], + names: SvelteBindingAssignmentBindingList [ + SvelteName { + ident_token: IDENT@311..312 "a" [] [], + }, + COMMA@312..314 "," [] [Whitespace(" ")], + SvelteName { + ident_token: IDENT@314..316 "b" [] [Whitespace(" ")], + }, + ], + r_curly_token: R_CURLY@316..317 "}" [] [], + }, + ], + r_brack_token: R_BRACKET@317..318 "]" [] [], + }, + index: missing (optional), + key: missing (optional), + }, + r_curly_token: R_CURLY@318..319 "}" [] [], + }, + children: HtmlElementList [ + HtmlSingleTextExpression { + l_curly_token: L_CURLY@319..323 "{" [Newline("\n"), Whitespace(" ")] [], + expression: HtmlTextExpression { + html_literal_token: HTML_LITERAL@323..324 "a" [] [], + }, + r_curly_token: R_CURLY@324..326 "}" [] [Whitespace(" ")], + }, + HtmlSingleTextExpression { + l_curly_token: L_CURLY@326..327 "{" [] [], + expression: HtmlTextExpression { + html_literal_token: HTML_LITERAL@327..328 "b" [] [], + }, + r_curly_token: R_CURLY@328..329 "}" [] [], + }, + ], + else_clause: missing (optional), + closing_block: SvelteEachClosingBlock { + sv_curly_slash_token: SV_CURLY_SLASH@329..332 "{/" [Newline("\n")] [], + each_token: EACH_KW@332..336 "each" [] [], + r_curly_token: R_CURLY@336..337 "}" [] [], + }, + }, ], - eof_token: EOF@271..272 "" [Newline("\n")] [], + eof_token: EOF@337..338 "" [Newline("\n")] [], } ``` ## CST ``` -0: HTML_ROOT@0..272 +0: HTML_ROOT@0..338 0: (empty) 1: (empty) 2: (empty) - 3: HTML_ELEMENT_LIST@0..271 + 3: HTML_ELEMENT_LIST@0..337 0: SVELTE_EACH_BLOCK@0..63 0: SVELTE_EACH_OPENING_BLOCK@0..29 0: SV_CURLY_HASH@0..2 "{#" [] [] @@ -564,6 +629,49 @@ HtmlRoot { 0: SV_CURLY_SLASH@263..266 "{/" [Newline("\n")] [] 1: EACH_KW@266..270 "each" [] [] 2: R_CURLY@270..271 "}" [] [] - 4: EOF@271..272 "" [Newline("\n")] [] + 4: SVELTE_EACH_BLOCK@271..337 + 0: SVELTE_EACH_OPENING_BLOCK@271..319 + 0: SV_CURLY_HASH@271..275 "{#" [Newline("\n"), Newline("\n")] [] + 1: EACH_KW@275..279 "each" [] [] + 2: HTML_TEXT_EXPRESSION@279..300 + 0: HTML_LITERAL@279..300 " Object.entries(foo) " [] [] + 3: SVELTE_EACH_AS_KEYED_ITEM@300..318 + 0: AS_KW@300..303 "as" [] [Whitespace(" ")] + 1: SVELTE_SQUARE_DESTRUCTURED_NAME@303..318 + 0: L_BRACKET@303..304 "[" [] [] + 1: SVELTE_BINDING_ASSIGNMENT_BINDING_LIST@304..317 + 0: SVELTE_NAME@304..307 + 0: IDENT@304..307 "key" [] [] + 1: COMMA@307..309 "," [] [Whitespace(" ")] + 2: SVELTE_CURLY_DESTRUCTURED_NAME@309..317 + 0: L_CURLY@309..311 "{" [] [Whitespace(" ")] + 1: SVELTE_BINDING_ASSIGNMENT_BINDING_LIST@311..316 + 0: SVELTE_NAME@311..312 + 0: IDENT@311..312 "a" [] [] + 1: COMMA@312..314 "," [] [Whitespace(" ")] + 2: SVELTE_NAME@314..316 + 0: IDENT@314..316 "b" [] [Whitespace(" ")] + 2: R_CURLY@316..317 "}" [] [] + 2: R_BRACKET@317..318 "]" [] [] + 2: (empty) + 3: (empty) + 4: R_CURLY@318..319 "}" [] [] + 1: HTML_ELEMENT_LIST@319..329 + 0: HTML_SINGLE_TEXT_EXPRESSION@319..326 + 0: L_CURLY@319..323 "{" [Newline("\n"), Whitespace(" ")] [] + 1: HTML_TEXT_EXPRESSION@323..324 + 0: HTML_LITERAL@323..324 "a" [] [] + 2: R_CURLY@324..326 "}" [] [Whitespace(" ")] + 1: HTML_SINGLE_TEXT_EXPRESSION@326..329 + 0: L_CURLY@326..327 "{" [] [] + 1: HTML_TEXT_EXPRESSION@327..328 + 0: HTML_LITERAL@327..328 "b" [] [] + 2: R_CURLY@328..329 "}" [] [] + 2: (empty) + 3: SVELTE_EACH_CLOSING_BLOCK@329..337 + 0: SV_CURLY_SLASH@329..332 "{/" [Newline("\n")] [] + 1: EACH_KW@332..336 "each" [] [] + 2: R_CURLY@336..337 "}" [] [] + 4: EOF@337..338 "" [Newline("\n")] [] ``` diff --git a/crates/biome_html_syntax/src/generated/nodes.rs b/crates/biome_html_syntax/src/generated/nodes.rs index c408eee07478..8d07db4198ae 100644 --- a/crates/biome_html_syntax/src/generated/nodes.rs +++ b/crates/biome_html_syntax/src/generated/nodes.rs @@ -4112,10 +4112,17 @@ impl AnySvelteAwaitClauses { } #[derive(Clone, PartialEq, Eq, Hash, Serialize)] pub enum AnySvelteBindingAssignmentBinding { + AnySvelteDestructuredName(AnySvelteDestructuredName), SvelteName(SvelteName), SvelteRestBinding(SvelteRestBinding), } impl AnySvelteBindingAssignmentBinding { + pub fn as_any_svelte_destructured_name(&self) -> Option<&AnySvelteDestructuredName> { + match &self { + Self::AnySvelteDestructuredName(item) => Some(item), + _ => None, + } + } pub fn as_svelte_name(&self) -> Option<&SvelteName> { match &self { Self::SvelteName(item) => Some(item), @@ -9833,16 +9840,29 @@ impl From for AnySvelteBindingAssignmentBinding { } impl AstNode for AnySvelteBindingAssignmentBinding { type Language = Language; - const KIND_SET: SyntaxKindSet = - SvelteName::KIND_SET.union(SvelteRestBinding::KIND_SET); + const KIND_SET: SyntaxKindSet = AnySvelteDestructuredName::KIND_SET + .union(SvelteName::KIND_SET) + .union(SvelteRestBinding::KIND_SET); fn can_cast(kind: SyntaxKind) -> bool { - matches!(kind, SVELTE_NAME | SVELTE_REST_BINDING) + match kind { + SVELTE_NAME | SVELTE_REST_BINDING => true, + k if AnySvelteDestructuredName::can_cast(k) => true, + _ => false, + } } fn cast(syntax: SyntaxNode) -> Option { let res = match syntax.kind() { SVELTE_NAME => Self::SvelteName(SvelteName { syntax }), SVELTE_REST_BINDING => Self::SvelteRestBinding(SvelteRestBinding { syntax }), - _ => return None, + _ => { + if let Some(any_svelte_destructured_name) = AnySvelteDestructuredName::cast(syntax) + { + return Some(Self::AnySvelteDestructuredName( + any_svelte_destructured_name, + )); + } + return None; + } }; Some(res) } @@ -9850,18 +9870,21 @@ impl AstNode for AnySvelteBindingAssignmentBinding { match self { Self::SvelteName(it) => it.syntax(), Self::SvelteRestBinding(it) => it.syntax(), + Self::AnySvelteDestructuredName(it) => it.syntax(), } } fn into_syntax(self) -> SyntaxNode { match self { Self::SvelteName(it) => it.into_syntax(), Self::SvelteRestBinding(it) => it.into_syntax(), + Self::AnySvelteDestructuredName(it) => it.into_syntax(), } } } impl std::fmt::Debug for AnySvelteBindingAssignmentBinding { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { + Self::AnySvelteDestructuredName(it) => std::fmt::Debug::fmt(it, f), Self::SvelteName(it) => std::fmt::Debug::fmt(it, f), Self::SvelteRestBinding(it) => std::fmt::Debug::fmt(it, f), } @@ -9870,6 +9893,7 @@ impl std::fmt::Debug for AnySvelteBindingAssignmentBinding { impl From for SyntaxNode { fn from(n: AnySvelteBindingAssignmentBinding) -> Self { match n { + AnySvelteBindingAssignmentBinding::AnySvelteDestructuredName(it) => it.into_syntax(), AnySvelteBindingAssignmentBinding::SvelteName(it) => it.into_syntax(), AnySvelteBindingAssignmentBinding::SvelteRestBinding(it) => it.into_syntax(), } diff --git a/xtask/codegen/html.ungram b/xtask/codegen/html.ungram index 5894b1550f28..0cf490476e31 100644 --- a/xtask/codegen/html.ungram +++ b/xtask/codegen/html.ungram @@ -413,6 +413,7 @@ SvelteBindingAssignmentBindingList = (AnySvelteBindingAssignmentBinding (',' Any AnySvelteBindingAssignmentBinding = SvelteName + | AnySvelteDestructuredName | SvelteRestBinding /// { ...rest }