From 7300cbca8946750005cec10a984d3191481821f3 Mon Sep 17 00:00:00 2001 From: Carson McManus Date: Thu, 7 May 2026 15:54:41 -0400 Subject: [PATCH] feat(parse/html): parse svelte function bindings more precisely --- .changeset/fix-svelte-bind-comma.md | 5 + .../src/generated/node_factory.rs | 34 +- .../src/generated/syntax_factory.rs | 75 +++- crates/biome_html_formatter/src/generated.rs | 107 ++++++ .../any/directive_initializer_clause.rs | 24 ++ .../src/svelte/any/mod.rs | 1 + .../bind_function_binding_expression.rs | 38 +++ ...ind_function_binding_initializer_clause.rs | 22 ++ .../src/svelte/auxiliary/mod.rs | 2 + .../src/svelte/value/directive_value.rs | 8 +- crates/biome_html_parser/src/syntax/svelte.rs | 97 +++++- crates/biome_html_parser/src/token_source.rs | 5 +- .../ok/svelte/directives/bind_function.svelte | 6 + .../directives/bind_function.svelte.snap | 238 +++++++++++-- crates/biome_html_syntax/src/directive_ext.rs | 7 +- .../biome_html_syntax/src/generated/kind.rs | 2 + .../biome_html_syntax/src/generated/macros.rs | 11 + .../biome_html_syntax/src/generated/nodes.rs | 320 +++++++++++++++++- .../src/generated/nodes_mut.rs | 48 ++- .../invalid-svelte-template.svelte | 1 + .../invalid-svelte-template.svelte.snap | 20 +- .../valid-svelte-bind-function.svelte | 14 + .../valid-svelte-bind-function.svelte.snap | 22 ++ .../html/parse_embedded_nodes.rs | 80 ++++- xtask/codegen/html.ungram | 17 +- xtask/codegen/src/html_kinds_src.rs | 2 + 26 files changed, 1152 insertions(+), 54 deletions(-) create mode 100644 .changeset/fix-svelte-bind-comma.md create mode 100644 crates/biome_html_formatter/src/svelte/any/directive_initializer_clause.rs create mode 100644 crates/biome_html_formatter/src/svelte/auxiliary/bind_function_binding_expression.rs create mode 100644 crates/biome_html_formatter/src/svelte/auxiliary/bind_function_binding_initializer_clause.rs create mode 100644 crates/biome_js_analyze/tests/specs/complexity/noCommaOperator/valid-svelte-bind-function.svelte create mode 100644 crates/biome_js_analyze/tests/specs/complexity/noCommaOperator/valid-svelte-bind-function.svelte.snap diff --git a/.changeset/fix-svelte-bind-comma.md b/.changeset/fix-svelte-bind-comma.md new file mode 100644 index 000000000000..6dec97589c38 --- /dev/null +++ b/.changeset/fix-svelte-bind-comma.md @@ -0,0 +1,5 @@ +--- +"@biomejs/biome": patch +--- + +Fixed [#10265](https://github.com/biomejs/biome/issues/10265): Svelte function bindings such as `bind:value={get, set}` are now parsed more precisely, so [`noCommaOperator`](https://biomejs.dev/linter/rules/no-comma-operator/) won't emit false positives for that syntax anymore. diff --git a/crates/biome_html_factory/src/generated/node_factory.rs b/crates/biome_html_factory/src/generated/node_factory.rs index d8ce05ad75d6..4e8084922b26 100644 --- a/crates/biome_html_factory/src/generated/node_factory.rs +++ b/crates/biome_html_factory/src/generated/node_factory.rs @@ -692,6 +692,36 @@ pub fn svelte_bind_directive( ], )) } +pub fn svelte_bind_function_binding_expression( + l_curly_token: SyntaxToken, + get: HtmlTextExpression, + comma_token: SyntaxToken, + set: HtmlTextExpression, + r_curly_token: SyntaxToken, +) -> SvelteBindFunctionBindingExpression { + SvelteBindFunctionBindingExpression::unwrap_cast(SyntaxNode::new_detached( + HtmlSyntaxKind::SVELTE_BIND_FUNCTION_BINDING_EXPRESSION, + [ + Some(SyntaxElement::Token(l_curly_token)), + Some(SyntaxElement::Node(get.into_syntax())), + Some(SyntaxElement::Token(comma_token)), + Some(SyntaxElement::Node(set.into_syntax())), + Some(SyntaxElement::Token(r_curly_token)), + ], + )) +} +pub fn svelte_bind_function_binding_initializer_clause( + eq_token: SyntaxToken, + value: SvelteBindFunctionBindingExpression, +) -> SvelteBindFunctionBindingInitializerClause { + SvelteBindFunctionBindingInitializerClause::unwrap_cast(SyntaxNode::new_detached( + HtmlSyntaxKind::SVELTE_BIND_FUNCTION_BINDING_INITIALIZER_CLAUSE, + [ + Some(SyntaxElement::Token(eq_token)), + Some(SyntaxElement::Node(value.into_syntax())), + ], + )) +} pub fn svelte_class_directive( class_token: SyntaxToken, value: SvelteDirectiveValue, @@ -778,10 +808,10 @@ pub struct SvelteDirectiveValueBuilder { colon_token: SyntaxToken, property: AnySvelteBindingProperty, modifiers: SvelteDirectiveModifierList, - initializer: Option, + initializer: Option, } impl SvelteDirectiveValueBuilder { - pub fn with_initializer(mut self, initializer: HtmlAttributeInitializerClause) -> Self { + pub fn with_initializer(mut self, initializer: AnySvelteDirectiveInitializerClause) -> Self { self.initializer = Some(initializer); self } diff --git a/crates/biome_html_factory/src/generated/syntax_factory.rs b/crates/biome_html_factory/src/generated/syntax_factory.rs index 433a2f36c04a..61daed43213d 100644 --- a/crates/biome_html_factory/src/generated/syntax_factory.rs +++ b/crates/biome_html_factory/src/generated/syntax_factory.rs @@ -1328,6 +1328,79 @@ impl SyntaxFactory for HtmlSyntaxFactory { } slots.into_node(SVELTE_BIND_DIRECTIVE, children) } + SVELTE_BIND_FUNCTION_BINDING_EXPRESSION => { + let mut elements = (&children).into_iter(); + let mut slots: RawNodeSlots<5usize> = 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 + && HtmlTextExpression::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 let Some(element) = ¤t_element + && HtmlTextExpression::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( + SVELTE_BIND_FUNCTION_BINDING_EXPRESSION.to_bogus(), + children.into_iter().map(Some), + ); + } + slots.into_node(SVELTE_BIND_FUNCTION_BINDING_EXPRESSION, children) + } + SVELTE_BIND_FUNCTION_BINDING_INITIALIZER_CLAUSE => { + let mut elements = (&children).into_iter(); + let mut slots: RawNodeSlots<2usize> = 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 + && SvelteBindFunctionBindingExpression::can_cast(element.kind()) + { + slots.mark_present(); + current_element = elements.next(); + } + slots.next_slot(); + if current_element.is_some() { + return RawSyntaxNode::new( + SVELTE_BIND_FUNCTION_BINDING_INITIALIZER_CLAUSE.to_bogus(), + children.into_iter().map(Some), + ); + } + slots.into_node(SVELTE_BIND_FUNCTION_BINDING_INITIALIZER_CLAUSE, children) + } SVELTE_CLASS_DIRECTIVE => { let mut elements = (&children).into_iter(); let mut slots: RawNodeSlots<2usize> = RawNodeSlots::default(); @@ -1519,7 +1592,7 @@ impl SyntaxFactory for HtmlSyntaxFactory { } slots.next_slot(); if let Some(element) = ¤t_element - && HtmlAttributeInitializerClause::can_cast(element.kind()) + && AnySvelteDirectiveInitializerClause::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 7b9eeb9f405f..d0f932790c88 100644 --- a/crates/biome_html_formatter/src/generated.rs +++ b/crates/biome_html_formatter/src/generated.rs @@ -3490,6 +3490,113 @@ impl IntoFormat for biome_html_syntax::VueVSlotShorthandDirec FormatOwnedWithRule :: new (self , crate :: vue :: auxiliary :: v_slot_shorthand_directive :: FormatVueVSlotShorthandDirective :: default ()) } } +impl FormatRule + for crate::svelte::auxiliary::bind_function_binding_expression::FormatSvelteBindFunctionBindingExpression +{ + type Context = HtmlFormatContext; + #[inline(always)] + fn fmt( + &self, + node: &biome_html_syntax::SvelteBindFunctionBindingExpression, + f: &mut HtmlFormatter, + ) -> FormatResult<()> { + FormatNodeRule::::fmt( + self, node, f, + ) + } +} +impl AsFormat for biome_html_syntax::SvelteBindFunctionBindingExpression { + type Format<'a> = FormatRefWithRule< + 'a, + biome_html_syntax::SvelteBindFunctionBindingExpression, + crate::svelte::auxiliary::bind_function_binding_expression::FormatSvelteBindFunctionBindingExpression, + >; + fn format(&self) -> Self::Format<'_> { + FormatRefWithRule::new( + self, + crate::svelte::auxiliary::bind_function_binding_expression::FormatSvelteBindFunctionBindingExpression::default(), + ) + } +} +impl IntoFormat for biome_html_syntax::SvelteBindFunctionBindingExpression { + type Format = FormatOwnedWithRule< + biome_html_syntax::SvelteBindFunctionBindingExpression, + crate::svelte::auxiliary::bind_function_binding_expression::FormatSvelteBindFunctionBindingExpression, + >; + fn into_format(self) -> Self::Format { + FormatOwnedWithRule::new( + self, + crate::svelte::auxiliary::bind_function_binding_expression::FormatSvelteBindFunctionBindingExpression::default(), + ) + } +} +impl FormatRule + for crate::svelte::auxiliary::bind_function_binding_initializer_clause::FormatSvelteBindFunctionBindingInitializerClause +{ + type Context = HtmlFormatContext; + #[inline(always)] + fn fmt( + &self, + node: &biome_html_syntax::SvelteBindFunctionBindingInitializerClause, + f: &mut HtmlFormatter, + ) -> FormatResult<()> { + FormatNodeRule::::fmt( + self, node, f, + ) + } +} +impl AsFormat for biome_html_syntax::SvelteBindFunctionBindingInitializerClause { + type Format<'a> = FormatRefWithRule< + 'a, + biome_html_syntax::SvelteBindFunctionBindingInitializerClause, + crate::svelte::auxiliary::bind_function_binding_initializer_clause::FormatSvelteBindFunctionBindingInitializerClause, + >; + fn format(&self) -> Self::Format<'_> { + FormatRefWithRule::new( + self, + crate::svelte::auxiliary::bind_function_binding_initializer_clause::FormatSvelteBindFunctionBindingInitializerClause::default(), + ) + } +} +impl IntoFormat + for biome_html_syntax::SvelteBindFunctionBindingInitializerClause +{ + type Format = FormatOwnedWithRule< + biome_html_syntax::SvelteBindFunctionBindingInitializerClause, + crate::svelte::auxiliary::bind_function_binding_initializer_clause::FormatSvelteBindFunctionBindingInitializerClause, + >; + fn into_format(self) -> Self::Format { + FormatOwnedWithRule::new( + self, + crate::svelte::auxiliary::bind_function_binding_initializer_clause::FormatSvelteBindFunctionBindingInitializerClause::default(), + ) + } +} +impl AsFormat for biome_html_syntax::AnySvelteDirectiveInitializerClause { + type Format<'a> = FormatRefWithRule< + 'a, + biome_html_syntax::AnySvelteDirectiveInitializerClause, + crate::svelte::any::directive_initializer_clause::FormatAnySvelteDirectiveInitializerClause, + >; + fn format(&self) -> Self::Format<'_> { + FormatRefWithRule::new( + self, + crate::svelte::any::directive_initializer_clause::FormatAnySvelteDirectiveInitializerClause::default(), + ) + } +} +impl IntoFormat for biome_html_syntax::AnySvelteDirectiveInitializerClause { + type Format = FormatOwnedWithRule< + biome_html_syntax::AnySvelteDirectiveInitializerClause, + crate::svelte::any::directive_initializer_clause::FormatAnySvelteDirectiveInitializerClause, + >; + fn into_format(self) -> Self::Format { + FormatOwnedWithRule::new( + self, + crate::svelte::any::directive_initializer_clause::FormatAnySvelteDirectiveInitializerClause::default(), + ) + } +} impl AsFormat for biome_html_syntax::HtmlAttributeList { type Format<'a> = FormatRefWithRule< 'a, diff --git a/crates/biome_html_formatter/src/svelte/any/directive_initializer_clause.rs b/crates/biome_html_formatter/src/svelte/any/directive_initializer_clause.rs new file mode 100644 index 000000000000..ac91b265f325 --- /dev/null +++ b/crates/biome_html_formatter/src/svelte/any/directive_initializer_clause.rs @@ -0,0 +1,24 @@ +use crate::prelude::*; +use biome_html_syntax::AnySvelteDirectiveInitializerClause; + +#[derive(Debug, Clone, Default)] +pub(crate) struct FormatAnySvelteDirectiveInitializerClause; + +impl FormatRule for FormatAnySvelteDirectiveInitializerClause { + type Context = HtmlFormatContext; + + fn fmt( + &self, + node: &AnySvelteDirectiveInitializerClause, + f: &mut HtmlFormatter, + ) -> FormatResult<()> { + match node { + AnySvelteDirectiveInitializerClause::HtmlAttributeInitializerClause(node) => { + node.format().fmt(f) + } + AnySvelteDirectiveInitializerClause::SvelteBindFunctionBindingInitializerClause( + node, + ) => node.format().fmt(f), + } + } +} diff --git a/crates/biome_html_formatter/src/svelte/any/mod.rs b/crates/biome_html_formatter/src/svelte/any/mod.rs index 852e1f92c6fd..06e6a490ee13 100644 --- a/crates/biome_html_formatter/src/svelte/any/mod.rs +++ b/crates/biome_html_formatter/src/svelte/any/mod.rs @@ -7,5 +7,6 @@ pub(crate) mod block; pub(crate) mod block_item; pub(crate) mod destructured_name; pub(crate) mod directive; +pub(crate) mod directive_initializer_clause; pub(crate) mod each_name; pub(crate) mod member_object; diff --git a/crates/biome_html_formatter/src/svelte/auxiliary/bind_function_binding_expression.rs b/crates/biome_html_formatter/src/svelte/auxiliary/bind_function_binding_expression.rs new file mode 100644 index 000000000000..095cafae147d --- /dev/null +++ b/crates/biome_html_formatter/src/svelte/auxiliary/bind_function_binding_expression.rs @@ -0,0 +1,38 @@ +use crate::prelude::*; +use biome_formatter::write; +use biome_html_syntax::{ + SvelteBindFunctionBindingExpression, SvelteBindFunctionBindingExpressionFields, +}; + +#[derive(Debug, Clone, Default)] +pub(crate) struct FormatSvelteBindFunctionBindingExpression; + +impl FormatNodeRule + for FormatSvelteBindFunctionBindingExpression +{ + fn fmt_fields( + &self, + node: &SvelteBindFunctionBindingExpression, + f: &mut HtmlFormatter, + ) -> FormatResult<()> { + let SvelteBindFunctionBindingExpressionFields { + l_curly_token, + get, + comma_token, + set, + r_curly_token, + } = node.as_fields(); + + write!( + f, + [group(&biome_formatter::format_args![ + l_curly_token.format(), + get.format(), + comma_token.format(), + space(), + set.format(), + r_curly_token.format() + ])] + ) + } +} diff --git a/crates/biome_html_formatter/src/svelte/auxiliary/bind_function_binding_initializer_clause.rs b/crates/biome_html_formatter/src/svelte/auxiliary/bind_function_binding_initializer_clause.rs new file mode 100644 index 000000000000..6b2a0d326516 --- /dev/null +++ b/crates/biome_html_formatter/src/svelte/auxiliary/bind_function_binding_initializer_clause.rs @@ -0,0 +1,22 @@ +use crate::prelude::*; +use biome_formatter::write; +use biome_html_syntax::{ + SvelteBindFunctionBindingInitializerClause, SvelteBindFunctionBindingInitializerClauseFields, +}; + +#[derive(Debug, Clone, Default)] +pub(crate) struct FormatSvelteBindFunctionBindingInitializerClause; + +impl FormatNodeRule + for FormatSvelteBindFunctionBindingInitializerClause +{ + fn fmt_fields( + &self, + node: &SvelteBindFunctionBindingInitializerClause, + f: &mut HtmlFormatter, + ) -> FormatResult<()> { + let SvelteBindFunctionBindingInitializerClauseFields { eq_token, value } = node.as_fields(); + + write!(f, [eq_token.format(), value.format()]) + } +} diff --git a/crates/biome_html_formatter/src/svelte/auxiliary/mod.rs b/crates/biome_html_formatter/src/svelte/auxiliary/mod.rs index 6c846be6f2a2..ba8caf72a15b 100644 --- a/crates/biome_html_formatter/src/svelte/auxiliary/mod.rs +++ b/crates/biome_html_formatter/src/svelte/auxiliary/mod.rs @@ -10,6 +10,8 @@ pub(crate) mod await_opening_block; pub(crate) mod await_then_block; pub(crate) mod await_then_clause; pub(crate) mod bind_directive; +pub(crate) mod bind_function_binding_expression; +pub(crate) mod bind_function_binding_initializer_clause; pub(crate) mod class_directive; pub(crate) mod const_block; pub(crate) mod curly_destructured_name; diff --git a/crates/biome_html_formatter/src/svelte/value/directive_value.rs b/crates/biome_html_formatter/src/svelte/value/directive_value.rs index 6c0e64b58d56..b203e0ca8a02 100644 --- a/crates/biome_html_formatter/src/svelte/value/directive_value.rs +++ b/crates/biome_html_formatter/src/svelte/value/directive_value.rs @@ -5,7 +5,8 @@ use crate::prelude::*; use crate::shared::FmtAnySvelteBindingProperty; use biome_formatter::{FormatRuleWithOptions, write}; use biome_html_syntax::{ - AnySvelteBindingProperty, SvelteDirectiveValue, SvelteDirectiveValueFields, + AnySvelteBindingProperty, AnySvelteDirectiveInitializerClause, SvelteDirectiveValue, + SvelteDirectiveValueFields, }; #[derive(Debug, Clone, Default)] @@ -56,7 +57,10 @@ impl FormatSvelteDirectiveValue { AnySvelteBindingProperty::SvelteName(name) => name.ident_token(), }?; - let Some(initializer) = initializer.clone() else { + let Some(AnySvelteDirectiveInitializerClause::HtmlAttributeInitializerClause( + initializer, + )) = initializer.clone() + else { return Ok(false); }; let Some(initializer_value) = initializer.value().ok().and_then(|v| v.string_value()) diff --git a/crates/biome_html_parser/src/syntax/svelte.rs b/crates/biome_html_parser/src/syntax/svelte.rs index 48574addf546..70783e329fb1 100644 --- a/crates/biome_html_parser/src/syntax/svelte.rs +++ b/crates/biome_html_parser/src/syntax/svelte.rs @@ -1152,7 +1152,7 @@ pub(crate) fn parse_svelte_directive(p: &mut HtmlParser) -> ParsedSyntax { p.re_lex(HtmlReLexContext::Svelte); match p.cur() { - T![bind] => parse_directive(p, T![bind], SVELTE_BIND_DIRECTIVE, HtmlLexContext::Svelte), + T![bind] => parse_bind_directive(p), T![transition] => parse_directive( p, T![transition], @@ -1211,6 +1211,101 @@ fn parse_directive_value(p: &mut HtmlParser, context_after_colon: HtmlLexContext Present(m.complete(p, SVELTE_DIRECTIVE_VALUE)) } +fn parse_bind_directive(p: &mut HtmlParser) -> ParsedSyntax { + if !p.at(T![bind]) { + return Absent; + } + + let m = p.start(); + p.bump_with_context(T![bind], HtmlLexContext::Svelte); + + parse_bind_directive_value(p).or_add_diagnostic(p, expected_valid_directive); + + Present(m.complete(p, SVELTE_BIND_DIRECTIVE)) +} + +fn parse_bind_directive_value(p: &mut HtmlParser) -> ParsedSyntax { + if !p.at(T![:]) { + return Absent; + } + + let m = p.start(); + + p.bump_with_context(T![:], HtmlLexContext::Svelte); + if p.cur_text().is_empty() { + p.error(p.err_builder("The directive can't be empty.", p.cur_range())) + } else { + parse_svelte_binding_property(p).or_add_diagnostic(p, expected_name); + } + + ModifiersList.parse_list(p); + + if p.at(T![=]) { + parse_bind_initializer(p).ok(); + } else { + p.re_lex(HtmlReLexContext::InsideTag); + } + + Present(m.complete(p, SVELTE_DIRECTIVE_VALUE)) +} + +fn parse_bind_initializer(p: &mut HtmlParser) -> ParsedSyntax { + if !p.at(T![=]) { + return Absent; + } + + let checkpoint = p.checkpoint(); + let m = p.start(); + + p.bump_with_context(T![=], HtmlLexContext::AttributeValue); + + if p.at(T!['{']) { + let expression = p.start(); + p.bump_with_context( + T!['{'], + HtmlLexContext::restricted_expression(RestrictedExpressionStopAt::Comma), + ); + + parse_bind_function_text_expression(p, HtmlLexContext::Svelte).ok(); + + if p.at(T![,]) { + p.bump_with_context(T![,], HtmlLexContext::single_expression()); + parse_bind_function_text_expression(p, HtmlLexContext::Svelte).ok(); + + if p.at(T!['}']) { + p.bump_remap_with_context(T!['}'], HtmlLexContext::InsideTagSvelte); + expression.complete(p, SVELTE_BIND_FUNCTION_BINDING_EXPRESSION); + return Present(m.complete(p, SVELTE_BIND_FUNCTION_BINDING_INITIALIZER_CLAUSE)); + } + } + + expression.abandon(p); + } + + m.abandon(p); + p.rewind(checkpoint); + parse_attribute_initializer(p, AttrInitializerContext::Regular) +} + +fn parse_bind_function_text_expression( + p: &mut HtmlParser, + context: HtmlLexContext, +) -> ParsedSyntax { + if p.at_ts(token_set![T![<], T!['}'], T![,], EOF]) { + return Absent; + } + + let m = p.start(); + if p.cur_text().is_empty() { + m.abandon(p); + p.re_lex(HtmlReLexContext::Svelte); + return Absent; + } + + p.bump_remap_with_context(HTML_LITERAL, context); + Present(m.complete(p, HTML_TEXT_EXPRESSION)) +} + /// Parses a general directive. `token` is the keyword to parse, and `node_kind` is the kind of the node to emit. fn parse_directive( p: &mut HtmlParser, diff --git a/crates/biome_html_parser/src/token_source.rs b/crates/biome_html_parser/src/token_source.rs index c740521ac3e2..42933539adef 100644 --- a/crates/biome_html_parser/src/token_source.rs +++ b/crates/biome_html_parser/src/token_source.rs @@ -104,6 +104,8 @@ pub(crate) enum TextExpressionKind { pub(crate) enum RestrictedExpressionStopAt { /// Stops at 'as' keyword or ',' (for Svelte #each blocks) AsOrComma, + /// Stops at `,` + Comma, /// Stops at `)` ClosingParen, /// Stops at `then` or `catch` keywords @@ -113,7 +115,7 @@ pub(crate) enum RestrictedExpressionStopAt { impl RestrictedExpressionStopAt { pub(crate) fn matches_punct(&self, byte: u8) -> bool { match self { - Self::AsOrComma => byte == b',', + Self::AsOrComma | Self::Comma => byte == b',', Self::ClosingParen => byte == b')', Self::ThenOrCatch => false, } @@ -122,6 +124,7 @@ impl RestrictedExpressionStopAt { pub(crate) fn matches_keyword(&self, keyword: HtmlSyntaxKind) -> bool { match self { Self::AsOrComma => keyword == AS_KW, + Self::Comma => false, Self::ClosingParen => false, Self::ThenOrCatch => keyword == THEN_KW || keyword == CATCH_KW, } diff --git a/crates/biome_html_parser/tests/html_specs/ok/svelte/directives/bind_function.svelte b/crates/biome_html_parser/tests/html_specs/ok/svelte/directives/bind_function.svelte index 8baf952ffbd2..f2402c24560f 100644 --- a/crates/biome_html_parser/tests/html_specs/ok/svelte/directives/bind_function.svelte +++ b/crates/biome_html_parser/tests/html_specs/ok/svelte/directives/bind_function.svelte @@ -1,2 +1,8 @@ + + [a, b], (v) => set(v)} /> + value, + (v) => value = v.toLowerCase() +} /> diff --git a/crates/biome_html_parser/tests/html_specs/ok/svelte/directives/bind_function.svelte.snap b/crates/biome_html_parser/tests/html_specs/ok/svelte/directives/bind_function.svelte.snap index 72a142857707..7f123337adc3 100644 --- a/crates/biome_html_parser/tests/html_specs/ok/svelte/directives/bind_function.svelte.snap +++ b/crates/biome_html_parser/tests/html_specs/ok/svelte/directives/bind_function.svelte.snap @@ -8,6 +8,12 @@ expression: snapshot ```svelte + + [a, b], (v) => set(v)} /> + value, + (v) => value = v.toLowerCase() +} /> ``` @@ -34,12 +40,16 @@ HtmlRoot { ident_token: IDENT@12..17 "value" [] [], }, modifiers: SvelteDirectiveModifierList [], - initializer: HtmlAttributeInitializerClause { + initializer: SvelteBindFunctionBindingInitializerClause { eq_token: EQ@17..18 "=" [] [], - value: HtmlAttributeSingleTextExpression { + value: SvelteBindFunctionBindingExpression { l_curly_token: L_CURLY@18..19 "{" [] [], - expression: HtmlTextExpression { - html_literal_token: HTML_LITERAL@19..27 "get, set" [] [], + get: HtmlTextExpression { + html_literal_token: HTML_LITERAL@19..22 "get" [] [], + }, + comma_token: COMMA@22..24 "," [] [Whitespace(" ")], + set: HtmlTextExpression { + html_literal_token: HTML_LITERAL@24..27 "set" [] [], }, r_curly_token: R_CURLY@27..29 "}" [] [Whitespace(" ")], }, @@ -64,12 +74,16 @@ HtmlRoot { ident_token: IDENT@44..49 "value" [] [], }, modifiers: SvelteDirectiveModifierList [], - initializer: HtmlAttributeInitializerClause { + initializer: SvelteBindFunctionBindingInitializerClause { eq_token: EQ@49..50 "=" [] [], - value: HtmlAttributeSingleTextExpression { + value: SvelteBindFunctionBindingExpression { l_curly_token: L_CURLY@50..51 "{" [] [], - expression: HtmlTextExpression { - html_literal_token: HTML_LITERAL@51..63 "null, setter" [] [], + get: HtmlTextExpression { + html_literal_token: HTML_LITERAL@51..55 "null" [] [], + }, + comma_token: COMMA@55..57 "," [] [Whitespace(" ")], + set: HtmlTextExpression { + html_literal_token: HTML_LITERAL@57..63 "setter" [] [], }, r_curly_token: R_CURLY@63..65 "}" [] [Whitespace(" ")], }, @@ -80,19 +94,121 @@ HtmlRoot { slash_token: SLASH@65..66 "/" [] [], r_angle_token: R_ANGLE@66..67 ">" [] [], }, + HtmlSelfClosingElement { + l_angle_token: L_ANGLE@67..69 "<" [Newline("\n")] [], + name: HtmlComponentName { + value_token: HTML_LITERAL@69..76 "Toggle" [] [Whitespace(" ")], + }, + attributes: HtmlAttributeList [ + SvelteBindDirective { + bind_token: BIND_KW@76..80 "bind" [] [], + value: SvelteDirectiveValue { + colon_token: COLON@80..81 ":" [] [], + property: SvelteName { + ident_token: IDENT@81..88 "pressed" [] [], + }, + modifiers: SvelteDirectiveModifierList [], + initializer: SvelteBindFunctionBindingInitializerClause { + eq_token: EQ@88..89 "=" [] [], + value: SvelteBindFunctionBindingExpression { + l_curly_token: L_CURLY@89..90 "{" [] [], + get: HtmlTextExpression { + html_literal_token: HTML_LITERAL@90..100 "getToggled" [] [], + }, + comma_token: COMMA@100..102 "," [] [Whitespace(" ")], + set: HtmlTextExpression { + html_literal_token: HTML_LITERAL@102..112 "setToggled" [] [], + }, + r_curly_token: R_CURLY@112..114 "}" [] [Whitespace(" ")], + }, + }, + }, + }, + ], + slash_token: SLASH@114..115 "/" [] [], + r_angle_token: R_ANGLE@115..116 ">" [] [], + }, + HtmlSelfClosingElement { + l_angle_token: L_ANGLE@116..118 "<" [Newline("\n")] [], + name: HtmlTagName { + value_token: HTML_LITERAL@118..124 "input" [] [Whitespace(" ")], + }, + attributes: HtmlAttributeList [ + SvelteBindDirective { + bind_token: BIND_KW@124..128 "bind" [] [], + value: SvelteDirectiveValue { + colon_token: COLON@128..129 ":" [] [], + property: SvelteName { + ident_token: IDENT@129..134 "value" [] [], + }, + modifiers: SvelteDirectiveModifierList [], + initializer: SvelteBindFunctionBindingInitializerClause { + eq_token: EQ@134..135 "=" [] [], + value: SvelteBindFunctionBindingExpression { + l_curly_token: L_CURLY@135..136 "{" [] [], + get: HtmlTextExpression { + html_literal_token: HTML_LITERAL@136..148 "() => [a, b]" [] [], + }, + comma_token: COMMA@148..150 "," [] [Whitespace(" ")], + set: HtmlTextExpression { + html_literal_token: HTML_LITERAL@150..163 "(v) => set(v)" [] [], + }, + r_curly_token: R_CURLY@163..165 "}" [] [Whitespace(" ")], + }, + }, + }, + }, + ], + slash_token: SLASH@165..166 "/" [] [], + r_angle_token: R_ANGLE@166..167 ">" [] [], + }, + HtmlSelfClosingElement { + l_angle_token: L_ANGLE@167..169 "<" [Newline("\n")] [], + name: HtmlTagName { + value_token: HTML_LITERAL@169..175 "input" [] [Whitespace(" ")], + }, + attributes: HtmlAttributeList [ + SvelteBindDirective { + bind_token: BIND_KW@175..179 "bind" [] [], + value: SvelteDirectiveValue { + colon_token: COLON@179..180 ":" [] [], + property: SvelteName { + ident_token: IDENT@180..185 "value" [] [], + }, + modifiers: SvelteDirectiveModifierList [], + initializer: SvelteBindFunctionBindingInitializerClause { + eq_token: EQ@185..186 "=" [] [], + value: SvelteBindFunctionBindingExpression { + l_curly_token: L_CURLY@186..187 "{" [] [], + get: HtmlTextExpression { + html_literal_token: HTML_LITERAL@187..200 "\n\t() => value" [] [], + }, + comma_token: COMMA@200..201 "," [] [], + set: HtmlTextExpression { + html_literal_token: HTML_LITERAL@201..234 "(v) => value = v.toLowerCase()\n" [Newline("\n"), Whitespace("\t")] [], + }, + r_curly_token: R_CURLY@234..236 "}" [] [Whitespace(" ")], + }, + }, + }, + }, + ], + slash_token: SLASH@236..237 "/" [] [], + r_angle_token: R_ANGLE@237..238 ">" [] [], + }, ], - eof_token: EOF@67..68 "" [Newline("\n")] [], + eof_token: EOF@238..239 "" [Newline("\n")] [], } ``` ## CST ``` -0: HTML_ROOT@0..68 +0: HTML_ROOT@0..239 0: (empty) 1: (empty) 2: (empty) - 3: HTML_ELEMENT_LIST@0..67 + 3: HTML_ELEMENT_LIST@0..238 0: HTML_SELF_CLOSING_ELEMENT@0..31 0: L_ANGLE@0..1 "<" [] [] 1: HTML_TAG_NAME@1..7 @@ -105,13 +221,16 @@ HtmlRoot { 1: SVELTE_NAME@12..17 0: IDENT@12..17 "value" [] [] 2: SVELTE_DIRECTIVE_MODIFIER_LIST@17..17 - 3: HTML_ATTRIBUTE_INITIALIZER_CLAUSE@17..29 + 3: SVELTE_BIND_FUNCTION_BINDING_INITIALIZER_CLAUSE@17..29 0: EQ@17..18 "=" [] [] - 1: HTML_ATTRIBUTE_SINGLE_TEXT_EXPRESSION@18..29 + 1: SVELTE_BIND_FUNCTION_BINDING_EXPRESSION@18..29 0: L_CURLY@18..19 "{" [] [] - 1: HTML_TEXT_EXPRESSION@19..27 - 0: HTML_LITERAL@19..27 "get, set" [] [] - 2: R_CURLY@27..29 "}" [] [Whitespace(" ")] + 1: HTML_TEXT_EXPRESSION@19..22 + 0: HTML_LITERAL@19..22 "get" [] [] + 2: COMMA@22..24 "," [] [Whitespace(" ")] + 3: HTML_TEXT_EXPRESSION@24..27 + 0: HTML_LITERAL@24..27 "set" [] [] + 4: R_CURLY@27..29 "}" [] [Whitespace(" ")] 3: SLASH@29..30 "/" [] [] 4: R_ANGLE@30..31 ">" [] [] 1: HTML_SELF_CLOSING_ELEMENT@31..67 @@ -126,15 +245,90 @@ HtmlRoot { 1: SVELTE_NAME@44..49 0: IDENT@44..49 "value" [] [] 2: SVELTE_DIRECTIVE_MODIFIER_LIST@49..49 - 3: HTML_ATTRIBUTE_INITIALIZER_CLAUSE@49..65 + 3: SVELTE_BIND_FUNCTION_BINDING_INITIALIZER_CLAUSE@49..65 0: EQ@49..50 "=" [] [] - 1: HTML_ATTRIBUTE_SINGLE_TEXT_EXPRESSION@50..65 + 1: SVELTE_BIND_FUNCTION_BINDING_EXPRESSION@50..65 0: L_CURLY@50..51 "{" [] [] - 1: HTML_TEXT_EXPRESSION@51..63 - 0: HTML_LITERAL@51..63 "null, setter" [] [] - 2: R_CURLY@63..65 "}" [] [Whitespace(" ")] + 1: HTML_TEXT_EXPRESSION@51..55 + 0: HTML_LITERAL@51..55 "null" [] [] + 2: COMMA@55..57 "," [] [Whitespace(" ")] + 3: HTML_TEXT_EXPRESSION@57..63 + 0: HTML_LITERAL@57..63 "setter" [] [] + 4: R_CURLY@63..65 "}" [] [Whitespace(" ")] 3: SLASH@65..66 "/" [] [] 4: R_ANGLE@66..67 ">" [] [] - 4: EOF@67..68 "" [Newline("\n")] [] + 2: HTML_SELF_CLOSING_ELEMENT@67..116 + 0: L_ANGLE@67..69 "<" [Newline("\n")] [] + 1: HTML_COMPONENT_NAME@69..76 + 0: HTML_LITERAL@69..76 "Toggle" [] [Whitespace(" ")] + 2: HTML_ATTRIBUTE_LIST@76..114 + 0: SVELTE_BIND_DIRECTIVE@76..114 + 0: BIND_KW@76..80 "bind" [] [] + 1: SVELTE_DIRECTIVE_VALUE@80..114 + 0: COLON@80..81 ":" [] [] + 1: SVELTE_NAME@81..88 + 0: IDENT@81..88 "pressed" [] [] + 2: SVELTE_DIRECTIVE_MODIFIER_LIST@88..88 + 3: SVELTE_BIND_FUNCTION_BINDING_INITIALIZER_CLAUSE@88..114 + 0: EQ@88..89 "=" [] [] + 1: SVELTE_BIND_FUNCTION_BINDING_EXPRESSION@89..114 + 0: L_CURLY@89..90 "{" [] [] + 1: HTML_TEXT_EXPRESSION@90..100 + 0: HTML_LITERAL@90..100 "getToggled" [] [] + 2: COMMA@100..102 "," [] [Whitespace(" ")] + 3: HTML_TEXT_EXPRESSION@102..112 + 0: HTML_LITERAL@102..112 "setToggled" [] [] + 4: R_CURLY@112..114 "}" [] [Whitespace(" ")] + 3: SLASH@114..115 "/" [] [] + 4: R_ANGLE@115..116 ">" [] [] + 3: HTML_SELF_CLOSING_ELEMENT@116..167 + 0: L_ANGLE@116..118 "<" [Newline("\n")] [] + 1: HTML_TAG_NAME@118..124 + 0: HTML_LITERAL@118..124 "input" [] [Whitespace(" ")] + 2: HTML_ATTRIBUTE_LIST@124..165 + 0: SVELTE_BIND_DIRECTIVE@124..165 + 0: BIND_KW@124..128 "bind" [] [] + 1: SVELTE_DIRECTIVE_VALUE@128..165 + 0: COLON@128..129 ":" [] [] + 1: SVELTE_NAME@129..134 + 0: IDENT@129..134 "value" [] [] + 2: SVELTE_DIRECTIVE_MODIFIER_LIST@134..134 + 3: SVELTE_BIND_FUNCTION_BINDING_INITIALIZER_CLAUSE@134..165 + 0: EQ@134..135 "=" [] [] + 1: SVELTE_BIND_FUNCTION_BINDING_EXPRESSION@135..165 + 0: L_CURLY@135..136 "{" [] [] + 1: HTML_TEXT_EXPRESSION@136..148 + 0: HTML_LITERAL@136..148 "() => [a, b]" [] [] + 2: COMMA@148..150 "," [] [Whitespace(" ")] + 3: HTML_TEXT_EXPRESSION@150..163 + 0: HTML_LITERAL@150..163 "(v) => set(v)" [] [] + 4: R_CURLY@163..165 "}" [] [Whitespace(" ")] + 3: SLASH@165..166 "/" [] [] + 4: R_ANGLE@166..167 ">" [] [] + 4: HTML_SELF_CLOSING_ELEMENT@167..238 + 0: L_ANGLE@167..169 "<" [Newline("\n")] [] + 1: HTML_TAG_NAME@169..175 + 0: HTML_LITERAL@169..175 "input" [] [Whitespace(" ")] + 2: HTML_ATTRIBUTE_LIST@175..236 + 0: SVELTE_BIND_DIRECTIVE@175..236 + 0: BIND_KW@175..179 "bind" [] [] + 1: SVELTE_DIRECTIVE_VALUE@179..236 + 0: COLON@179..180 ":" [] [] + 1: SVELTE_NAME@180..185 + 0: IDENT@180..185 "value" [] [] + 2: SVELTE_DIRECTIVE_MODIFIER_LIST@185..185 + 3: SVELTE_BIND_FUNCTION_BINDING_INITIALIZER_CLAUSE@185..236 + 0: EQ@185..186 "=" [] [] + 1: SVELTE_BIND_FUNCTION_BINDING_EXPRESSION@186..236 + 0: L_CURLY@186..187 "{" [] [] + 1: HTML_TEXT_EXPRESSION@187..200 + 0: HTML_LITERAL@187..200 "\n\t() => value" [] [] + 2: COMMA@200..201 "," [] [] + 3: HTML_TEXT_EXPRESSION@201..234 + 0: HTML_LITERAL@201..234 "(v) => value = v.toLowerCase()\n" [Newline("\n"), Whitespace("\t")] [] + 4: R_CURLY@234..236 "}" [] [Whitespace(" ")] + 3: SLASH@236..237 "/" [] [] + 4: R_ANGLE@237..238 ">" [] [] + 4: EOF@238..239 "" [Newline("\n")] [] ``` diff --git a/crates/biome_html_syntax/src/directive_ext.rs b/crates/biome_html_syntax/src/directive_ext.rs index 0ac52bb97579..85828c45ee10 100644 --- a/crates/biome_html_syntax/src/directive_ext.rs +++ b/crates/biome_html_syntax/src/directive_ext.rs @@ -1,4 +1,7 @@ -use crate::{AnyAstroDirective, AnySvelteDirective, HtmlAttributeInitializerClause}; +use crate::{ + AnyAstroDirective, AnySvelteDirective, AnySvelteDirectiveInitializerClause, + HtmlAttributeInitializerClause, +}; impl AnyAstroDirective { /// Returns the initializer from an Astro directive's value, if available. @@ -16,7 +19,7 @@ impl AnyAstroDirective { impl AnySvelteDirective { /// Returns the initializer from a Svelte directive's value, if available. - pub fn initializer(&self) -> Option { + pub fn initializer(&self) -> Option { match self { Self::SvelteBindDirective(dir) => dir.value().ok()?.initializer(), Self::SvelteTransitionDirective(dir) => dir.value().ok()?.initializer(), diff --git a/crates/biome_html_syntax/src/generated/kind.rs b/crates/biome_html_syntax/src/generated/kind.rs index df9964c6ed35..601a329128a8 100644 --- a/crates/biome_html_syntax/src/generated/kind.rs +++ b/crates/biome_html_syntax/src/generated/kind.rs @@ -162,6 +162,8 @@ pub enum HtmlSyntaxKind { SVELTE_STYLE_DIRECTIVE, SVELTE_CLASS_DIRECTIVE, SVELTE_DIRECTIVE_VALUE, + SVELTE_BIND_FUNCTION_BINDING_INITIALIZER_CLAUSE, + SVELTE_BIND_FUNCTION_BINDING_EXPRESSION, SVELTE_DIRECTIVE_MODIFIER, SVELTE_DIRECTIVE_MODIFIER_LIST, SVELTE_LITERAL, diff --git a/crates/biome_html_syntax/src/generated/macros.rs b/crates/biome_html_syntax/src/generated/macros.rs index db3df78283d7..1be9ac0ed225 100644 --- a/crates/biome_html_syntax/src/generated/macros.rs +++ b/crates/biome_html_syntax/src/generated/macros.rs @@ -183,6 +183,17 @@ macro_rules! map_syntax_node { let $pattern = unsafe { $crate::SvelteBindDirective::new_unchecked(node) }; $body } + $crate::HtmlSyntaxKind::SVELTE_BIND_FUNCTION_BINDING_EXPRESSION => { + let $pattern = + unsafe { $crate::SvelteBindFunctionBindingExpression::new_unchecked(node) }; + $body + } + $crate::HtmlSyntaxKind::SVELTE_BIND_FUNCTION_BINDING_INITIALIZER_CLAUSE => { + let $pattern = unsafe { + $crate::SvelteBindFunctionBindingInitializerClause::new_unchecked(node) + }; + $body + } $crate::HtmlSyntaxKind::SVELTE_CLASS_DIRECTIVE => { let $pattern = unsafe { $crate::SvelteClassDirective::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 2feaf46486db..dec46bea3992 100644 --- a/crates/biome_html_syntax/src/generated/nodes.rs +++ b/crates/biome_html_syntax/src/generated/nodes.rs @@ -1830,6 +1830,101 @@ pub struct SvelteBindDirectiveFields { pub value: SyntaxResult, } #[derive(Clone, PartialEq, Eq, Hash)] +pub struct SvelteBindFunctionBindingExpression { + pub(crate) syntax: SyntaxNode, +} +impl SvelteBindFunctionBindingExpression { + #[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) -> SvelteBindFunctionBindingExpressionFields { + SvelteBindFunctionBindingExpressionFields { + l_curly_token: self.l_curly_token(), + get: self.get(), + comma_token: self.comma_token(), + set: self.set(), + r_curly_token: self.r_curly_token(), + } + } + pub fn l_curly_token(&self) -> SyntaxResult { + support::required_token(&self.syntax, 0usize) + } + pub fn get(&self) -> SyntaxResult { + support::required_node(&self.syntax, 1usize) + } + pub fn comma_token(&self) -> SyntaxResult { + support::required_token(&self.syntax, 2usize) + } + pub fn set(&self) -> SyntaxResult { + support::required_node(&self.syntax, 3usize) + } + pub fn r_curly_token(&self) -> SyntaxResult { + support::required_token(&self.syntax, 4usize) + } +} +impl Serialize for SvelteBindFunctionBindingExpression { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + self.as_fields().serialize(serializer) + } +} +#[derive(Serialize)] +pub struct SvelteBindFunctionBindingExpressionFields { + pub l_curly_token: SyntaxResult, + pub get: SyntaxResult, + pub comma_token: SyntaxResult, + pub set: SyntaxResult, + pub r_curly_token: SyntaxResult, +} +#[derive(Clone, PartialEq, Eq, Hash)] +pub struct SvelteBindFunctionBindingInitializerClause { + pub(crate) syntax: SyntaxNode, +} +impl SvelteBindFunctionBindingInitializerClause { + #[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) -> SvelteBindFunctionBindingInitializerClauseFields { + SvelteBindFunctionBindingInitializerClauseFields { + eq_token: self.eq_token(), + value: self.value(), + } + } + pub fn eq_token(&self) -> SyntaxResult { + support::required_token(&self.syntax, 0usize) + } + pub fn value(&self) -> SyntaxResult { + support::required_node(&self.syntax, 1usize) + } +} +impl Serialize for SvelteBindFunctionBindingInitializerClause { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + self.as_fields().serialize(serializer) + } +} +#[derive(Serialize)] +pub struct SvelteBindFunctionBindingInitializerClauseFields { + pub eq_token: SyntaxResult, + pub value: SyntaxResult, +} +#[derive(Clone, PartialEq, Eq, Hash)] pub struct SvelteClassDirective { pub(crate) syntax: SyntaxNode, } @@ -2085,7 +2180,7 @@ impl SvelteDirectiveValue { pub fn modifiers(&self) -> SvelteDirectiveModifierList { support::list(&self.syntax, 2usize) } - pub fn initializer(&self) -> Option { + pub fn initializer(&self) -> Option { support::node(&self.syntax, 3usize) } } @@ -2102,7 +2197,7 @@ pub struct SvelteDirectiveValueFields { pub colon_token: SyntaxResult, pub property: SyntaxResult, pub modifiers: SvelteDirectiveModifierList, - pub initializer: Option, + pub initializer: Option, } #[derive(Clone, PartialEq, Eq, Hash)] pub struct SvelteEachAsKeyedItem { @@ -4819,6 +4914,27 @@ impl AnySvelteDirective { } } #[derive(Clone, PartialEq, Eq, Hash, Serialize)] +pub enum AnySvelteDirectiveInitializerClause { + HtmlAttributeInitializerClause(HtmlAttributeInitializerClause), + SvelteBindFunctionBindingInitializerClause(SvelteBindFunctionBindingInitializerClause), +} +impl AnySvelteDirectiveInitializerClause { + pub fn as_html_attribute_initializer_clause(&self) -> Option<&HtmlAttributeInitializerClause> { + match &self { + Self::HtmlAttributeInitializerClause(item) => Some(item), + _ => None, + } + } + pub fn as_svelte_bind_function_binding_initializer_clause( + &self, + ) -> Option<&SvelteBindFunctionBindingInitializerClause> { + match &self { + Self::SvelteBindFunctionBindingInitializerClause(item) => Some(item), + _ => None, + } + } +} +#[derive(Clone, PartialEq, Eq, Hash, Serialize)] pub enum AnySvelteEachName { AnySvelteDestructuredName(AnySvelteDestructuredName), HtmlTextExpression(HtmlTextExpression), @@ -7241,6 +7357,118 @@ impl From for SyntaxElement { n.syntax.into() } } +impl AstNode for SvelteBindFunctionBindingExpression { + type Language = Language; + const KIND_SET: SyntaxKindSet = SyntaxKindSet::from_raw(RawSyntaxKind( + SVELTE_BIND_FUNCTION_BINDING_EXPRESSION as u16, + )); + fn can_cast(kind: SyntaxKind) -> bool { + kind == SVELTE_BIND_FUNCTION_BINDING_EXPRESSION + } + 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 SvelteBindFunctionBindingExpression { + 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("SvelteBindFunctionBindingExpression") + .field( + "l_curly_token", + &support::DebugSyntaxResult(self.l_curly_token()), + ) + .field("get", &support::DebugSyntaxResult(self.get())) + .field( + "comma_token", + &support::DebugSyntaxResult(self.comma_token()), + ) + .field("set", &support::DebugSyntaxResult(self.set())) + .field( + "r_curly_token", + &support::DebugSyntaxResult(self.r_curly_token()), + ) + .finish() + } else { + f.debug_struct("SvelteBindFunctionBindingExpression") + .finish() + }; + DEPTH.set(current_depth); + result + } +} +impl From for SyntaxNode { + fn from(n: SvelteBindFunctionBindingExpression) -> Self { + n.syntax + } +} +impl From for SyntaxElement { + fn from(n: SvelteBindFunctionBindingExpression) -> Self { + n.syntax.into() + } +} +impl AstNode for SvelteBindFunctionBindingInitializerClause { + type Language = Language; + const KIND_SET: SyntaxKindSet = SyntaxKindSet::from_raw(RawSyntaxKind( + SVELTE_BIND_FUNCTION_BINDING_INITIALIZER_CLAUSE as u16, + )); + fn can_cast(kind: SyntaxKind) -> bool { + kind == SVELTE_BIND_FUNCTION_BINDING_INITIALIZER_CLAUSE + } + 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 SvelteBindFunctionBindingInitializerClause { + 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("SvelteBindFunctionBindingInitializerClause") + .field("eq_token", &support::DebugSyntaxResult(self.eq_token())) + .field("value", &support::DebugSyntaxResult(self.value())) + .finish() + } else { + f.debug_struct("SvelteBindFunctionBindingInitializerClause") + .finish() + }; + DEPTH.set(current_depth); + result + } +} +impl From for SyntaxNode { + fn from(n: SvelteBindFunctionBindingInitializerClause) -> Self { + n.syntax + } +} +impl From for SyntaxElement { + fn from(n: SvelteBindFunctionBindingInitializerClause) -> Self { + n.syntax.into() + } +} impl AstNode for SvelteClassDirective { type Language = Language; const KIND_SET: SyntaxKindSet = @@ -11586,6 +11814,79 @@ impl From for SyntaxElement { node.into() } } +impl From for AnySvelteDirectiveInitializerClause { + fn from(node: HtmlAttributeInitializerClause) -> Self { + Self::HtmlAttributeInitializerClause(node) + } +} +impl From for AnySvelteDirectiveInitializerClause { + fn from(node: SvelteBindFunctionBindingInitializerClause) -> Self { + Self::SvelteBindFunctionBindingInitializerClause(node) + } +} +impl AstNode for AnySvelteDirectiveInitializerClause { + type Language = Language; + const KIND_SET: SyntaxKindSet = HtmlAttributeInitializerClause::KIND_SET + .union(SvelteBindFunctionBindingInitializerClause::KIND_SET); + fn can_cast(kind: SyntaxKind) -> bool { + matches!( + kind, + HTML_ATTRIBUTE_INITIALIZER_CLAUSE | SVELTE_BIND_FUNCTION_BINDING_INITIALIZER_CLAUSE + ) + } + fn cast(syntax: SyntaxNode) -> Option { + let res = match syntax.kind() { + HTML_ATTRIBUTE_INITIALIZER_CLAUSE => { + Self::HtmlAttributeInitializerClause(HtmlAttributeInitializerClause { syntax }) + } + SVELTE_BIND_FUNCTION_BINDING_INITIALIZER_CLAUSE => { + Self::SvelteBindFunctionBindingInitializerClause( + SvelteBindFunctionBindingInitializerClause { syntax }, + ) + } + _ => return None, + }; + Some(res) + } + fn syntax(&self) -> &SyntaxNode { + match self { + Self::HtmlAttributeInitializerClause(it) => it.syntax(), + Self::SvelteBindFunctionBindingInitializerClause(it) => it.syntax(), + } + } + fn into_syntax(self) -> SyntaxNode { + match self { + Self::HtmlAttributeInitializerClause(it) => it.into_syntax(), + Self::SvelteBindFunctionBindingInitializerClause(it) => it.into_syntax(), + } + } +} +impl std::fmt::Debug for AnySvelteDirectiveInitializerClause { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::HtmlAttributeInitializerClause(it) => std::fmt::Debug::fmt(it, f), + Self::SvelteBindFunctionBindingInitializerClause(it) => std::fmt::Debug::fmt(it, f), + } + } +} +impl From for SyntaxNode { + fn from(n: AnySvelteDirectiveInitializerClause) -> Self { + match n { + AnySvelteDirectiveInitializerClause::HtmlAttributeInitializerClause(it) => { + it.into_syntax() + } + AnySvelteDirectiveInitializerClause::SvelteBindFunctionBindingInitializerClause(it) => { + it.into_syntax() + } + } + } +} +impl From for SyntaxElement { + fn from(n: AnySvelteDirectiveInitializerClause) -> Self { + let node: SyntaxNode = n.into(); + node.into() + } +} impl From for AnySvelteEachName { fn from(node: HtmlTextExpression) -> Self { Self::HtmlTextExpression(node) @@ -12280,6 +12581,11 @@ impl std::fmt::Display for AnySvelteDirective { std::fmt::Display::fmt(self.syntax(), f) } } +impl std::fmt::Display for AnySvelteDirectiveInitializerClause { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + std::fmt::Display::fmt(self.syntax(), f) + } +} impl std::fmt::Display for AnySvelteEachName { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { std::fmt::Display::fmt(self.syntax(), f) @@ -12525,6 +12831,16 @@ impl std::fmt::Display for SvelteBindDirective { std::fmt::Display::fmt(self.syntax(), f) } } +impl std::fmt::Display for SvelteBindFunctionBindingExpression { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + std::fmt::Display::fmt(self.syntax(), f) + } +} +impl std::fmt::Display for SvelteBindFunctionBindingInitializerClause { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + std::fmt::Display::fmt(self.syntax(), f) + } +} impl std::fmt::Display for SvelteClassDirective { 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 1901c46bc33b..1a11b98056c2 100644 --- a/crates/biome_html_syntax/src/generated/nodes_mut.rs +++ b/crates/biome_html_syntax/src/generated/nodes_mut.rs @@ -781,6 +781,52 @@ impl SvelteBindDirective { ) } } +impl SvelteBindFunctionBindingExpression { + 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_get(self, element: HtmlTextExpression) -> Self { + Self::unwrap_cast( + self.syntax + .splice_slots(1usize..=1usize, once(Some(element.into_syntax().into()))), + ) + } + pub fn with_comma_token(self, element: SyntaxToken) -> Self { + Self::unwrap_cast( + self.syntax + .splice_slots(2usize..=2usize, once(Some(element.into()))), + ) + } + pub fn with_set(self, element: HtmlTextExpression) -> Self { + Self::unwrap_cast( + self.syntax + .splice_slots(3usize..=3usize, once(Some(element.into_syntax().into()))), + ) + } + pub fn with_r_curly_token(self, element: SyntaxToken) -> Self { + Self::unwrap_cast( + self.syntax + .splice_slots(4usize..=4usize, once(Some(element.into()))), + ) + } +} +impl SvelteBindFunctionBindingInitializerClause { + pub fn with_eq_token(self, element: SyntaxToken) -> Self { + Self::unwrap_cast( + self.syntax + .splice_slots(0usize..=0usize, once(Some(element.into()))), + ) + } + pub fn with_value(self, element: SvelteBindFunctionBindingExpression) -> Self { + Self::unwrap_cast( + self.syntax + .splice_slots(1usize..=1usize, once(Some(element.into_syntax().into()))), + ) + } +} impl SvelteClassDirective { pub fn with_class_token(self, element: SyntaxToken) -> Self { Self::unwrap_cast( @@ -900,7 +946,7 @@ impl SvelteDirectiveValue { .splice_slots(2usize..=2usize, once(Some(element.into_syntax().into()))), ) } - pub fn with_initializer(self, element: Option) -> Self { + pub fn with_initializer(self, element: Option) -> Self { Self::unwrap_cast(self.syntax.splice_slots( 3usize..=3usize, once(element.map(|element| element.into_syntax().into())), diff --git a/crates/biome_js_analyze/tests/specs/complexity/noCommaOperator/invalid-svelte-template.svelte b/crates/biome_js_analyze/tests/specs/complexity/noCommaOperator/invalid-svelte-template.svelte index c58e28be8dc8..cb89aebc42e8 100644 --- a/crates/biome_js_analyze/tests/specs/complexity/noCommaOperator/invalid-svelte-template.svelte +++ b/crates/biome_js_analyze/tests/specs/complexity/noCommaOperator/invalid-svelte-template.svelte @@ -5,3 +5,4 @@

{(console.log("side effect"), x)}

+

real comma operator

diff --git a/crates/biome_js_analyze/tests/specs/complexity/noCommaOperator/invalid-svelte-template.svelte.snap b/crates/biome_js_analyze/tests/specs/complexity/noCommaOperator/invalid-svelte-template.svelte.snap index c4f0418cde5b..0ef84823a939 100644 --- a/crates/biome_js_analyze/tests/specs/complexity/noCommaOperator/invalid-svelte-template.svelte.snap +++ b/crates/biome_js_analyze/tests/specs/complexity/noCommaOperator/invalid-svelte-template.svelte.snap @@ -11,6 +11,7 @@ expression: invalid-svelte-template.svelte

{(console.log("side effect"), x)}

+

real comma operator

``` @@ -23,7 +24,24 @@ invalid-svelte-template.svelte:7:32 lint/complexity/noCommaOperator ━━━━ 6 │ > 7 │

{(console.log("side effect"), x)}

│ ^ - 8 │ + 8 │

real comma operator

+ 9 │ + + i Its use is often confusing and obscures side effects. + + +``` + +``` +invalid-svelte-template.svelte:8:12 lint/complexity/noCommaOperator ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! The comma operator is disallowed. + + 6 │ + 7 │

{(console.log("side effect"), x)}

+ > 8 │

real comma operator

+ │ ^ + 9 │ i Its use is often confusing and obscures side effects. diff --git a/crates/biome_js_analyze/tests/specs/complexity/noCommaOperator/valid-svelte-bind-function.svelte b/crates/biome_js_analyze/tests/specs/complexity/noCommaOperator/valid-svelte-bind-function.svelte new file mode 100644 index 000000000000..8b025e9c9595 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/complexity/noCommaOperator/valid-svelte-bind-function.svelte @@ -0,0 +1,14 @@ + + + + + + value, (v) => value = v.toLowerCase()} /> + [a, b], (v) => set(v)} /> diff --git a/crates/biome_js_analyze/tests/specs/complexity/noCommaOperator/valid-svelte-bind-function.svelte.snap b/crates/biome_js_analyze/tests/specs/complexity/noCommaOperator/valid-svelte-bind-function.svelte.snap new file mode 100644 index 000000000000..dab392027c08 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/complexity/noCommaOperator/valid-svelte-bind-function.svelte.snap @@ -0,0 +1,22 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +expression: valid-svelte-bind-function.svelte +--- +# Input +```svelte + + + + + + value, (v) => value = v.toLowerCase()} /> + [a, b], (v) => set(v)} /> + +``` diff --git a/crates/biome_service/src/file_handlers/html/parse_embedded_nodes.rs b/crates/biome_service/src/file_handlers/html/parse_embedded_nodes.rs index 391e67c13623..6bcfa90ee5e3 100644 --- a/crates/biome_service/src/file_handlers/html/parse_embedded_nodes.rs +++ b/crates/biome_service/src/file_handlers/html/parse_embedded_nodes.rs @@ -14,11 +14,12 @@ use biome_css_syntax::{CssFileSource, CssLanguage, TextSize}; use biome_fs::BiomePath; use biome_html_syntax::{ AnyAstroDirective, AnySvelteBindingAssignmentBinding, AnySvelteBlock, AnySvelteBlockItem, - AnySvelteDestructuredName, AnySvelteDirective, AnySvelteEachName, AstroEmbeddedContent, - HtmlAttribute, HtmlAttributeInitializerClause, HtmlAttributeSingleTextExpression, - HtmlDoubleTextExpression, HtmlElement, HtmlRoot, HtmlSingleTextExpression, HtmlTextExpression, - HtmlTextExpressions, HtmlVariant, SvelteName, VueDirective, VueVBindShorthandDirective, - VueVForValue, VueVOnShorthandDirective, VueVSlotShorthandDirective, + AnySvelteDestructuredName, AnySvelteDirective, AnySvelteDirectiveInitializerClause, + AnySvelteEachName, AstroEmbeddedContent, HtmlAttribute, HtmlAttributeInitializerClause, + HtmlAttributeSingleTextExpression, HtmlDoubleTextExpression, HtmlElement, HtmlRoot, + HtmlSingleTextExpression, HtmlTextExpression, HtmlTextExpressions, HtmlVariant, SvelteName, + VueDirective, VueVBindShorthandDirective, VueVForValue, VueVOnShorthandDirective, + VueVSlotShorthandDirective, }; use biome_js_parser::parse_js_with_offset_and_cache; use biome_js_syntax::{EmbeddingKind, JsFileSource, JsLanguage, SvelteFileKind}; @@ -298,16 +299,15 @@ pub(crate) fn parse_embedded_nodes( // Pass 4: directive attributes and attributes which initializer is a text expression for element in html_root.syntax().descendants() { // Handle special Svelte directives (bind:, class:, etc.) - if let Some(directive) = AnySvelteDirective::cast_ref(&element) - && let Some(initializer) = directive.initializer() - && let Some(candidate) = build_svelte_directive_candidate(&initializer) - { - ctx.parse_and_push( - &candidate, - &doc_file_source, - Some(embedded_file_source), - &mut nodes, - ); + if let Some(directive) = AnySvelteDirective::cast_ref(&element) { + for candidate in build_svelte_directive_candidates(&directive) { + ctx.parse_and_push( + &candidate, + &doc_file_source, + Some(embedded_file_source), + &mut nodes, + ); + } } if let Some(attr) = HtmlAttribute::cast_ref(&element) @@ -535,10 +535,54 @@ fn parse_svelte_blocks( /// /// Svelte directives use curly brace text expressions (`on:click={handler}`). /// The JS content is the literal token inside the expression node. -fn build_svelte_directive_candidate( - initializer: &HtmlAttributeInitializerClause, +fn build_svelte_directive_candidates(directive: &AnySvelteDirective) -> Vec { + let Some(initializer) = directive.initializer() else { + return Vec::new(); + }; + + match initializer { + AnySvelteDirectiveInitializerClause::HtmlAttributeInitializerClause(initializer) => { + build_attribute_expression_candidate(&initializer) + .into_iter() + .collect() + } + AnySvelteDirectiveInitializerClause::SvelteBindFunctionBindingInitializerClause( + initializer, + ) => { + let Ok(value) = initializer.value() else { + return Vec::new(); + }; + + let mut candidates = Vec::new(); + if let Ok(get) = value.get() + && let Some(candidate) = build_text_expression_directive_candidate(&get) + { + candidates.push(candidate); + } + if let Ok(set) = value.set() + && let Some(candidate) = build_text_expression_directive_candidate(&set) + { + candidates.push(candidate); + } + candidates + } + } +} + +fn build_text_expression_directive_candidate( + expression: &HtmlTextExpression, ) -> Option { - build_attribute_expression_candidate(initializer) + let content_token = expression.html_literal_token().ok()?; + + Some(EmbedCandidate::Directive { + content: EmbedContent { + element_range: expression.range(), + content_range: content_token.text_range(), + content_offset: content_token.text_range().start(), + text: content_token.token_text(), + }, + is_event_handler: false, + }) } /// Build an `EmbedCandidate::Directive` from an initializer clause containing diff --git a/xtask/codegen/html.ungram b/xtask/codegen/html.ungram index 5d85297b2834..5de0ecdc742b 100644 --- a/xtask/codegen/html.ungram +++ b/xtask/codegen/html.ungram @@ -598,7 +598,22 @@ SvelteDirectiveValue = ':' property: AnySvelteBindingProperty modifiers: SvelteDirectiveModifierList - initializer: HtmlAttributeInitializerClause? + initializer: AnySvelteDirectiveInitializerClause? + +AnySvelteDirectiveInitializerClause = + HtmlAttributeInitializerClause + | SvelteBindFunctionBindingInitializerClause + +SvelteBindFunctionBindingInitializerClause = + '=' + value: SvelteBindFunctionBindingExpression + +SvelteBindFunctionBindingExpression = + '{' + get: HtmlTextExpression + ',' + set: HtmlTextExpression + '}' AnySvelteBindingProperty = SvelteName diff --git a/xtask/codegen/src/html_kinds_src.rs b/xtask/codegen/src/html_kinds_src.rs index f5092b3319ac..eb39f35f6d49 100644 --- a/xtask/codegen/src/html_kinds_src.rs +++ b/xtask/codegen/src/html_kinds_src.rs @@ -161,6 +161,8 @@ pub const HTML_KINDS_SRC: KindsSrc = KindsSrc { "SVELTE_STYLE_DIRECTIVE", "SVELTE_CLASS_DIRECTIVE", "SVELTE_DIRECTIVE_VALUE", + "SVELTE_BIND_FUNCTION_BINDING_INITIALIZER_CLAUSE", + "SVELTE_BIND_FUNCTION_BINDING_EXPRESSION", "SVELTE_DIRECTIVE_MODIFIER", "SVELTE_DIRECTIVE_MODIFIER_LIST", "SVELTE_LITERAL",