From abbfc67a7558d82a9acd138e6a900d3909d63e1f Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Wed, 28 Jan 2026 10:07:32 +0000 Subject: [PATCH 1/2] feat(html): improved parsing spread attributes --- .changeset/ninety-rice-like.md | 5 + .../tests/cases/handle_astro_files.rs | 2 + .../tests/cases/handle_svelte_files.rs | 2 + ...bedded_bindings_are_tracked_correctly.snap | 2 + ...bedded_bindings_are_tracked_correctly.snap | 2 + .../src/generated/node_factory.rs | 22 ++ .../src/generated/syntax_factory.rs | 59 +++++ crates/biome_html_formatter/src/generated.rs | 70 ++++++ .../src/html/any/attribute.rs | 1 + .../src/html/auxiliary/mod.rs | 2 + .../src/html/auxiliary/name.rs | 9 + .../src/html/auxiliary/spread_attribute.rs | 25 ++ .../src/html/lists/attribute_list.rs | 3 + crates/biome_html_parser/src/syntax/astro.rs | 38 ++- crates/biome_html_parser/src/syntax/mod.rs | 39 ++- crates/biome_html_parser/src/syntax/svelte.rs | 74 ++++-- .../tests/html_specs/error/astro/spread.astro | 2 + .../html_specs/error/astro/spread.astro.snap | 115 +++++++++ .../html_specs/error/svelte/spread.svelte | 2 + .../error/svelte/spread.svelte.snap | 155 ++++++++++++ .../tests/html_specs/ok/astro/spread.astro | 2 + .../html_specs/ok/astro/spread.astro.snap | 99 ++++++++ .../svelte/shorthand-spread-props.svelte.snap | 16 +- .../tests/html_specs/ok/svelte/spread.svelte | 2 + .../html_specs/ok/svelte/spread.svelte.snap | 99 ++++++++ crates/biome_html_parser/tests/quick_test.rs | 2 +- .../biome_html_syntax/src/generated/kind.rs | 2 + .../biome_html_syntax/src/generated/macros.rs | 8 + .../biome_html_syntax/src/generated/nodes.rs | 223 ++++++++++++++++++ .../src/generated/nodes_mut.rs | 34 +++ .../services/embedded_value_references.rs | 16 ++ xtask/codegen/html.ungram | 10 +- xtask/codegen/src/html_kinds_src.rs | 2 + 33 files changed, 1105 insertions(+), 39 deletions(-) create mode 100644 .changeset/ninety-rice-like.md create mode 100644 crates/biome_html_formatter/src/html/auxiliary/name.rs create mode 100644 crates/biome_html_formatter/src/html/auxiliary/spread_attribute.rs create mode 100644 crates/biome_html_parser/tests/html_specs/error/astro/spread.astro create mode 100644 crates/biome_html_parser/tests/html_specs/error/astro/spread.astro.snap create mode 100644 crates/biome_html_parser/tests/html_specs/error/svelte/spread.svelte create mode 100644 crates/biome_html_parser/tests/html_specs/error/svelte/spread.svelte.snap create mode 100644 crates/biome_html_parser/tests/html_specs/ok/astro/spread.astro create mode 100644 crates/biome_html_parser/tests/html_specs/ok/astro/spread.astro.snap create mode 100644 crates/biome_html_parser/tests/html_specs/ok/svelte/spread.svelte create mode 100644 crates/biome_html_parser/tests/html_specs/ok/svelte/spread.svelte.snap diff --git a/.changeset/ninety-rice-like.md b/.changeset/ninety-rice-like.md new file mode 100644 index 000000000000..07319338b8ab --- /dev/null +++ b/.changeset/ninety-rice-like.md @@ -0,0 +1,5 @@ +--- +"@biomejs/biome": patch +--- + +Added proper parsing for spread attributes `{...props}` in Svelte and Astro files. diff --git a/crates/biome_cli/tests/cases/handle_astro_files.rs b/crates/biome_cli/tests/cases/handle_astro_files.rs index c9c5f59da998..3c55bf6a2e41 100644 --- a/crates/biome_cli/tests/cases/handle_astro_files.rs +++ b/crates/biome_cli/tests/cases/handle_astro_files.rs @@ -770,6 +770,7 @@ fn embedded_bindings_are_tracked_correctly() { import { Component } from "./component.svelte"; let hello = "Hello World"; let array = []; +let props = []; --- @@ -777,6 +778,7 @@ let array = []; {notDefined} { array.map(item => ({item})) } + "# .as_bytes(), diff --git a/crates/biome_cli/tests/cases/handle_svelte_files.rs b/crates/biome_cli/tests/cases/handle_svelte_files.rs index fd31ae5e23c5..b803107c8d6f 100644 --- a/crates/biome_cli/tests/cases/handle_svelte_files.rs +++ b/crates/biome_cli/tests/cases/handle_svelte_files.rs @@ -561,6 +561,7 @@ fn embedded_bindings_are_tracked_correctly() { import { Component } from "./component.svelte"; let hello = "Hello World"; let array = []; +let props = []; @@ -569,6 +570,7 @@ let array = []; {#each array as item} {/each} + "# .as_bytes(), diff --git a/crates/biome_cli/tests/snapshots/main_cases_handle_astro_files/embedded_bindings_are_tracked_correctly.snap b/crates/biome_cli/tests/snapshots/main_cases_handle_astro_files/embedded_bindings_are_tracked_correctly.snap index 651c7678cf0f..d6fddd3c4a5c 100644 --- a/crates/biome_cli/tests/snapshots/main_cases_handle_astro_files/embedded_bindings_are_tracked_correctly.snap +++ b/crates/biome_cli/tests/snapshots/main_cases_handle_astro_files/embedded_bindings_are_tracked_correctly.snap @@ -20,6 +20,7 @@ expression: redactor(content) import { Component } from "./component.svelte"; let hello = "Hello World"; let array = []; +let props = []; --- @@ -27,6 +28,7 @@ let array = []; {notDefined} { array.map(item => ({item})) } + ``` diff --git a/crates/biome_cli/tests/snapshots/main_cases_handle_svelte_files/embedded_bindings_are_tracked_correctly.snap b/crates/biome_cli/tests/snapshots/main_cases_handle_svelte_files/embedded_bindings_are_tracked_correctly.snap index a4b4c3701102..9550d0017295 100644 --- a/crates/biome_cli/tests/snapshots/main_cases_handle_svelte_files/embedded_bindings_are_tracked_correctly.snap +++ b/crates/biome_cli/tests/snapshots/main_cases_handle_svelte_files/embedded_bindings_are_tracked_correctly.snap @@ -20,6 +20,7 @@ expression: redactor(content) import { Component } from "./component.svelte"; let hello = "Hello World"; let array = []; +let props = []; @@ -28,6 +29,7 @@ let array = []; {#each array as item} {/each} + ``` diff --git a/crates/biome_html_factory/src/generated/node_factory.rs b/crates/biome_html_factory/src/generated/node_factory.rs index 2ff3ca5c4bc4..3884c7e12e66 100644 --- a/crates/biome_html_factory/src/generated/node_factory.rs +++ b/crates/biome_html_factory/src/generated/node_factory.rs @@ -236,6 +236,12 @@ pub fn html_member_name( ], )) } +pub fn html_name(ident_token: SyntaxToken) -> HtmlName { + HtmlName::unwrap_cast(SyntaxNode::new_detached( + HtmlSyntaxKind::HTML_NAME, + [Some(SyntaxElement::Token(ident_token))], + )) +} pub fn html_opening_element( l_angle_token: SyntaxToken, name: AnyHtmlTagName, @@ -349,6 +355,22 @@ pub fn html_single_text_expression( ], )) } +pub fn html_spread_attribute( + l_curly_token: SyntaxToken, + dotdotdot_token: SyntaxToken, + argument: HtmlName, + r_curly_token: SyntaxToken, +) -> HtmlSpreadAttribute { + HtmlSpreadAttribute::unwrap_cast(SyntaxNode::new_detached( + HtmlSyntaxKind::HTML_SPREAD_ATTRIBUTE, + [ + Some(SyntaxElement::Token(l_curly_token)), + Some(SyntaxElement::Token(dotdotdot_token)), + Some(SyntaxElement::Node(argument.into_syntax())), + Some(SyntaxElement::Token(r_curly_token)), + ], + )) +} pub fn html_string(value_token: SyntaxToken) -> HtmlString { HtmlString::unwrap_cast(SyntaxNode::new_detached( HtmlSyntaxKind::HTML_STRING, diff --git a/crates/biome_html_factory/src/generated/syntax_factory.rs b/crates/biome_html_factory/src/generated/syntax_factory.rs index cae37e3be3ec..fef06aa7e1c9 100644 --- a/crates/biome_html_factory/src/generated/syntax_factory.rs +++ b/crates/biome_html_factory/src/generated/syntax_factory.rs @@ -444,6 +444,25 @@ impl SyntaxFactory for HtmlSyntaxFactory { } slots.into_node(HTML_MEMBER_NAME, children) } + HTML_NAME => { + let mut elements = (&children).into_iter(); + let mut slots: RawNodeSlots<1usize> = RawNodeSlots::default(); + let mut current_element = elements.next(); + if let Some(element) = ¤t_element + && element.kind() == IDENT + { + slots.mark_present(); + current_element = elements.next(); + } + slots.next_slot(); + if current_element.is_some() { + return RawSyntaxNode::new( + HTML_NAME.to_bogus(), + children.into_iter().map(Some), + ); + } + slots.into_node(HTML_NAME, children) + } HTML_OPENING_ELEMENT => { let mut elements = (&children).into_iter(); let mut slots: RawNodeSlots<4usize> = RawNodeSlots::default(); @@ -611,6 +630,46 @@ impl SyntaxFactory for HtmlSyntaxFactory { } slots.into_node(HTML_SINGLE_TEXT_EXPRESSION, children) } + HTML_SPREAD_ATTRIBUTE => { + let mut elements = (&children).into_iter(); + let mut slots: RawNodeSlots<4usize> = RawNodeSlots::default(); + let mut current_element = elements.next(); + if let Some(element) = ¤t_element + && element.kind() == T!['{'] + { + slots.mark_present(); + current_element = elements.next(); + } + slots.next_slot(); + if let Some(element) = ¤t_element + && element.kind() == T ! [...] + { + slots.mark_present(); + current_element = elements.next(); + } + slots.next_slot(); + if let Some(element) = ¤t_element + && HtmlName::can_cast(element.kind()) + { + slots.mark_present(); + current_element = elements.next(); + } + slots.next_slot(); + if let Some(element) = ¤t_element + && element.kind() == T!['}'] + { + slots.mark_present(); + current_element = elements.next(); + } + slots.next_slot(); + if current_element.is_some() { + return RawSyntaxNode::new( + HTML_SPREAD_ATTRIBUTE.to_bogus(), + children.into_iter().map(Some), + ); + } + slots.into_node(HTML_SPREAD_ATTRIBUTE, children) + } HTML_STRING => { let mut elements = (&children).into_iter(); let mut slots: RawNodeSlots<1usize> = RawNodeSlots::default(); diff --git a/crates/biome_html_formatter/src/generated.rs b/crates/biome_html_formatter/src/generated.rs index 3010205ee9db..17b386eca661 100644 --- a/crates/biome_html_formatter/src/generated.rs +++ b/crates/biome_html_formatter/src/generated.rs @@ -534,6 +534,38 @@ impl IntoFormat for biome_html_syntax::HtmlMemberName { ) } } +impl FormatRule for crate::html::auxiliary::name::FormatHtmlName { + type Context = HtmlFormatContext; + #[inline(always)] + fn fmt(&self, node: &biome_html_syntax::HtmlName, f: &mut HtmlFormatter) -> FormatResult<()> { + FormatNodeRule::::fmt(self, node, f) + } +} +impl AsFormat for biome_html_syntax::HtmlName { + type Format<'a> = FormatRefWithRule< + 'a, + biome_html_syntax::HtmlName, + crate::html::auxiliary::name::FormatHtmlName, + >; + fn format(&self) -> Self::Format<'_> { + FormatRefWithRule::new( + self, + crate::html::auxiliary::name::FormatHtmlName::default(), + ) + } +} +impl IntoFormat for biome_html_syntax::HtmlName { + type Format = FormatOwnedWithRule< + biome_html_syntax::HtmlName, + crate::html::auxiliary::name::FormatHtmlName, + >; + fn into_format(self) -> Self::Format { + FormatOwnedWithRule::new( + self, + crate::html::auxiliary::name::FormatHtmlName::default(), + ) + } +} impl FormatRule for crate::html::auxiliary::opening_element::FormatHtmlOpeningElement { @@ -682,6 +714,44 @@ impl IntoFormat for biome_html_syntax::HtmlSingleTextExpressi ) } } +impl FormatRule + for crate::html::auxiliary::spread_attribute::FormatHtmlSpreadAttribute +{ + type Context = HtmlFormatContext; + #[inline(always)] + fn fmt( + &self, + node: &biome_html_syntax::HtmlSpreadAttribute, + f: &mut HtmlFormatter, + ) -> FormatResult<()> { + FormatNodeRule::::fmt(self, node, f) + } +} +impl AsFormat for biome_html_syntax::HtmlSpreadAttribute { + type Format<'a> = FormatRefWithRule< + 'a, + biome_html_syntax::HtmlSpreadAttribute, + crate::html::auxiliary::spread_attribute::FormatHtmlSpreadAttribute, + >; + fn format(&self) -> Self::Format<'_> { + FormatRefWithRule::new( + self, + crate::html::auxiliary::spread_attribute::FormatHtmlSpreadAttribute::default(), + ) + } +} +impl IntoFormat for biome_html_syntax::HtmlSpreadAttribute { + type Format = FormatOwnedWithRule< + biome_html_syntax::HtmlSpreadAttribute, + crate::html::auxiliary::spread_attribute::FormatHtmlSpreadAttribute, + >; + fn into_format(self) -> Self::Format { + FormatOwnedWithRule::new( + self, + crate::html::auxiliary::spread_attribute::FormatHtmlSpreadAttribute::default(), + ) + } +} impl FormatRule for crate::html::auxiliary::string::FormatHtmlString { diff --git a/crates/biome_html_formatter/src/html/any/attribute.rs b/crates/biome_html_formatter/src/html/any/attribute.rs index b7f2a967d2e3..2eef1d2f93d2 100644 --- a/crates/biome_html_formatter/src/html/any/attribute.rs +++ b/crates/biome_html_formatter/src/html/any/attribute.rs @@ -14,6 +14,7 @@ impl FormatRule for FormatAnyHtmlAttribute { AnyHtmlAttribute::HtmlBogusAttribute(node) => node.format().fmt(f), AnyHtmlAttribute::HtmlDoubleTextExpression(node) => node.format().fmt(f), AnyHtmlAttribute::HtmlSingleTextExpression(node) => node.format().fmt(f), + AnyHtmlAttribute::HtmlSpreadAttribute(node) => node.format().fmt(f), AnyHtmlAttribute::SvelteAttachAttribute(node) => node.format().fmt(f), } } diff --git a/crates/biome_html_formatter/src/html/auxiliary/mod.rs b/crates/biome_html_formatter/src/html/auxiliary/mod.rs index efa516139811..6d4ca51826c5 100644 --- a/crates/biome_html_formatter/src/html/auxiliary/mod.rs +++ b/crates/biome_html_formatter/src/html/auxiliary/mod.rs @@ -12,10 +12,12 @@ pub(crate) mod double_text_expression; pub(crate) mod element; pub(crate) mod embedded_content; pub(crate) mod member_name; +pub(crate) mod name; pub(crate) mod opening_element; pub(crate) mod root; pub(crate) mod self_closing_element; pub(crate) mod single_text_expression; +pub(crate) mod spread_attribute; pub(crate) mod string; pub(crate) mod tag_name; pub(crate) mod text_expression; diff --git a/crates/biome_html_formatter/src/html/auxiliary/name.rs b/crates/biome_html_formatter/src/html/auxiliary/name.rs new file mode 100644 index 000000000000..e6a2a9b2f747 --- /dev/null +++ b/crates/biome_html_formatter/src/html/auxiliary/name.rs @@ -0,0 +1,9 @@ +use crate::prelude::*; +use biome_html_syntax::HtmlName; +#[derive(Debug, Clone, Default)] +pub(crate) struct FormatHtmlName; +impl FormatNodeRule for FormatHtmlName { + fn fmt_fields(&self, node: &HtmlName, f: &mut HtmlFormatter) -> FormatResult<()> { + node.as_fields().ident_token.format().fmt(f) + } +} diff --git a/crates/biome_html_formatter/src/html/auxiliary/spread_attribute.rs b/crates/biome_html_formatter/src/html/auxiliary/spread_attribute.rs new file mode 100644 index 000000000000..bf80eb93d6bd --- /dev/null +++ b/crates/biome_html_formatter/src/html/auxiliary/spread_attribute.rs @@ -0,0 +1,25 @@ +use crate::prelude::*; +use biome_formatter::write; +use biome_html_syntax::{HtmlSpreadAttribute, HtmlSpreadAttributeFields}; +#[derive(Debug, Clone, Default)] +pub(crate) struct FormatHtmlSpreadAttribute; +impl FormatNodeRule for FormatHtmlSpreadAttribute { + fn fmt_fields(&self, node: &HtmlSpreadAttribute, f: &mut HtmlFormatter) -> FormatResult<()> { + let HtmlSpreadAttributeFields { + l_curly_token, + dotdotdot_token, + argument, + r_curly_token, + } = node.as_fields(); + + write!( + f, + [ + l_curly_token.format(), + dotdotdot_token.format(), + argument.format(), + r_curly_token.format() + ] + ) + } +} diff --git a/crates/biome_html_formatter/src/html/lists/attribute_list.rs b/crates/biome_html_formatter/src/html/lists/attribute_list.rs index 0507e9f3888d..3d4e56b26a4f 100644 --- a/crates/biome_html_formatter/src/html/lists/attribute_list.rs +++ b/crates/biome_html_formatter/src/html/lists/attribute_list.rs @@ -77,6 +77,9 @@ impl FormatRule for FormatHtmlAttributeList { AnyHtmlAttribute::AnySvelteDirective(attr) => { attr.format().fmt(f) } + AnyHtmlAttribute::HtmlSpreadAttribute(attr) => { + attr.format().fmt(f) + } }) })) .finish()?; diff --git a/crates/biome_html_parser/src/syntax/astro.rs b/crates/biome_html_parser/src/syntax/astro.rs index 458a111fbdee..60d6c32f3340 100644 --- a/crates/biome_html_parser/src/syntax/astro.rs +++ b/crates/biome_html_parser/src/syntax/astro.rs @@ -1,13 +1,16 @@ use crate::parser::HtmlParser; +use crate::syntax::HtmlSyntaxFeatures::Astro; use crate::syntax::parse_error::expected_closed_fence; +use crate::syntax::{parse_name, parse_single_text_expression}; use crate::token_source::HtmlLexContext; use biome_html_syntax::HtmlSyntaxKind::{ - ASTRO_EMBEDDED_CONTENT, ASTRO_FRONTMATTER_ELEMENT, FENCE, HTML_LITERAL, + ASTRO_EMBEDDED_CONTENT, ASTRO_FRONTMATTER_ELEMENT, FENCE, HTML_LITERAL, HTML_SPREAD_ATTRIBUTE, }; use biome_html_syntax::T; -use biome_parser::Parser; +use biome_parser::parsed_syntax::ParsedSyntax::Present; use biome_parser::prelude::ParsedSyntax; use biome_parser::prelude::ParsedSyntax::Absent; +use biome_parser::{Parser, SyntaxFeature}; pub(crate) fn parse_astro_fence(p: &mut HtmlParser) -> ParsedSyntax { if !p.at(T![---]) { @@ -39,3 +42,34 @@ pub(crate) fn parse_astro_embedded(p: &mut HtmlParser) -> ParsedSyntax { ParsedSyntax::Present(m.complete(p, ASTRO_EMBEDDED_CONTENT)) } + +/// Parses a spread attribute or a single text expression. +pub(crate) fn parse_astro_spread_or_expression(p: &mut HtmlParser) -> ParsedSyntax { + if !Astro.is_supported(p) { + return Absent; + } + + if !p.at(T!['{']) { + return Absent; + } + + let checkpoint = p.checkpoint(); + let m = p.start(); + + // We bump using svelte context because it's faster to lex a possible ..., which is also + // only consumable when using the Svelte context + p.bump_with_context(T!['{'], HtmlLexContext::Svelte); + + if p.at(T![...]) { + p.bump_with_context(T![...], HtmlLexContext::Svelte); + parse_name(p, HtmlLexContext::Svelte, |_| true).or_add_diagnostic(p, |p, range| { + p.err_builder("Expected a name after '...'", range) + }); + p.expect_with_context(T!['}'], HtmlLexContext::InsideTag); + Present(m.complete(p, HTML_SPREAD_ATTRIBUTE)) + } else { + p.rewind(checkpoint); + m.abandon(p); + parse_single_text_expression(p, HtmlLexContext::InsideTag) + } +} diff --git a/crates/biome_html_parser/src/syntax/mod.rs b/crates/biome_html_parser/src/syntax/mod.rs index 03b64e9b4b09..97172824dc44 100644 --- a/crates/biome_html_parser/src/syntax/mod.rs +++ b/crates/biome_html_parser/src/syntax/mod.rs @@ -5,11 +5,12 @@ mod vue; use crate::parser::HtmlParser; use crate::syntax::HtmlSyntaxFeatures::{Astro, DoubleTextExpressions, SingleTextExpressions, Vue}; -use crate::syntax::astro::parse_astro_fence; +use crate::syntax::astro::{parse_astro_fence, parse_astro_spread_or_expression}; use crate::syntax::parse_error::*; use crate::syntax::svelte::{ is_at_svelte_directive_start, is_at_svelte_keyword, parse_attach_attribute, parse_svelte_at_block, parse_svelte_directive, parse_svelte_hash_block, + parse_svelte_spread_or_expression, }; use crate::syntax::vue::{ parse_vue_directive, parse_vue_v_bind_shorthand_directive, parse_vue_v_on_shorthand_directive, @@ -461,9 +462,12 @@ fn parse_attribute(p: &mut HtmlParser) -> ParsedSyntax { parse_vue_v_slot_shorthand_directive, |p, m| disabled_vue(p, m.range(p)), ), + T!['{'] if SingleTextExpressions.is_supported(p) => parse_svelte_spread_or_expression(p), + T!['{'] if Astro.is_supported(p) => parse_astro_spread_or_expression(p), + // Keep previous behaviour so that invalid documents are still parsed. T!['{'] => SingleTextExpressions.parse_exclusive_syntax( p, - |p| parse_single_text_expression(p, HtmlLexContext::InsideTag), + |p| parse_svelte_spread_or_expression(p), |p: &HtmlParser<'_>, m: &CompletedMarker| disabled_svelte(p, m.range(p)), ), T!["{@"] => SingleTextExpressions.parse_exclusive_syntax( @@ -472,8 +476,7 @@ fn parse_attribute(p: &mut HtmlParser) -> ParsedSyntax { |p: &HtmlParser<'_>, m: &CompletedMarker| disabled_svelte(p, m.range(p)), ), _ if p.cur_text().starts_with("v-") => { - HtmlSyntaxFeatures::Vue - .parse_exclusive_syntax(p, parse_vue_directive, |p, m| disabled_vue(p, m.range(p))) + Vue.parse_exclusive_syntax(p, parse_vue_directive, |p, m| disabled_vue(p, m.range(p))) } _ if is_at_svelte_directive_start(p) => { SingleTextExpressions.parse_exclusive_syntax(p, parse_svelte_directive, |p, m| { @@ -486,10 +489,8 @@ fn parse_attribute(p: &mut HtmlParser) -> ParsedSyntax { if p.at(T![=]) { parse_attribute_initializer(p).ok(); - Present(m.complete(p, HTML_ATTRIBUTE)) - } else { - Present(m.complete(p, HTML_ATTRIBUTE)) } + Present(m.complete(p, HTML_ATTRIBUTE)) } } } @@ -669,7 +670,7 @@ pub(crate) fn parse_single_text_expression( p: &mut HtmlParser, context: HtmlLexContext, ) -> ParsedSyntax { - if !HtmlSyntaxFeatures::SingleTextExpressions.is_supported(p) { + if !SingleTextExpressions.is_supported(p) { return Absent; } @@ -735,6 +736,28 @@ fn parse_single_text_expression_content(p: &mut HtmlParser) -> ParsedSyntax { Present(m.complete(p, HTML_TEXT_EXPRESSION)) } +/// Parses a generic HTML name. +/// +/// # Arguments +/// - `next_context`: context to apply after bumping the name +/// - `should_bail`: condition to bail early +pub(crate) fn parse_name( + p: &mut HtmlParser, + next_context: HtmlLexContext, + should_bail_if: F, +) -> ParsedSyntax +where + F: FnOnce(&HtmlParser) -> bool, +{ + if !p.at(IDENT) && !should_bail_if(p) { + return Absent; + } + let m = p.start(); + p.bump_remap_with_context(IDENT, next_context); + + Present(m.complete(p, HTML_NAME)) +} + impl TextExpression { fn parse_element(&mut self, p: &mut HtmlParser) -> ParsedSyntax { if p.at(EOF) || p.at(T![<]) { diff --git a/crates/biome_html_parser/src/syntax/svelte.rs b/crates/biome_html_parser/src/syntax/svelte.rs index b25f0822f107..639fc59441de 100644 --- a/crates/biome_html_parser/src/syntax/svelte.rs +++ b/crates/biome_html_parser/src/syntax/svelte.rs @@ -1,23 +1,25 @@ use crate::parser::HtmlParser; +use crate::syntax::HtmlSyntaxFeatures::SingleTextExpressions; use crate::syntax::parse_error::{ expected_child_or_block, expected_expression, expected_name, expected_svelte_closing_block, expected_svelte_property, expected_text_expression, expected_valid_directive, }; use crate::syntax::{ - parse_attribute_initializer, parse_html_element, parse_single_text_expression_content, + parse_attribute_initializer, parse_html_element, parse_name, parse_single_text_expression, + parse_single_text_expression_content, }; use crate::token_source::{HtmlLexContext, HtmlReLexContext, RestrictedExpressionStopAt}; use biome_html_syntax::HtmlSyntaxKind::{ - EOF, HTML_BOGUS_ELEMENT, HTML_ELEMENT_LIST, HTML_LITERAL, IDENT, SVELTE_ANIMATE_DIRECTIVE, - SVELTE_ATTACH_ATTRIBUTE, SVELTE_AWAIT_BLOCK, SVELTE_AWAIT_CATCH_BLOCK, - SVELTE_AWAIT_CATCH_CLAUSE, SVELTE_AWAIT_CLAUSES_LIST, SVELTE_AWAIT_CLOSING_BLOCK, - SVELTE_AWAIT_OPENING_BLOCK, SVELTE_AWAIT_THEN_BLOCK, SVELTE_AWAIT_THEN_CLAUSE, - SVELTE_BIND_DIRECTIVE, SVELTE_BINDING_ASSIGNMENT_BINDING_LIST, SVELTE_BINDING_LIST, - SVELTE_BOGUS_BLOCK, SVELTE_CLASS_DIRECTIVE, SVELTE_CONST_BLOCK, SVELTE_CURLY_DESTRUCTURED_NAME, - SVELTE_DEBUG_BLOCK, SVELTE_DIRECTIVE_MODIFIER, SVELTE_DIRECTIVE_MODIFIER_LIST, - SVELTE_DIRECTIVE_VALUE, SVELTE_EACH_AS_KEYED_ITEM, SVELTE_EACH_BLOCK, - SVELTE_EACH_CLOSING_BLOCK, SVELTE_EACH_INDEX, SVELTE_EACH_KEY, SVELTE_EACH_KEYED_ITEM, - SVELTE_EACH_OPENING_BLOCK, SVELTE_ELSE_CLAUSE, SVELTE_ELSE_IF_CLAUSE, + EOF, HTML_BOGUS_ELEMENT, HTML_ELEMENT_LIST, HTML_LITERAL, HTML_SPREAD_ATTRIBUTE, IDENT, + SVELTE_ANIMATE_DIRECTIVE, SVELTE_ATTACH_ATTRIBUTE, SVELTE_AWAIT_BLOCK, + SVELTE_AWAIT_CATCH_BLOCK, SVELTE_AWAIT_CATCH_CLAUSE, SVELTE_AWAIT_CLAUSES_LIST, + SVELTE_AWAIT_CLOSING_BLOCK, SVELTE_AWAIT_OPENING_BLOCK, SVELTE_AWAIT_THEN_BLOCK, + SVELTE_AWAIT_THEN_CLAUSE, SVELTE_BIND_DIRECTIVE, SVELTE_BINDING_ASSIGNMENT_BINDING_LIST, + SVELTE_BINDING_LIST, SVELTE_BOGUS_BLOCK, SVELTE_CLASS_DIRECTIVE, SVELTE_CONST_BLOCK, + SVELTE_CURLY_DESTRUCTURED_NAME, SVELTE_DEBUG_BLOCK, SVELTE_DIRECTIVE_MODIFIER, + SVELTE_DIRECTIVE_MODIFIER_LIST, SVELTE_DIRECTIVE_VALUE, SVELTE_EACH_AS_KEYED_ITEM, + SVELTE_EACH_BLOCK, SVELTE_EACH_CLOSING_BLOCK, SVELTE_EACH_INDEX, SVELTE_EACH_KEY, + SVELTE_EACH_KEYED_ITEM, SVELTE_EACH_OPENING_BLOCK, SVELTE_ELSE_CLAUSE, SVELTE_ELSE_IF_CLAUSE, SVELTE_ELSE_IF_CLAUSE_LIST, SVELTE_HTML_BLOCK, SVELTE_IF_BLOCK, SVELTE_IF_CLOSING_BLOCK, SVELTE_IF_OPENING_BLOCK, SVELTE_IN_DIRECTIVE, SVELTE_KEY_BLOCK, SVELTE_KEY_CLOSING_BLOCK, SVELTE_KEY_OPENING_BLOCK, SVELTE_LITERAL, SVELTE_NAME, SVELTE_OUT_DIRECTIVE, @@ -30,7 +32,7 @@ use biome_parser::parse_lists::{ParseNodeList, ParseSeparatedList}; use biome_parser::parse_recovery::{ParseRecoveryTokenSet, RecoveryResult}; use biome_parser::prelude::ParsedSyntax; use biome_parser::prelude::ParsedSyntax::{Absent, Present}; -use biome_parser::{Marker, Parser, TokenSet, token_set}; +use biome_parser::{Marker, Parser, SyntaxFeature, TokenSet, token_set}; use biome_rowan::TextRange; use std::ops::Sub; @@ -212,7 +214,7 @@ fn parse_each_as_keyed_item(p: &mut HtmlParser) -> ParsedSyntax { parse_square_destructured_name(p) } else { // Parse name (required) - parse_name(p) + parse_svelte_name(p) } .or_add_diagnostic(p, |p, range| { p.err_builder("Expected a binding pattern after 'as'", range) @@ -283,7 +285,7 @@ fn parse_each_index(p: &mut HtmlParser) -> ParsedSyntax { // Parse the index let m = p.start(); p.bump_with_context(T![,], HtmlLexContext::Svelte); - parse_name(p).or_add_diagnostic(p, |p, range| { + parse_svelte_name(p).or_add_diagnostic(p, |p, range| { p.err_builder("Expected an index binding after ','", range) }); Present(m.complete(p, SVELTE_EACH_INDEX)) @@ -354,6 +356,38 @@ fn parse_each_opening_block(p: &mut HtmlParser, parent_marker: Marker) -> (Parse } // #endregion +/// Parses a spread attribute or a single text expression. +pub(crate) fn parse_svelte_spread_or_expression(p: &mut HtmlParser) -> ParsedSyntax { + if !SingleTextExpressions.is_supported(p) { + return Absent; + } + + if !p.at(T!['{']) { + return Absent; + } + + let checkpoint = p.checkpoint(); + let m = p.start(); + + // We bump using svelte context because it's faster to lex a possible ..., which is also + // only consumable when using the Svelte context + p.bump_with_context(T!['{'], HtmlLexContext::Svelte); + + if p.at(T![...]) { + p.bump_with_context(T![...], HtmlLexContext::Svelte); + parse_name(p, HtmlLexContext::Svelte, is_at_svelte_keyword) + .or_add_diagnostic(p, |p, range| { + p.err_builder("Expected a name after '...'", range) + }); + p.expect_with_context(T!['}'], HtmlLexContext::InsideTag); + Present(m.complete(p, HTML_SPREAD_ATTRIBUTE)) + } else { + p.rewind(checkpoint); + m.abandon(p); + parse_single_text_expression(p, HtmlLexContext::InsideTag) + } +} + // #region await parse functions fn parse_await_block(p: &mut HtmlParser, parent_marker: Marker) -> ParsedSyntax { @@ -899,7 +933,7 @@ impl ParseSeparatedList for BindingList { const LIST_KIND: Self::Kind = SVELTE_BINDING_LIST; fn parse_element(&mut self, p: &mut Self::Parser<'_>) -> ParsedSyntax { - parse_name(p) + parse_svelte_name(p) } fn is_at_list_end(&self, p: &mut Self::Parser<'_>) -> bool { @@ -928,7 +962,7 @@ impl ParseSeparatedList for BindingList { } /// Parses a Svelte name -fn parse_name(p: &mut HtmlParser) -> ParsedSyntax { +fn parse_svelte_name(p: &mut HtmlParser) -> ParsedSyntax { if !p.at(IDENT) && !is_at_svelte_keyword(p) { return Absent; } @@ -951,7 +985,7 @@ fn parse_rest_name(p: &mut HtmlParser) -> ParsedSyntax { } let m = p.start(); p.bump_with_context(T![...], HtmlLexContext::Svelte); - parse_name(p).or_add_diagnostic(p, |p, range| { + parse_svelte_name(p).or_add_diagnostic(p, |p, range| { p.err_builder("Expected a valid Svelte name after '...'", range) }); @@ -1055,7 +1089,7 @@ impl ParseSeparatedList for SvelteBindingAssignmentBindingList { if p.at(T![...]) { parse_rest_name(p) } else { - parse_name(p) + parse_svelte_name(p) } } @@ -1140,7 +1174,7 @@ fn parse_directive_value(p: &mut HtmlParser, context_after_colon: HtmlLexContext } else if context_after_colon == HtmlLexContext::SvelteBindingLiteral { parse_binding_literal(p).or_add_diagnostic(p, expected_svelte_property); } else { - parse_name(p).or_add_diagnostic(p, expected_name); + parse_svelte_name(p).or_add_diagnostic(p, expected_name); } ModifiersList.parse_list(p); @@ -1183,7 +1217,7 @@ impl ParseNodeList for ModifiersList { fn parse_element(&mut self, p: &mut Self::Parser<'_>) -> ParsedSyntax { let m = p.start(); p.expect_with_context(T![|], HtmlLexContext::Svelte); - parse_name(p).or_add_diagnostic(p, |p, range| { + parse_svelte_name(p).or_add_diagnostic(p, |p, range| { p.err_builder("Expected a valid Svelte modifier name", range) }); Present(m.complete(p, SVELTE_DIRECTIVE_MODIFIER)) diff --git a/crates/biome_html_parser/tests/html_specs/error/astro/spread.astro b/crates/biome_html_parser/tests/html_specs/error/astro/spread.astro new file mode 100644 index 000000000000..5e0ae6752ffb --- /dev/null +++ b/crates/biome_html_parser/tests/html_specs/error/astro/spread.astro @@ -0,0 +1,2 @@ + + diff --git a/crates/biome_html_parser/tests/html_specs/error/astro/spread.astro.snap b/crates/biome_html_parser/tests/html_specs/error/astro/spread.astro.snap new file mode 100644 index 000000000000..b7efe37552ea --- /dev/null +++ b/crates/biome_html_parser/tests/html_specs/error/astro/spread.astro.snap @@ -0,0 +1,115 @@ +--- +source: crates/biome_html_parser/tests/spec_test.rs +expression: snapshot +--- +## Input + +```astro + + + +``` + + +## AST + +``` +HtmlRoot { + bom_token: missing (optional), + frontmatter: missing (optional), + directive: missing (optional), + html: HtmlElementList [ + HtmlSelfClosingElement { + l_angle_token: L_ANGLE@0..1 "<" [] [], + name: HtmlTagName { + value_token: HTML_LITERAL@1..7 "input" [] [Whitespace(" ")], + }, + attributes: HtmlAttributeList [ + HtmlSpreadAttribute { + l_curly_token: L_CURLY@7..8 "{" [] [], + dotdotdot_token: DOT3@8..11 "..." [] [], + argument: HtmlName { + ident_token: IDENT@11..17 "props" [] [Whitespace(" ")], + }, + r_curly_token: missing (required), + }, + HtmlAttribute { + name: HtmlAttributeName { + value_token: HTML_LITERAL@17..43 ">\n\n" [] [], + }, + initializer: missing (optional), + }, + ], + slash_token: missing (optional), + r_angle_token: missing (required), + }, + ], + eof_token: EOF@43..43 "" [] [], +} +``` + +## CST + +``` +0: HTML_ROOT@0..43 + 0: (empty) + 1: (empty) + 2: (empty) + 3: HTML_ELEMENT_LIST@0..43 + 0: HTML_SELF_CLOSING_ELEMENT@0..43 + 0: L_ANGLE@0..1 "<" [] [] + 1: HTML_TAG_NAME@1..7 + 0: HTML_LITERAL@1..7 "input" [] [Whitespace(" ")] + 2: HTML_ATTRIBUTE_LIST@7..43 + 0: HTML_SPREAD_ATTRIBUTE@7..17 + 0: L_CURLY@7..8 "{" [] [] + 1: DOT3@8..11 "..." [] [] + 2: HTML_NAME@11..17 + 0: IDENT@11..17 "props" [] [Whitespace(" ")] + 3: (empty) + 1: HTML_ATTRIBUTE@17..43 + 0: HTML_ATTRIBUTE_NAME@17..43 + 0: HTML_LITERAL@17..43 ">\n\n" [] [] + 1: (empty) + 3: (empty) + 4: (empty) + 4: EOF@43..43 "" [] [] + +``` + +## Diagnostics + +``` +spread.astro:1:18 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × expected `}` but instead found `> + + ` + + > 1 │ + │ ^ + > 2 │ + > 3 │ + │ + + i Remove > + + + +spread.astro:3:1 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × expected `>` but instead the file ends + + 1 │ + 2 │ + > 3 │ + │ + + i the file ends here + + 1 │ + 2 │ + > 3 │ + │ + +``` diff --git a/crates/biome_html_parser/tests/html_specs/error/svelte/spread.svelte b/crates/biome_html_parser/tests/html_specs/error/svelte/spread.svelte new file mode 100644 index 000000000000..b1a2cba138b4 --- /dev/null +++ b/crates/biome_html_parser/tests/html_specs/error/svelte/spread.svelte @@ -0,0 +1,2 @@ + + diff --git a/crates/biome_html_parser/tests/html_specs/error/svelte/spread.svelte.snap b/crates/biome_html_parser/tests/html_specs/error/svelte/spread.svelte.snap new file mode 100644 index 000000000000..4e3f24707153 --- /dev/null +++ b/crates/biome_html_parser/tests/html_specs/error/svelte/spread.svelte.snap @@ -0,0 +1,155 @@ +--- +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 [ + HtmlSelfClosingElement { + l_angle_token: L_ANGLE@0..1 "<" [] [], + name: HtmlTagName { + value_token: HTML_LITERAL@1..7 "input" [] [Whitespace(" ")], + }, + attributes: HtmlAttributeList [ + HtmlSpreadAttribute { + l_curly_token: L_CURLY@7..8 "{" [] [], + dotdotdot_token: DOT3@8..11 "..." [] [], + argument: missing (required), + r_curly_token: R_CURLY@11..13 "}" [] [Whitespace(" ")], + }, + ], + slash_token: SLASH@13..14 "/" [] [], + r_angle_token: R_ANGLE@14..15 ">" [] [], + }, + HtmlElement { + opening_element: HtmlOpeningElement { + l_angle_token: L_ANGLE@15..17 "<" [Newline("\n")] [], + name: HtmlComponentName { + value_token: HTML_LITERAL@17..27 "Component" [] [Whitespace(" ")], + }, + attributes: HtmlAttributeList [ + HtmlSpreadAttribute { + l_curly_token: L_CURLY@27..28 "{" [] [], + dotdotdot_token: DOT3@28..31 "..." [] [], + argument: HtmlName { + ident_token: IDENT@31..37 "props" [] [Whitespace(" ")], + }, + r_curly_token: missing (required), + }, + HtmlAttribute { + name: HtmlAttributeName { + value_token: HTML_LITERAL@37..40 "/>\n" [] [], + }, + initializer: missing (optional), + }, + ], + r_angle_token: missing (required), + }, + children: HtmlElementList [], + closing_element: missing (required), + }, + ], + eof_token: EOF@40..40 "" [] [], +} +``` + +## CST + +``` +0: HTML_ROOT@0..40 + 0: (empty) + 1: (empty) + 2: (empty) + 3: HTML_ELEMENT_LIST@0..40 + 0: HTML_SELF_CLOSING_ELEMENT@0..15 + 0: L_ANGLE@0..1 "<" [] [] + 1: HTML_TAG_NAME@1..7 + 0: HTML_LITERAL@1..7 "input" [] [Whitespace(" ")] + 2: HTML_ATTRIBUTE_LIST@7..13 + 0: HTML_SPREAD_ATTRIBUTE@7..13 + 0: L_CURLY@7..8 "{" [] [] + 1: DOT3@8..11 "..." [] [] + 2: (empty) + 3: R_CURLY@11..13 "}" [] [Whitespace(" ")] + 3: SLASH@13..14 "/" [] [] + 4: R_ANGLE@14..15 ">" [] [] + 1: HTML_ELEMENT@15..40 + 0: HTML_OPENING_ELEMENT@15..40 + 0: L_ANGLE@15..17 "<" [Newline("\n")] [] + 1: HTML_COMPONENT_NAME@17..27 + 0: HTML_LITERAL@17..27 "Component" [] [Whitespace(" ")] + 2: HTML_ATTRIBUTE_LIST@27..40 + 0: HTML_SPREAD_ATTRIBUTE@27..37 + 0: L_CURLY@27..28 "{" [] [] + 1: DOT3@28..31 "..." [] [] + 2: HTML_NAME@31..37 + 0: IDENT@31..37 "props" [] [Whitespace(" ")] + 3: (empty) + 1: HTML_ATTRIBUTE@37..40 + 0: HTML_ATTRIBUTE_NAME@37..40 + 0: HTML_LITERAL@37..40 "/>\n" [] [] + 1: (empty) + 3: (empty) + 1: HTML_ELEMENT_LIST@40..40 + 2: (empty) + 4: EOF@40..40 "" [] [] + +``` + +## Diagnostics + +``` +spread.svelte:1:12 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Expected a name after '...' + + > 1 │ + │ ^ + 2 │ + 3 │ + +spread.svelte:2:22 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × expected `}` but instead found `/> + ` + + 1 │ + > 2 │ + │ ^^ + > 3 │ + │ + + i Remove /> + + +spread.svelte:3:1 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × expected `>` but instead the file ends + + 1 │ + 2 │ + > 3 │ + │ + + i the file ends here + + 1 │ + 2 │ + > 3 │ + │ + +``` diff --git a/crates/biome_html_parser/tests/html_specs/ok/astro/spread.astro b/crates/biome_html_parser/tests/html_specs/ok/astro/spread.astro new file mode 100644 index 000000000000..0fc2e72c58b6 --- /dev/null +++ b/crates/biome_html_parser/tests/html_specs/ok/astro/spread.astro @@ -0,0 +1,2 @@ + + diff --git a/crates/biome_html_parser/tests/html_specs/ok/astro/spread.astro.snap b/crates/biome_html_parser/tests/html_specs/ok/astro/spread.astro.snap new file mode 100644 index 000000000000..8cf6b6568f5a --- /dev/null +++ b/crates/biome_html_parser/tests/html_specs/ok/astro/spread.astro.snap @@ -0,0 +1,99 @@ +--- +source: crates/biome_html_parser/tests/spec_test.rs +expression: snapshot +--- +## Input + +```astro + + + +``` + + +## AST + +``` +HtmlRoot { + bom_token: missing (optional), + frontmatter: missing (optional), + directive: missing (optional), + html: HtmlElementList [ + HtmlSelfClosingElement { + l_angle_token: L_ANGLE@0..1 "<" [] [], + name: HtmlTagName { + value_token: HTML_LITERAL@1..7 "input" [] [Whitespace(" ")], + }, + attributes: HtmlAttributeList [ + HtmlSpreadAttribute { + l_curly_token: L_CURLY@7..8 "{" [] [], + dotdotdot_token: DOT3@8..11 "..." [] [], + argument: HtmlName { + ident_token: IDENT@11..16 "props" [] [], + }, + r_curly_token: R_CURLY@16..18 "}" [] [Whitespace(" ")], + }, + ], + slash_token: missing (optional), + r_angle_token: R_ANGLE@18..19 ">" [] [], + }, + HtmlSelfClosingElement { + l_angle_token: L_ANGLE@19..21 "<" [Newline("\n")] [], + name: HtmlComponentName { + value_token: HTML_LITERAL@21..31 "Component" [] [Whitespace(" ")], + }, + attributes: HtmlAttributeList [ + HtmlSpreadAttribute { + l_curly_token: L_CURLY@31..32 "{" [] [], + dotdotdot_token: DOT3@32..35 "..." [] [], + argument: HtmlName { + ident_token: IDENT@35..40 "props" [] [], + }, + r_curly_token: R_CURLY@40..42 "}" [] [Whitespace(" ")], + }, + ], + slash_token: SLASH@42..43 "/" [] [], + r_angle_token: R_ANGLE@43..44 ">" [] [], + }, + ], + eof_token: EOF@44..45 "" [Newline("\n")] [], +} +``` + +## CST + +``` +0: HTML_ROOT@0..45 + 0: (empty) + 1: (empty) + 2: (empty) + 3: HTML_ELEMENT_LIST@0..44 + 0: HTML_SELF_CLOSING_ELEMENT@0..19 + 0: L_ANGLE@0..1 "<" [] [] + 1: HTML_TAG_NAME@1..7 + 0: HTML_LITERAL@1..7 "input" [] [Whitespace(" ")] + 2: HTML_ATTRIBUTE_LIST@7..18 + 0: HTML_SPREAD_ATTRIBUTE@7..18 + 0: L_CURLY@7..8 "{" [] [] + 1: DOT3@8..11 "..." [] [] + 2: HTML_NAME@11..16 + 0: IDENT@11..16 "props" [] [] + 3: R_CURLY@16..18 "}" [] [Whitespace(" ")] + 3: (empty) + 4: R_ANGLE@18..19 ">" [] [] + 1: HTML_SELF_CLOSING_ELEMENT@19..44 + 0: L_ANGLE@19..21 "<" [Newline("\n")] [] + 1: HTML_COMPONENT_NAME@21..31 + 0: HTML_LITERAL@21..31 "Component" [] [Whitespace(" ")] + 2: HTML_ATTRIBUTE_LIST@31..42 + 0: HTML_SPREAD_ATTRIBUTE@31..42 + 0: L_CURLY@31..32 "{" [] [] + 1: DOT3@32..35 "..." [] [] + 2: HTML_NAME@35..40 + 0: IDENT@35..40 "props" [] [] + 3: R_CURLY@40..42 "}" [] [Whitespace(" ")] + 3: SLASH@42..43 "/" [] [] + 4: R_ANGLE@43..44 ">" [] [] + 4: EOF@44..45 "" [Newline("\n")] [] + +``` diff --git a/crates/biome_html_parser/tests/html_specs/ok/svelte/shorthand-spread-props.svelte.snap b/crates/biome_html_parser/tests/html_specs/ok/svelte/shorthand-spread-props.svelte.snap index 385241c54392..412f32dd6a35 100644 --- a/crates/biome_html_parser/tests/html_specs/ok/svelte/shorthand-spread-props.svelte.snap +++ b/crates/biome_html_parser/tests/html_specs/ok/svelte/shorthand-spread-props.svelte.snap @@ -25,10 +25,11 @@ HtmlRoot { value_token: HTML_LITERAL@1..8 "button" [] [Whitespace(" ")], }, attributes: HtmlAttributeList [ - HtmlSingleTextExpression { + HtmlSpreadAttribute { l_curly_token: L_CURLY@8..9 "{" [] [], - expression: HtmlTextExpression { - html_literal_token: HTML_LITERAL@9..17 "...props" [] [], + dotdotdot_token: DOT3@9..12 "..." [] [], + argument: HtmlName { + ident_token: IDENT@12..17 "props" [] [], }, r_curly_token: R_CURLY@17..18 "}" [] [], }, @@ -68,11 +69,12 @@ HtmlRoot { 1: HTML_TAG_NAME@1..8 0: HTML_LITERAL@1..8 "button" [] [Whitespace(" ")] 2: HTML_ATTRIBUTE_LIST@8..18 - 0: HTML_SINGLE_TEXT_EXPRESSION@8..18 + 0: HTML_SPREAD_ATTRIBUTE@8..18 0: L_CURLY@8..9 "{" [] [] - 1: HTML_TEXT_EXPRESSION@9..17 - 0: HTML_LITERAL@9..17 "...props" [] [] - 2: R_CURLY@17..18 "}" [] [] + 1: DOT3@9..12 "..." [] [] + 2: HTML_NAME@12..17 + 0: IDENT@12..17 "props" [] [] + 3: R_CURLY@17..18 "}" [] [] 3: R_ANGLE@18..19 ">" [] [] 1: HTML_ELEMENT_LIST@19..27 0: HTML_CONTENT@19..27 diff --git a/crates/biome_html_parser/tests/html_specs/ok/svelte/spread.svelte b/crates/biome_html_parser/tests/html_specs/ok/svelte/spread.svelte new file mode 100644 index 000000000000..b4daabf5507c --- /dev/null +++ b/crates/biome_html_parser/tests/html_specs/ok/svelte/spread.svelte @@ -0,0 +1,2 @@ + + diff --git a/crates/biome_html_parser/tests/html_specs/ok/svelte/spread.svelte.snap b/crates/biome_html_parser/tests/html_specs/ok/svelte/spread.svelte.snap new file mode 100644 index 000000000000..494e64af186b --- /dev/null +++ b/crates/biome_html_parser/tests/html_specs/ok/svelte/spread.svelte.snap @@ -0,0 +1,99 @@ +--- +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 [ + HtmlSelfClosingElement { + l_angle_token: L_ANGLE@0..1 "<" [] [], + name: HtmlTagName { + value_token: HTML_LITERAL@1..7 "input" [] [Whitespace(" ")], + }, + attributes: HtmlAttributeList [ + HtmlSpreadAttribute { + l_curly_token: L_CURLY@7..8 "{" [] [], + dotdotdot_token: DOT3@8..11 "..." [] [], + argument: HtmlName { + ident_token: IDENT@11..14 "foo" [] [], + }, + r_curly_token: R_CURLY@14..16 "}" [] [Whitespace(" ")], + }, + ], + slash_token: SLASH@16..17 "/" [] [], + r_angle_token: R_ANGLE@17..18 ">" [] [], + }, + HtmlSelfClosingElement { + l_angle_token: L_ANGLE@18..20 "<" [Newline("\n")] [], + name: HtmlComponentName { + value_token: HTML_LITERAL@20..30 "Component" [] [Whitespace(" ")], + }, + attributes: HtmlAttributeList [ + HtmlSpreadAttribute { + l_curly_token: L_CURLY@30..31 "{" [] [], + dotdotdot_token: DOT3@31..34 "..." [] [], + argument: HtmlName { + ident_token: IDENT@34..37 "foo" [] [], + }, + r_curly_token: R_CURLY@37..39 "}" [] [Whitespace(" ")], + }, + ], + slash_token: SLASH@39..40 "/" [] [], + r_angle_token: R_ANGLE@40..41 ">" [] [], + }, + ], + eof_token: EOF@41..42 "" [Newline("\n")] [], +} +``` + +## CST + +``` +0: HTML_ROOT@0..42 + 0: (empty) + 1: (empty) + 2: (empty) + 3: HTML_ELEMENT_LIST@0..41 + 0: HTML_SELF_CLOSING_ELEMENT@0..18 + 0: L_ANGLE@0..1 "<" [] [] + 1: HTML_TAG_NAME@1..7 + 0: HTML_LITERAL@1..7 "input" [] [Whitespace(" ")] + 2: HTML_ATTRIBUTE_LIST@7..16 + 0: HTML_SPREAD_ATTRIBUTE@7..16 + 0: L_CURLY@7..8 "{" [] [] + 1: DOT3@8..11 "..." [] [] + 2: HTML_NAME@11..14 + 0: IDENT@11..14 "foo" [] [] + 3: R_CURLY@14..16 "}" [] [Whitespace(" ")] + 3: SLASH@16..17 "/" [] [] + 4: R_ANGLE@17..18 ">" [] [] + 1: HTML_SELF_CLOSING_ELEMENT@18..41 + 0: L_ANGLE@18..20 "<" [Newline("\n")] [] + 1: HTML_COMPONENT_NAME@20..30 + 0: HTML_LITERAL@20..30 "Component" [] [Whitespace(" ")] + 2: HTML_ATTRIBUTE_LIST@30..39 + 0: HTML_SPREAD_ATTRIBUTE@30..39 + 0: L_CURLY@30..31 "{" [] [] + 1: DOT3@31..34 "..." [] [] + 2: HTML_NAME@34..37 + 0: IDENT@34..37 "foo" [] [] + 3: R_CURLY@37..39 "}" [] [Whitespace(" ")] + 3: SLASH@39..40 "/" [] [] + 4: R_ANGLE@40..41 ">" [] [] + 4: EOF@41..42 "" [Newline("\n")] [] + +``` diff --git a/crates/biome_html_parser/tests/quick_test.rs b/crates/biome_html_parser/tests/quick_test.rs index 3c74e90185a0..019edf11a486 100644 --- a/crates/biome_html_parser/tests/quick_test.rs +++ b/crates/biome_html_parser/tests/quick_test.rs @@ -5,7 +5,7 @@ use biome_test_utils::has_bogus_nodes_or_empty_slots; #[ignore] #[test] pub fn quick_test() { - let code = r#"

Multiple shorthand

+ let code = r#"

Multiple shorthand

diff --git a/crates/biome_html_syntax/src/generated/kind.rs b/crates/biome_html_syntax/src/generated/kind.rs index b18ba3bbfd5c..5f3dd9a2f71e 100644 --- a/crates/biome_html_syntax/src/generated/kind.rs +++ b/crates/biome_html_syntax/src/generated/kind.rs @@ -95,6 +95,8 @@ pub enum HtmlSyntaxKind { HTML_DOUBLE_TEXT_EXPRESSION, HTML_SINGLE_TEXT_EXPRESSION, HTML_TEXT_EXPRESSION, + HTML_SPREAD_ATTRIBUTE, + HTML_NAME, ASTRO_FRONTMATTER_ELEMENT, ASTRO_EMBEDDED_CONTENT, SVELTE_DEBUG_BLOCK, diff --git a/crates/biome_html_syntax/src/generated/macros.rs b/crates/biome_html_syntax/src/generated/macros.rs index d96f6e40037f..1bdc6ea5057b 100644 --- a/crates/biome_html_syntax/src/generated/macros.rs +++ b/crates/biome_html_syntax/src/generated/macros.rs @@ -73,6 +73,10 @@ macro_rules! map_syntax_node { let $pattern = unsafe { $crate::HtmlMemberName::new_unchecked(node) }; $body } + $crate::HtmlSyntaxKind::HTML_NAME => { + let $pattern = unsafe { $crate::HtmlName::new_unchecked(node) }; + $body + } $crate::HtmlSyntaxKind::HTML_OPENING_ELEMENT => { let $pattern = unsafe { $crate::HtmlOpeningElement::new_unchecked(node) }; $body @@ -89,6 +93,10 @@ macro_rules! map_syntax_node { let $pattern = unsafe { $crate::HtmlSingleTextExpression::new_unchecked(node) }; $body } + $crate::HtmlSyntaxKind::HTML_SPREAD_ATTRIBUTE => { + let $pattern = unsafe { $crate::HtmlSpreadAttribute::new_unchecked(node) }; + $body + } $crate::HtmlSyntaxKind::HTML_STRING => { let $pattern = unsafe { $crate::HtmlString::new_unchecked(node) }; $body diff --git a/crates/biome_html_syntax/src/generated/nodes.rs b/crates/biome_html_syntax/src/generated/nodes.rs index 6a4f7e5ac019..3317795a1dc3 100644 --- a/crates/biome_html_syntax/src/generated/nodes.rs +++ b/crates/biome_html_syntax/src/generated/nodes.rs @@ -620,6 +620,41 @@ pub struct HtmlMemberNameFields { pub member: SyntaxResult, } #[derive(Clone, PartialEq, Eq, Hash)] +pub struct HtmlName { + pub(crate) syntax: SyntaxNode, +} +impl HtmlName { + #[doc = r" Create an AstNode from a SyntaxNode without checking its kind"] + #[doc = r""] + #[doc = r" # Safety"] + #[doc = r" This function must be guarded with a call to [AstNode::can_cast]"] + #[doc = r" or a match on [SyntaxNode::kind]"] + #[inline] + pub const unsafe fn new_unchecked(syntax: SyntaxNode) -> Self { + Self { syntax } + } + pub fn as_fields(&self) -> HtmlNameFields { + HtmlNameFields { + ident_token: self.ident_token(), + } + } + pub fn ident_token(&self) -> SyntaxResult { + support::required_token(&self.syntax, 0usize) + } +} +impl Serialize for HtmlName { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + self.as_fields().serialize(serializer) + } +} +#[derive(Serialize)] +pub struct HtmlNameFields { + pub ident_token: SyntaxResult, +} +#[derive(Clone, PartialEq, Eq, Hash)] pub struct HtmlOpeningElement { pub(crate) syntax: SyntaxNode, } @@ -825,6 +860,56 @@ pub struct HtmlSingleTextExpressionFields { pub r_curly_token: SyntaxResult, } #[derive(Clone, PartialEq, Eq, Hash)] +pub struct HtmlSpreadAttribute { + pub(crate) syntax: SyntaxNode, +} +impl HtmlSpreadAttribute { + #[doc = r" Create an AstNode from a SyntaxNode without checking its kind"] + #[doc = r""] + #[doc = r" # Safety"] + #[doc = r" This function must be guarded with a call to [AstNode::can_cast]"] + #[doc = r" or a match on [SyntaxNode::kind]"] + #[inline] + pub const unsafe fn new_unchecked(syntax: SyntaxNode) -> Self { + Self { syntax } + } + pub fn as_fields(&self) -> HtmlSpreadAttributeFields { + HtmlSpreadAttributeFields { + l_curly_token: self.l_curly_token(), + dotdotdot_token: self.dotdotdot_token(), + argument: self.argument(), + r_curly_token: self.r_curly_token(), + } + } + pub fn l_curly_token(&self) -> SyntaxResult { + support::required_token(&self.syntax, 0usize) + } + pub fn dotdotdot_token(&self) -> SyntaxResult { + support::required_token(&self.syntax, 1usize) + } + pub fn argument(&self) -> SyntaxResult { + support::required_node(&self.syntax, 2usize) + } + pub fn r_curly_token(&self) -> SyntaxResult { + support::required_token(&self.syntax, 3usize) + } +} +impl Serialize for HtmlSpreadAttribute { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + self.as_fields().serialize(serializer) + } +} +#[derive(Serialize)] +pub struct HtmlSpreadAttributeFields { + pub l_curly_token: SyntaxResult, + pub dotdotdot_token: SyntaxResult, + pub argument: SyntaxResult, + pub r_curly_token: SyntaxResult, +} +#[derive(Clone, PartialEq, Eq, Hash)] pub struct HtmlString { pub(crate) syntax: SyntaxNode, } @@ -3376,6 +3461,7 @@ pub enum AnyHtmlAttribute { HtmlBogusAttribute(HtmlBogusAttribute), HtmlDoubleTextExpression(HtmlDoubleTextExpression), HtmlSingleTextExpression(HtmlSingleTextExpression), + HtmlSpreadAttribute(HtmlSpreadAttribute), SvelteAttachAttribute(SvelteAttachAttribute), } impl AnyHtmlAttribute { @@ -3415,6 +3501,12 @@ impl AnyHtmlAttribute { _ => None, } } + pub fn as_html_spread_attribute(&self) -> Option<&HtmlSpreadAttribute> { + match &self { + Self::HtmlSpreadAttribute(item) => Some(item), + _ => None, + } + } pub fn as_svelte_attach_attribute(&self) -> Option<&SvelteAttachAttribute> { match &self { Self::SvelteAttachAttribute(item) => Some(item), @@ -4677,6 +4769,56 @@ impl From for SyntaxElement { n.syntax.into() } } +impl AstNode for HtmlName { + type Language = Language; + const KIND_SET: SyntaxKindSet = + SyntaxKindSet::from_raw(RawSyntaxKind(HTML_NAME as u16)); + fn can_cast(kind: SyntaxKind) -> bool { + kind == HTML_NAME + } + fn cast(syntax: SyntaxNode) -> Option { + if Self::can_cast(syntax.kind()) { + Some(Self { syntax }) + } else { + None + } + } + fn syntax(&self) -> &SyntaxNode { + &self.syntax + } + fn into_syntax(self) -> SyntaxNode { + self.syntax + } +} +impl std::fmt::Debug for HtmlName { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + thread_local! { static DEPTH : std :: cell :: Cell < u8 > = const { std :: cell :: Cell :: new (0) } }; + let current_depth = DEPTH.get(); + let result = if current_depth < 16 { + DEPTH.set(current_depth + 1); + f.debug_struct("HtmlName") + .field( + "ident_token", + &support::DebugSyntaxResult(self.ident_token()), + ) + .finish() + } else { + f.debug_struct("HtmlName").finish() + }; + DEPTH.set(current_depth); + result + } +} +impl From for SyntaxNode { + fn from(n: HtmlName) -> Self { + n.syntax + } +} +impl From for SyntaxElement { + fn from(n: HtmlName) -> Self { + n.syntax.into() + } +} impl AstNode for HtmlOpeningElement { type Language = Language; const KIND_SET: SyntaxKindSet = @@ -4908,6 +5050,65 @@ impl From for SyntaxElement { n.syntax.into() } } +impl AstNode for HtmlSpreadAttribute { + type Language = Language; + const KIND_SET: SyntaxKindSet = + SyntaxKindSet::from_raw(RawSyntaxKind(HTML_SPREAD_ATTRIBUTE as u16)); + fn can_cast(kind: SyntaxKind) -> bool { + kind == HTML_SPREAD_ATTRIBUTE + } + fn cast(syntax: SyntaxNode) -> Option { + if Self::can_cast(syntax.kind()) { + Some(Self { syntax }) + } else { + None + } + } + fn syntax(&self) -> &SyntaxNode { + &self.syntax + } + fn into_syntax(self) -> SyntaxNode { + self.syntax + } +} +impl std::fmt::Debug for HtmlSpreadAttribute { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + thread_local! { static DEPTH : std :: cell :: Cell < u8 > = const { std :: cell :: Cell :: new (0) } }; + let current_depth = DEPTH.get(); + let result = if current_depth < 16 { + DEPTH.set(current_depth + 1); + f.debug_struct("HtmlSpreadAttribute") + .field( + "l_curly_token", + &support::DebugSyntaxResult(self.l_curly_token()), + ) + .field( + "dotdotdot_token", + &support::DebugSyntaxResult(self.dotdotdot_token()), + ) + .field("argument", &support::DebugSyntaxResult(self.argument())) + .field( + "r_curly_token", + &support::DebugSyntaxResult(self.r_curly_token()), + ) + .finish() + } else { + f.debug_struct("HtmlSpreadAttribute").finish() + }; + DEPTH.set(current_depth); + result + } +} +impl From for SyntaxNode { + fn from(n: HtmlSpreadAttribute) -> Self { + n.syntax + } +} +impl From for SyntaxElement { + fn from(n: HtmlSpreadAttribute) -> Self { + n.syntax.into() + } +} impl AstNode for HtmlString { type Language = Language; const KIND_SET: SyntaxKindSet = @@ -8007,6 +8208,11 @@ impl From for AnyHtmlAttribute { Self::HtmlSingleTextExpression(node) } } +impl From for AnyHtmlAttribute { + fn from(node: HtmlSpreadAttribute) -> Self { + Self::HtmlSpreadAttribute(node) + } +} impl From for AnyHtmlAttribute { fn from(node: SvelteAttachAttribute) -> Self { Self::SvelteAttachAttribute(node) @@ -8020,6 +8226,7 @@ impl AstNode for AnyHtmlAttribute { .union(HtmlBogusAttribute::KIND_SET) .union(HtmlDoubleTextExpression::KIND_SET) .union(HtmlSingleTextExpression::KIND_SET) + .union(HtmlSpreadAttribute::KIND_SET) .union(SvelteAttachAttribute::KIND_SET); fn can_cast(kind: SyntaxKind) -> bool { match kind { @@ -8027,6 +8234,7 @@ impl AstNode for AnyHtmlAttribute { | HTML_BOGUS_ATTRIBUTE | HTML_DOUBLE_TEXT_EXPRESSION | HTML_SINGLE_TEXT_EXPRESSION + | HTML_SPREAD_ATTRIBUTE | SVELTE_ATTACH_ATTRIBUTE => true, k if AnySvelteDirective::can_cast(k) => true, k if AnyVueDirective::can_cast(k) => true, @@ -8043,6 +8251,7 @@ impl AstNode for AnyHtmlAttribute { HTML_SINGLE_TEXT_EXPRESSION => { Self::HtmlSingleTextExpression(HtmlSingleTextExpression { syntax }) } + HTML_SPREAD_ATTRIBUTE => Self::HtmlSpreadAttribute(HtmlSpreadAttribute { syntax }), SVELTE_ATTACH_ATTRIBUTE => { Self::SvelteAttachAttribute(SvelteAttachAttribute { syntax }) } @@ -8067,6 +8276,7 @@ impl AstNode for AnyHtmlAttribute { Self::HtmlBogusAttribute(it) => it.syntax(), Self::HtmlDoubleTextExpression(it) => it.syntax(), Self::HtmlSingleTextExpression(it) => it.syntax(), + Self::HtmlSpreadAttribute(it) => it.syntax(), Self::SvelteAttachAttribute(it) => it.syntax(), Self::AnySvelteDirective(it) => it.syntax(), Self::AnyVueDirective(it) => it.syntax(), @@ -8078,6 +8288,7 @@ impl AstNode for AnyHtmlAttribute { Self::HtmlBogusAttribute(it) => it.into_syntax(), Self::HtmlDoubleTextExpression(it) => it.into_syntax(), Self::HtmlSingleTextExpression(it) => it.into_syntax(), + Self::HtmlSpreadAttribute(it) => it.into_syntax(), Self::SvelteAttachAttribute(it) => it.into_syntax(), Self::AnySvelteDirective(it) => it.into_syntax(), Self::AnyVueDirective(it) => it.into_syntax(), @@ -8093,6 +8304,7 @@ impl std::fmt::Debug for AnyHtmlAttribute { Self::HtmlBogusAttribute(it) => std::fmt::Debug::fmt(it, f), Self::HtmlDoubleTextExpression(it) => std::fmt::Debug::fmt(it, f), Self::HtmlSingleTextExpression(it) => std::fmt::Debug::fmt(it, f), + Self::HtmlSpreadAttribute(it) => std::fmt::Debug::fmt(it, f), Self::SvelteAttachAttribute(it) => std::fmt::Debug::fmt(it, f), } } @@ -8106,6 +8318,7 @@ impl From for SyntaxNode { AnyHtmlAttribute::HtmlBogusAttribute(it) => it.into_syntax(), AnyHtmlAttribute::HtmlDoubleTextExpression(it) => it.into_syntax(), AnyHtmlAttribute::HtmlSingleTextExpression(it) => it.into_syntax(), + AnyHtmlAttribute::HtmlSpreadAttribute(it) => it.into_syntax(), AnyHtmlAttribute::SvelteAttachAttribute(it) => it.into_syntax(), } } @@ -9630,6 +9843,11 @@ impl std::fmt::Display for HtmlMemberName { std::fmt::Display::fmt(self.syntax(), f) } } +impl std::fmt::Display for HtmlName { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + std::fmt::Display::fmt(self.syntax(), f) + } +} impl std::fmt::Display for HtmlOpeningElement { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { std::fmt::Display::fmt(self.syntax(), f) @@ -9650,6 +9868,11 @@ impl std::fmt::Display for HtmlSingleTextExpression { std::fmt::Display::fmt(self.syntax(), f) } } +impl std::fmt::Display for HtmlSpreadAttribute { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + std::fmt::Display::fmt(self.syntax(), f) + } +} impl std::fmt::Display for HtmlString { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { std::fmt::Display::fmt(self.syntax(), f) diff --git a/crates/biome_html_syntax/src/generated/nodes_mut.rs b/crates/biome_html_syntax/src/generated/nodes_mut.rs index 34e63b00384b..eaf32a436ecd 100644 --- a/crates/biome_html_syntax/src/generated/nodes_mut.rs +++ b/crates/biome_html_syntax/src/generated/nodes_mut.rs @@ -247,6 +247,14 @@ impl HtmlMemberName { ) } } +impl HtmlName { + pub fn with_ident_token(self, element: SyntaxToken) -> Self { + Self::unwrap_cast( + self.syntax + .splice_slots(0usize..=0usize, once(Some(element.into()))), + ) + } +} impl HtmlOpeningElement { pub fn with_l_angle_token(self, element: SyntaxToken) -> Self { Self::unwrap_cast( @@ -357,6 +365,32 @@ impl HtmlSingleTextExpression { ) } } +impl HtmlSpreadAttribute { + pub fn with_l_curly_token(self, element: SyntaxToken) -> Self { + Self::unwrap_cast( + self.syntax + .splice_slots(0usize..=0usize, once(Some(element.into()))), + ) + } + pub fn with_dotdotdot_token(self, element: SyntaxToken) -> Self { + Self::unwrap_cast( + self.syntax + .splice_slots(1usize..=1usize, once(Some(element.into()))), + ) + } + pub fn with_argument(self, element: HtmlName) -> Self { + Self::unwrap_cast( + self.syntax + .splice_slots(2usize..=2usize, once(Some(element.into_syntax().into()))), + ) + } + pub fn with_r_curly_token(self, element: SyntaxToken) -> Self { + Self::unwrap_cast( + self.syntax + .splice_slots(3usize..=3usize, once(Some(element.into()))), + ) + } +} impl HtmlString { pub fn with_value_token(self, element: SyntaxToken) -> Self { Self::unwrap_cast( diff --git a/crates/biome_service/src/workspace/document/services/embedded_value_references.rs b/crates/biome_service/src/workspace/document/services/embedded_value_references.rs index 4cfc1db1c5ad..fa90c46500eb 100644 --- a/crates/biome_service/src/workspace/document/services/embedded_value_references.rs +++ b/crates/biome_service/src/workspace/document/services/embedded_value_references.rs @@ -1,5 +1,6 @@ use biome_html_syntax::{ AnyHtmlComponentObjectName, AnyHtmlTagName, HtmlElement, HtmlRoot, HtmlSelfClosingElement, + HtmlSpreadAttribute, }; use biome_js_syntax::{ AnyJsIdentifierUsage, AnyJsRoot, JsReferenceIdentifier, JsStaticMemberExpression, @@ -68,9 +69,24 @@ impl EmbeddedValueReferencesBuilder { if let Some(element) = HtmlSelfClosingElement::cast_ref(&node) { self.visit_html_self_closing_element(&element); } + + if let Some(spread_attribute) = HtmlSpreadAttribute::cast_ref(&node) { + self.visit_spread_attribute(&spread_attribute); + } } } + fn visit_spread_attribute(&mut self, attribute: &HtmlSpreadAttribute) -> Option<()> { + let argument = attribute.argument().ok()?; + + let name = argument.ident_token().ok()?; + + self.references + .insert(name.text_trimmed_range(), name.token_text_trimmed()); + + Some(()) + } + fn visit_html_element(&mut self, element: &HtmlElement) -> Option<()> { // Skip script and style tags - these are not component references if element.is_script_tag() || element.is_style_tag() { diff --git a/xtask/codegen/html.ungram b/xtask/codegen/html.ungram index 90348abd385a..35aae1da35b4 100644 --- a/xtask/codegen/html.ungram +++ b/xtask/codegen/html.ungram @@ -185,6 +185,7 @@ AnyHtmlAttribute = | HtmlDoubleTextExpression | HtmlSingleTextExpression | SvelteAttachAttribute + | HtmlSpreadAttribute | AnySvelteDirective | AnyVueDirective | HtmlBogusAttribute @@ -208,6 +209,7 @@ AnyHtmlAttributeInitializer = | HtmlSingleTextExpression +HtmlName = 'ident' // ================================== // Svelte @@ -593,7 +595,13 @@ SvelteDirectiveModifier = '|' name: SvelteName - +// +// ^^^^^^^^^^ +HtmlSpreadAttribute = + '{' + '...' + argument: HtmlName + '}' // Keep it different just for svelte diff --git a/xtask/codegen/src/html_kinds_src.rs b/xtask/codegen/src/html_kinds_src.rs index 0aa942ea6d14..a60fd9688981 100644 --- a/xtask/codegen/src/html_kinds_src.rs +++ b/xtask/codegen/src/html_kinds_src.rs @@ -89,6 +89,8 @@ pub const HTML_KINDS_SRC: KindsSrc = KindsSrc { "HTML_DOUBLE_TEXT_EXPRESSION", "HTML_SINGLE_TEXT_EXPRESSION", "HTML_TEXT_EXPRESSION", + "HTML_SPREAD_ATTRIBUTE", + "HTML_NAME", // Astro nodes "ASTRO_FRONTMATTER_ELEMENT", "ASTRO_EMBEDDED_CONTENT", From d1930a36a2c208c6ce35ba37253d7c5a28b1ca5c Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Wed, 28 Jan 2026 14:55:03 +0000 Subject: [PATCH 2/2] address feedback --- .../src/generated/node_factory.rs | 8 +- .../src/generated/syntax_factory.rs | 21 +---- crates/biome_html_formatter/src/generated.rs | 32 ------- .../src/html/auxiliary/mod.rs | 1 - .../src/html/auxiliary/name.rs | 9 -- crates/biome_html_parser/src/syntax/astro.rs | 14 +-- crates/biome_html_parser/src/syntax/mod.rs | 22 ----- crates/biome_html_parser/src/syntax/svelte.rs | 13 +-- .../html_specs/error/astro/spread.astro.snap | 38 ++------ .../error/svelte/spread.svelte.snap | 52 +++------- .../html_specs/ok/astro/spread.astro.snap | 16 ++-- .../svelte/shorthand-spread-props.svelte.snap | 8 +- .../tests/html_specs/ok/svelte/spread.svelte | 1 + .../html_specs/ok/svelte/spread.svelte.snap | 56 ++++++++--- .../biome_html_syntax/src/generated/kind.rs | 1 - .../biome_html_syntax/src/generated/macros.rs | 4 - .../biome_html_syntax/src/generated/nodes.rs | 94 +------------------ .../src/generated/nodes_mut.rs | 10 +- .../services/embedded_value_references.rs | 16 ---- xtask/codegen/html.ungram | 4 +- xtask/codegen/src/html_kinds_src.rs | 1 - 21 files changed, 95 insertions(+), 326 deletions(-) delete mode 100644 crates/biome_html_formatter/src/html/auxiliary/name.rs diff --git a/crates/biome_html_factory/src/generated/node_factory.rs b/crates/biome_html_factory/src/generated/node_factory.rs index 3884c7e12e66..56bba86ffee2 100644 --- a/crates/biome_html_factory/src/generated/node_factory.rs +++ b/crates/biome_html_factory/src/generated/node_factory.rs @@ -236,12 +236,6 @@ pub fn html_member_name( ], )) } -pub fn html_name(ident_token: SyntaxToken) -> HtmlName { - HtmlName::unwrap_cast(SyntaxNode::new_detached( - HtmlSyntaxKind::HTML_NAME, - [Some(SyntaxElement::Token(ident_token))], - )) -} pub fn html_opening_element( l_angle_token: SyntaxToken, name: AnyHtmlTagName, @@ -358,7 +352,7 @@ pub fn html_single_text_expression( pub fn html_spread_attribute( l_curly_token: SyntaxToken, dotdotdot_token: SyntaxToken, - argument: HtmlName, + argument: HtmlTextExpression, r_curly_token: SyntaxToken, ) -> HtmlSpreadAttribute { HtmlSpreadAttribute::unwrap_cast(SyntaxNode::new_detached( diff --git a/crates/biome_html_factory/src/generated/syntax_factory.rs b/crates/biome_html_factory/src/generated/syntax_factory.rs index fef06aa7e1c9..2f695a4f5b1e 100644 --- a/crates/biome_html_factory/src/generated/syntax_factory.rs +++ b/crates/biome_html_factory/src/generated/syntax_factory.rs @@ -444,25 +444,6 @@ impl SyntaxFactory for HtmlSyntaxFactory { } slots.into_node(HTML_MEMBER_NAME, children) } - HTML_NAME => { - let mut elements = (&children).into_iter(); - let mut slots: RawNodeSlots<1usize> = RawNodeSlots::default(); - let mut current_element = elements.next(); - if let Some(element) = ¤t_element - && element.kind() == IDENT - { - slots.mark_present(); - current_element = elements.next(); - } - slots.next_slot(); - if current_element.is_some() { - return RawSyntaxNode::new( - HTML_NAME.to_bogus(), - children.into_iter().map(Some), - ); - } - slots.into_node(HTML_NAME, children) - } HTML_OPENING_ELEMENT => { let mut elements = (&children).into_iter(); let mut slots: RawNodeSlots<4usize> = RawNodeSlots::default(); @@ -649,7 +630,7 @@ impl SyntaxFactory for HtmlSyntaxFactory { } slots.next_slot(); if let Some(element) = ¤t_element - && HtmlName::can_cast(element.kind()) + && HtmlTextExpression::can_cast(element.kind()) { slots.mark_present(); current_element = elements.next(); diff --git a/crates/biome_html_formatter/src/generated.rs b/crates/biome_html_formatter/src/generated.rs index 17b386eca661..bbefa8c4aa1b 100644 --- a/crates/biome_html_formatter/src/generated.rs +++ b/crates/biome_html_formatter/src/generated.rs @@ -534,38 +534,6 @@ impl IntoFormat for biome_html_syntax::HtmlMemberName { ) } } -impl FormatRule for crate::html::auxiliary::name::FormatHtmlName { - type Context = HtmlFormatContext; - #[inline(always)] - fn fmt(&self, node: &biome_html_syntax::HtmlName, f: &mut HtmlFormatter) -> FormatResult<()> { - FormatNodeRule::::fmt(self, node, f) - } -} -impl AsFormat for biome_html_syntax::HtmlName { - type Format<'a> = FormatRefWithRule< - 'a, - biome_html_syntax::HtmlName, - crate::html::auxiliary::name::FormatHtmlName, - >; - fn format(&self) -> Self::Format<'_> { - FormatRefWithRule::new( - self, - crate::html::auxiliary::name::FormatHtmlName::default(), - ) - } -} -impl IntoFormat for biome_html_syntax::HtmlName { - type Format = FormatOwnedWithRule< - biome_html_syntax::HtmlName, - crate::html::auxiliary::name::FormatHtmlName, - >; - fn into_format(self) -> Self::Format { - FormatOwnedWithRule::new( - self, - crate::html::auxiliary::name::FormatHtmlName::default(), - ) - } -} impl FormatRule for crate::html::auxiliary::opening_element::FormatHtmlOpeningElement { diff --git a/crates/biome_html_formatter/src/html/auxiliary/mod.rs b/crates/biome_html_formatter/src/html/auxiliary/mod.rs index 6d4ca51826c5..5ab437627397 100644 --- a/crates/biome_html_formatter/src/html/auxiliary/mod.rs +++ b/crates/biome_html_formatter/src/html/auxiliary/mod.rs @@ -12,7 +12,6 @@ pub(crate) mod double_text_expression; pub(crate) mod element; pub(crate) mod embedded_content; pub(crate) mod member_name; -pub(crate) mod name; pub(crate) mod opening_element; pub(crate) mod root; pub(crate) mod self_closing_element; diff --git a/crates/biome_html_formatter/src/html/auxiliary/name.rs b/crates/biome_html_formatter/src/html/auxiliary/name.rs deleted file mode 100644 index e6a2a9b2f747..000000000000 --- a/crates/biome_html_formatter/src/html/auxiliary/name.rs +++ /dev/null @@ -1,9 +0,0 @@ -use crate::prelude::*; -use biome_html_syntax::HtmlName; -#[derive(Debug, Clone, Default)] -pub(crate) struct FormatHtmlName; -impl FormatNodeRule for FormatHtmlName { - fn fmt_fields(&self, node: &HtmlName, f: &mut HtmlFormatter) -> FormatResult<()> { - node.as_fields().ident_token.format().fmt(f) - } -} diff --git a/crates/biome_html_parser/src/syntax/astro.rs b/crates/biome_html_parser/src/syntax/astro.rs index 60d6c32f3340..fc3db10bb11d 100644 --- a/crates/biome_html_parser/src/syntax/astro.rs +++ b/crates/biome_html_parser/src/syntax/astro.rs @@ -1,7 +1,7 @@ use crate::parser::HtmlParser; use crate::syntax::HtmlSyntaxFeatures::Astro; -use crate::syntax::parse_error::expected_closed_fence; -use crate::syntax::{parse_name, parse_single_text_expression}; +use crate::syntax::parse_error::{expected_closed_fence, expected_expression}; +use crate::syntax::{TextExpression, parse_single_text_expression}; use crate::token_source::HtmlLexContext; use biome_html_syntax::HtmlSyntaxKind::{ ASTRO_EMBEDDED_CONTENT, ASTRO_FRONTMATTER_ELEMENT, FENCE, HTML_LITERAL, HTML_SPREAD_ATTRIBUTE, @@ -61,11 +61,13 @@ pub(crate) fn parse_astro_spread_or_expression(p: &mut HtmlParser) -> ParsedSynt p.bump_with_context(T!['{'], HtmlLexContext::Svelte); if p.at(T![...]) { - p.bump_with_context(T![...], HtmlLexContext::Svelte); - parse_name(p, HtmlLexContext::Svelte, |_| true).or_add_diagnostic(p, |p, range| { - p.err_builder("Expected a name after '...'", range) - }); + p.bump_with_context(T![...], HtmlLexContext::single_expression()); + TextExpression::new_single() + .parse_element(p) + .or_add_diagnostic(p, expected_expression); + p.expect_with_context(T!['}'], HtmlLexContext::InsideTag); + Present(m.complete(p, HTML_SPREAD_ATTRIBUTE)) } else { p.rewind(checkpoint); diff --git a/crates/biome_html_parser/src/syntax/mod.rs b/crates/biome_html_parser/src/syntax/mod.rs index 97172824dc44..2b6f97510352 100644 --- a/crates/biome_html_parser/src/syntax/mod.rs +++ b/crates/biome_html_parser/src/syntax/mod.rs @@ -736,28 +736,6 @@ fn parse_single_text_expression_content(p: &mut HtmlParser) -> ParsedSyntax { Present(m.complete(p, HTML_TEXT_EXPRESSION)) } -/// Parses a generic HTML name. -/// -/// # Arguments -/// - `next_context`: context to apply after bumping the name -/// - `should_bail`: condition to bail early -pub(crate) fn parse_name( - p: &mut HtmlParser, - next_context: HtmlLexContext, - should_bail_if: F, -) -> ParsedSyntax -where - F: FnOnce(&HtmlParser) -> bool, -{ - if !p.at(IDENT) && !should_bail_if(p) { - return Absent; - } - let m = p.start(); - p.bump_remap_with_context(IDENT, next_context); - - Present(m.complete(p, HTML_NAME)) -} - impl TextExpression { fn parse_element(&mut self, p: &mut HtmlParser) -> ParsedSyntax { if p.at(EOF) || p.at(T![<]) { diff --git a/crates/biome_html_parser/src/syntax/svelte.rs b/crates/biome_html_parser/src/syntax/svelte.rs index 639fc59441de..94837cad73fe 100644 --- a/crates/biome_html_parser/src/syntax/svelte.rs +++ b/crates/biome_html_parser/src/syntax/svelte.rs @@ -5,7 +5,7 @@ use crate::syntax::parse_error::{ expected_svelte_property, expected_text_expression, expected_valid_directive, }; use crate::syntax::{ - parse_attribute_initializer, parse_html_element, parse_name, parse_single_text_expression, + TextExpression, parse_attribute_initializer, parse_html_element, parse_single_text_expression, parse_single_text_expression_content, }; use crate::token_source::{HtmlLexContext, HtmlReLexContext, RestrictedExpressionStopAt}; @@ -374,11 +374,12 @@ pub(crate) fn parse_svelte_spread_or_expression(p: &mut HtmlParser) -> ParsedSyn p.bump_with_context(T!['{'], HtmlLexContext::Svelte); if p.at(T![...]) { - p.bump_with_context(T![...], HtmlLexContext::Svelte); - parse_name(p, HtmlLexContext::Svelte, is_at_svelte_keyword) - .or_add_diagnostic(p, |p, range| { - p.err_builder("Expected a name after '...'", range) - }); + p.bump_with_context(T![...], HtmlLexContext::single_expression()); + + TextExpression::new_single() + .parse_element(p) + .or_add_diagnostic(p, expected_expression); + p.expect_with_context(T!['}'], HtmlLexContext::InsideTag); Present(m.complete(p, HTML_SPREAD_ATTRIBUTE)) } else { diff --git a/crates/biome_html_parser/tests/html_specs/error/astro/spread.astro.snap b/crates/biome_html_parser/tests/html_specs/error/astro/spread.astro.snap index b7efe37552ea..cb7975395ff6 100644 --- a/crates/biome_html_parser/tests/html_specs/error/astro/spread.astro.snap +++ b/crates/biome_html_parser/tests/html_specs/error/astro/spread.astro.snap @@ -28,17 +28,11 @@ HtmlRoot { HtmlSpreadAttribute { l_curly_token: L_CURLY@7..8 "{" [] [], dotdotdot_token: DOT3@8..11 "..." [] [], - argument: HtmlName { - ident_token: IDENT@11..17 "props" [] [Whitespace(" ")], + argument: HtmlTextExpression { + html_literal_token: HTML_LITERAL@11..43 "props >\n\n" [] [], }, r_curly_token: missing (required), }, - HtmlAttribute { - name: HtmlAttributeName { - value_token: HTML_LITERAL@17..43 ">\n\n" [] [], - }, - initializer: missing (optional), - }, ], slash_token: missing (optional), r_angle_token: missing (required), @@ -61,16 +55,12 @@ HtmlRoot { 1: HTML_TAG_NAME@1..7 0: HTML_LITERAL@1..7 "input" [] [Whitespace(" ")] 2: HTML_ATTRIBUTE_LIST@7..43 - 0: HTML_SPREAD_ATTRIBUTE@7..17 + 0: HTML_SPREAD_ATTRIBUTE@7..43 0: L_CURLY@7..8 "{" [] [] 1: DOT3@8..11 "..." [] [] - 2: HTML_NAME@11..17 - 0: IDENT@11..17 "props" [] [Whitespace(" ")] + 2: HTML_TEXT_EXPRESSION@11..43 + 0: HTML_LITERAL@11..43 "props >\n\n" [] [] 3: (empty) - 1: HTML_ATTRIBUTE@17..43 - 0: HTML_ATTRIBUTE_NAME@17..43 - 0: HTML_LITERAL@17..43 ">\n\n" [] [] - 1: (empty) 3: (empty) 4: (empty) 4: EOF@43..43 "" [] [] @@ -80,25 +70,9 @@ HtmlRoot { ## Diagnostics ``` -spread.astro:1:18 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - - × expected `}` but instead found `> - - ` - - > 1 │ - │ ^ - > 2 │ - > 3 │ - │ - - i Remove > - - - spread.astro:3:1 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - × expected `>` but instead the file ends + × expected `}` but instead the file ends 1 │ 2 │ diff --git a/crates/biome_html_parser/tests/html_specs/error/svelte/spread.svelte.snap b/crates/biome_html_parser/tests/html_specs/error/svelte/spread.svelte.snap index 4e3f24707153..57932e17da63 100644 --- a/crates/biome_html_parser/tests/html_specs/error/svelte/spread.svelte.snap +++ b/crates/biome_html_parser/tests/html_specs/error/svelte/spread.svelte.snap @@ -28,7 +28,9 @@ HtmlRoot { HtmlSpreadAttribute { l_curly_token: L_CURLY@7..8 "{" [] [], dotdotdot_token: DOT3@8..11 "..." [] [], - argument: missing (required), + argument: HtmlTextExpression { + html_literal_token: HTML_LITERAL@11..11 "" [] [], + }, r_curly_token: R_CURLY@11..13 "}" [] [Whitespace(" ")], }, ], @@ -45,17 +47,11 @@ HtmlRoot { HtmlSpreadAttribute { l_curly_token: L_CURLY@27..28 "{" [] [], dotdotdot_token: DOT3@28..31 "..." [] [], - argument: HtmlName { - ident_token: IDENT@31..37 "props" [] [Whitespace(" ")], + argument: HtmlTextExpression { + html_literal_token: HTML_LITERAL@31..40 "props />\n" [] [], }, r_curly_token: missing (required), }, - HtmlAttribute { - name: HtmlAttributeName { - value_token: HTML_LITERAL@37..40 "/>\n" [] [], - }, - initializer: missing (optional), - }, ], r_angle_token: missing (required), }, @@ -83,7 +79,8 @@ HtmlRoot { 0: HTML_SPREAD_ATTRIBUTE@7..13 0: L_CURLY@7..8 "{" [] [] 1: DOT3@8..11 "..." [] [] - 2: (empty) + 2: HTML_TEXT_EXPRESSION@11..11 + 0: HTML_LITERAL@11..11 "" [] [] 3: R_CURLY@11..13 "}" [] [Whitespace(" ")] 3: SLASH@13..14 "/" [] [] 4: R_ANGLE@14..15 ">" [] [] @@ -93,16 +90,12 @@ HtmlRoot { 1: HTML_COMPONENT_NAME@17..27 0: HTML_LITERAL@17..27 "Component" [] [Whitespace(" ")] 2: HTML_ATTRIBUTE_LIST@27..40 - 0: HTML_SPREAD_ATTRIBUTE@27..37 + 0: HTML_SPREAD_ATTRIBUTE@27..40 0: L_CURLY@27..28 "{" [] [] 1: DOT3@28..31 "..." [] [] - 2: HTML_NAME@31..37 - 0: IDENT@31..37 "props" [] [Whitespace(" ")] + 2: HTML_TEXT_EXPRESSION@31..40 + 0: HTML_LITERAL@31..40 "props />\n" [] [] 3: (empty) - 1: HTML_ATTRIBUTE@37..40 - 0: HTML_ATTRIBUTE_NAME@37..40 - 0: HTML_LITERAL@37..40 "/>\n" [] [] - 1: (empty) 3: (empty) 1: HTML_ELEMENT_LIST@40..40 2: (empty) @@ -113,32 +106,9 @@ HtmlRoot { ## Diagnostics ``` -spread.svelte:1:12 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - - × Expected a name after '...' - - > 1 │ - │ ^ - 2 │ - 3 │ - -spread.svelte:2:22 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - - × expected `}` but instead found `/> - ` - - 1 │ - > 2 │ - │ ^^ - > 3 │ - │ - - i Remove /> - - spread.svelte:3:1 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - × expected `>` but instead the file ends + × expected `}` but instead the file ends 1 │ 2 │ diff --git a/crates/biome_html_parser/tests/html_specs/ok/astro/spread.astro.snap b/crates/biome_html_parser/tests/html_specs/ok/astro/spread.astro.snap index 8cf6b6568f5a..e60f34126fe1 100644 --- a/crates/biome_html_parser/tests/html_specs/ok/astro/spread.astro.snap +++ b/crates/biome_html_parser/tests/html_specs/ok/astro/spread.astro.snap @@ -28,8 +28,8 @@ HtmlRoot { HtmlSpreadAttribute { l_curly_token: L_CURLY@7..8 "{" [] [], dotdotdot_token: DOT3@8..11 "..." [] [], - argument: HtmlName { - ident_token: IDENT@11..16 "props" [] [], + argument: HtmlTextExpression { + html_literal_token: HTML_LITERAL@11..16 "props" [] [], }, r_curly_token: R_CURLY@16..18 "}" [] [Whitespace(" ")], }, @@ -46,8 +46,8 @@ HtmlRoot { HtmlSpreadAttribute { l_curly_token: L_CURLY@31..32 "{" [] [], dotdotdot_token: DOT3@32..35 "..." [] [], - argument: HtmlName { - ident_token: IDENT@35..40 "props" [] [], + argument: HtmlTextExpression { + html_literal_token: HTML_LITERAL@35..40 "props" [] [], }, r_curly_token: R_CURLY@40..42 "}" [] [Whitespace(" ")], }, @@ -76,8 +76,8 @@ HtmlRoot { 0: HTML_SPREAD_ATTRIBUTE@7..18 0: L_CURLY@7..8 "{" [] [] 1: DOT3@8..11 "..." [] [] - 2: HTML_NAME@11..16 - 0: IDENT@11..16 "props" [] [] + 2: HTML_TEXT_EXPRESSION@11..16 + 0: HTML_LITERAL@11..16 "props" [] [] 3: R_CURLY@16..18 "}" [] [Whitespace(" ")] 3: (empty) 4: R_ANGLE@18..19 ">" [] [] @@ -89,8 +89,8 @@ HtmlRoot { 0: HTML_SPREAD_ATTRIBUTE@31..42 0: L_CURLY@31..32 "{" [] [] 1: DOT3@32..35 "..." [] [] - 2: HTML_NAME@35..40 - 0: IDENT@35..40 "props" [] [] + 2: HTML_TEXT_EXPRESSION@35..40 + 0: HTML_LITERAL@35..40 "props" [] [] 3: R_CURLY@40..42 "}" [] [Whitespace(" ")] 3: SLASH@42..43 "/" [] [] 4: R_ANGLE@43..44 ">" [] [] diff --git a/crates/biome_html_parser/tests/html_specs/ok/svelte/shorthand-spread-props.svelte.snap b/crates/biome_html_parser/tests/html_specs/ok/svelte/shorthand-spread-props.svelte.snap index 412f32dd6a35..d0a083d92d1e 100644 --- a/crates/biome_html_parser/tests/html_specs/ok/svelte/shorthand-spread-props.svelte.snap +++ b/crates/biome_html_parser/tests/html_specs/ok/svelte/shorthand-spread-props.svelte.snap @@ -28,8 +28,8 @@ HtmlRoot { HtmlSpreadAttribute { l_curly_token: L_CURLY@8..9 "{" [] [], dotdotdot_token: DOT3@9..12 "..." [] [], - argument: HtmlName { - ident_token: IDENT@12..17 "props" [] [], + argument: HtmlTextExpression { + html_literal_token: HTML_LITERAL@12..17 "props" [] [], }, r_curly_token: R_CURLY@17..18 "}" [] [], }, @@ -72,8 +72,8 @@ HtmlRoot { 0: HTML_SPREAD_ATTRIBUTE@8..18 0: L_CURLY@8..9 "{" [] [] 1: DOT3@9..12 "..." [] [] - 2: HTML_NAME@12..17 - 0: IDENT@12..17 "props" [] [] + 2: HTML_TEXT_EXPRESSION@12..17 + 0: HTML_LITERAL@12..17 "props" [] [] 3: R_CURLY@17..18 "}" [] [] 3: R_ANGLE@18..19 ">" [] [] 1: HTML_ELEMENT_LIST@19..27 diff --git a/crates/biome_html_parser/tests/html_specs/ok/svelte/spread.svelte b/crates/biome_html_parser/tests/html_specs/ok/svelte/spread.svelte index b4daabf5507c..6095f7ff303d 100644 --- a/crates/biome_html_parser/tests/html_specs/ok/svelte/spread.svelte +++ b/crates/biome_html_parser/tests/html_specs/ok/svelte/spread.svelte @@ -1,2 +1,3 @@ + diff --git a/crates/biome_html_parser/tests/html_specs/ok/svelte/spread.svelte.snap b/crates/biome_html_parser/tests/html_specs/ok/svelte/spread.svelte.snap index 494e64af186b..81925398af89 100644 --- a/crates/biome_html_parser/tests/html_specs/ok/svelte/spread.svelte.snap +++ b/crates/biome_html_parser/tests/html_specs/ok/svelte/spread.svelte.snap @@ -7,6 +7,7 @@ expression: snapshot ```svelte + ``` @@ -28,8 +29,8 @@ HtmlRoot { HtmlSpreadAttribute { l_curly_token: L_CURLY@7..8 "{" [] [], dotdotdot_token: DOT3@8..11 "..." [] [], - argument: HtmlName { - ident_token: IDENT@11..14 "foo" [] [], + argument: HtmlTextExpression { + html_literal_token: HTML_LITERAL@11..14 "foo" [] [], }, r_curly_token: R_CURLY@14..16 "}" [] [Whitespace(" ")], }, @@ -46,8 +47,8 @@ HtmlRoot { HtmlSpreadAttribute { l_curly_token: L_CURLY@30..31 "{" [] [], dotdotdot_token: DOT3@31..34 "..." [] [], - argument: HtmlName { - ident_token: IDENT@34..37 "foo" [] [], + argument: HtmlTextExpression { + html_literal_token: HTML_LITERAL@34..37 "foo" [] [], }, r_curly_token: R_CURLY@37..39 "}" [] [Whitespace(" ")], }, @@ -55,19 +56,37 @@ HtmlRoot { slash_token: SLASH@39..40 "/" [] [], r_angle_token: R_ANGLE@40..41 ">" [] [], }, + HtmlSelfClosingElement { + l_angle_token: L_ANGLE@41..43 "<" [Newline("\n")] [], + name: HtmlComponentName { + value_token: HTML_LITERAL@43..53 "Component" [] [Whitespace(" ")], + }, + attributes: HtmlAttributeList [ + HtmlSpreadAttribute { + l_curly_token: L_CURLY@53..54 "{" [] [], + dotdotdot_token: DOT3@54..57 "..." [] [], + argument: HtmlTextExpression { + html_literal_token: HTML_LITERAL@57..70 "foo ? [] : []" [] [], + }, + r_curly_token: R_CURLY@70..72 "}" [] [Whitespace(" ")], + }, + ], + slash_token: SLASH@72..73 "/" [] [], + r_angle_token: R_ANGLE@73..74 ">" [] [], + }, ], - eof_token: EOF@41..42 "" [Newline("\n")] [], + eof_token: EOF@74..75 "" [Newline("\n")] [], } ``` ## CST ``` -0: HTML_ROOT@0..42 +0: HTML_ROOT@0..75 0: (empty) 1: (empty) 2: (empty) - 3: HTML_ELEMENT_LIST@0..41 + 3: HTML_ELEMENT_LIST@0..74 0: HTML_SELF_CLOSING_ELEMENT@0..18 0: L_ANGLE@0..1 "<" [] [] 1: HTML_TAG_NAME@1..7 @@ -76,8 +95,8 @@ HtmlRoot { 0: HTML_SPREAD_ATTRIBUTE@7..16 0: L_CURLY@7..8 "{" [] [] 1: DOT3@8..11 "..." [] [] - 2: HTML_NAME@11..14 - 0: IDENT@11..14 "foo" [] [] + 2: HTML_TEXT_EXPRESSION@11..14 + 0: HTML_LITERAL@11..14 "foo" [] [] 3: R_CURLY@14..16 "}" [] [Whitespace(" ")] 3: SLASH@16..17 "/" [] [] 4: R_ANGLE@17..18 ">" [] [] @@ -89,11 +108,24 @@ HtmlRoot { 0: HTML_SPREAD_ATTRIBUTE@30..39 0: L_CURLY@30..31 "{" [] [] 1: DOT3@31..34 "..." [] [] - 2: HTML_NAME@34..37 - 0: IDENT@34..37 "foo" [] [] + 2: HTML_TEXT_EXPRESSION@34..37 + 0: HTML_LITERAL@34..37 "foo" [] [] 3: R_CURLY@37..39 "}" [] [Whitespace(" ")] 3: SLASH@39..40 "/" [] [] 4: R_ANGLE@40..41 ">" [] [] - 4: EOF@41..42 "" [Newline("\n")] [] + 2: HTML_SELF_CLOSING_ELEMENT@41..74 + 0: L_ANGLE@41..43 "<" [Newline("\n")] [] + 1: HTML_COMPONENT_NAME@43..53 + 0: HTML_LITERAL@43..53 "Component" [] [Whitespace(" ")] + 2: HTML_ATTRIBUTE_LIST@53..72 + 0: HTML_SPREAD_ATTRIBUTE@53..72 + 0: L_CURLY@53..54 "{" [] [] + 1: DOT3@54..57 "..." [] [] + 2: HTML_TEXT_EXPRESSION@57..70 + 0: HTML_LITERAL@57..70 "foo ? [] : []" [] [] + 3: R_CURLY@70..72 "}" [] [Whitespace(" ")] + 3: SLASH@72..73 "/" [] [] + 4: R_ANGLE@73..74 ">" [] [] + 4: EOF@74..75 "" [Newline("\n")] [] ``` diff --git a/crates/biome_html_syntax/src/generated/kind.rs b/crates/biome_html_syntax/src/generated/kind.rs index 5f3dd9a2f71e..b0b4f72f5ac1 100644 --- a/crates/biome_html_syntax/src/generated/kind.rs +++ b/crates/biome_html_syntax/src/generated/kind.rs @@ -96,7 +96,6 @@ pub enum HtmlSyntaxKind { HTML_SINGLE_TEXT_EXPRESSION, HTML_TEXT_EXPRESSION, HTML_SPREAD_ATTRIBUTE, - HTML_NAME, ASTRO_FRONTMATTER_ELEMENT, ASTRO_EMBEDDED_CONTENT, SVELTE_DEBUG_BLOCK, diff --git a/crates/biome_html_syntax/src/generated/macros.rs b/crates/biome_html_syntax/src/generated/macros.rs index 1bdc6ea5057b..bab2c31d41ae 100644 --- a/crates/biome_html_syntax/src/generated/macros.rs +++ b/crates/biome_html_syntax/src/generated/macros.rs @@ -73,10 +73,6 @@ macro_rules! map_syntax_node { let $pattern = unsafe { $crate::HtmlMemberName::new_unchecked(node) }; $body } - $crate::HtmlSyntaxKind::HTML_NAME => { - let $pattern = unsafe { $crate::HtmlName::new_unchecked(node) }; - $body - } $crate::HtmlSyntaxKind::HTML_OPENING_ELEMENT => { let $pattern = unsafe { $crate::HtmlOpeningElement::new_unchecked(node) }; $body diff --git a/crates/biome_html_syntax/src/generated/nodes.rs b/crates/biome_html_syntax/src/generated/nodes.rs index 3317795a1dc3..d44346324b0f 100644 --- a/crates/biome_html_syntax/src/generated/nodes.rs +++ b/crates/biome_html_syntax/src/generated/nodes.rs @@ -620,41 +620,6 @@ pub struct HtmlMemberNameFields { pub member: SyntaxResult, } #[derive(Clone, PartialEq, Eq, Hash)] -pub struct HtmlName { - pub(crate) syntax: SyntaxNode, -} -impl HtmlName { - #[doc = r" Create an AstNode from a SyntaxNode without checking its kind"] - #[doc = r""] - #[doc = r" # Safety"] - #[doc = r" This function must be guarded with a call to [AstNode::can_cast]"] - #[doc = r" or a match on [SyntaxNode::kind]"] - #[inline] - pub const unsafe fn new_unchecked(syntax: SyntaxNode) -> Self { - Self { syntax } - } - pub fn as_fields(&self) -> HtmlNameFields { - HtmlNameFields { - ident_token: self.ident_token(), - } - } - pub fn ident_token(&self) -> SyntaxResult { - support::required_token(&self.syntax, 0usize) - } -} -impl Serialize for HtmlName { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - self.as_fields().serialize(serializer) - } -} -#[derive(Serialize)] -pub struct HtmlNameFields { - pub ident_token: SyntaxResult, -} -#[derive(Clone, PartialEq, Eq, Hash)] pub struct HtmlOpeningElement { pub(crate) syntax: SyntaxNode, } @@ -887,7 +852,7 @@ impl HtmlSpreadAttribute { pub fn dotdotdot_token(&self) -> SyntaxResult { support::required_token(&self.syntax, 1usize) } - pub fn argument(&self) -> SyntaxResult { + pub fn argument(&self) -> SyntaxResult { support::required_node(&self.syntax, 2usize) } pub fn r_curly_token(&self) -> SyntaxResult { @@ -906,7 +871,7 @@ impl Serialize for HtmlSpreadAttribute { pub struct HtmlSpreadAttributeFields { pub l_curly_token: SyntaxResult, pub dotdotdot_token: SyntaxResult, - pub argument: SyntaxResult, + pub argument: SyntaxResult, pub r_curly_token: SyntaxResult, } #[derive(Clone, PartialEq, Eq, Hash)] @@ -4769,56 +4734,6 @@ impl From for SyntaxElement { n.syntax.into() } } -impl AstNode for HtmlName { - type Language = Language; - const KIND_SET: SyntaxKindSet = - SyntaxKindSet::from_raw(RawSyntaxKind(HTML_NAME as u16)); - fn can_cast(kind: SyntaxKind) -> bool { - kind == HTML_NAME - } - fn cast(syntax: SyntaxNode) -> Option { - if Self::can_cast(syntax.kind()) { - Some(Self { syntax }) - } else { - None - } - } - fn syntax(&self) -> &SyntaxNode { - &self.syntax - } - fn into_syntax(self) -> SyntaxNode { - self.syntax - } -} -impl std::fmt::Debug for HtmlName { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - thread_local! { static DEPTH : std :: cell :: Cell < u8 > = const { std :: cell :: Cell :: new (0) } }; - let current_depth = DEPTH.get(); - let result = if current_depth < 16 { - DEPTH.set(current_depth + 1); - f.debug_struct("HtmlName") - .field( - "ident_token", - &support::DebugSyntaxResult(self.ident_token()), - ) - .finish() - } else { - f.debug_struct("HtmlName").finish() - }; - DEPTH.set(current_depth); - result - } -} -impl From for SyntaxNode { - fn from(n: HtmlName) -> Self { - n.syntax - } -} -impl From for SyntaxElement { - fn from(n: HtmlName) -> Self { - n.syntax.into() - } -} impl AstNode for HtmlOpeningElement { type Language = Language; const KIND_SET: SyntaxKindSet = @@ -9843,11 +9758,6 @@ impl std::fmt::Display for HtmlMemberName { std::fmt::Display::fmt(self.syntax(), f) } } -impl std::fmt::Display for HtmlName { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - std::fmt::Display::fmt(self.syntax(), f) - } -} impl std::fmt::Display for HtmlOpeningElement { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { std::fmt::Display::fmt(self.syntax(), f) diff --git a/crates/biome_html_syntax/src/generated/nodes_mut.rs b/crates/biome_html_syntax/src/generated/nodes_mut.rs index eaf32a436ecd..64a24326c6fa 100644 --- a/crates/biome_html_syntax/src/generated/nodes_mut.rs +++ b/crates/biome_html_syntax/src/generated/nodes_mut.rs @@ -247,14 +247,6 @@ impl HtmlMemberName { ) } } -impl HtmlName { - pub fn with_ident_token(self, element: SyntaxToken) -> Self { - Self::unwrap_cast( - self.syntax - .splice_slots(0usize..=0usize, once(Some(element.into()))), - ) - } -} impl HtmlOpeningElement { pub fn with_l_angle_token(self, element: SyntaxToken) -> Self { Self::unwrap_cast( @@ -378,7 +370,7 @@ impl HtmlSpreadAttribute { .splice_slots(1usize..=1usize, once(Some(element.into()))), ) } - pub fn with_argument(self, element: HtmlName) -> Self { + pub fn with_argument(self, element: HtmlTextExpression) -> Self { Self::unwrap_cast( self.syntax .splice_slots(2usize..=2usize, once(Some(element.into_syntax().into()))), diff --git a/crates/biome_service/src/workspace/document/services/embedded_value_references.rs b/crates/biome_service/src/workspace/document/services/embedded_value_references.rs index fa90c46500eb..4cfc1db1c5ad 100644 --- a/crates/biome_service/src/workspace/document/services/embedded_value_references.rs +++ b/crates/biome_service/src/workspace/document/services/embedded_value_references.rs @@ -1,6 +1,5 @@ use biome_html_syntax::{ AnyHtmlComponentObjectName, AnyHtmlTagName, HtmlElement, HtmlRoot, HtmlSelfClosingElement, - HtmlSpreadAttribute, }; use biome_js_syntax::{ AnyJsIdentifierUsage, AnyJsRoot, JsReferenceIdentifier, JsStaticMemberExpression, @@ -69,24 +68,9 @@ impl EmbeddedValueReferencesBuilder { if let Some(element) = HtmlSelfClosingElement::cast_ref(&node) { self.visit_html_self_closing_element(&element); } - - if let Some(spread_attribute) = HtmlSpreadAttribute::cast_ref(&node) { - self.visit_spread_attribute(&spread_attribute); - } } } - fn visit_spread_attribute(&mut self, attribute: &HtmlSpreadAttribute) -> Option<()> { - let argument = attribute.argument().ok()?; - - let name = argument.ident_token().ok()?; - - self.references - .insert(name.text_trimmed_range(), name.token_text_trimmed()); - - Some(()) - } - fn visit_html_element(&mut self, element: &HtmlElement) -> Option<()> { // Skip script and style tags - these are not component references if element.is_script_tag() || element.is_style_tag() { diff --git a/xtask/codegen/html.ungram b/xtask/codegen/html.ungram index 35aae1da35b4..d8802387d973 100644 --- a/xtask/codegen/html.ungram +++ b/xtask/codegen/html.ungram @@ -209,8 +209,6 @@ AnyHtmlAttributeInitializer = | HtmlSingleTextExpression -HtmlName = 'ident' - // ================================== // Svelte // ================================== @@ -600,7 +598,7 @@ SvelteDirectiveModifier = HtmlSpreadAttribute = '{' '...' - argument: HtmlName + argument: HtmlTextExpression '}' diff --git a/xtask/codegen/src/html_kinds_src.rs b/xtask/codegen/src/html_kinds_src.rs index a60fd9688981..ffba3bdfdfb9 100644 --- a/xtask/codegen/src/html_kinds_src.rs +++ b/xtask/codegen/src/html_kinds_src.rs @@ -90,7 +90,6 @@ pub const HTML_KINDS_SRC: KindsSrc = KindsSrc { "HTML_SINGLE_TEXT_EXPRESSION", "HTML_TEXT_EXPRESSION", "HTML_SPREAD_ATTRIBUTE", - "HTML_NAME", // Astro nodes "ASTRO_FRONTMATTER_ELEMENT", "ASTRO_EMBEDDED_CONTENT",