diff --git a/crates/biome_css_formatter/src/css/auxiliary/metavariable.rs b/crates/biome_css_formatter/src/css/auxiliary/metavariable.rs index cb87ff13e33c..52db9cf53bf8 100644 --- a/crates/biome_css_formatter/src/css/auxiliary/metavariable.rs +++ b/crates/biome_css_formatter/src/css/auxiliary/metavariable.rs @@ -1,10 +1,12 @@ use crate::prelude::*; +use crate::verbatim::format_css_verbatim_node; use biome_css_syntax::CssMetavariable; use biome_rowan::AstNode; + #[derive(Debug, Clone, Default)] pub(crate) struct FormatCssMetavariable; impl FormatNodeRule for FormatCssMetavariable { fn fmt_fields(&self, node: &CssMetavariable, f: &mut CssFormatter) -> FormatResult<()> { - format_verbatim_node(node.syntax()).fmt(f) + format_css_verbatim_node(node.syntax()).fmt(f) } } diff --git a/crates/biome_css_formatter/src/css/auxiliary/value_at_rule_declaration_clause.rs b/crates/biome_css_formatter/src/css/auxiliary/value_at_rule_declaration_clause.rs index 8827f37a2f3e..434eac4eeee2 100644 --- a/crates/biome_css_formatter/src/css/auxiliary/value_at_rule_declaration_clause.rs +++ b/crates/biome_css_formatter/src/css/auxiliary/value_at_rule_declaration_clause.rs @@ -1,6 +1,7 @@ use crate::prelude::*; -use biome_css_syntax::CssValueAtRuleDeclarationClause; -use biome_rowan::AstNode; +use biome_css_syntax::{CssValueAtRuleDeclarationClause, CssValueAtRuleDeclarationClauseFields}; +use biome_formatter::write; + #[derive(Debug, Clone, Default)] pub(crate) struct FormatCssValueAtRuleDeclarationClause; impl FormatNodeRule for FormatCssValueAtRuleDeclarationClause { @@ -9,6 +10,8 @@ impl FormatNodeRule for FormatCssValueAtRuleDec node: &CssValueAtRuleDeclarationClause, f: &mut CssFormatter, ) -> FormatResult<()> { - format_verbatim_node(node.syntax()).fmt(f) + let CssValueAtRuleDeclarationClauseFields { properties } = node.as_fields(); + + write!(f, [properties.format()]) } } diff --git a/crates/biome_css_formatter/src/lib.rs b/crates/biome_css_formatter/src/lib.rs index ae160a49e27e..421234a5d093 100644 --- a/crates/biome_css_formatter/src/lib.rs +++ b/crates/biome_css_formatter/src/lib.rs @@ -8,6 +8,7 @@ mod generated; mod prelude; mod separated; mod utils; +mod verbatim; use std::borrow::Cow; @@ -15,6 +16,7 @@ use crate::comments::CssCommentStyle; pub(crate) use crate::context::CssFormatContext; use crate::context::CssFormatOptions; use crate::cst::FormatCssSyntaxNode; +use crate::prelude::{format_bogus_node, format_suppressed_node}; use biome_css_syntax::{ AnyCssDeclarationBlock, AnyCssRule, AnyCssRuleBlock, AnyCssValue, CssLanguage, CssSyntaxKind, CssSyntaxNode, CssSyntaxToken, diff --git a/crates/biome_css_formatter/src/prelude.rs b/crates/biome_css_formatter/src/prelude.rs index 92be37fabab3..77a2767beb4b 100644 --- a/crates/biome_css_formatter/src/prelude.rs +++ b/crates/biome_css_formatter/src/prelude.rs @@ -5,6 +5,7 @@ pub(crate) use crate::separated::FormatAstSeparatedListExtension; pub(crate) use crate::{ AsFormat, CssFormatContext, CssFormatter, FormatNodeRule, FormattedIterExt as _, IntoFormat, + verbatim::*, }; pub(crate) use biome_formatter::prelude::*; pub(crate) use biome_rowan::{ diff --git a/crates/biome_css_formatter/src/verbatim.rs b/crates/biome_css_formatter/src/verbatim.rs new file mode 100644 index 000000000000..214a0cda9b40 --- /dev/null +++ b/crates/biome_css_formatter/src/verbatim.rs @@ -0,0 +1,203 @@ +use crate::context::CssFormatContext; +use biome_css_syntax::{CssLanguage, CssSyntaxNode}; +use biome_formatter::format_element::tag::VerbatimKind; +use biome_formatter::formatter::Formatter; +use biome_formatter::prelude::{Tag, dynamic_text}; +use biome_formatter::trivia::{FormatLeadingComments, FormatTrailingComments}; +use biome_formatter::{ + Buffer, CstFormatContext, Format, FormatContext, FormatElement, FormatError, FormatResult, + FormatWithRule, LINE_TERMINATORS, normalize_newlines, +}; +use biome_rowan::{AstNode, Direction, SyntaxElement, TextRange}; + +/// "Formats" a node according to its original formatting in the source text. Being able to format +/// a node "as is" is useful if a node contains syntax errors. Formatting a node with syntax errors +/// has the risk that Biome misinterprets the structure of the code and formatting it could +/// "mess up" the developers, yet incomplete, work or accidentally introduce new syntax errors. +/// +/// You may be inclined to call `node.text` directly. However, using `text` doesn't track the nodes +/// nor its children source mapping information, resulting in incorrect source maps for this subtree. +/// +/// These nodes and tokens get tracked as [VerbatimKind::Verbatim], useful to understand +/// if these nodes still need to have their own implementation. +pub fn format_css_verbatim_node(node: &CssSyntaxNode) -> FormatCssVerbatimNode { + FormatCssVerbatimNode { + node, + kind: VerbatimKind::Verbatim { + length: node.text_range_with_trivia().len(), + }, + format_comments: true, + } +} + +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +pub struct FormatCssVerbatimNode<'node> { + node: &'node CssSyntaxNode, + kind: VerbatimKind, + format_comments: bool, +} + +impl Format for FormatCssVerbatimNode<'_> { + fn fmt(&self, f: &mut Formatter) -> FormatResult<()> { + for element in self.node.descendants_with_tokens(Direction::Next) { + match element { + SyntaxElement::Token(token) => f.state_mut().track_token(&token), + SyntaxElement::Node(node) => { + let comments = f.context().comments(); + comments.mark_suppression_checked(&node); + + for comment in comments.leading_dangling_trailing_comments(&node) { + comment.mark_formatted(); + } + } + } + } + + // The trimmed range of a node is its range without any of its leading or trailing trivia. + // Except for nodes that used to be parenthesized, the range than covers the source from the + // `(` to the `)` (the trimmed range of the parenthesized expression, not the inner expression) + let trimmed_source_range = f.context().source_map().map_or_else( + || self.node.text_trimmed_range(), + |source_map| source_map.trimmed_source_range(self.node), + ); + + f.write_element(FormatElement::Tag(Tag::StartVerbatim(self.kind)))?; + + fn source_range(f: &Formatter, range: TextRange) -> TextRange + where + Context: CstFormatContext, + { + f.context() + .source_map() + .map_or_else(|| range, |source_map| source_map.source_range(range)) + } + + // Format all leading comments that are outside of the node's source range. + if self.format_comments { + let comments = f.context().comments().clone(); + let leading_comments = comments.leading_comments(self.node); + + let outside_trimmed_range = leading_comments.partition_point(|comment| { + comment.piece().text_range().end() <= trimmed_source_range.start() + }); + + let (outside_trimmed_range, in_trimmed_range) = + leading_comments.split_at(outside_trimmed_range); + + biome_formatter::write!(f, [FormatLeadingComments::Comments(outside_trimmed_range)])?; + + for comment in in_trimmed_range { + comment.mark_formatted(); + } + } + + // Find the first skipped token trivia, if any, and include it in the verbatim range because + // the comments only format **up to** but not including skipped token trivia. + let start_source = self + .node + .first_leading_trivia() + .into_iter() + .flat_map(|trivia| trivia.pieces()) + .filter(|trivia| trivia.is_skipped()) + .map(|trivia| source_range(f, trivia.text_range()).start()) + .take_while(|start| *start < trimmed_source_range.start()) + .next() + .unwrap_or_else(|| trimmed_source_range.start()); + + let original_source = f.context().source_map().map_or_else( + || self.node.text_trimmed().to_string(), + |source_map| { + source_map + .source() + .text_slice(trimmed_source_range.cover_offset(start_source)) + .to_string() + }, + ); + + dynamic_text( + &normalize_newlines(&original_source, LINE_TERMINATORS), + self.node.text_trimmed_range().start(), + ) + .fmt(f)?; + + for comment in f.context().comments().dangling_comments(self.node) { + comment.mark_formatted(); + } + + // Format all trailing comments that are outside of the trimmed range. + if self.format_comments { + let comments = f.context().comments().clone(); + + let trailing_comments = comments.trailing_comments(self.node); + + let outside_trimmed_range_start = trailing_comments.partition_point(|comment| { + source_range(f, comment.piece().text_range()).end() <= trimmed_source_range.end() + }); + + let (in_trimmed_range, outside_trimmed_range) = + trailing_comments.split_at(outside_trimmed_range_start); + + for comment in in_trimmed_range { + comment.mark_formatted(); + } + + biome_formatter::write!(f, [FormatTrailingComments::Comments(outside_trimmed_range)])?; + } + + f.write_element(FormatElement::Tag(Tag::EndVerbatim)) + } +} + +/// Formats bogus nodes. The difference between this method and `format_verbatim` is that this method +/// doesn't track nodes/tokens as [VerbatimKind::Verbatim]. They are just printed as they are. +pub fn format_bogus_node(node: &CssSyntaxNode) -> FormatCssVerbatimNode { + FormatCssVerbatimNode { + node, + kind: VerbatimKind::Bogus, + format_comments: true, + } +} + +/// Format a node having formatter suppression comment applied to it +pub fn format_suppressed_node(node: &CssSyntaxNode) -> FormatCssVerbatimNode { + FormatCssVerbatimNode { + node, + kind: VerbatimKind::Suppressed, + format_comments: true, + } +} + +/// Formats an object using its [`Format`] implementation but falls back to printing the object as +/// it is in the source document if formatting it returns an [`FormatError::SyntaxError`]. +pub const fn format_or_verbatim(inner: F) -> FormatNodeOrVerbatim { + FormatNodeOrVerbatim { inner } +} + +/// Formats a node or falls back to verbatim printing if formating this node fails. +#[derive(Copy, Clone)] +pub struct FormatNodeOrVerbatim { + inner: F, +} + +impl Format for FormatNodeOrVerbatim +where + F: FormatWithRule, + Item: AstNode, +{ + fn fmt(&self, f: &mut Formatter) -> FormatResult<()> { + let snapshot = Formatter::state_snapshot(f); + + match self.inner.fmt(f) { + Ok(result) => Ok(result), + + Err(FormatError::SyntaxError) => { + f.restore_state_snapshot(snapshot); + + // Lists that yield errors are formatted as they were suppressed nodes. + // Doing so, the formatter formats the nodes/tokens as is. + format_suppressed_node(self.inner.item().syntax()).fmt(f) + } + Err(err) => Err(err), + } + } +} diff --git a/crates/biome_css_formatter/tests/specs/css/atrule/value.css.snap b/crates/biome_css_formatter/tests/specs/css/atrule/value.css.snap index 2e9a7ae3479e..48fe0da4b2b3 100644 --- a/crates/biome_css_formatter/tests/specs/css/atrule/value.css.snap +++ b/crates/biome_css_formatter/tests/specs/css/atrule/value.css.snap @@ -64,38 +64,17 @@ Quote style: Double Quotes ----- ```css -@value colors: -"./colors.css"; +@value colors: "./colors.css"; @value primary, secondary from colors; @value small as bp-small, medium, large as bp-large from "./breakpoints.css"; -@value selectorValue: -secondary-color; -@value small: -(max-width: +@value selectorValue: secondary-color; +@value small: (max-width: 599px); -@value medium: -(min-width: 600px) and (max-width: 959px); -@value large: -(min-width: 960px); -@value primary: -#BF4040; -@value secondary: -#1F4F7F; -@value common-gradient: -transparent 75%, +@value medium: (min-width: 600px) and (max-width: 959px); +@value large: (min-width: 960px); +@value primary: #BF4040; +@value secondary: #1F4F7F; +@value common-gradient: transparent 75%, var(--ring-line-color) 75%, currentColor 79%; ``` - - - -## Unimplemented nodes/tokens - -" colors:\n\"./colors.css\"" => 6..29 -" selectorValue:\nsecondary-color" => 154..185 -" small:\n(max-width:\n599px)" => 193..219 -" medium:\n(min-width: 600px) and (max-width: 959px)" => 227..277 -" large:\n(min-width: 960px)" => 285..311 -" primary:\n#BF4040" => 319..336 -" secondary:\n#1F4F7F" => 344..363 -" common-gradient:\ntransparent 75%,\nvar(--ring-line-color) 75%,\ncurrentColor 79%" => 371..450 diff --git a/crates/biome_formatter/src/lib.rs b/crates/biome_formatter/src/lib.rs index 09c7d3d17b60..0914709c5e0f 100644 --- a/crates/biome_formatter/src/lib.rs +++ b/crates/biome_formatter/src/lib.rs @@ -40,7 +40,6 @@ pub mod separated; mod source_map; pub mod token; pub mod trivia; -mod verbatim; use crate::formatter::Formatter; use crate::group_id::UniqueGroupIdBuilder; diff --git a/crates/biome_formatter/src/prelude.rs b/crates/biome_formatter/src/prelude.rs index b550d1353d22..22f0a6926702 100644 --- a/crates/biome_formatter/src/prelude.rs +++ b/crates/biome_formatter/src/prelude.rs @@ -11,10 +11,6 @@ pub use crate::trivia::{ pub use crate::diagnostics::FormatError; pub use crate::format_element::document::Document; pub use crate::format_element::tag::{LabelId, Tag, TagKind}; -pub use crate::verbatim::{ - format_bogus_node, format_or_verbatim, format_suppressed_node, format_verbatim_node, - format_verbatim_skipped, -}; pub use crate::{ Buffer as _, BufferExtensions, Format, Format as _, FormatResult, FormatRule, diff --git a/crates/biome_graphql_formatter/src/lib.rs b/crates/biome_graphql_formatter/src/lib.rs index 66e4ed956d55..2f3e2bae3f11 100644 --- a/crates/biome_graphql_formatter/src/lib.rs +++ b/crates/biome_graphql_formatter/src/lib.rs @@ -7,11 +7,13 @@ mod generated; mod graphql; mod prelude; mod utils; +mod verbatim; use crate::comments::GraphqlCommentStyle; pub(crate) use crate::context::GraphqlFormatContext; use crate::context::GraphqlFormatOptions; use crate::cst::FormatGraphqlSyntaxNode; +use crate::prelude::{format_bogus_node, format_suppressed_node}; use biome_formatter::comments::Comments; use biome_formatter::prelude::*; use biome_formatter::{ diff --git a/crates/biome_graphql_formatter/src/prelude.rs b/crates/biome_graphql_formatter/src/prelude.rs index 7f3244a7f450..efc2e8b6a58c 100644 --- a/crates/biome_graphql_formatter/src/prelude.rs +++ b/crates/biome_graphql_formatter/src/prelude.rs @@ -4,7 +4,7 @@ pub(crate) use crate::{ AsFormat, FormatNodeRule, FormattedIterExt as _, GraphqlFormatContext, GraphqlFormatter, - IntoFormat, + IntoFormat, verbatim::*, }; pub(crate) use biome_formatter::prelude::*; pub(crate) use biome_rowan::{ diff --git a/crates/biome_graphql_formatter/src/verbatim.rs b/crates/biome_graphql_formatter/src/verbatim.rs new file mode 100644 index 000000000000..fec0d2614392 --- /dev/null +++ b/crates/biome_graphql_formatter/src/verbatim.rs @@ -0,0 +1,204 @@ +use crate::context::GraphqlFormatContext; +use biome_formatter::format_element::tag::VerbatimKind; +use biome_formatter::formatter::Formatter; +use biome_formatter::prelude::{Tag, dynamic_text}; +use biome_formatter::trivia::{FormatLeadingComments, FormatTrailingComments}; +use biome_formatter::{ + Buffer, CstFormatContext, Format, FormatContext, FormatElement, FormatError, FormatResult, + FormatWithRule, LINE_TERMINATORS, normalize_newlines, +}; +use biome_graphql_syntax::{GraphqlLanguage, GraphqlSyntaxNode}; +use biome_rowan::{AstNode, Direction, SyntaxElement, TextRange}; + +/// "Formats" a node according to its original formatting in the source text. Being able to format +/// a node "as is" is useful if a node contains syntax errors. Formatting a node with syntax errors +/// has the risk that Biome misinterprets the structure of the code and formatting it could +/// "mess up" the developers, yet incomplete, work or accidentally introduce new syntax errors. +/// +/// You may be inclined to call `node.text` directly. However, using `text` doesn't track the nodes +/// nor its children source mapping information, resulting in incorrect source maps for this subtree. +/// +/// These nodes and tokens get tracked as [VerbatimKind::Verbatim], useful to understand +/// if these nodes still need to have their own implementation. +#[expect(unused)] +pub fn format_graphql_verbatim_node(node: &GraphqlSyntaxNode) -> FormatGraphqlVerbatimNode { + FormatGraphqlVerbatimNode { + node, + kind: VerbatimKind::Verbatim { + length: node.text_range_with_trivia().len(), + }, + format_comments: true, + } +} + +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +pub struct FormatGraphqlVerbatimNode<'node> { + node: &'node GraphqlSyntaxNode, + kind: VerbatimKind, + format_comments: bool, +} + +impl Format for FormatGraphqlVerbatimNode<'_> { + fn fmt(&self, f: &mut Formatter) -> FormatResult<()> { + for element in self.node.descendants_with_tokens(Direction::Next) { + match element { + SyntaxElement::Token(token) => f.state_mut().track_token(&token), + SyntaxElement::Node(node) => { + let comments = f.context().comments(); + comments.mark_suppression_checked(&node); + + for comment in comments.leading_dangling_trailing_comments(&node) { + comment.mark_formatted(); + } + } + } + } + + // The trimmed range of a node is its range without any of its leading or trailing trivia. + // Except for nodes that used to be parenthesized, the range than covers the source from the + // `(` to the `)` (the trimmed range of the parenthesized expression, not the inner expression) + let trimmed_source_range = f.context().source_map().map_or_else( + || self.node.text_trimmed_range(), + |source_map| source_map.trimmed_source_range(self.node), + ); + + f.write_element(FormatElement::Tag(Tag::StartVerbatim(self.kind)))?; + + fn source_range(f: &Formatter, range: TextRange) -> TextRange + where + Context: CstFormatContext, + { + f.context() + .source_map() + .map_or_else(|| range, |source_map| source_map.source_range(range)) + } + + // Format all leading comments that are outside of the node's source range. + if self.format_comments { + let comments = f.context().comments().clone(); + let leading_comments = comments.leading_comments(self.node); + + let outside_trimmed_range = leading_comments.partition_point(|comment| { + comment.piece().text_range().end() <= trimmed_source_range.start() + }); + + let (outside_trimmed_range, in_trimmed_range) = + leading_comments.split_at(outside_trimmed_range); + + biome_formatter::write!(f, [FormatLeadingComments::Comments(outside_trimmed_range)])?; + + for comment in in_trimmed_range { + comment.mark_formatted(); + } + } + + // Find the first skipped token trivia, if any, and include it in the verbatim range because + // the comments only format **up to** but not including skipped token trivia. + let start_source = self + .node + .first_leading_trivia() + .into_iter() + .flat_map(|trivia| trivia.pieces()) + .filter(|trivia| trivia.is_skipped()) + .map(|trivia| source_range(f, trivia.text_range()).start()) + .take_while(|start| *start < trimmed_source_range.start()) + .next() + .unwrap_or_else(|| trimmed_source_range.start()); + + let original_source = f.context().source_map().map_or_else( + || self.node.text_trimmed().to_string(), + |source_map| { + source_map + .source() + .text_slice(trimmed_source_range.cover_offset(start_source)) + .to_string() + }, + ); + + dynamic_text( + &normalize_newlines(&original_source, LINE_TERMINATORS), + self.node.text_trimmed_range().start(), + ) + .fmt(f)?; + + for comment in f.context().comments().dangling_comments(self.node) { + comment.mark_formatted(); + } + + // Format all trailing comments that are outside of the trimmed range. + if self.format_comments { + let comments = f.context().comments().clone(); + + let trailing_comments = comments.trailing_comments(self.node); + + let outside_trimmed_range_start = trailing_comments.partition_point(|comment| { + source_range(f, comment.piece().text_range()).end() <= trimmed_source_range.end() + }); + + let (in_trimmed_range, outside_trimmed_range) = + trailing_comments.split_at(outside_trimmed_range_start); + + for comment in in_trimmed_range { + comment.mark_formatted(); + } + + biome_formatter::write!(f, [FormatTrailingComments::Comments(outside_trimmed_range)])?; + } + + f.write_element(FormatElement::Tag(Tag::EndVerbatim)) + } +} + +/// Formats bogus nodes. The difference between this method and `format_verbatim` is that this method +/// doesn't track nodes/tokens as [VerbatimKind::Verbatim]. They are just printed as they are. +pub fn format_bogus_node(node: &GraphqlSyntaxNode) -> FormatGraphqlVerbatimNode { + FormatGraphqlVerbatimNode { + node, + kind: VerbatimKind::Bogus, + format_comments: true, + } +} + +/// Format a node having formatter suppression comment applied to it +pub fn format_suppressed_node(node: &GraphqlSyntaxNode) -> FormatGraphqlVerbatimNode { + FormatGraphqlVerbatimNode { + node, + kind: VerbatimKind::Suppressed, + format_comments: true, + } +} + +/// Formats an object using its [`Format`] implementation but falls back to printing the object as +/// it is in the source document if formatting it returns an [`FormatError::SyntaxError`]. +pub const fn format_or_verbatim(inner: F) -> FormatNodeOrVerbatim { + FormatNodeOrVerbatim { inner } +} + +/// Formats a node or falls back to verbatim printing if formating this node fails. +#[derive(Copy, Clone)] +pub struct FormatNodeOrVerbatim { + inner: F, +} + +impl Format for FormatNodeOrVerbatim +where + F: FormatWithRule, + Item: AstNode, +{ + fn fmt(&self, f: &mut Formatter) -> FormatResult<()> { + let snapshot = Formatter::state_snapshot(f); + + match self.inner.fmt(f) { + Ok(result) => Ok(result), + + Err(FormatError::SyntaxError) => { + f.restore_state_snapshot(snapshot); + + // Lists that yield errors are formatted as they were suppressed nodes. + // Doing so, the formatter formats the nodes/tokens as is. + format_suppressed_node(self.inner.item().syntax()).fmt(f) + } + Err(err) => Err(err), + } + } +} diff --git a/crates/biome_grit_formatter/src/lib.rs b/crates/biome_grit_formatter/src/lib.rs index a2495e986841..5221001a3dfb 100644 --- a/crates/biome_grit_formatter/src/lib.rs +++ b/crates/biome_grit_formatter/src/lib.rs @@ -7,6 +7,7 @@ mod generated; mod grit; mod prelude; pub(crate) mod separated; +mod verbatim; use biome_formatter::{ CstFormatContext, Format, FormatLanguage, FormatResult, Formatted, Printed, @@ -20,6 +21,8 @@ use comments::GritCommentStyle; pub(crate) use crate::context::GritFormatContext; +use crate::prelude::*; +use crate::verbatim::format_suppressed_node; use biome_rowan::{AstNode, TextRange}; use context::GritFormatOptions; use cst::FormatGritSyntaxNode; diff --git a/crates/biome_grit_formatter/src/prelude.rs b/crates/biome_grit_formatter/src/prelude.rs index 5f7cdd2de1bf..d19ebbf6d271 100644 --- a/crates/biome_grit_formatter/src/prelude.rs +++ b/crates/biome_grit_formatter/src/prelude.rs @@ -4,7 +4,7 @@ pub(crate) use crate::{ AsFormat, FormatNodeRule, FormattedIterExt as _, FormattedIterExt, GritFormatContext, - GritFormatter, IntoFormat, + GritFormatter, IntoFormat, verbatim::*, }; pub(crate) use biome_formatter::prelude::*; pub(crate) use biome_rowan::{AstNode as _, AstSeparatedList}; diff --git a/crates/biome_grit_formatter/src/verbatim.rs b/crates/biome_grit_formatter/src/verbatim.rs new file mode 100644 index 000000000000..c8b7b85fa3f2 --- /dev/null +++ b/crates/biome_grit_formatter/src/verbatim.rs @@ -0,0 +1,204 @@ +use crate::context::GritFormatContext; +use biome_formatter::format_element::tag::VerbatimKind; +use biome_formatter::formatter::Formatter; +use biome_formatter::prelude::{Tag, dynamic_text}; +use biome_formatter::trivia::{FormatLeadingComments, FormatTrailingComments}; +use biome_formatter::{ + Buffer, CstFormatContext, Format, FormatContext, FormatElement, FormatError, FormatResult, + FormatWithRule, LINE_TERMINATORS, normalize_newlines, +}; +use biome_grit_syntax::{GritLanguage, GritSyntaxNode}; +use biome_rowan::{AstNode, Direction, SyntaxElement, TextRange}; + +/// "Formats" a node according to its original formatting in the source text. Being able to format +/// a node "as is" is useful if a node contains syntax errors. Formatting a node with syntax errors +/// has the risk that Biome misinterprets the structure of the code and formatting it could +/// "mess up" the developers, yet incomplete, work or accidentally introduce new syntax errors. +/// +/// You may be inclined to call `node.text` directly. However, using `text` doesn't track the nodes +/// nor its children source mapping information, resulting in incorrect source maps for this subtree. +/// +/// These nodes and tokens get tracked as [VerbatimKind::Verbatim], useful to understand +/// if these nodes still need to have their own implementation. +#[expect(unused)] +pub fn format_graphql_verbatim_node(node: &GritSyntaxNode) -> FormatGraphqlVerbatimNode { + FormatGraphqlVerbatimNode { + node, + kind: VerbatimKind::Verbatim { + length: node.text_range_with_trivia().len(), + }, + format_comments: true, + } +} + +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +pub struct FormatGraphqlVerbatimNode<'node> { + node: &'node GritSyntaxNode, + kind: VerbatimKind, + format_comments: bool, +} + +impl Format for FormatGraphqlVerbatimNode<'_> { + fn fmt(&self, f: &mut Formatter) -> FormatResult<()> { + for element in self.node.descendants_with_tokens(Direction::Next) { + match element { + SyntaxElement::Token(token) => f.state_mut().track_token(&token), + SyntaxElement::Node(node) => { + let comments = f.context().comments(); + comments.mark_suppression_checked(&node); + + for comment in comments.leading_dangling_trailing_comments(&node) { + comment.mark_formatted(); + } + } + } + } + + // The trimmed range of a node is its range without any of its leading or trailing trivia. + // Except for nodes that used to be parenthesized, the range than covers the source from the + // `(` to the `)` (the trimmed range of the parenthesized expression, not the inner expression) + let trimmed_source_range = f.context().source_map().map_or_else( + || self.node.text_trimmed_range(), + |source_map| source_map.trimmed_source_range(self.node), + ); + + f.write_element(FormatElement::Tag(Tag::StartVerbatim(self.kind)))?; + + fn source_range(f: &Formatter, range: TextRange) -> TextRange + where + Context: CstFormatContext, + { + f.context() + .source_map() + .map_or_else(|| range, |source_map| source_map.source_range(range)) + } + + // Format all leading comments that are outside of the node's source range. + if self.format_comments { + let comments = f.context().comments().clone(); + let leading_comments = comments.leading_comments(self.node); + + let outside_trimmed_range = leading_comments.partition_point(|comment| { + comment.piece().text_range().end() <= trimmed_source_range.start() + }); + + let (outside_trimmed_range, in_trimmed_range) = + leading_comments.split_at(outside_trimmed_range); + + biome_formatter::write!(f, [FormatLeadingComments::Comments(outside_trimmed_range)])?; + + for comment in in_trimmed_range { + comment.mark_formatted(); + } + } + + // Find the first skipped token trivia, if any, and include it in the verbatim range because + // the comments only format **up to** but not including skipped token trivia. + let start_source = self + .node + .first_leading_trivia() + .into_iter() + .flat_map(|trivia| trivia.pieces()) + .filter(|trivia| trivia.is_skipped()) + .map(|trivia| source_range(f, trivia.text_range()).start()) + .take_while(|start| *start < trimmed_source_range.start()) + .next() + .unwrap_or_else(|| trimmed_source_range.start()); + + let original_source = f.context().source_map().map_or_else( + || self.node.text_trimmed().to_string(), + |source_map| { + source_map + .source() + .text_slice(trimmed_source_range.cover_offset(start_source)) + .to_string() + }, + ); + + dynamic_text( + &normalize_newlines(&original_source, LINE_TERMINATORS), + self.node.text_trimmed_range().start(), + ) + .fmt(f)?; + + for comment in f.context().comments().dangling_comments(self.node) { + comment.mark_formatted(); + } + + // Format all trailing comments that are outside of the trimmed range. + if self.format_comments { + let comments = f.context().comments().clone(); + + let trailing_comments = comments.trailing_comments(self.node); + + let outside_trimmed_range_start = trailing_comments.partition_point(|comment| { + source_range(f, comment.piece().text_range()).end() <= trimmed_source_range.end() + }); + + let (in_trimmed_range, outside_trimmed_range) = + trailing_comments.split_at(outside_trimmed_range_start); + + for comment in in_trimmed_range { + comment.mark_formatted(); + } + + biome_formatter::write!(f, [FormatTrailingComments::Comments(outside_trimmed_range)])?; + } + + f.write_element(FormatElement::Tag(Tag::EndVerbatim)) + } +} + +/// Formats bogus nodes. The difference between this method and `format_verbatim` is that this method +/// doesn't track nodes/tokens as [VerbatimKind::Verbatim]. They are just printed as they are. +pub fn format_bogus_node(node: &GritSyntaxNode) -> FormatGraphqlVerbatimNode { + FormatGraphqlVerbatimNode { + node, + kind: VerbatimKind::Bogus, + format_comments: true, + } +} + +/// Format a node having formatter suppression comment applied to it +pub fn format_suppressed_node(node: &GritSyntaxNode) -> FormatGraphqlVerbatimNode { + FormatGraphqlVerbatimNode { + node, + kind: VerbatimKind::Suppressed, + format_comments: true, + } +} + +/// Formats an object using its [`Format`] implementation but falls back to printing the object as +/// it is in the source document if formatting it returns an [`FormatError::SyntaxError`]. +pub const fn format_or_verbatim(inner: F) -> FormatNodeOrVerbatim { + FormatNodeOrVerbatim { inner } +} + +/// Formats a node or falls back to verbatim printing if formating this node fails. +#[derive(Copy, Clone)] +pub struct FormatNodeOrVerbatim { + inner: F, +} + +impl Format for FormatNodeOrVerbatim +where + F: FormatWithRule, + Item: AstNode, +{ + fn fmt(&self, f: &mut Formatter) -> FormatResult<()> { + let snapshot = Formatter::state_snapshot(f); + + match self.inner.fmt(f) { + Ok(result) => Ok(result), + + Err(FormatError::SyntaxError) => { + f.restore_state_snapshot(snapshot); + + // Lists that yield errors are formatted as they were suppressed nodes. + // Doing so, the formatter formats the nodes/tokens as is. + format_suppressed_node(self.inner.item().syntax()).fmt(f) + } + Err(err) => Err(err), + } + } +} diff --git a/crates/biome_html_formatter/src/html/auxiliary/comment.rs b/crates/biome_html_formatter/src/html/auxiliary/comment.rs index a793f4946f53..59d2c29f086c 100644 --- a/crates/biome_html_formatter/src/html/auxiliary/comment.rs +++ b/crates/biome_html_formatter/src/html/auxiliary/comment.rs @@ -1,10 +1,12 @@ use crate::prelude::*; +use crate::verbatim::format_html_verbatim_node; use biome_html_syntax::HtmlComment; use biome_rowan::AstNode; + #[derive(Debug, Clone, Default)] pub(crate) struct FormatHtmlComment; impl FormatNodeRule for FormatHtmlComment { fn fmt_fields(&self, node: &HtmlComment, f: &mut HtmlFormatter) -> FormatResult<()> { - format_verbatim_node(node.syntax()).fmt(f) + format_html_verbatim_node(node.syntax()).fmt(f) } } diff --git a/crates/biome_html_formatter/src/html/auxiliary/content.rs b/crates/biome_html_formatter/src/html/auxiliary/content.rs index 97f162daf04d..b0dd88f5e72b 100644 --- a/crates/biome_html_formatter/src/html/auxiliary/content.rs +++ b/crates/biome_html_formatter/src/html/auxiliary/content.rs @@ -1,10 +1,12 @@ use crate::prelude::*; +use crate::verbatim::format_html_verbatim_node; use biome_html_syntax::HtmlContent; use biome_rowan::AstNode; + #[derive(Debug, Clone, Default)] pub(crate) struct FormatHtmlContent; impl FormatNodeRule for FormatHtmlContent { fn fmt_fields(&self, node: &HtmlContent, f: &mut HtmlFormatter) -> FormatResult<()> { - format_verbatim_node(node.syntax()).fmt(f) + format_html_verbatim_node(node.syntax()).fmt(f) } } diff --git a/crates/biome_html_formatter/src/lib.rs b/crates/biome_html_formatter/src/lib.rs index 81ea136e0251..5f5125d7d7c8 100644 --- a/crates/biome_html_formatter/src/lib.rs +++ b/crates/biome_html_formatter/src/lib.rs @@ -1,5 +1,6 @@ #![deny(clippy::use_self)] +use crate::prelude::{format_bogus_node, format_suppressed_node}; use biome_formatter::comments::Comments; use biome_formatter::{CstFormatContext, FormatOwnedWithRule, FormatRefWithRule, prelude::*}; use biome_formatter::{FormatLanguage, FormatResult, FormatToken, Formatted, write}; @@ -19,6 +20,7 @@ mod html; pub(crate) mod prelude; mod svelte; pub mod utils; +mod verbatim; /// Formats a Html file based on its features. /// diff --git a/crates/biome_html_formatter/src/prelude.rs b/crates/biome_html_formatter/src/prelude.rs index d76fed57baaf..0cd89daacca9 100644 --- a/crates/biome_html_formatter/src/prelude.rs +++ b/crates/biome_html_formatter/src/prelude.rs @@ -5,7 +5,7 @@ pub(crate) use crate::{ AsFormat, FormatNodeRule, FormatResult, FormatRule, FormattedIterExt, HtmlFormatContext, - HtmlFormatter, format_verbatim_node, format_verbatim_skipped, + HtmlFormatter, verbatim::*, }; pub(crate) use biome_formatter::prelude::*; pub(crate) use biome_rowan::{AstNode, AstNodeList}; diff --git a/crates/biome_html_formatter/src/svelte/auxiliary/text_expression.rs b/crates/biome_html_formatter/src/svelte/auxiliary/text_expression.rs index fafc172c434b..bbc456377e04 100644 --- a/crates/biome_html_formatter/src/svelte/auxiliary/text_expression.rs +++ b/crates/biome_html_formatter/src/svelte/auxiliary/text_expression.rs @@ -5,6 +5,6 @@ use biome_rowan::AstNode; pub(crate) struct FormatSvelteTextExpression; impl FormatNodeRule for FormatSvelteTextExpression { fn fmt_fields(&self, node: &SvelteTextExpression, f: &mut HtmlFormatter) -> FormatResult<()> { - format_verbatim_node(node.syntax()).fmt(f) + format_html_verbatim_node(node.syntax()).fmt(f) } } diff --git a/crates/biome_html_formatter/src/verbatim.rs b/crates/biome_html_formatter/src/verbatim.rs new file mode 100644 index 000000000000..e501fbbf3334 --- /dev/null +++ b/crates/biome_html_formatter/src/verbatim.rs @@ -0,0 +1,207 @@ +use crate::context::HtmlFormatContext; +use biome_formatter::format_element::tag::VerbatimKind; +use biome_formatter::formatter::Formatter; +use biome_formatter::prelude::{Tag, dynamic_text}; +use biome_formatter::trivia::{FormatLeadingComments, FormatTrailingComments}; +use biome_formatter::{ + Buffer, CstFormatContext, Format, FormatContext, FormatElement, FormatError, FormatResult, + FormatWithRule, LINE_TERMINATORS, normalize_newlines, +}; +use biome_html_syntax::{HtmlLanguage, HtmlSyntaxNode}; +use biome_rowan::{AstNode, Direction, SyntaxElement, TextRange}; + +/// "Formats" a node according to its original formatting in the source text. Being able to format +/// a node "as is" is useful if a node contains syntax errors. Formatting a node with syntax errors +/// has the risk that Biome misinterprets the structure of the code and formatting it could +/// "mess up" the developers, yet incomplete, work or accidentally introduce new syntax errors. +/// +/// You may be inclined to call `node.text` directly. However, using `text` doesn't track the nodes +/// nor its children source mapping information, resulting in incorrect source maps for this subtree. +/// +/// These nodes and tokens get tracked as [VerbatimKind::Verbatim], useful to understand +/// if these nodes still need to have their own implementation. +pub fn format_html_verbatim_node(node: &HtmlSyntaxNode) -> FormatHtmlVerbatimNode { + FormatHtmlVerbatimNode { + node, + kind: VerbatimKind::Verbatim { + length: node.text_range_with_trivia().len(), + }, + format_comments: true, + } +} + +/// "Formats" a node according to its original formatting in the source text. It's functionally equal to +/// [`format_html_verbatim_node`], but it doesn't track the node as [VerbatimKind::Verbatim]. +pub fn format_verbatim_skipped(node: &HtmlSyntaxNode) -> FormatHtmlVerbatimNode { + FormatHtmlVerbatimNode { + node, + kind: VerbatimKind::Skipped, + format_comments: true, + } +} + +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +pub struct FormatHtmlVerbatimNode<'node> { + node: &'node HtmlSyntaxNode, + kind: VerbatimKind, + format_comments: bool, +} + +impl Format for FormatHtmlVerbatimNode<'_> { + fn fmt(&self, f: &mut Formatter) -> FormatResult<()> { + for element in self.node.descendants_with_tokens(Direction::Next) { + match element { + SyntaxElement::Token(token) => f.state_mut().track_token(&token), + SyntaxElement::Node(node) => { + let comments = f.context().comments(); + comments.mark_suppression_checked(&node); + + for comment in comments.leading_dangling_trailing_comments(&node) { + comment.mark_formatted(); + } + } + } + } + + // The trimmed range of a node is its range without any of its leading or trailing trivia. + // Except for nodes that used to be parenthesized, the range than covers the source from the + // `(` to the `)` (the trimmed range of the parenthesized expression, not the inner expression) + let trimmed_source_range = f.context().source_map().map_or_else( + || self.node.text_trimmed_range(), + |source_map| source_map.trimmed_source_range(self.node), + ); + + f.write_element(FormatElement::Tag(Tag::StartVerbatim(self.kind)))?; + + fn source_range(f: &Formatter, range: TextRange) -> TextRange + where + Context: CstFormatContext, + { + f.context() + .source_map() + .map_or_else(|| range, |source_map| source_map.source_range(range)) + } + + // Format all leading comments that are outside of the node's source range. + if self.format_comments { + let comments = f.context().comments().clone(); + let leading_comments = comments.leading_comments(self.node); + + let outside_trimmed_range = leading_comments.partition_point(|comment| { + comment.piece().text_range().end() <= trimmed_source_range.start() + }); + + let (outside_trimmed_range, in_trimmed_range) = + leading_comments.split_at(outside_trimmed_range); + + biome_formatter::write!(f, [FormatLeadingComments::Comments(outside_trimmed_range)])?; + + for comment in in_trimmed_range { + comment.mark_formatted(); + } + } + + // Find the first skipped token trivia, if any, and include it in the verbatim range because + // the comments only format **up to** but not including skipped token trivia. + let start_source = self + .node + .first_leading_trivia() + .into_iter() + .flat_map(|trivia| trivia.pieces()) + .filter(|trivia| trivia.is_skipped()) + .map(|trivia| source_range(f, trivia.text_range()).start()) + .take_while(|start| *start < trimmed_source_range.start()) + .next() + .unwrap_or_else(|| trimmed_source_range.start()); + + let original_source = f.context().source_map().map_or_else( + || self.node.text_trimmed().to_string(), + |source_map| { + source_map + .source() + .text_slice(trimmed_source_range.cover_offset(start_source)) + .to_string() + }, + ); + + dynamic_text( + &normalize_newlines(&original_source, LINE_TERMINATORS), + self.node.text_trimmed_range().start(), + ) + .fmt(f)?; + + for comment in f.context().comments().dangling_comments(self.node) { + comment.mark_formatted(); + } + + // Format all trailing comments that are outside of the trimmed range. + if self.format_comments { + let comments = f.context().comments().clone(); + + let trailing_comments = comments.trailing_comments(self.node); + + let outside_trimmed_range_start = trailing_comments.partition_point(|comment| { + source_range(f, comment.piece().text_range()).end() <= trimmed_source_range.end() + }); + + let (in_trimmed_range, outside_trimmed_range) = + trailing_comments.split_at(outside_trimmed_range_start); + + for comment in in_trimmed_range { + comment.mark_formatted(); + } + + biome_formatter::write!(f, [FormatTrailingComments::Comments(outside_trimmed_range)])?; + } + + f.write_element(FormatElement::Tag(Tag::EndVerbatim)) + } +} + +/// Formats bogus nodes. The difference between this method and `format_verbatim` is that this method +/// doesn't track nodes/tokens as [VerbatimKind::Verbatim]. They are just printed as they are. +pub fn format_bogus_node(node: &HtmlSyntaxNode) -> FormatHtmlVerbatimNode { + FormatHtmlVerbatimNode { + node, + kind: VerbatimKind::Bogus, + format_comments: true, + } +} + +/// Format a node having formatter suppression comment applied to it +pub fn format_suppressed_node(node: &HtmlSyntaxNode) -> FormatHtmlVerbatimNode { + FormatHtmlVerbatimNode { + node, + kind: VerbatimKind::Suppressed, + format_comments: true, + } +} + +/// Formats a node or falls back to verbatim printing if formating this node fails. +#[derive(Copy, Clone)] +pub struct FormatNodeOrVerbatim { + inner: F, +} + +impl Format for FormatNodeOrVerbatim +where + F: FormatWithRule, + Item: AstNode, +{ + fn fmt(&self, f: &mut Formatter) -> FormatResult<()> { + let snapshot = Formatter::state_snapshot(f); + + match self.inner.fmt(f) { + Ok(result) => Ok(result), + + Err(FormatError::SyntaxError) => { + f.restore_state_snapshot(snapshot); + + // Lists that yield errors are formatted as they were suppressed nodes. + // Doing so, the formatter formats the nodes/tokens as is. + format_suppressed_node(self.inner.item().syntax()).fmt(f) + } + Err(err) => Err(err), + } + } +} diff --git a/crates/biome_js_formatter/src/js/auxiliary/metavariable.rs b/crates/biome_js_formatter/src/js/auxiliary/metavariable.rs index b653107c363d..1638c64e546f 100644 --- a/crates/biome_js_formatter/src/js/auxiliary/metavariable.rs +++ b/crates/biome_js_formatter/src/js/auxiliary/metavariable.rs @@ -1,10 +1,12 @@ use crate::prelude::*; +use crate::verbatim::format_js_verbatim_node; use biome_js_syntax::JsMetavariable; use biome_rowan::AstNode; + #[derive(Debug, Clone, Default)] pub(crate) struct FormatJsMetavariable; impl FormatNodeRule for FormatJsMetavariable { fn fmt_fields(&self, node: &JsMetavariable, f: &mut JsFormatter) -> FormatResult<()> { - format_verbatim_node(node.syntax()).fmt(f) + format_js_verbatim_node(node.syntax()).fmt(f) } } diff --git a/crates/biome_js_formatter/src/jsx/auxiliary/text.rs b/crates/biome_js_formatter/src/jsx/auxiliary/text.rs index 0b26a5693112..6d5c8a471b57 100644 --- a/crates/biome_js_formatter/src/jsx/auxiliary/text.rs +++ b/crates/biome_js_formatter/src/jsx/auxiliary/text.rs @@ -1,5 +1,5 @@ use crate::prelude::*; - +use crate::verbatim::format_js_verbatim_node; use biome_formatter::FormatResult; use biome_js_syntax::JsxText; @@ -10,6 +10,6 @@ impl FormatNodeRule for FormatJsxText { fn fmt_fields(&self, node: &JsxText, f: &mut JsFormatter) -> FormatResult<()> { // Formatting a [JsxText] on its own isn't supported. Format as verbatim. A text should always be formatted // through its [JsxChildList] - format_verbatim_node(node.syntax()).fmt(f) + format_js_verbatim_node(node.syntax()).fmt(f) } } diff --git a/crates/biome_js_formatter/src/lib.rs b/crates/biome_js_formatter/src/lib.rs index 4375b03a9b94..dd2d4a273404 100644 --- a/crates/biome_js_formatter/src/lib.rs +++ b/crates/biome_js_formatter/src/lib.rs @@ -179,6 +179,7 @@ pub mod context; mod parentheses; pub(crate) mod separated; mod syntax_rewriter; +mod verbatim; use biome_formatter::format_element::tag::Label; use biome_formatter::prelude::*; @@ -197,6 +198,7 @@ use crate::comments::JsCommentStyle; use crate::context::{JsFormatContext, JsFormatOptions}; use crate::cst::FormatJsSyntaxNode; use crate::syntax_rewriter::transform; +use crate::verbatim::{format_bogus_node, format_suppressed_node}; /// Used to get an object that knows how to format this object. pub(crate) trait AsFormat { diff --git a/crates/biome_js_formatter/src/prelude.rs b/crates/biome_js_formatter/src/prelude.rs index b78d4d48e23f..556da4308688 100644 --- a/crates/biome_js_formatter/src/prelude.rs +++ b/crates/biome_js_formatter/src/prelude.rs @@ -3,7 +3,7 @@ pub(crate) use crate::{ AsFormat as _, FormatNodeRule, FormattedIterExt, JsFormatContext, JsFormatter, - comments::JsComments, + comments::JsComments, verbatim::*, }; pub use biome_formatter::prelude::*; pub use biome_formatter::separated::TrailingSeparator; diff --git a/crates/biome_js_formatter/src/ts/lists/union_type_variant_list.rs b/crates/biome_js_formatter/src/ts/lists/union_type_variant_list.rs index a07dfbf45745..4e73047ba485 100644 --- a/crates/biome_js_formatter/src/ts/lists/union_type_variant_list.rs +++ b/crates/biome_js_formatter/src/ts/lists/union_type_variant_list.rs @@ -34,6 +34,7 @@ use crate::ts::types::undefined_type::FormatTsUndefinedType; use crate::ts::types::union_type::FormatTsUnionType; use crate::ts::types::unknown_type::FormatTsUnknownType; use crate::ts::types::void_type::FormatTsVoidType; +use crate::verbatim::format_suppressed_node_skip_comments; use crate::{js::auxiliary::metavariable::FormatJsMetavariable, prelude::*}; use biome_formatter::{FormatRuleWithOptions, comments::CommentStyle, write}; use biome_js_syntax::{AnyTsType, JsLanguage, TsUnionType, TsUnionTypeVariantList}; @@ -92,7 +93,7 @@ impl Format for FormatTypeVariant<'_> { // This is a hack: It by passes the regular format node to only format the node without its comments. let format_node = format_with(|f: &mut JsFormatter| { if is_suppressed { - write!(f, [format_suppressed_node(node.syntax()).skip_comments()]) + write!(f, [format_suppressed_node_skip_comments(node.syntax())]) } else { match node { AnyTsType::TsAnyType(ty) => FormatTsAnyType.fmt_node(ty, f), diff --git a/crates/biome_js_formatter/src/utils/assignment_like.rs b/crates/biome_js_formatter/src/utils/assignment_like.rs index 6e109581d0f7..dd48adbbf8ab 100644 --- a/crates/biome_js_formatter/src/utils/assignment_like.rs +++ b/crates/biome_js_formatter/src/utils/assignment_like.rs @@ -5,6 +5,7 @@ use crate::ts::bindings::type_parameters::FormatTsTypeParametersOptions; use crate::utils::member_chain::is_member_call_chain; use crate::utils::object::write_member_name; use crate::utils::{FormatLiteralStringToken, StringLiteralParentKind}; +use crate::verbatim::format_suppressed_node; use biome_formatter::{CstFormatContext, FormatOptions, VecBuffer, format_args, write}; use biome_js_syntax::binary_like_expression::AnyJsBinaryLikeExpression; use biome_js_syntax::{ diff --git a/crates/biome_formatter/src/verbatim.rs b/crates/biome_js_formatter/src/verbatim.rs similarity index 78% rename from crates/biome_formatter/src/verbatim.rs rename to crates/biome_js_formatter/src/verbatim.rs index 9a031adeb7ae..8ad8321dc213 100644 --- a/crates/biome_formatter/src/verbatim.rs +++ b/crates/biome_js_formatter/src/verbatim.rs @@ -1,8 +1,15 @@ -use crate::format_element::tag::VerbatimKind; -use crate::prelude::*; -use crate::trivia::{FormatLeadingComments, FormatTrailingComments}; -use crate::{CstFormatContext, FormatWithRule, write}; -use biome_rowan::{AstNode, Direction, Language, SyntaxElement, SyntaxNode, TextRange}; +use crate::context::JsFormatContext; +use biome_formatter::format_element::tag::VerbatimKind; +use biome_formatter::formatter::Formatter; +use biome_formatter::prelude::{Tag, dynamic_text}; +use biome_formatter::trivia::{FormatLeadingComments, FormatTrailingComments}; +use biome_formatter::{ + Buffer, CstFormatContext, Format, FormatContext, FormatElement, FormatError, FormatResult, + FormatWithRule, LINE_TERMINATORS, normalize_newlines, +}; +use biome_js_syntax::{JsLanguage, JsSyntaxNode}; +use biome_rowan::{AstNode, Direction, SyntaxElement}; +use biome_text_size::TextRange; /// "Formats" a node according to its original formatting in the source text. Being able to format /// a node "as is" is useful if a node contains syntax errors. Formatting a node with syntax errors @@ -14,8 +21,8 @@ use biome_rowan::{AstNode, Direction, Language, SyntaxElement, SyntaxNode, TextR /// /// These nodes and tokens get tracked as [VerbatimKind::Verbatim], useful to understand /// if these nodes still need to have their own implementation. -pub fn format_verbatim_node(node: &SyntaxNode) -> FormatVerbatimNode { - FormatVerbatimNode { +pub fn format_js_verbatim_node(node: &JsSyntaxNode) -> FormatJsVerbatimNode { + FormatJsVerbatimNode { node, kind: VerbatimKind::Verbatim { length: node.text_range_with_trivia().len(), @@ -24,28 +31,15 @@ pub fn format_verbatim_node(node: &SyntaxNode) -> FormatVerbatim } } -/// "Formats" a node according to its original formatting in the source text. It's functionally equal to -/// [`format_verbatim_node`], but it doesn't track the node as [VerbatimKind::Verbatim]. -pub fn format_verbatim_skipped(node: &SyntaxNode) -> FormatVerbatimNode { - FormatVerbatimNode { - node, - kind: VerbatimKind::Skipped, - format_comments: true, - } -} - #[derive(Debug, Clone, Copy, Eq, PartialEq)] -pub struct FormatVerbatimNode<'node, L: Language> { - node: &'node SyntaxNode, +pub struct FormatJsVerbatimNode<'node> { + node: &'node JsSyntaxNode, kind: VerbatimKind, format_comments: bool, } -impl Format for FormatVerbatimNode<'_, Context::Language> -where - Context: CstFormatContext, -{ - fn fmt(&self, f: &mut Formatter) -> FormatResult<()> { +impl Format for FormatJsVerbatimNode<'_> { + fn fmt(&self, f: &mut Formatter) -> FormatResult<()> { for element in self.node.descendants_with_tokens(Direction::Next) { match element { SyntaxElement::Token(token) => f.state_mut().track_token(&token), @@ -91,7 +85,7 @@ where let (outside_trimmed_range, in_trimmed_range) = leading_comments.split_at(outside_trimmed_range); - write!(f, [FormatLeadingComments::Comments(outside_trimmed_range)])?; + biome_formatter::write!(f, [FormatLeadingComments::Comments(outside_trimmed_range)])?; for comment in in_trimmed_range { comment.mark_formatted(); @@ -148,24 +142,17 @@ where comment.mark_formatted(); } - write!(f, [FormatTrailingComments::Comments(outside_trimmed_range)])?; + biome_formatter::write!(f, [FormatTrailingComments::Comments(outside_trimmed_range)])?; } f.write_element(FormatElement::Tag(Tag::EndVerbatim)) } } -impl FormatVerbatimNode<'_, L> { - pub fn skip_comments(mut self) -> Self { - self.format_comments = false; - self - } -} - /// Formats bogus nodes. The difference between this method and `format_verbatim` is that this method /// doesn't track nodes/tokens as [VerbatimKind::Verbatim]. They are just printed as they are. -pub fn format_bogus_node(node: &SyntaxNode) -> FormatVerbatimNode { - FormatVerbatimNode { +pub fn format_bogus_node(node: &JsSyntaxNode) -> FormatJsVerbatimNode { + FormatJsVerbatimNode { node, kind: VerbatimKind::Bogus, format_comments: true, @@ -173,14 +160,23 @@ pub fn format_bogus_node(node: &SyntaxNode) -> FormatVerbatimNod } /// Format a node having formatter suppression comment applied to it -pub fn format_suppressed_node(node: &SyntaxNode) -> FormatVerbatimNode { - FormatVerbatimNode { +pub fn format_suppressed_node(node: &JsSyntaxNode) -> FormatJsVerbatimNode { + FormatJsVerbatimNode { node, kind: VerbatimKind::Suppressed, format_comments: true, } } +/// Format a node having formatter suppression comment applied to it +pub fn format_suppressed_node_skip_comments(node: &JsSyntaxNode) -> FormatJsVerbatimNode { + FormatJsVerbatimNode { + node, + kind: VerbatimKind::Suppressed, + format_comments: false, + } +} + /// Formats an object using its [`Format`] implementation but falls back to printing the object as /// it is in the source document if formatting it returns an [`FormatError::SyntaxError`]. pub const fn format_or_verbatim(inner: F) -> FormatNodeOrVerbatim { @@ -193,13 +189,12 @@ pub struct FormatNodeOrVerbatim { inner: F, } -impl Format for FormatNodeOrVerbatim +impl Format for FormatNodeOrVerbatim where - F: FormatWithRule, - Item: AstNode, - Context: CstFormatContext, + F: FormatWithRule, + Item: AstNode, { - fn fmt(&self, f: &mut Formatter) -> FormatResult<()> { + fn fmt(&self, f: &mut Formatter) -> FormatResult<()> { let snapshot = Formatter::state_snapshot(f); match self.inner.fmt(f) { diff --git a/crates/biome_json_formatter/src/json/auxiliary/root.rs b/crates/biome_json_formatter/src/json/auxiliary/root.rs index 91c931803cbd..5795d7b96898 100644 --- a/crates/biome_json_formatter/src/json/auxiliary/root.rs +++ b/crates/biome_json_formatter/src/json/auxiliary/root.rs @@ -1,4 +1,5 @@ use crate::prelude::*; +use crate::verbatim::format_json_verbatim_node; use biome_formatter::write; use biome_json_syntax::{JsonRoot, JsonRootFields}; @@ -27,7 +28,7 @@ impl FormatNodeRule for FormatJsonRoot { } // Don't fail formatting if the root contains no root value Err(_) => { - write!(f, [format_verbatim_node(node.syntax())]) + write!(f, [format_json_verbatim_node(node.syntax())]) } } } diff --git a/crates/biome_json_formatter/src/lib.rs b/crates/biome_json_formatter/src/lib.rs index b4efd865c590..bf44e563ee0c 100644 --- a/crates/biome_json_formatter/src/lib.rs +++ b/crates/biome_json_formatter/src/lib.rs @@ -7,11 +7,13 @@ mod generated; mod json; mod prelude; mod separated; +mod verbatim; use crate::comments::JsonCommentStyle; pub(crate) use crate::context::JsonFormatContext; use crate::context::JsonFormatOptions; use crate::cst::FormatJsonSyntaxNode; +use crate::verbatim::{format_bogus_node, format_suppressed_node}; use biome_formatter::comments::Comments; use biome_formatter::prelude::*; use biome_formatter::{ diff --git a/crates/biome_json_formatter/src/prelude.rs b/crates/biome_json_formatter/src/prelude.rs index dddc45723716..b6caded11474 100644 --- a/crates/biome_json_formatter/src/prelude.rs +++ b/crates/biome_json_formatter/src/prelude.rs @@ -4,6 +4,7 @@ #![allow(unused_imports)] pub(crate) use crate::{ AsFormat, FormatNodeRule, FormattedIterExt as _, IntoFormat, JsonFormatContext, JsonFormatter, + verbatim::*, }; pub(crate) use biome_formatter::prelude::*; pub(crate) use biome_rowan::{AstNode as _, AstNodeList as _, AstSeparatedList as _}; diff --git a/crates/biome_json_formatter/src/verbatim.rs b/crates/biome_json_formatter/src/verbatim.rs new file mode 100644 index 000000000000..ca84ce2119f8 --- /dev/null +++ b/crates/biome_json_formatter/src/verbatim.rs @@ -0,0 +1,203 @@ +use crate::context::JsonFormatContext; +use biome_formatter::format_element::tag::VerbatimKind; +use biome_formatter::formatter::Formatter; +use biome_formatter::prelude::{Tag, dynamic_text}; +use biome_formatter::trivia::{FormatLeadingComments, FormatTrailingComments}; +use biome_formatter::{ + Buffer, CstFormatContext, Format, FormatContext, FormatElement, FormatError, FormatResult, + FormatWithRule, LINE_TERMINATORS, normalize_newlines, +}; +use biome_json_syntax::{JsonLanguage, JsonSyntaxNode}; +use biome_rowan::{AstNode, Direction, SyntaxElement, TextRange}; + +/// "Formats" a node according to its original formatting in the source text. Being able to format +/// a node "as is" is useful if a node contains syntax errors. Formatting a node with syntax errors +/// has the risk that Biome misinterprets the structure of the code and formatting it could +/// "mess up" the developers, yet incomplete, work or accidentally introduce new syntax errors. +/// +/// You may be inclined to call `node.text` directly. However, using `text` doesn't track the nodes +/// nor its children source mapping information, resulting in incorrect source maps for this subtree. +/// +/// These nodes and tokens get tracked as [VerbatimKind::Verbatim], useful to understand +/// if these nodes still need to have their own implementation. +pub fn format_json_verbatim_node(node: &JsonSyntaxNode) -> FormatJsonVerbatimNode { + FormatJsonVerbatimNode { + node, + kind: VerbatimKind::Verbatim { + length: node.text_range_with_trivia().len(), + }, + format_comments: true, + } +} + +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +pub struct FormatJsonVerbatimNode<'node> { + node: &'node JsonSyntaxNode, + kind: VerbatimKind, + format_comments: bool, +} + +impl Format for FormatJsonVerbatimNode<'_> { + fn fmt(&self, f: &mut Formatter) -> FormatResult<()> { + for element in self.node.descendants_with_tokens(Direction::Next) { + match element { + SyntaxElement::Token(token) => f.state_mut().track_token(&token), + SyntaxElement::Node(node) => { + let comments = f.context().comments(); + comments.mark_suppression_checked(&node); + + for comment in comments.leading_dangling_trailing_comments(&node) { + comment.mark_formatted(); + } + } + } + } + + // The trimmed range of a node is its range without any of its leading or trailing trivia. + // Except for nodes that used to be parenthesized, the range than covers the source from the + // `(` to the `)` (the trimmed range of the parenthesized expression, not the inner expression) + let trimmed_source_range = f.context().source_map().map_or_else( + || self.node.text_trimmed_range(), + |source_map| source_map.trimmed_source_range(self.node), + ); + + f.write_element(FormatElement::Tag(Tag::StartVerbatim(self.kind)))?; + + fn source_range(f: &Formatter, range: TextRange) -> TextRange + where + Context: CstFormatContext, + { + f.context() + .source_map() + .map_or_else(|| range, |source_map| source_map.source_range(range)) + } + + // Format all leading comments that are outside of the node's source range. + if self.format_comments { + let comments = f.context().comments().clone(); + let leading_comments = comments.leading_comments(self.node); + + let outside_trimmed_range = leading_comments.partition_point(|comment| { + comment.piece().text_range().end() <= trimmed_source_range.start() + }); + + let (outside_trimmed_range, in_trimmed_range) = + leading_comments.split_at(outside_trimmed_range); + + biome_formatter::write!(f, [FormatLeadingComments::Comments(outside_trimmed_range)])?; + + for comment in in_trimmed_range { + comment.mark_formatted(); + } + } + + // Find the first skipped token trivia, if any, and include it in the verbatim range because + // the comments only format **up to** but not including skipped token trivia. + let start_source = self + .node + .first_leading_trivia() + .into_iter() + .flat_map(|trivia| trivia.pieces()) + .filter(|trivia| trivia.is_skipped()) + .map(|trivia| source_range(f, trivia.text_range()).start()) + .take_while(|start| *start < trimmed_source_range.start()) + .next() + .unwrap_or_else(|| trimmed_source_range.start()); + + let original_source = f.context().source_map().map_or_else( + || self.node.text_trimmed().to_string(), + |source_map| { + source_map + .source() + .text_slice(trimmed_source_range.cover_offset(start_source)) + .to_string() + }, + ); + + dynamic_text( + &normalize_newlines(&original_source, LINE_TERMINATORS), + self.node.text_trimmed_range().start(), + ) + .fmt(f)?; + + for comment in f.context().comments().dangling_comments(self.node) { + comment.mark_formatted(); + } + + // Format all trailing comments that are outside of the trimmed range. + if self.format_comments { + let comments = f.context().comments().clone(); + + let trailing_comments = comments.trailing_comments(self.node); + + let outside_trimmed_range_start = trailing_comments.partition_point(|comment| { + source_range(f, comment.piece().text_range()).end() <= trimmed_source_range.end() + }); + + let (in_trimmed_range, outside_trimmed_range) = + trailing_comments.split_at(outside_trimmed_range_start); + + for comment in in_trimmed_range { + comment.mark_formatted(); + } + + biome_formatter::write!(f, [FormatTrailingComments::Comments(outside_trimmed_range)])?; + } + + f.write_element(FormatElement::Tag(Tag::EndVerbatim)) + } +} + +/// Formats bogus nodes. The difference between this method and `format_verbatim` is that this method +/// doesn't track nodes/tokens as [VerbatimKind::Verbatim]. They are just printed as they are. +pub fn format_bogus_node(node: &JsonSyntaxNode) -> FormatJsonVerbatimNode { + FormatJsonVerbatimNode { + node, + kind: VerbatimKind::Bogus, + format_comments: true, + } +} + +/// Format a node having formatter suppression comment applied to it +pub fn format_suppressed_node(node: &JsonSyntaxNode) -> FormatJsonVerbatimNode { + FormatJsonVerbatimNode { + node, + kind: VerbatimKind::Suppressed, + format_comments: true, + } +} + +/// Formats an object using its [`Format`] implementation but falls back to printing the object as +/// it is in the source document if formatting it returns an [`FormatError::SyntaxError`]. +pub const fn format_or_verbatim(inner: F) -> FormatNodeOrVerbatim { + FormatNodeOrVerbatim { inner } +} + +/// Formats a node or falls back to verbatim printing if formating this node fails. +#[derive(Copy, Clone)] +pub struct FormatNodeOrVerbatim { + inner: F, +} + +impl Format for FormatNodeOrVerbatim +where + F: FormatWithRule, + Item: AstNode, +{ + fn fmt(&self, f: &mut Formatter) -> FormatResult<()> { + let snapshot = Formatter::state_snapshot(f); + + match self.inner.fmt(f) { + Ok(result) => Ok(result), + + Err(FormatError::SyntaxError) => { + f.restore_state_snapshot(snapshot); + + // Lists that yield errors are formatted as they were suppressed nodes. + // Doing so, the formatter formats the nodes/tokens as is. + format_suppressed_node(self.inner.item().syntax()).fmt(f) + } + Err(err) => Err(err), + } + } +}