From 7b9dd644e9dd7be06580bda461709c2972c31e42 Mon Sep 17 00:00:00 2001 From: Zheyu Zhang Date: Fri, 10 May 2024 15:18:47 +0800 Subject: [PATCH] fix(js_formatter): avoid introducing linebreaks for single line string interpolations (#2500) --- CHANGELOG.md | 4 + crates/biome_formatter/src/buffer.rs | 161 +-- .../src/js/auxiliary/template_element.rs | 71 +- .../src/js/lists/template_element_list.rs | 92 +- .../src/utils/test_each_template.rs | 3 +- .../expression/logical_expression.js.snap | 11 +- .../specs/js/module/template/template.js | 14 + .../specs/js/module/template/template.js.snap | 65 +- .../tests/specs/jsx/element.jsx.snap | 18 +- .../js/chain-expression/issue-15785-3.js.snap | 59 - .../js/multiparser-css/issue-5697.js.snap | 71 ++ .../js/multiparser-html/lit-html.js.snap | 22 +- .../js/strings/template-literals.js.snap | 259 ---- .../js/template-literals/expressions.js.snap | 178 --- .../js/template-literals/indention.js.snap | 228 ---- .../sequence-expressions.js.snap | 31 - .../specs/prettier/jsx/text-wrap/test.js.snap | 1070 ----------------- .../template-literal.ts.snap | 65 - .../template-literals/as-expression.ts.snap | 65 - .../tests/specs/ts/type/template_type.ts.snap | 17 +- 20 files changed, 310 insertions(+), 2194 deletions(-) delete mode 100644 crates/biome_js_formatter/tests/specs/prettier/js/chain-expression/issue-15785-3.js.snap create mode 100644 crates/biome_js_formatter/tests/specs/prettier/js/multiparser-css/issue-5697.js.snap delete mode 100644 crates/biome_js_formatter/tests/specs/prettier/js/strings/template-literals.js.snap delete mode 100644 crates/biome_js_formatter/tests/specs/prettier/js/template-literals/expressions.js.snap delete mode 100644 crates/biome_js_formatter/tests/specs/prettier/js/template-literals/indention.js.snap delete mode 100644 crates/biome_js_formatter/tests/specs/prettier/js/template-literals/sequence-expressions.js.snap delete mode 100644 crates/biome_js_formatter/tests/specs/prettier/jsx/text-wrap/test.js.snap delete mode 100644 crates/biome_js_formatter/tests/specs/prettier/typescript/satisfies-operators/template-literal.ts.snap delete mode 100644 crates/biome_js_formatter/tests/specs/prettier/typescript/template-literals/as-expression.ts.snap diff --git a/CHANGELOG.md b/CHANGELOG.md index 3867a2c0a017..51f16b02de3a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,10 @@ our [guidelines for writing a good changelog entry](https://github.com/biomejs/b ### Formatter +#### Bug fixes + +- Fix [#2470](https://github.com/biomejs/biome/issues/2470) by avoid introducing linebreaks in single line string interpolations. Contributed by @ah-yu + ### JavaScript APIs ### Linter diff --git a/crates/biome_formatter/src/buffer.rs b/crates/biome_formatter/src/buffer.rs index 5012067d2c96..ef60ca773947 100644 --- a/crates/biome_formatter/src/buffer.rs +++ b/crates/biome_formatter/src/buffer.rs @@ -1,5 +1,6 @@ use super::{write, Arguments, FormatElement}; use crate::format_element::Interned; +use crate::prelude::tag::Condition; use crate::prelude::{LineMode, PrintMode, Tag}; use crate::{Format, FormatResult, FormatState}; use rustc_hash::FxHashMap; @@ -488,10 +489,8 @@ pub struct RemoveSoftLinesBuffer<'a, Context> { /// that are now unused. But there's little harm in that and the cache is cleaned when dropping the buffer. interned_cache: FxHashMap, - /// Marker for whether a `StartConditionalContent(mode: Expanded)` has been - /// written but not yet closed. Expanded content gets removed from this - /// buffer, so anything written while in this state simply gets dropped. - is_in_expanded_conditional_content: bool, + /// Store the conditional content stack to help determine if the current element is within expanded conditional content. + conditional_content_stack: Vec, } impl<'a, Context> RemoveSoftLinesBuffer<'a, Context> { @@ -500,13 +499,26 @@ impl<'a, Context> RemoveSoftLinesBuffer<'a, Context> { Self { inner, interned_cache: FxHashMap::default(), - is_in_expanded_conditional_content: false, + conditional_content_stack: Vec::new(), } } /// Removes the soft line breaks from an interned element. fn clean_interned(&mut self, interned: &Interned) -> Interned { - clean_interned(interned, &mut self.interned_cache) + clean_interned( + interned, + &mut self.interned_cache, + &mut self.conditional_content_stack, + ) + } + + /// Marker for whether a `StartConditionalContent(mode: Expanded)` has been + /// written but not yet closed. + fn is_in_expanded_conditional_content(&self) -> bool { + self.conditional_content_stack + .iter() + .last() + .is_some_and(|condition| condition.mode == PrintMode::Expanded) } } @@ -514,6 +526,7 @@ impl<'a, Context> RemoveSoftLinesBuffer<'a, Context> { fn clean_interned( interned: &Interned, interned_cache: &mut FxHashMap, + condition_content_stack: &mut Vec, ) -> Interned { match interned_cache.get(interned) { Some(cleaned) => cleaned.clone(), @@ -524,20 +537,18 @@ fn clean_interned( .iter() .enumerate() .find_map(|(index, element)| match element { - FormatElement::Line(LineMode::Soft | LineMode::SoftOrSpace) => { - let mut cleaned = Vec::new(); - cleaned.extend_from_slice(&interned[..index]); - Some((cleaned, &interned[index..])) - } - FormatElement::Tag(Tag::StartConditionalContent(condition)) - if condition.mode == PrintMode::Expanded => - { + FormatElement::Line(LineMode::Soft | LineMode::SoftOrSpace) + | FormatElement::Tag( + Tag::StartConditionalContent(_) | Tag::EndConditionalContent, + ) + | FormatElement::BestFitting(_) => { let mut cleaned = Vec::new(); cleaned.extend_from_slice(&interned[..index]); Some((cleaned, &interned[index..])) } FormatElement::Interned(inner) => { - let cleaned_inner = clean_interned(inner, interned_cache); + let cleaned_inner = + clean_interned(inner, interned_cache, condition_content_stack); if &cleaned_inner != inner { let mut cleaned = Vec::with_capacity(interned.len()); @@ -548,41 +559,56 @@ fn clean_interned( None } } - _ => None, }); let result = match result { // Copy the whole interned buffer so that becomes possible to change the necessary elements. Some((mut cleaned, rest)) => { - let mut is_in_expanded_conditional_content = false; - for element in rest { - let element = match element { - FormatElement::Tag(Tag::StartConditionalContent(condition)) - if condition.mode == PrintMode::Expanded => - { - is_in_expanded_conditional_content = true; + let mut element_stack = rest.iter().rev().collect::>(); + while let Some(element) = element_stack.pop() { + match element { + FormatElement::Tag(Tag::StartConditionalContent(condition)) => { + condition_content_stack.push(condition.clone()); continue; } - FormatElement::Tag(Tag::EndConditionalContent) - if is_in_expanded_conditional_content => - { - is_in_expanded_conditional_content = false; + FormatElement::Tag(Tag::EndConditionalContent) => { + condition_content_stack.pop(); continue; } // All content within an expanded conditional gets dropped. If there's a // matching flat variant, that will still get kept. - _ if is_in_expanded_conditional_content => continue, + _ if condition_content_stack + .iter() + .last() + .is_some_and(|condition| condition.mode == PrintMode::Expanded) => + { + continue + } FormatElement::Line(LineMode::Soft) => continue, - FormatElement::Line(LineMode::SoftOrSpace) => FormatElement::Space, + FormatElement::Line(LineMode::SoftOrSpace) => { + cleaned.push(FormatElement::Space) + } FormatElement::Interned(interned) => { - FormatElement::Interned(clean_interned(interned, interned_cache)) + cleaned.push(FormatElement::Interned(clean_interned( + interned, + interned_cache, + condition_content_stack, + ))) + } + // Since this buffer aims to simulate infinite print width, we don't need to retain the best fitting. + // Just extract the flattest variant and then handle elements within it. + FormatElement::BestFitting(best_fitting) => { + let most_flat = best_fitting.most_flat(); + most_flat + .iter() + .rev() + .for_each(|element| element_stack.push(element)); } - element => element.clone(), + element => cleaned.push(element.clone()), }; - cleaned.push(element) } Interned::new(cleaned) @@ -601,47 +627,42 @@ impl Buffer for RemoveSoftLinesBuffer<'_, Context> { type Context = Context; fn write_element(&mut self, element: FormatElement) -> FormatResult<()> { - let element = match element { - FormatElement::Tag(Tag::StartConditionalContent(condition)) => { - match condition.mode { - PrintMode::Expanded => { - // Mark that we're within expanded content so that it - // can be dropped in all future writes until the ending - // tag. - self.is_in_expanded_conditional_content = true; - return Ok(()); - } - PrintMode::Flat => { - // Flat groups have the conditional tag dropped as - // well, since the content within it will _always_ be - // printed by this buffer. - return Ok(()); - } - } - } - FormatElement::Tag(Tag::EndConditionalContent) => { - // NOTE: This assumes that conditional content cannot be nested. - // This is true for all practical cases, but it's _possible_ to - // write IR that breaks this. - self.is_in_expanded_conditional_content = false; - // No matter if this was flat or expanded content, the ending - // tag gets dropped, since the starting tag was also dropped. - return Ok(()); - } - // All content within an expanded conditional gets dropped. If there's a - // matching flat variant, that will still get kept. - _ if self.is_in_expanded_conditional_content => return Ok(()), + let mut element_statck = Vec::new(); + element_statck.push(element); - FormatElement::Line(LineMode::Soft) => return Ok(()), - FormatElement::Line(LineMode::SoftOrSpace) => FormatElement::Space, + while let Some(element) = element_statck.pop() { + match element { + FormatElement::Tag(Tag::StartConditionalContent(condition)) => { + self.conditional_content_stack.push(condition.clone()); + } + FormatElement::Tag(Tag::EndConditionalContent) => { + self.conditional_content_stack.pop(); + } + // All content within an expanded conditional gets dropped. If there's a + // matching flat variant, that will still get kept. + _ if self.is_in_expanded_conditional_content() => continue, - FormatElement::Interned(interned) => { - FormatElement::Interned(self.clean_interned(&interned)) + FormatElement::Line(LineMode::Soft) => continue, + FormatElement::Line(LineMode::SoftOrSpace) => { + self.inner.write_element(FormatElement::Space)? + } + FormatElement::Interned(interned) => { + let cleaned = self.clean_interned(&interned); + self.inner.write_element(FormatElement::Interned(cleaned))? + } + // Since this buffer aims to simulate infinite print width, we don't need to retain the best fitting. + // Just extract the flattest variant and then handle elements within it. + FormatElement::BestFitting(best_fitting) => { + let most_flat = best_fitting.most_flat(); + most_flat + .iter() + .rev() + .for_each(|element| element_statck.push(element.clone())); + } + element => self.inner.write_element(element)?, } - element => element, - }; - - self.inner.write_element(element) + } + Ok(()) } fn elements(&self) -> &[FormatElement] { diff --git a/crates/biome_js_formatter/src/js/auxiliary/template_element.rs b/crates/biome_js_formatter/src/js/auxiliary/template_element.rs index bb969c821eab..8e454c1ee55f 100644 --- a/crates/biome_js_formatter/src/js/auxiliary/template_element.rs +++ b/crates/biome_js_formatter/src/js/auxiliary/template_element.rs @@ -6,11 +6,19 @@ use biome_formatter::{ use crate::context::TabWidth; use crate::js::expressions::array_expression::FormatJsArrayExpressionOptions; -use crate::js::lists::template_element_list::{TemplateElementIndention, TemplateElementLayout}; +use crate::js::lists::template_element_list::TemplateElementIndention; use biome_js_syntax::{ AnyJsExpression, JsSyntaxNode, JsSyntaxToken, JsTemplateElement, TsTemplateElement, }; -use biome_rowan::{declare_node_union, AstNode, SyntaxResult}; +use biome_rowan::{declare_node_union, AstNode, NodeOrToken, SyntaxResult}; + +enum TemplateElementLayout { + /// Tries to format the expression on a single line regardless of the print width. + SingleLine, + + /// Tries to format the expression on a single line but may break the expression if the line otherwise exceeds the print width. + Fit, +} #[derive(Debug, Clone, Default)] pub(crate) struct FormatJsTemplateElement { @@ -44,8 +52,6 @@ declare_node_union! { #[derive(Debug, Copy, Clone, Default)] pub struct TemplateElementOptions { - pub(crate) layout: TemplateElementLayout, - /// The indention to use for this element pub(crate) indention: TemplateElementIndention, @@ -82,10 +88,33 @@ impl Format for FormatTemplateElement { } }); - let format_inner = format_with(|f: &mut JsFormatter| match self.options.layout { + let interned_expression = f.intern(&format_expression)?; + + let layout = if !self.element.has_new_line_in_range() { + let will_break = if let Some(element) = &interned_expression { + element.will_break() + } else { + false + }; + + // make sure the expression won't break to prevent reformat issue + if will_break { + TemplateElementLayout::Fit + } else { + TemplateElementLayout::SingleLine + } + } else { + TemplateElementLayout::Fit + }; + + let format_inner = format_with(|f: &mut JsFormatter| match layout { TemplateElementLayout::SingleLine => { let mut buffer = RemoveSoftLinesBuffer::new(f); - write!(buffer, [format_expression]) + + match &interned_expression { + Some(element) => buffer.write_element(element.clone()), + None => Ok(()), + } } TemplateElementLayout::Fit => { use AnyJsExpression::*; @@ -111,13 +140,21 @@ impl Format for FormatTemplateElement { | JsLogicalExpression(_) | JsInstanceofExpression(_) | JsInExpression(_) + | JsIdentifierExpression(_) ) ); - if indent { - write!(f, [soft_block_indent(&format_expression)]) - } else { - write!(f, [format_expression]) + match &interned_expression { + Some(element) if indent => { + write!( + f, + [soft_block_indent(&format_with( + |f| f.write_element(element.clone()) + ))] + ) + } + Some(element) => f.write_element(element.clone()), + None => Ok(()), } } }); @@ -179,6 +216,20 @@ impl AnyTemplateElement { AnyTemplateElement::TsTemplateElement(template) => template.r_curly_token(), } } + + fn has_new_line_in_range(&self) -> bool { + fn has_new_line_in_node(node: &JsSyntaxNode) -> bool { + node.children_with_tokens().any(|child| match child { + NodeOrToken::Token(token) => token + // no need to check for trailing trivia as it's not possible to have a new line + .leading_trivia() + .pieces() + .any(|trivia| trivia.is_newline()), + NodeOrToken::Node(node) => has_new_line_in_node(&node), + }) + } + has_new_line_in_node(self.syntax()) + } } /// Writes `content` with the specified `indention`. diff --git a/crates/biome_js_formatter/src/js/lists/template_element_list.rs b/crates/biome_js_formatter/src/js/lists/template_element_list.rs index 0c2df6f7997a..185c14c145ee 100644 --- a/crates/biome_js_formatter/src/js/lists/template_element_list.rs +++ b/crates/biome_js_formatter/src/js/lists/template_element_list.rs @@ -5,10 +5,10 @@ use crate::prelude::*; use crate::utils::test_each_template::EachTemplateTable; use biome_formatter::FormatRuleWithOptions; use biome_js_syntax::{ - AnyJsExpression, AnyJsLiteralExpression, AnyJsTemplateElement, AnyTsTemplateElement, - JsLanguage, JsTemplateElementList, TsTemplateElementList, + AnyJsTemplateElement, AnyTsTemplateElement, JsLanguage, JsTemplateElementList, + TsTemplateElementList, }; -use biome_rowan::{declare_node_union, AstNodeListIterator, SyntaxResult}; +use biome_rowan::{declare_node_union, AstNodeListIterator}; use std::iter::FusedIterator; #[derive(Debug, Clone, Default)] @@ -49,12 +49,6 @@ pub(crate) enum AnyTemplateElementList { impl Format for AnyTemplateElementList { fn fmt(&self, f: &mut Formatter) -> FormatResult<()> { - let layout = if self.is_simple(f.comments()) { - TemplateElementLayout::SingleLine - } else { - TemplateElementLayout::Fit - }; - let mut indention = TemplateElementIndention::default(); let mut after_new_line = false; @@ -64,7 +58,6 @@ impl Format for AnyTemplateElementList { let options = TemplateElementOptions { after_new_line, indention, - layout, }; match &element { @@ -104,36 +97,6 @@ impl Format for AnyTemplateElementList { } impl AnyTemplateElementList { - /// Returns `true` for `JsTemplate` if all elements are simple expressions that should be printed on a single line. - /// - /// Simple expressions are: - /// * Identifiers: `this`, `a` - /// * Members: `a.b`, `a[b]`, `a.b[c].d`, `a.b[5]`, `a.b["test"]` - fn is_simple(&self, comments: &JsComments) -> bool { - match self { - AnyTemplateElementList::JsTemplateElementList(list) => { - if list.is_empty() { - return false; - } - - let mut expression_elements = list.iter().filter_map(|element| match element { - AnyJsTemplateElement::JsTemplateElement(element) => Some(element), - _ => None, - }); - - expression_elements.all(|expression_element| { - match expression_element.expression() { - Ok(expression) => { - is_simple_member_expression(expression, comments).unwrap_or(false) - } - Err(_) => false, - } - }) - } - AnyTemplateElementList::TsTemplateElementList(_) => false, - } - } - fn elements(&self) -> TemplateElementIterator { match self { AnyTemplateElementList::JsTemplateElementList(list) => { @@ -146,59 +109,10 @@ impl AnyTemplateElementList { } } -#[derive(Debug, Copy, Clone, Default)] -pub enum TemplateElementLayout { - /// Applied when all expressions are identifiers, `this`, static member expressions, or computed member expressions with number or string literals. - /// Formats the expressions on a single line, even if their width otherwise would exceed the print width. - SingleLine, - - /// Tries to format the expression on a single line but may break the expression if the line otherwise exceeds the print width. - #[default] - Fit, -} - declare_node_union! { AnyTemplateElementOrChunk = AnyTemplateElement | AnyTemplateChunkElement } -fn is_simple_member_expression( - expression: AnyJsExpression, - comments: &JsComments, -) -> SyntaxResult { - let mut current = expression; - - loop { - if comments.has_comments(current.syntax()) { - return Ok(false); - } - - current = match current { - AnyJsExpression::JsStaticMemberExpression(expression) => expression.object()?, - AnyJsExpression::JsComputedMemberExpression(expression) => { - if matches!( - expression.member()?, - AnyJsExpression::AnyJsLiteralExpression( - AnyJsLiteralExpression::JsStringLiteralExpression(_) - | AnyJsLiteralExpression::JsNumberLiteralExpression(_) - ) | AnyJsExpression::JsIdentifierExpression(_) - ) { - expression.object()? - } else { - break; - } - } - AnyJsExpression::JsIdentifierExpression(_) | AnyJsExpression::JsThisExpression(_) => { - return Ok(true); - } - _ => { - break; - } - } - } - - Ok(false) -} - enum TemplateElementIterator { JsTemplateElementList(AstNodeListIterator), TsTemplateElementList(AstNodeListIterator), diff --git a/crates/biome_js_formatter/src/utils/test_each_template.rs b/crates/biome_js_formatter/src/utils/test_each_template.rs index 3832055fe63e..504e91002cd1 100644 --- a/crates/biome_js_formatter/src/utils/test_each_template.rs +++ b/crates/biome_js_formatter/src/utils/test_each_template.rs @@ -1,5 +1,5 @@ use crate::js::auxiliary::template_element::TemplateElementOptions; -use crate::js::lists::template_element_list::{TemplateElementIndention, TemplateElementLayout}; +use crate::js::lists::template_element_list::TemplateElementIndention; use crate::prelude::*; use biome_formatter::printer::Printer; use biome_formatter::{ @@ -243,7 +243,6 @@ impl EachTemplateTable { let options = TemplateElementOptions { after_new_line: false, indention: TemplateElementIndention::default(), - layout: TemplateElementLayout::Fit, }; // print the current column with infinite print width diff --git a/crates/biome_js_formatter/tests/specs/js/module/expression/logical_expression.js.snap b/crates/biome_js_formatter/tests/specs/js/module/expression/logical_expression.js.snap index 76dd0ec7c760..5fb1a5ed8d9a 100644 --- a/crates/biome_js_formatter/tests/specs/js/module/expression/logical_expression.js.snap +++ b/crates/biome_js_formatter/tests/specs/js/module/expression/logical_expression.js.snap @@ -2,7 +2,6 @@ source: crates/biome_formatter_test/src/snapshot_builder.rs info: js/module/expression/logical_expression.js --- - # Input ```js @@ -265,10 +264,7 @@ undefined === throw undefined; }; -const b = `${ - veryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongFoo + - veryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongBar -}`; +const b = `${veryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongFoo + veryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongBar}`; const a = (aa && @@ -329,4 +325,7 @@ a in veryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongBar instanceof Boolean; ``` - +# Lines exceeding max width of 80 characters +``` + 121: const b = `${veryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongFoo + veryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongBar}`; +``` diff --git a/crates/biome_js_formatter/tests/specs/js/module/template/template.js b/crates/biome_js_formatter/tests/specs/js/module/template/template.js index 7c4d033b70d8..fc8de9fed12f 100644 --- a/crates/biome_js_formatter/tests/specs/js/module/template/template.js +++ b/crates/biome_js_formatter/tests/specs/js/module/template/template.js @@ -45,3 +45,17 @@ const foo = `but where will ${ `${// $FlowFixMe found when converting React.createClass to ES6 ExampleStory.getFragment('story')} `; + +// https://github.com/biomejs/biome/issues/2470 +let message = `this is a long message which contains an interpolation: ${format(data)} <- like this`; + +let otherMessage = `this template contains two interpolations: ${this(one)}, which should be kept on its line, +and this other one: ${this(long.placeholder.text.goes.here.so.we.get.a.linebreak) + } +which already had a linebreak so can be broken up +`; + +message = `this is a long messsage a simple interpolation without a linebreak \${foo} <- like this\`; +message = \`whereas this messsage has a linebreak in the interpolation \${ + foo +} <- like this`; \ No newline at end of file diff --git a/crates/biome_js_formatter/tests/specs/js/module/template/template.js.snap b/crates/biome_js_formatter/tests/specs/js/module/template/template.js.snap index afb123478aac..0b1d70aeb42b 100644 --- a/crates/biome_js_formatter/tests/specs/js/module/template/template.js.snap +++ b/crates/biome_js_formatter/tests/specs/js/module/template/template.js.snap @@ -2,7 +2,6 @@ source: crates/biome_formatter_test/src/snapshot_builder.rs info: js/module/template/template.js --- - # Input ```js @@ -54,6 +53,19 @@ const foo = `but where will ${ ExampleStory.getFragment('story')} `; +// https://github.com/biomejs/biome/issues/2470 +let message = `this is a long message which contains an interpolation: ${format(data)} <- like this`; + +let otherMessage = `this template contains two interpolations: ${this(one)}, which should be kept on its line, +and this other one: ${this(long.placeholder.text.goes.here.so.we.get.a.linebreak) + } +which already had a linebreak so can be broken up +`; + +message = `this is a long messsage a simple interpolation without a linebreak \${foo} <- like this\`; +message = \`whereas this messsage has a linebreak in the interpolation \${ + foo +} <- like this`; ``` @@ -109,33 +121,16 @@ output `; // Single Line -const bar = `but where will ${ - this.fanta -} wrap ${baz} ${"hello"} template literal? ${bar.ff.sss} long long long long ${ - foo[3] -} long long long long long long`; +const bar = `but where will ${this.fanta} wrap ${baz} ${"hello"} template literal? ${bar.ff.sss} long long long long ${foo[3]} long long long long long long`; // Fit -const foo = `but where will ${ - (a && b && bar) || (c && d && g) -} wrap long long long long long long`; +const foo = `but where will ${(a && b && bar) || (c && d && g)} wrap long long long long long long`; -const foo = `but where will ${ - (lorem && loremlorem && loremlorem) || (loremc && lorem && loremlorem) -} wrap long long long long long long`; +const foo = `but where will ${(lorem && loremlorem && loremlorem) || (loremc && lorem && loremlorem)} wrap long long long long long long`; const a = ` let expression_is_simple = is_plain_expression(&expression)?; -${ - loooooong || - loooooong || - loooooong || - loooooong || - loooooong || - loooooong || - loooooong || - loooooong -} +${loooooong || loooooong || loooooong || loooooong || loooooong || loooooong || loooooong || loooooong} let expression_is_simple = is_plain_expression(&expression)?; `; @@ -159,6 +154,30 @@ const foo = `but where will ${ ExampleStory.getFragment("story") } `; -``` +// https://github.com/biomejs/biome/issues/2470 +let message = `this is a long message which contains an interpolation: ${format(data)} <- like this`; + +let otherMessage = `this template contains two interpolations: ${this(one)}, which should be kept on its line, +and this other one: ${this( + long.placeholder.text.goes.here.so.we.get.a.linebreak, +)} +which already had a linebreak so can be broken up +`; + +message = `this is a long messsage a simple interpolation without a linebreak \${foo} <- like this\`; +message = \`whereas this messsage has a linebreak in the interpolation \${ + foo +} <- like this`; +``` +# Lines exceeding max width of 80 characters +``` + 30: const bar = `but where will ${this.fanta} wrap ${baz} ${"hello"} template literal? ${bar.ff.sss} long long long long ${foo[3]} long long long long long long`; + 33: const foo = `but where will ${(a && b && bar) || (c && d && g)} wrap long long long long long long`; + 35: const foo = `but where will ${(lorem && loremlorem && loremlorem) || (loremc && lorem && loremlorem)} wrap long long long long long long`; + 39: ${loooooong || loooooong || loooooong || loooooong || loooooong || loooooong || loooooong || loooooong} + 65: let message = `this is a long message which contains an interpolation: ${format(data)} <- like this`; + 67: let otherMessage = `this template contains two interpolations: ${this(one)}, which should be kept on its line, + 74: message = `this is a long messsage a simple interpolation without a linebreak \${foo} <- like this\`; +``` diff --git a/crates/biome_js_formatter/tests/specs/jsx/element.jsx.snap b/crates/biome_js_formatter/tests/specs/jsx/element.jsx.snap index f6c963b93870..fb4f6d43ab18 100644 --- a/crates/biome_js_formatter/tests/specs/jsx/element.jsx.snap +++ b/crates/biome_js_formatter/tests/specs/jsx/element.jsx.snap @@ -2,7 +2,6 @@ source: crates/biome_formatter_test/src/snapshot_builder.rs info: jsx/element.jsx --- - # Input ```jsx @@ -398,9 +397,7 @@ a =
; // Template a = ( -
{`A Long Tempalte String That uses ${ - 5 + 4 - } that will eventually break across multiple lines ${(40 / 3) * 45}`}
+
{`A Long Tempalte String That uses ${5 + 4} that will eventually break across multiple lines ${(40 / 3) * 45}`}
); // Meaningful text after self closing element adds a hard line break @@ -767,11 +764,10 @@ function Component() { 2:
; 7: tooltip="A very long tooltip text that would otherwise make the attribute break 14: - 179: "ui-monospace,SFMono-Regular,SF Mono,Consolas,Liberation Mono,Menlo,monospace", - 200: "ui-monospace,SFMono-Regular,SF Mono,Consolas,Liberation Mono,Menlo,monospace", - 213: "ui-monospace,SFMono-Regular,SF Mono,Consolas,Liberation Mono,Menlo,monospace", - 238:
-  287: 		Uncle Boonmee Who Can Recall His Past Lives dir. Apichatpong Weerasethakul{" "}
+   36: 	
{`A Long Tempalte String That uses ${5 + 4} that will eventually break across multiple lines ${(40 / 3) * 45}`}
+ 177: "ui-monospace,SFMono-Regular,SF Mono,Consolas,Liberation Mono,Menlo,monospace", + 198: "ui-monospace,SFMono-Regular,SF Mono,Consolas,Liberation Mono,Menlo,monospace", + 211: "ui-monospace,SFMono-Regular,SF Mono,Consolas,Liberation Mono,Menlo,monospace", + 236:
+  285: 		Uncle Boonmee Who Can Recall His Past Lives dir. Apichatpong Weerasethakul{" "}
 ```
-
-
diff --git a/crates/biome_js_formatter/tests/specs/prettier/js/chain-expression/issue-15785-3.js.snap b/crates/biome_js_formatter/tests/specs/prettier/js/chain-expression/issue-15785-3.js.snap
deleted file mode 100644
index c1d6f1320a57..000000000000
--- a/crates/biome_js_formatter/tests/specs/prettier/js/chain-expression/issue-15785-3.js.snap
+++ /dev/null
@@ -1,59 +0,0 @@
----
-source: crates/biome_formatter_test/src/snapshot_builder.rs
-info: js/chain-expression/issue-15785-3.js
----
-# Input
-
-```js
-logger.log(
-  `A long template string with a conditional: ${channel?.id}, and then some more content that continues until ${JSON.stringify(location)}`
-);
-logger.log(
-  `A long template string with a conditional: ${channel.id}, and then some more content that continues until ${JSON.stringify(location)}`
-);
-
-```
-
-
-# Prettier differences
-
-```diff
---- Prettier
-+++ Biome
-@@ -1,6 +1,14 @@
- logger.log(
--  `A long template string with a conditional: ${channel?.id}, and then some more content that continues until ${JSON.stringify(location)}`,
-+  `A long template string with a conditional: ${
-+    channel?.id
-+  }, and then some more content that continues until ${JSON.stringify(
-+    location,
-+  )}`,
- );
- logger.log(
--  `A long template string with a conditional: ${channel.id}, and then some more content that continues until ${JSON.stringify(location)}`,
-+  `A long template string with a conditional: ${
-+    channel.id
-+  }, and then some more content that continues until ${JSON.stringify(
-+    location,
-+  )}`,
- );
-```
-
-# Output
-
-```js
-logger.log(
-  `A long template string with a conditional: ${
-    channel?.id
-  }, and then some more content that continues until ${JSON.stringify(
-    location,
-  )}`,
-);
-logger.log(
-  `A long template string with a conditional: ${
-    channel.id
-  }, and then some more content that continues until ${JSON.stringify(
-    location,
-  )}`,
-);
-```
diff --git a/crates/biome_js_formatter/tests/specs/prettier/js/multiparser-css/issue-5697.js.snap b/crates/biome_js_formatter/tests/specs/prettier/js/multiparser-css/issue-5697.js.snap
new file mode 100644
index 000000000000..6853cab36e3f
--- /dev/null
+++ b/crates/biome_js_formatter/tests/specs/prettier/js/multiparser-css/issue-5697.js.snap
@@ -0,0 +1,71 @@
+---
+source: crates/biome_formatter_test/src/snapshot_builder.rs
+info: js/multiparser-css/issue-5697.js
+---
+# Input
+
+```js
+const StyledH1 = styled.div`
+  font-size: 2.5em;
+  font-weight: ${(props) => (props.strong ? 500 : 100)};
+  font-family: ${constants.text.displayFont.fontFamily};
+  letter-spacing: ${(props) => (props.light ? '0.04em' : 0)};
+  color: ${(props) => props.textColor};
+  ${(props) =>
+    props.center
+      ? ` display: flex;
+                align-items: center;
+                justify-content: center;
+                text-align: center;`
+      : ''}
+  @media (max-width: ${(props) => (props.noBreakPoint ? '0' : constants.layout.breakpoint.break1)}px) {
+    font-size: 2em;
+  }
+`;
+
+```
+
+
+# Prettier differences
+
+```diff
+--- Prettier
++++ Biome
+@@ -11,8 +11,7 @@
+                 justify-content: center;
+                 text-align: center;`
+       : ""}
+-  @media (max-width: ${(props) =>
+-    props.noBreakPoint ? "0" : constants.layout.breakpoint.break1}px) {
++  @media (max-width: ${(props) => (props.noBreakPoint ? "0" : constants.layout.breakpoint.break1)}px) {
+     font-size: 2em;
+   }
+ `;
+```
+
+# Output
+
+```js
+const StyledH1 = styled.div`
+  font-size: 2.5em;
+  font-weight: ${(props) => (props.strong ? 500 : 100)};
+  font-family: ${constants.text.displayFont.fontFamily};
+  letter-spacing: ${(props) => (props.light ? "0.04em" : 0)};
+  color: ${(props) => props.textColor};
+  ${(props) =>
+    props.center
+      ? ` display: flex;
+                align-items: center;
+                justify-content: center;
+                text-align: center;`
+      : ""}
+  @media (max-width: ${(props) => (props.noBreakPoint ? "0" : constants.layout.breakpoint.break1)}px) {
+    font-size: 2em;
+  }
+`;
+```
+
+# Lines exceeding max width of 80 characters
+```
+   14:   @media (max-width: ${(props) => (props.noBreakPoint ? "0" : constants.layout.breakpoint.break1)}px) {
+```
diff --git a/crates/biome_js_formatter/tests/specs/prettier/js/multiparser-html/lit-html.js.snap b/crates/biome_js_formatter/tests/specs/prettier/js/multiparser-html/lit-html.js.snap
index c1e16894fbad..7b07d4799d73 100644
--- a/crates/biome_js_formatter/tests/specs/prettier/js/multiparser-html/lit-html.js.snap
+++ b/crates/biome_js_formatter/tests/specs/prettier/js/multiparser-html/lit-html.js.snap
@@ -2,7 +2,6 @@
 source: crates/biome_formatter_test/src/snapshot_builder.rs
 info: js/multiparser-html/lit-html.js
 ---
-
 # Input
 
 ```js
@@ -117,7 +116,7 @@ ${ foo}:${bar};
 ```diff
 --- Prettier
 +++ Biome
-@@ -14,48 +14,63 @@
+@@ -14,48 +14,59 @@
  
    render() {
      return html`
@@ -192,15 +191,11 @@ ${ foo}:${bar};
 -    const tpl = html\`
\${innerExpr(1)} ${outerExpr(2)}
\`; - `; +const trickyParens = html``; -+const nestedFun = /* HTML */ `${outerExpr( -+ 1, -+)} `; ++const nestedFun = /* HTML */ `${outerExpr(1)} `; const closingScriptTagShouldBeEscapedProperly = /* HTML */ ` `; @@ -320,11 +315,7 @@ function HelloWorld() { } const trickyParens = html``; -const nestedFun = /* HTML */ `${outerExpr( - 1, -)} `; +const nestedFun = /* HTML */ `${outerExpr(1)} `; const closingScriptTagShouldBeEscapedProperly = /* HTML */ ` `; + 77: const closingScriptTag2 = /* HTML */ `