From ae401b1466448ed568b87e6ae0d95740c8c51f4e Mon Sep 17 00:00:00 2001 From: mujpao Date: Sun, 19 Apr 2026 01:54:47 -0700 Subject: [PATCH 1/3] fix(parser): fix crash when parsing Svelte templates --- .changeset/honest-shirts-wear.md | 5 ++ crates/biome_html_parser/src/syntax/svelte.rs | 16 +++-- .../svelte/at_block_invalid_block_type.svelte | 1 + .../at_block_invalid_block_type.svelte.snap | 66 +++++++++++++++++++ .../svelte/at_block_missing_block_type.svelte | 1 + .../at_block_missing_block_type.svelte.snap | 59 +++++++++++++++++ ...issing_block_type_and_closing_brace.svelte | 1 + ...g_block_type_and_closing_brace.svelte.snap | 57 ++++++++++++++++ .../svelte/template_invalid_block_type.svelte | 1 + .../template_invalid_block_type.svelte.snap | 64 ++++++++++++++++++ .../svelte/template_missing_block_type.svelte | 1 + .../template_missing_block_type.svelte.snap | 59 +++++++++++++++++ ...issing_block_type_and_closing_brace.svelte | 1 + ...g_block_type_and_closing_brace.svelte.snap | 57 ++++++++++++++++ 14 files changed, 385 insertions(+), 4 deletions(-) create mode 100644 .changeset/honest-shirts-wear.md create mode 100644 crates/biome_html_parser/tests/html_specs/error/svelte/at_block_invalid_block_type.svelte create mode 100644 crates/biome_html_parser/tests/html_specs/error/svelte/at_block_invalid_block_type.svelte.snap create mode 100644 crates/biome_html_parser/tests/html_specs/error/svelte/at_block_missing_block_type.svelte create mode 100644 crates/biome_html_parser/tests/html_specs/error/svelte/at_block_missing_block_type.svelte.snap create mode 100644 crates/biome_html_parser/tests/html_specs/error/svelte/at_block_missing_block_type_and_closing_brace.svelte create mode 100644 crates/biome_html_parser/tests/html_specs/error/svelte/at_block_missing_block_type_and_closing_brace.svelte.snap create mode 100644 crates/biome_html_parser/tests/html_specs/error/svelte/template_invalid_block_type.svelte create mode 100644 crates/biome_html_parser/tests/html_specs/error/svelte/template_invalid_block_type.svelte.snap create mode 100644 crates/biome_html_parser/tests/html_specs/error/svelte/template_missing_block_type.svelte create mode 100644 crates/biome_html_parser/tests/html_specs/error/svelte/template_missing_block_type.svelte.snap create mode 100644 crates/biome_html_parser/tests/html_specs/error/svelte/template_missing_block_type_and_closing_brace.svelte create mode 100644 crates/biome_html_parser/tests/html_specs/error/svelte/template_missing_block_type_and_closing_brace.svelte.snap diff --git a/.changeset/honest-shirts-wear.md b/.changeset/honest-shirts-wear.md new file mode 100644 index 000000000000..487e7b5d4725 --- /dev/null +++ b/.changeset/honest-shirts-wear.md @@ -0,0 +1,5 @@ +--- +"@biomejs/biome": patch +--- + +Fixed #10003: Biome no longer panics when parsing Svelte files containing `{#}`. diff --git a/crates/biome_html_parser/src/syntax/svelte.rs b/crates/biome_html_parser/src/syntax/svelte.rs index 3dc739f2ec5c..9a5b38621097 100644 --- a/crates/biome_html_parser/src/syntax/svelte.rs +++ b/crates/biome_html_parser/src/syntax/svelte.rs @@ -32,8 +32,12 @@ pub(crate) fn parse_svelte_hash_block(p: &mut HtmlParser) -> ParsedSyntax { T![await] => parse_await_block(p, m), T![snippet] => parse_snippet_block(p, m), _ => { - m.abandon(p); - Absent + p.error(p.err_builder( + "Expected `if`, `each`, `key`, `await`, or `snippet`", + p.cur_range(), + )); + p.expect(T!['}']); + Present(m.complete(p, SVELTE_BOGUS_BLOCK)) } } } @@ -826,8 +830,12 @@ pub(crate) fn parse_svelte_at_block(p: &mut HtmlParser) -> ParsedSyntax { T![render] => parse_render_block(p, m), T![const] => parse_const_block(p, m), _ => { - m.abandon(p); - Absent + p.error(p.err_builder( + "Expected `render`, `html`, `const`, or `debug`", + p.cur_range(), + )); + p.expect(T!['}']); + Present(m.complete(p, SVELTE_BOGUS_BLOCK)) } } } diff --git a/crates/biome_html_parser/tests/html_specs/error/svelte/at_block_invalid_block_type.svelte b/crates/biome_html_parser/tests/html_specs/error/svelte/at_block_invalid_block_type.svelte new file mode 100644 index 000000000000..dd95b0a2dac6 --- /dev/null +++ b/crates/biome_html_parser/tests/html_specs/error/svelte/at_block_invalid_block_type.svelte @@ -0,0 +1 @@ +{@notarealblocktype} diff --git a/crates/biome_html_parser/tests/html_specs/error/svelte/at_block_invalid_block_type.svelte.snap b/crates/biome_html_parser/tests/html_specs/error/svelte/at_block_invalid_block_type.svelte.snap new file mode 100644 index 000000000000..4ca089bbcbe6 --- /dev/null +++ b/crates/biome_html_parser/tests/html_specs/error/svelte/at_block_invalid_block_type.svelte.snap @@ -0,0 +1,66 @@ +--- +source: crates/biome_html_parser/tests/spec_test.rs +expression: snapshot +--- + +## Input + +```svelte +{@notarealblocktype} + +``` + + +## AST + +``` +HtmlRoot { + bom_token: missing (optional), + frontmatter: missing (optional), + directive: missing (optional), + html: HtmlElementList [ + SvelteBogusBlock { + items: [ + SV_CURLY_AT@0..2 "{@" [] [], + ], + }, + HtmlBogusElement { + items: [ + IDENT@2..19 "notarealblocktype" [] [], + R_CURLY@19..20 "}" [] [], + ], + }, + ], + eof_token: EOF@20..21 "" [Newline("\n")] [], +} +``` + +## CST + +``` +0: HTML_ROOT@0..21 + 0: (empty) + 1: (empty) + 2: (empty) + 3: HTML_ELEMENT_LIST@0..20 + 0: SVELTE_BOGUS_BLOCK@0..2 + 0: SV_CURLY_AT@0..2 "{@" [] [] + 1: HTML_BOGUS_ELEMENT@2..20 + 0: IDENT@2..19 "notarealblocktype" [] [] + 1: R_CURLY@19..20 "}" [] [] + 4: EOF@20..21 "" [Newline("\n")] [] + +``` + +## Diagnostics + +``` +at_block_invalid_block_type.svelte:1:3 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Expected `render`, `html`, `const`, or `debug` + + > 1 │ {@notarealblocktype} + │ ^^^^^^^^^^^^^^^^^ + 2 │ + +``` diff --git a/crates/biome_html_parser/tests/html_specs/error/svelte/at_block_missing_block_type.svelte b/crates/biome_html_parser/tests/html_specs/error/svelte/at_block_missing_block_type.svelte new file mode 100644 index 000000000000..93210a57aa24 --- /dev/null +++ b/crates/biome_html_parser/tests/html_specs/error/svelte/at_block_missing_block_type.svelte @@ -0,0 +1 @@ +{@} diff --git a/crates/biome_html_parser/tests/html_specs/error/svelte/at_block_missing_block_type.svelte.snap b/crates/biome_html_parser/tests/html_specs/error/svelte/at_block_missing_block_type.svelte.snap new file mode 100644 index 000000000000..b73afa7e5e1f --- /dev/null +++ b/crates/biome_html_parser/tests/html_specs/error/svelte/at_block_missing_block_type.svelte.snap @@ -0,0 +1,59 @@ +--- +source: crates/biome_html_parser/tests/spec_test.rs +expression: snapshot +--- + +## Input + +```svelte +{@} + +``` + + +## AST + +``` +HtmlRoot { + bom_token: missing (optional), + frontmatter: missing (optional), + directive: missing (optional), + html: HtmlElementList [ + SvelteBogusBlock { + items: [ + SV_CURLY_AT@0..2 "{@" [] [], + R_CURLY@2..3 "}" [] [], + ], + }, + ], + eof_token: EOF@3..4 "" [Newline("\n")] [], +} +``` + +## CST + +``` +0: HTML_ROOT@0..4 + 0: (empty) + 1: (empty) + 2: (empty) + 3: HTML_ELEMENT_LIST@0..3 + 0: SVELTE_BOGUS_BLOCK@0..3 + 0: SV_CURLY_AT@0..2 "{@" [] [] + 1: R_CURLY@2..3 "}" [] [] + 4: EOF@3..4 "" [Newline("\n")] [] + +``` + +## Diagnostics + +``` +at_block_missing_block_type.svelte:1:3 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Expected `render`, `html`, `const`, or `debug` + + > 1 │ {@} + │ ^ + 2 │ + +``` diff --git a/crates/biome_html_parser/tests/html_specs/error/svelte/at_block_missing_block_type_and_closing_brace.svelte b/crates/biome_html_parser/tests/html_specs/error/svelte/at_block_missing_block_type_and_closing_brace.svelte new file mode 100644 index 000000000000..8e98a4ed6291 --- /dev/null +++ b/crates/biome_html_parser/tests/html_specs/error/svelte/at_block_missing_block_type_and_closing_brace.svelte @@ -0,0 +1 @@ +{@ diff --git a/crates/biome_html_parser/tests/html_specs/error/svelte/at_block_missing_block_type_and_closing_brace.svelte.snap b/crates/biome_html_parser/tests/html_specs/error/svelte/at_block_missing_block_type_and_closing_brace.svelte.snap new file mode 100644 index 000000000000..f435bfdc33b9 --- /dev/null +++ b/crates/biome_html_parser/tests/html_specs/error/svelte/at_block_missing_block_type_and_closing_brace.svelte.snap @@ -0,0 +1,57 @@ +--- +source: crates/biome_html_parser/tests/spec_test.rs +expression: snapshot +--- + +## Input + +```svelte +{@ + +``` + + +## AST + +``` +HtmlRoot { + bom_token: missing (optional), + frontmatter: missing (optional), + directive: missing (optional), + html: HtmlElementList [ + SvelteBogusBlock { + items: [ + SV_CURLY_AT@0..2 "{@" [] [], + ], + }, + ], + eof_token: EOF@2..3 "" [Newline("\n")] [], +} +``` + +## CST + +``` +0: HTML_ROOT@0..3 + 0: (empty) + 1: (empty) + 2: (empty) + 3: HTML_ELEMENT_LIST@0..2 + 0: SVELTE_BOGUS_BLOCK@0..2 + 0: SV_CURLY_AT@0..2 "{@" [] [] + 4: EOF@2..3 "" [Newline("\n")] [] + +``` + +## Diagnostics + +``` +at_block_missing_block_type_and_closing_brace.svelte:2:1 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Expected `render`, `html`, `const`, or `debug` + + 1 │ {@ + > 2 │ + │ + +``` diff --git a/crates/biome_html_parser/tests/html_specs/error/svelte/template_invalid_block_type.svelte b/crates/biome_html_parser/tests/html_specs/error/svelte/template_invalid_block_type.svelte new file mode 100644 index 000000000000..02166e0fbe13 --- /dev/null +++ b/crates/biome_html_parser/tests/html_specs/error/svelte/template_invalid_block_type.svelte @@ -0,0 +1 @@ +{#notarealblocktype} \ No newline at end of file diff --git a/crates/biome_html_parser/tests/html_specs/error/svelte/template_invalid_block_type.svelte.snap b/crates/biome_html_parser/tests/html_specs/error/svelte/template_invalid_block_type.svelte.snap new file mode 100644 index 000000000000..488ff3ad456c --- /dev/null +++ b/crates/biome_html_parser/tests/html_specs/error/svelte/template_invalid_block_type.svelte.snap @@ -0,0 +1,64 @@ +--- +source: crates/biome_html_parser/tests/spec_test.rs +expression: snapshot +--- + +## Input + +```svelte +{#notarealblocktype} +``` + + +## AST + +``` +HtmlRoot { + bom_token: missing (optional), + frontmatter: missing (optional), + directive: missing (optional), + html: HtmlElementList [ + SvelteBogusBlock { + items: [ + SV_CURLY_HASH@0..2 "{#" [] [], + ], + }, + HtmlBogusElement { + items: [ + IDENT@2..19 "notarealblocktype" [] [], + R_CURLY@19..20 "}" [] [], + ], + }, + ], + eof_token: EOF@20..20 "" [] [], +} +``` + +## CST + +``` +0: HTML_ROOT@0..20 + 0: (empty) + 1: (empty) + 2: (empty) + 3: HTML_ELEMENT_LIST@0..20 + 0: SVELTE_BOGUS_BLOCK@0..2 + 0: SV_CURLY_HASH@0..2 "{#" [] [] + 1: HTML_BOGUS_ELEMENT@2..20 + 0: IDENT@2..19 "notarealblocktype" [] [] + 1: R_CURLY@19..20 "}" [] [] + 4: EOF@20..20 "" [] [] + +``` + +## Diagnostics + +``` +template_invalid_block_type.svelte:1:3 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Expected `if`, `each`, `key`, `await`, or `snippet` + + > 1 │ {#notarealblocktype} + │ ^^^^^^^^^^^^^^^^^ + +``` diff --git a/crates/biome_html_parser/tests/html_specs/error/svelte/template_missing_block_type.svelte b/crates/biome_html_parser/tests/html_specs/error/svelte/template_missing_block_type.svelte new file mode 100644 index 000000000000..46676ddfd85d --- /dev/null +++ b/crates/biome_html_parser/tests/html_specs/error/svelte/template_missing_block_type.svelte @@ -0,0 +1 @@ +{#} diff --git a/crates/biome_html_parser/tests/html_specs/error/svelte/template_missing_block_type.svelte.snap b/crates/biome_html_parser/tests/html_specs/error/svelte/template_missing_block_type.svelte.snap new file mode 100644 index 000000000000..09fc526e34ca --- /dev/null +++ b/crates/biome_html_parser/tests/html_specs/error/svelte/template_missing_block_type.svelte.snap @@ -0,0 +1,59 @@ +--- +source: crates/biome_html_parser/tests/spec_test.rs +expression: snapshot +--- + +## Input + +```svelte +{#} + +``` + + +## AST + +``` +HtmlRoot { + bom_token: missing (optional), + frontmatter: missing (optional), + directive: missing (optional), + html: HtmlElementList [ + SvelteBogusBlock { + items: [ + SV_CURLY_HASH@0..2 "{#" [] [], + R_CURLY@2..3 "}" [] [], + ], + }, + ], + eof_token: EOF@3..4 "" [Newline("\n")] [], +} +``` + +## CST + +``` +0: HTML_ROOT@0..4 + 0: (empty) + 1: (empty) + 2: (empty) + 3: HTML_ELEMENT_LIST@0..3 + 0: SVELTE_BOGUS_BLOCK@0..3 + 0: SV_CURLY_HASH@0..2 "{#" [] [] + 1: R_CURLY@2..3 "}" [] [] + 4: EOF@3..4 "" [Newline("\n")] [] + +``` + +## Diagnostics + +``` +template_missing_block_type.svelte:1:3 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Expected `if`, `each`, `key`, `await`, or `snippet` + + > 1 │ {#} + │ ^ + 2 │ + +``` diff --git a/crates/biome_html_parser/tests/html_specs/error/svelte/template_missing_block_type_and_closing_brace.svelte b/crates/biome_html_parser/tests/html_specs/error/svelte/template_missing_block_type_and_closing_brace.svelte new file mode 100644 index 000000000000..dc98d676069c --- /dev/null +++ b/crates/biome_html_parser/tests/html_specs/error/svelte/template_missing_block_type_and_closing_brace.svelte @@ -0,0 +1 @@ +{# diff --git a/crates/biome_html_parser/tests/html_specs/error/svelte/template_missing_block_type_and_closing_brace.svelte.snap b/crates/biome_html_parser/tests/html_specs/error/svelte/template_missing_block_type_and_closing_brace.svelte.snap new file mode 100644 index 000000000000..450823b5ef5e --- /dev/null +++ b/crates/biome_html_parser/tests/html_specs/error/svelte/template_missing_block_type_and_closing_brace.svelte.snap @@ -0,0 +1,57 @@ +--- +source: crates/biome_html_parser/tests/spec_test.rs +expression: snapshot +--- + +## Input + +```svelte +{# + +``` + + +## AST + +``` +HtmlRoot { + bom_token: missing (optional), + frontmatter: missing (optional), + directive: missing (optional), + html: HtmlElementList [ + SvelteBogusBlock { + items: [ + SV_CURLY_HASH@0..2 "{#" [] [], + ], + }, + ], + eof_token: EOF@2..3 "" [Newline("\n")] [], +} +``` + +## CST + +``` +0: HTML_ROOT@0..3 + 0: (empty) + 1: (empty) + 2: (empty) + 3: HTML_ELEMENT_LIST@0..2 + 0: SVELTE_BOGUS_BLOCK@0..2 + 0: SV_CURLY_HASH@0..2 "{#" [] [] + 4: EOF@2..3 "" [Newline("\n")] [] + +``` + +## Diagnostics + +``` +template_missing_block_type_and_closing_brace.svelte:2:1 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Expected `if`, `each`, `key`, `await`, or `snippet` + + 1 │ {# + > 2 │ + │ + +``` From 024611a92c24d45474adf6b2a5feed059a6f67e7 Mon Sep 17 00:00:00 2001 From: mujpao Date: Sun, 19 Apr 2026 02:30:21 -0700 Subject: [PATCH 2/3] Add link to issue in changeset --- .changeset/honest-shirts-wear.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/honest-shirts-wear.md b/.changeset/honest-shirts-wear.md index 487e7b5d4725..92e10931b0e4 100644 --- a/.changeset/honest-shirts-wear.md +++ b/.changeset/honest-shirts-wear.md @@ -2,4 +2,4 @@ "@biomejs/biome": patch --- -Fixed #10003: Biome no longer panics when parsing Svelte files containing `{#}`. +Fixed [#10003](https://github.com/biomejs/biome/issues/10003): Biome no longer panics when parsing Svelte files containing `{#}`. From 3371e257d683323825ebf10a98b61fede94ebbae Mon Sep 17 00:00:00 2001 From: mujpao Date: Sun, 19 Apr 2026 23:56:58 -0700 Subject: [PATCH 3/3] Enter parser recovery mode --- crates/biome_html_parser/src/syntax/svelte.rs | 10 ++++++++++ .../at_block_invalid_block_type.svelte.snap | 18 +++++++++--------- .../template_invalid_block_type.svelte.snap | 18 +++++++++--------- 3 files changed, 28 insertions(+), 18 deletions(-) diff --git a/crates/biome_html_parser/src/syntax/svelte.rs b/crates/biome_html_parser/src/syntax/svelte.rs index 9a5b38621097..1af1c26f8f44 100644 --- a/crates/biome_html_parser/src/syntax/svelte.rs +++ b/crates/biome_html_parser/src/syntax/svelte.rs @@ -36,6 +36,11 @@ pub(crate) fn parse_svelte_hash_block(p: &mut HtmlParser) -> ParsedSyntax { "Expected `if`, `each`, `key`, `await`, or `snippet`", p.cur_range(), )); + + if !p.at(T!['}']) { + let _ = ParseRecoveryTokenSet::new(HTML_BOGUS, token_set![T!['}']]).recover(p); + } + p.expect(T!['}']); Present(m.complete(p, SVELTE_BOGUS_BLOCK)) } @@ -834,6 +839,11 @@ pub(crate) fn parse_svelte_at_block(p: &mut HtmlParser) -> ParsedSyntax { "Expected `render`, `html`, `const`, or `debug`", p.cur_range(), )); + + if !p.at(T!['}']) { + let _ = ParseRecoveryTokenSet::new(HTML_BOGUS, token_set![T!['}']]).recover(p); + } + p.expect(T!['}']); Present(m.complete(p, SVELTE_BOGUS_BLOCK)) } diff --git a/crates/biome_html_parser/tests/html_specs/error/svelte/at_block_invalid_block_type.svelte.snap b/crates/biome_html_parser/tests/html_specs/error/svelte/at_block_invalid_block_type.svelte.snap index 4ca089bbcbe6..1d4b8181a704 100644 --- a/crates/biome_html_parser/tests/html_specs/error/svelte/at_block_invalid_block_type.svelte.snap +++ b/crates/biome_html_parser/tests/html_specs/error/svelte/at_block_invalid_block_type.svelte.snap @@ -22,11 +22,11 @@ HtmlRoot { SvelteBogusBlock { items: [ SV_CURLY_AT@0..2 "{@" [] [], - ], - }, - HtmlBogusElement { - items: [ - IDENT@2..19 "notarealblocktype" [] [], + HtmlBogus { + items: [ + IDENT@2..19 "notarealblocktype" [] [], + ], + }, R_CURLY@19..20 "}" [] [], ], }, @@ -43,11 +43,11 @@ HtmlRoot { 1: (empty) 2: (empty) 3: HTML_ELEMENT_LIST@0..20 - 0: SVELTE_BOGUS_BLOCK@0..2 + 0: SVELTE_BOGUS_BLOCK@0..20 0: SV_CURLY_AT@0..2 "{@" [] [] - 1: HTML_BOGUS_ELEMENT@2..20 - 0: IDENT@2..19 "notarealblocktype" [] [] - 1: R_CURLY@19..20 "}" [] [] + 1: HTML_BOGUS@2..19 + 0: IDENT@2..19 "notarealblocktype" [] [] + 2: R_CURLY@19..20 "}" [] [] 4: EOF@20..21 "" [Newline("\n")] [] ``` diff --git a/crates/biome_html_parser/tests/html_specs/error/svelte/template_invalid_block_type.svelte.snap b/crates/biome_html_parser/tests/html_specs/error/svelte/template_invalid_block_type.svelte.snap index 488ff3ad456c..ed6ce56f3427 100644 --- a/crates/biome_html_parser/tests/html_specs/error/svelte/template_invalid_block_type.svelte.snap +++ b/crates/biome_html_parser/tests/html_specs/error/svelte/template_invalid_block_type.svelte.snap @@ -21,11 +21,11 @@ HtmlRoot { SvelteBogusBlock { items: [ SV_CURLY_HASH@0..2 "{#" [] [], - ], - }, - HtmlBogusElement { - items: [ - IDENT@2..19 "notarealblocktype" [] [], + HtmlBogus { + items: [ + IDENT@2..19 "notarealblocktype" [] [], + ], + }, R_CURLY@19..20 "}" [] [], ], }, @@ -42,11 +42,11 @@ HtmlRoot { 1: (empty) 2: (empty) 3: HTML_ELEMENT_LIST@0..20 - 0: SVELTE_BOGUS_BLOCK@0..2 + 0: SVELTE_BOGUS_BLOCK@0..20 0: SV_CURLY_HASH@0..2 "{#" [] [] - 1: HTML_BOGUS_ELEMENT@2..20 - 0: IDENT@2..19 "notarealblocktype" [] [] - 1: R_CURLY@19..20 "}" [] [] + 1: HTML_BOGUS@2..19 + 0: IDENT@2..19 "notarealblocktype" [] [] + 2: R_CURLY@19..20 "}" [] [] 4: EOF@20..20 "" [] [] ```