diff --git a/crates/biome_html_formatter/src/html/lists/element_list.rs b/crates/biome_html_formatter/src/html/lists/element_list.rs index 76a19aa8e46c..51bb19994098 100644 --- a/crates/biome_html_formatter/src/html/lists/element_list.rs +++ b/crates/biome_html_formatter/src/html/lists/element_list.rs @@ -842,22 +842,97 @@ pub(crate) struct FormatMultilineChildren { impl Format for FormatMultilineChildren { fn fmt(&self, f: &mut Formatter) -> FormatResult<()> { - let format_inner = format_once(|f| { - let prefix = f.intern_vec(self.elements_prefix.take()); + // We do not need the block ident when the list node is at the html root node + if self.is_root { + let format_inner = format_once(|f| { + let prefix = f.intern_vec(self.elements_prefix.take()); + + if let Some(elements) = f.intern_vec(self.elements.take()) { + match self.layout { + MultilineLayout::Fill => { + if let Some(prefix) = prefix { + f.write_elements([prefix])?; + } + f.write_elements([ + FormatElement::Tag(Tag::StartFill), + elements, + FormatElement::Tag(Tag::EndFill), + ])?; + } + MultilineLayout::NoFill => { + f.write_elements([FormatElement::Tag(Tag::StartGroup( + tag::Group::new().with_mode(GroupMode::Expand), + ))])?; + if let Some(prefix) = prefix { + f.write_elements([prefix])?; + } + f.write_elements([elements, FormatElement::Tag(Tag::EndGroup)])?; + } + }; + } - if let Some(elements) = f.intern_vec(self.elements.take()) { - match self.layout { - MultilineLayout::Fill => { + Ok(()) + }); + return write!(f, [format_inner]); + } + + // For Fill layout, we need to ensure the Indent tags are inside the Fill tags + // to avoid tag mismatch errors when interned elements contain nested Indent tags. + match self.layout { + MultilineLayout::Fill => { + let prefix = self.elements_prefix.take(); + let elements = self.elements.take(); + + let format_inner = format_once(|f| { + let prefix = f.intern_vec(prefix); + + if let Some(elements) = f.intern_vec(elements) { if let Some(prefix) = prefix { f.write_elements([prefix])?; } - f.write_elements([ - FormatElement::Tag(Tag::StartFill), - elements, - FormatElement::Tag(Tag::EndFill), - ])?; + // Put the Fill tags on the outside, with block_indent inside + // This ensures that when interned elements contain nested Indent tags, + // they are properly closed before the Fill tag is closed. + f.write_elements([FormatElement::Tag(Tag::StartFill)])?; + // Create a format closure for the interned elements and wrap it in block_indent + let elements_format = format_with(|f| { + f.write_elements([elements]) + }); + write!(f, [block_indent(&elements_format)])?; + f.write_elements([FormatElement::Tag(Tag::EndFill)])?; } - MultilineLayout::NoFill => { + + Ok(()) + }); + // This indent is wrapped with a group to ensure that the print mode is + // set to `Expanded` when the group prints and will guarantee that the + // content _does not_ fit when printed as part of a `Fill`. Example: + //
+ // + // + // {" "} + // ({variable}) + //
+ // The `...` is the element that gets wrapped in the group + // by this line. Importantly, it contains a hard line break, and because + // [FitsMeasurer::fits_element] considers all hard lines as `Fits::Yes`, + // it will cause the element and the following separator to be printed + // in flat mode due to the logic of `Fill`. But because the we know the + // item breaks over multiple lines, we want it to _not_ fit and print + // both the content and the separator in Expanded mode, keeping the + // formatting as shown above. + // + // The `group` here allows us to opt-in to telling the `FitsMeasurer` + // that content that breaks shouldn't be considered flat and should be + // expanded. This is in contrast to something like a concise array fill, + // which _does_ allow breaks to fit and preserves density. + write!(f, [group(&format_inner)]) + } + MultilineLayout::NoFill => { + let format_inner = format_once(|f| { + let prefix = f.intern_vec(self.elements_prefix.take()); + + if let Some(elements) = f.intern_vec(self.elements.take()) { f.write_elements([FormatElement::Tag(Tag::StartGroup( tag::Group::new().with_mode(GroupMode::Expand), ))])?; @@ -866,39 +941,34 @@ impl Format for FormatMultilineChildren { } f.write_elements([elements, FormatElement::Tag(Tag::EndGroup)])?; } - }; - } - Ok(()) - }); - // We do not need the block ident when the list node is at the html root node - if self.is_root { - return write!(f, [format_inner]); + Ok(()) + }); + // This indent is wrapped with a group to ensure that the print mode is + // set to `Expanded` when the group prints and will guarantee that the + // content _does not_ fit when printed as part of a `Fill`. Example: + //
+ // + // + // {" "} + // ({variable}) + //
+ // The `...` is the element that gets wrapped in the group + // by this line. Importantly, it contains a hard line break, and because + // [FitsMeasurer::fits_element] considers all hard lines as `Fits::Yes`, + // it will cause the element and the following separator to be printed + // in flat mode due to the logic of `Fill`. But because the we know the + // item breaks over multiple lines, we want it to _not_ fit and print + // both the content and the separator in Expanded mode, keeping the + // formatting as shown above. + // + // The `group` here allows us to opt-in to telling the `FitsMeasurer` + // that content that breaks shouldn't be considered flat and should be + // expanded. This is in contrast to something like a concise array fill, + // which _does_ allow breaks to fit and preserves density. + write!(f, [group(&block_indent(&format_inner))]) + } } - - // This indent is wrapped with a group to ensure that the print mode is - // set to `Expanded` when the group prints and will guarantee that the - // content _does not_ fit when printed as part of a `Fill`. Example: - //
- // - // - // {" "} - // ({variable}) - //
- // The `...` is the element that gets wrapped in the group - // by this line. Importantly, it contains a hard line break, and because - // [FitsMeasurer::fits_element] considers all hard lines as `Fits::Yes`, - // it will cause the element and the following separator to be printed - // in flat mode due to the logic of `Fill`. But because the we know the - // item breaks over multiple lines, we want it to _not_ fit and print - // both the content and the separator in Expanded mode, keeping the - // formatting as shown above. - // - // The `group` here allows us to opt-in to telling the `FitsMeasurer` - // that content that breaks shouldn't be considered flat and should be - // expanded. This is in contrast to something like a concise array fill, - // which _does_ allow breaks to fit and preserves density. - write!(f, [group(&block_indent(&format_inner))]) } } diff --git a/crates/biome_html_formatter/tests/specs/prettier/html/angular-control-flow.html b/crates/biome_html_formatter/tests/specs/prettier/html/angular-control-flow.html new file mode 100644 index 000000000000..c84f1263bd9b --- /dev/null +++ b/crates/biome_html_formatter/tests/specs/prettier/html/angular-control-flow.html @@ -0,0 +1,48 @@ +
+ @if (message.status === "Failure") { +
+ +
+ } + + + {{ message.sender }} + + + @switch (message.type) { @case ("Text") {{{ message.text }}} @case + ("File") {{{ message.fileName }}({{ message.fileSize | fileSize }}) + @if (isImage) { + image} } } + + + @switch (message.type) { @case ("Text") { + + } @case ("File") { + + } }{{ message.timestamp | date: "HH:mm" }} + + + @if (message.status === "Queued") { + } + + +
+ diff --git a/crates/biome_js_analyze/src/lint/correctness/use_jsx_key_in_iterable.rs b/crates/biome_js_analyze/src/lint/correctness/use_jsx_key_in_iterable.rs index db0f2b433242..d8a92ccc7be4 100644 --- a/crates/biome_js_analyze/src/lint/correctness/use_jsx_key_in_iterable.rs +++ b/crates/biome_js_analyze/src/lint/correctness/use_jsx_key_in_iterable.rs @@ -128,6 +128,11 @@ fn handle_collections( options: &UseJsxKeyInIterableOptions, ) -> Vec { let is_inside_jsx = node.parent::().is_some(); + // Only check for keys when the array is inside JSX context (used for rendering) + // Arrays outside JSX context (e.g., const arr = []) don't need keys + if !is_inside_jsx { + return vec![]; + } node.elements() .iter() .filter_map(|node| { diff --git a/crates/biome_js_analyze/tests/specs/correctness/useJsxKeyInIterable/invalid.jsx b/crates/biome_js_analyze/tests/specs/correctness/useJsxKeyInIterable/invalid.jsx index 47b14d4776bf..baefb818c5d8 100644 --- a/crates/biome_js_analyze/tests/specs/correctness/useJsxKeyInIterable/invalid.jsx +++ b/crates/biome_js_analyze/tests/specs/correctness/useJsxKeyInIterable/invalid.jsx @@ -1,11 +1,5 @@ import React from "react"; -[, , ]; - -[...[, ], ]; - -[, xyz ? : , ]; - data.map(x => {x}); data.map(x => <>{x}); @@ -18,12 +12,13 @@ Array.from([1, 2, 3], (x) => { return {x} }); -[React.createElement("h1"), React.createElement("h1"), React.createElement("h1")]; - data.map(c => React.createElement("h1")); React.Children.map(c => React.cloneElement(c)); +// Standalone arrays (not inside JSX) should not trigger the rule +// These cases are now handled in valid.jsx + (

{data.map(x =>

{x}

)}) (

{[

,

,

]}) diff --git a/crates/biome_js_analyze/tests/specs/correctness/useJsxKeyInIterable/valid.jsx b/crates/biome_js_analyze/tests/specs/correctness/useJsxKeyInIterable/valid.jsx index a5ac83641b49..219b1cf30ba4 100644 --- a/crates/biome_js_analyze/tests/specs/correctness/useJsxKeyInIterable/valid.jsx +++ b/crates/biome_js_analyze/tests/specs/correctness/useJsxKeyInIterable/valid.jsx @@ -108,6 +108,25 @@ const Valid = [<>

Test 3

] +// Arrays outside JSX context should not require keys +const components = [, , ]; + +const routes = [, ]; + +export const menuItems = [, ]; + +let items = []; + +var config = [, ]; + +// Standalone array literals (not inside JSX) should not require keys +[, , ]; + +[...[, ], ]; + +[, xyz ? : , ]; + +[React.createElement("h1"), React.createElement("h1"), React.createElement("h1")]; // should not generate diagnostics import { component$ } from "@builder.io/qwik";