diff --git a/.changeset/proud-ends-hear.md b/.changeset/proud-ends-hear.md new file mode 100644 index 000000000000..5795313a7352 --- /dev/null +++ b/.changeset/proud-ends-hear.md @@ -0,0 +1,21 @@ +--- +"@biomejs/biome": minor +--- + +Added support for the top-level suppression comment `biome-ignore-all format: `. + +When the comment `biome-ignore-all format: ` is placed at the beginning of the document, Biome won't format the code. + +The feature works for all supported languages. In the following JavaScript snippet, the code isn't formatted and will stay as is. + +```js +// biome-ignore-all format: generated + +const a = [ ] + + +const a = [ ] + + +const a = [ ] +``` diff --git a/Cargo.lock b/Cargo.lock index d99c99455c8a..4e032862962e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -678,6 +678,7 @@ name = "biome_grit_formatter" version = "0.0.0" dependencies = [ "biome_configuration", + "biome_diagnostics_categories", "biome_formatter", "biome_formatter_test", "biome_fs", @@ -687,6 +688,7 @@ dependencies = [ "biome_parser", "biome_rowan", "biome_service", + "biome_suppression", "camino", "serde", "serde_json", diff --git a/crates/biome_css_formatter/src/comments.rs b/crates/biome_css_formatter/src/comments.rs index 595ae140c803..cd9cfadf1c44 100644 --- a/crates/biome_css_formatter/src/comments.rs +++ b/crates/biome_css_formatter/src/comments.rs @@ -1,7 +1,7 @@ use crate::prelude::*; use biome_css_syntax::{ - AnyCssDeclarationName, CssComplexSelector, CssFunction, CssIdentifier, CssLanguage, - CssSyntaxKind, TextLen, + AnyCssDeclarationName, AnyCssRoot, CssComplexSelector, CssFunction, CssIdentifier, CssLanguage, + CssSyntaxKind, TextLen, TextSize, }; use biome_diagnostics::category; use biome_formatter::comments::{ @@ -11,7 +11,7 @@ use biome_formatter::comments::{ use biome_formatter::formatter::Formatter; use biome_formatter::{FormatResult, FormatRule, write}; use biome_rowan::SyntaxTriviaPieceComments; -use biome_suppression::parse_suppression_comment; +use biome_suppression::{SuppressionKind, parse_suppression_comment}; pub type CssComments = Comments; @@ -68,6 +68,15 @@ impl CommentStyle for CssCommentStyle { fn is_suppression(text: &str) -> bool { parse_suppression_comment(text) .filter_map(Result::ok) + .filter(|suppression| suppression.kind == SuppressionKind::Classic) + .flat_map(|suppression| suppression.categories) + .any(|(key, ..)| key == category!("format")) + } + + fn is_global_suppression(text: &str) -> bool { + parse_suppression_comment(text) + .filter_map(Result::ok) + .filter(|suppression| suppression.kind == SuppressionKind::All) .flat_map(|suppression| suppression.categories) .any(|(key, ..)| key == category!("format")) } @@ -91,13 +100,16 @@ impl CommentStyle for CssCommentStyle { match comment.text_position() { CommentTextPosition::EndOfLine => handle_function_comment(comment) .or_else(handle_declaration_name_comment) - .or_else(handle_complex_selector_comment), + .or_else(handle_complex_selector_comment) + .or_else(handle_global_suppression), CommentTextPosition::OwnLine => handle_function_comment(comment) .or_else(handle_declaration_name_comment) - .or_else(handle_complex_selector_comment), + .or_else(handle_complex_selector_comment) + .or_else(handle_global_suppression), CommentTextPosition::SameLine => handle_function_comment(comment) .or_else(handle_declaration_name_comment) - .or_else(handle_complex_selector_comment), + .or_else(handle_complex_selector_comment) + .or_else(handle_global_suppression), } } } @@ -148,3 +160,26 @@ fn handle_complex_selector_comment( } CommentPlacement::Default(comment) } + +fn handle_global_suppression( + comment: DecoratedComment, +) -> CommentPlacement { + let node = comment.enclosing_node(); + + if node.text_range_with_trivia().start() == TextSize::from(0) { + let has_global_suppression = node.first_leading_trivia().is_some_and(|trivia| { + trivia + .pieces() + .filter(|piece| piece.is_comments()) + .any(|piece| CssCommentStyle::is_global_suppression(piece.text())) + }); + let root = node.ancestors().find_map(AnyCssRoot::cast); + if let Some(root) = root + && has_global_suppression + { + return CommentPlacement::leading(root.syntax().clone(), comment); + } + } + + CommentPlacement::Default(comment) +} diff --git a/crates/biome_css_formatter/src/lib.rs b/crates/biome_css_formatter/src/lib.rs index 84ac48d15d43..a3964f7b601c 100644 --- a/crates/biome_css_formatter/src/lib.rs +++ b/crates/biome_css_formatter/src/lib.rs @@ -190,7 +190,7 @@ where N: AstNode, { fn fmt(&self, node: &N, f: &mut CssFormatter) -> FormatResult<()> { - if self.is_suppressed(node, f) { + if self.is_suppressed(node, f) || self.is_global_suppressed(node, f) { return write!(f, [format_suppressed_node(node.syntax())]); } @@ -207,6 +207,11 @@ where f.context().comments().is_suppressed(node.syntax()) } + /// Returns `true` if the node has a global suppression comment and should use the same formatting as in the source document. + fn is_global_suppressed(&self, node: &N, f: &CssFormatter) -> bool { + f.context().comments().is_global_suppressed(node.syntax()) + } + /// Formats the [leading comments](biome_formatter::comments#leading-comments) of the node. /// /// You may want to override this method if you want to manually handle the formatting of comments diff --git a/crates/biome_css_formatter/tests/specs/css/global_suppression.css b/crates/biome_css_formatter/tests/specs/css/global_suppression.css new file mode 100644 index 000000000000..5c8d0d1f1ebe --- /dev/null +++ b/crates/biome_css_formatter/tests/specs/css/global_suppression.css @@ -0,0 +1,10 @@ +/* biome-ignore-all format: test */ + +.class { + scroll-snap-type: + + x mandatory; +} + +#exampleInputEmail1 { color: red; +} diff --git a/crates/biome_css_formatter/tests/specs/css/global_suppression.css.snap b/crates/biome_css_formatter/tests/specs/css/global_suppression.css.snap new file mode 100644 index 000000000000..e38bbc8f336b --- /dev/null +++ b/crates/biome_css_formatter/tests/specs/css/global_suppression.css.snap @@ -0,0 +1,46 @@ +--- +source: crates/biome_formatter_test/src/snapshot_builder.rs +info: css/global_suppression.css +--- +# Input + +```css +/* biome-ignore-all format: test */ + +.class { + scroll-snap-type: + + x mandatory; +} + +#exampleInputEmail1 { color: red; +} + +``` + + +============================= + +# Outputs + +## Output 1 + +----- +Indent style: Tab +Indent width: 2 +Line ending: LF +Line width: 80 +Quote style: Double Quotes +----- + +```css +/* biome-ignore-all format: test */ + +.class { + scroll-snap-type: + + x mandatory; +} + +#exampleInputEmail1 { color: red; +}``` diff --git a/crates/biome_formatter/src/comments.rs b/crates/biome_formatter/src/comments.rs index 88ae5c3282d8..9dd341992fb5 100644 --- a/crates/biome_formatter/src/comments.rs +++ b/crates/biome_formatter/src/comments.rs @@ -768,6 +768,10 @@ pub trait CommentStyle: Default { false } + fn is_global_suppression(_text: &str) -> bool { + false + } + /// Returns the (kind)[CommentKind] of the comment fn get_comment_kind(comment: &SyntaxTriviaPieceComments) -> CommentKind; @@ -822,8 +826,8 @@ impl Comments { Self { data: Rc::new(CommentsData { root: Some(root.clone()), - is_suppression: Style::is_suppression, - + is_node_suppression: Style::is_suppression, + is_global_suppression: Style::is_global_suppression, comments, with_skipped: skipped, #[cfg(debug_assertions)] @@ -925,12 +929,27 @@ impl Comments { /// call expression is nested inside of the expression statement. pub fn is_suppressed(&self, node: &SyntaxNode) -> bool { self.mark_suppression_checked(node); - let is_suppression = self.data.is_suppression; + let is_suppression = self.data.is_node_suppression; self.leading_dangling_trailing_comments(node) .any(|comment| is_suppression(comment.piece().text())) } + pub fn is_global_suppressed(&self, node: &SyntaxNode) -> bool { + self.mark_suppression_checked(node); + let start = node.text_range_with_trivia().start(); + // global suppression comments must start at the beginning of the file + if start >= TextSize::from(0) { + let is_global_suppression = self.data.is_global_suppression; + // only leading comments can be global suppression comments + return self + .leading_comments(node) + .iter() + .any(|comment| is_global_suppression(comment.piece().text())); + } + false + } + #[cfg(not(debug_assertions))] #[inline(always)] pub fn mark_suppression_checked(&self, _: &SyntaxNode) {} @@ -1031,8 +1050,11 @@ Node: struct CommentsData { root: Option>, + /// Returns true if the comment is node suppression + is_node_suppression: fn(&str) -> bool, - is_suppression: fn(&str) -> bool, + /// Returns true if the comment is global suppression + is_global_suppression: fn(&str) -> bool, /// Stores all leading node comments by node comments: CommentsMap>, @@ -1054,7 +1076,8 @@ impl Default for CommentsData { fn default() -> Self { Self { root: None, - is_suppression: |_| false, + is_node_suppression: |_| false, + is_global_suppression: |_| false, comments: Default::default(), with_skipped: Default::default(), #[cfg(debug_assertions)] diff --git a/crates/biome_formatter/src/comments/builder.rs b/crates/biome_formatter/src/comments/builder.rs index 787575afd266..54eddbbe20d8 100644 --- a/crates/biome_formatter/src/comments/builder.rs +++ b/crates/biome_formatter/src/comments/builder.rs @@ -1059,6 +1059,10 @@ b;"#; false } + fn is_global_suppression(_: &str) -> bool { + false + } + fn get_comment_kind(_: &SyntaxTriviaPieceComments) -> CommentKind { CommentKind::Block } diff --git a/crates/biome_graphql_formatter/src/comments.rs b/crates/biome_graphql_formatter/src/comments.rs index f26a8de489a5..ed80bc292fa1 100644 --- a/crates/biome_graphql_formatter/src/comments.rs +++ b/crates/biome_graphql_formatter/src/comments.rs @@ -1,13 +1,14 @@ use crate::prelude::*; use biome_diagnostics::category; use biome_formatter::comments::{ - CommentKind, CommentStyle, Comments, SourceComment, is_doc_comment, + CommentKind, CommentPlacement, CommentStyle, CommentTextPosition, Comments, DecoratedComment, + SourceComment, is_doc_comment, }; use biome_formatter::formatter::Formatter; use biome_formatter::{FormatResult, FormatRule, write}; -use biome_graphql_syntax::{GraphqlLanguage, TextLen}; -use biome_rowan::SyntaxTriviaPieceComments; -use biome_suppression::parse_suppression_comment; +use biome_graphql_syntax::{GraphqlLanguage, GraphqlRoot, TextLen}; +use biome_rowan::{SyntaxTriviaPieceComments, TextSize}; +use biome_suppression::{SuppressionKind, parse_suppression_comment}; pub type GraphqlComments = Comments; @@ -64,6 +65,15 @@ impl CommentStyle for GraphqlCommentStyle { fn is_suppression(text: &str) -> bool { parse_suppression_comment(text) .filter_map(Result::ok) + .filter(|suppression| suppression.kind == SuppressionKind::Classic) + .flat_map(|suppression| suppression.categories) + .any(|(key, ..)| key == category!("format")) + } + + fn is_global_suppression(text: &str) -> bool { + parse_suppression_comment(text) + .filter_map(Result::ok) + .filter(|suppression| suppression.kind == SuppressionKind::All) .flat_map(|suppression| suppression.categories) .any(|(key, ..)| key == category!("format")) } @@ -71,4 +81,37 @@ impl CommentStyle for GraphqlCommentStyle { fn get_comment_kind(_comment: &SyntaxTriviaPieceComments) -> CommentKind { CommentKind::Line } + + fn place_comment( + &self, + comment: DecoratedComment, + ) -> CommentPlacement { + match comment.text_position() { + CommentTextPosition::EndOfLine => handle_global_suppression(comment), + CommentTextPosition::OwnLine => handle_global_suppression(comment), + CommentTextPosition::SameLine => CommentPlacement::Default(comment), + } + } +} + +fn handle_global_suppression( + comment: DecoratedComment, +) -> CommentPlacement { + let node = comment.enclosing_node(); + + if node.text_range_with_trivia().start() == TextSize::from(0) { + let has_global_suppression = node.first_leading_trivia().is_some_and(|trivia| { + trivia + .pieces() + .filter(|piece| piece.is_comments()) + .any(|piece| GraphqlCommentStyle::is_global_suppression(piece.text())) + }); + let root = node.ancestors().find_map(GraphqlRoot::cast); + if let Some(root) = root + && has_global_suppression + { + return CommentPlacement::leading(root.syntax().clone(), comment); + } + } + CommentPlacement::Default(comment) } diff --git a/crates/biome_graphql_formatter/src/lib.rs b/crates/biome_graphql_formatter/src/lib.rs index c4b356d87ae3..0a855e35d1b7 100644 --- a/crates/biome_graphql_formatter/src/lib.rs +++ b/crates/biome_graphql_formatter/src/lib.rs @@ -184,7 +184,7 @@ where N: AstNode, { fn fmt(&self, node: &N, f: &mut GraphqlFormatter) -> FormatResult<()> { - if self.is_suppressed(node, f) { + if self.is_suppressed(node, f) || self.is_global_suppressed(node, f) { return write!(f, [format_suppressed_node(node.syntax())]); } @@ -201,6 +201,11 @@ where f.context().comments().is_suppressed(node.syntax()) } + /// Returns `true` if the node has a global suppression comment and should use the same formatting as in the source document. + fn is_global_suppressed(&self, node: &N, f: &GraphqlFormatter) -> bool { + f.context().comments().is_global_suppressed(node.syntax()) + } + /// Formats the [leading comments](biome_formatter::comments#leading-comments) of the node. /// /// You may want to override this method if you want to manually handle the formatting of comments diff --git a/crates/biome_graphql_formatter/src/verbatim.rs b/crates/biome_graphql_formatter/src/verbatim.rs index 3e0922205ac4..81a2f6bdee13 100644 --- a/crates/biome_graphql_formatter/src/verbatim.rs +++ b/crates/biome_graphql_formatter/src/verbatim.rs @@ -76,6 +76,7 @@ impl Format for FormatGraphqlVerbatimNode<'_> { // 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| { diff --git a/crates/biome_graphql_formatter/tests/specs/graphql/suppression.graphql b/crates/biome_graphql_formatter/tests/specs/graphql/suppression.graphql new file mode 100644 index 000000000000..27b630dc069f --- /dev/null +++ b/crates/biome_graphql_formatter/tests/specs/graphql/suppression.graphql @@ -0,0 +1,20 @@ +# biome-ignore-all format: test reason + +{ + hero + @deprecated(reason: "Deprecated", reason: "Deprecated", + + + reason: "Deprecated") + @addExternalFields(source: "profiles") +} + + +{ + hero + @deprecated(reason: "Deprecated", reason: "Deprecated", + + + reason: "Deprecated") + @addExternalFields(source: "profiles") +} diff --git a/crates/biome_graphql_formatter/tests/specs/graphql/suppression.graphql.snap b/crates/biome_graphql_formatter/tests/specs/graphql/suppression.graphql.snap new file mode 100644 index 000000000000..f77935826b4d --- /dev/null +++ b/crates/biome_graphql_formatter/tests/specs/graphql/suppression.graphql.snap @@ -0,0 +1,67 @@ +--- +source: crates/biome_formatter_test/src/snapshot_builder.rs +info: graphql/suppression.graphql +--- +# Input + +```graphql +# biome-ignore-all format: test reason + +{ + hero + @deprecated(reason: "Deprecated", reason: "Deprecated", + + + reason: "Deprecated") + @addExternalFields(source: "profiles") +} + + +{ + hero + @deprecated(reason: "Deprecated", reason: "Deprecated", + + + reason: "Deprecated") + @addExternalFields(source: "profiles") +} + +``` + + +============================= + +# Outputs + +## Output 1 + +----- +Indent style: Tab +Indent width: 2 +Line ending: LF +Line width: 80 +Bracket spacing: true +Quote style: Double Quotes +----- + +```graphql +# biome-ignore-all format: test reason + +{ + hero + @deprecated(reason: "Deprecated", reason: "Deprecated", + + + reason: "Deprecated") + @addExternalFields(source: "profiles") +} + + +{ + hero + @deprecated(reason: "Deprecated", reason: "Deprecated", + + + reason: "Deprecated") + @addExternalFields(source: "profiles") +}``` diff --git a/crates/biome_grit_formatter/Cargo.toml b/crates/biome_grit_formatter/Cargo.toml index cd7a652666b4..1355ad25a33d 100644 --- a/crates/biome_grit_formatter/Cargo.toml +++ b/crates/biome_grit_formatter/Cargo.toml @@ -18,9 +18,11 @@ publish = true independent = true [dependencies] -biome_formatter = { workspace = true } -biome_grit_syntax = { workspace = true } -biome_rowan = { workspace = true } +biome_diagnostics_categories = { workspace = true } +biome_formatter = { workspace = true } +biome_grit_syntax = { workspace = true } +biome_rowan = { workspace = true } +biome_suppression = { workspace = true } [dev-dependencies] biome_configuration = { path = "../biome_configuration" } diff --git a/crates/biome_grit_formatter/src/comments.rs b/crates/biome_grit_formatter/src/comments.rs index 9f0f273e0dd4..7b1d8216d483 100644 --- a/crates/biome_grit_formatter/src/comments.rs +++ b/crates/biome_grit_formatter/src/comments.rs @@ -1,6 +1,7 @@ use crate::GritFormatContext; +use biome_diagnostics_categories::category; -use biome_formatter::comments::CommentKind; +use biome_formatter::comments::{CommentKind, CommentPlacement, DecoratedComment}; use biome_formatter::{ FormatResult, FormatRule, comments::{CommentStyle, Comments, SourceComment, is_doc_comment}, @@ -8,8 +9,9 @@ use biome_formatter::{ prelude::{Formatter, align, format_once, hard_line_break, text}, write, }; -use biome_grit_syntax::GritLanguage; -use biome_rowan::TextLen; +use biome_grit_syntax::{GritLanguage, GritRoot}; +use biome_rowan::{AstNode, TextLen, TextSize}; +use biome_suppression::{SuppressionKind, parse_suppression_comment}; pub type GritComments = Comments; @@ -19,8 +21,20 @@ pub struct GritCommentStyle; impl CommentStyle for GritCommentStyle { type Language = GritLanguage; - fn is_suppression(_text: &str) -> bool { - false + fn is_suppression(text: &str) -> bool { + parse_suppression_comment(text) + .filter_map(Result::ok) + .filter(|suppression| suppression.kind == SuppressionKind::Classic) + .flat_map(|suppression| suppression.categories) + .any(|(key, ..)| key == category!("format")) + } + + fn is_global_suppression(text: &str) -> bool { + parse_suppression_comment(text) + .filter_map(Result::ok) + .filter(|suppression| suppression.kind == SuppressionKind::All) + .flat_map(|suppression| suppression.categories) + .any(|(key, ..)| key == category!("format")) } fn get_comment_kind( @@ -31,9 +45,9 @@ impl CommentStyle for GritCommentStyle { fn place_comment( &self, - comment: biome_formatter::comments::DecoratedComment, - ) -> biome_formatter::comments::CommentPlacement { - biome_formatter::comments::CommentPlacement::Default(comment) + comment: DecoratedComment, + ) -> CommentPlacement { + handle_global_suppression(comment) } } @@ -80,3 +94,26 @@ impl FormatRule> for FormatGritLeadingComment { } } } + +fn handle_global_suppression( + comment: DecoratedComment, +) -> CommentPlacement { + let node = comment.enclosing_node(); + + if node.text_range_with_trivia().start() == TextSize::from(0) { + let has_global_suppression = node.first_leading_trivia().is_some_and(|trivia| { + trivia + .pieces() + .filter(|piece| piece.is_comments()) + .any(|piece| GritCommentStyle::is_global_suppression(piece.text())) + }); + let root = node.ancestors().find_map(GritRoot::cast); + if let Some(root) = root + && has_global_suppression + { + return CommentPlacement::leading(root.syntax().clone(), comment); + } + } + + CommentPlacement::Default(comment) +} diff --git a/crates/biome_grit_formatter/src/lib.rs b/crates/biome_grit_formatter/src/lib.rs index b88281ed72c7..9fcdc3886182 100644 --- a/crates/biome_grit_formatter/src/lib.rs +++ b/crates/biome_grit_formatter/src/lib.rs @@ -79,7 +79,7 @@ where { // this is the method that actually start the formatting fn fmt(&self, node: &N, f: &mut GritFormatter) -> FormatResult<()> { - if self.is_suppressed(node, f) { + if self.is_suppressed(node, f) || self.is_global_suppressed(node, f) { return write!(f, [format_suppressed_node(node.syntax())]); } @@ -96,6 +96,11 @@ where f.context().comments().is_suppressed(node.syntax()) } + /// Returns `true` if the node has a global suppression comment and should use the same formatting as in the source document. + fn is_global_suppressed(&self, node: &N, f: &GritFormatter) -> bool { + f.context().comments().is_global_suppressed(node.syntax()) + } + /// Formats the [leading comments](biome_formatter::comments#leading-comments) of the node. /// /// You may want to override this method if you want to manually handle the formatting of comments diff --git a/crates/biome_grit_formatter/tests/specs/grit/global_suppression.grit b/crates/biome_grit_formatter/tests/specs/grit/global_suppression.grit new file mode 100644 index 000000000000..a2550c92a0b8 --- /dev/null +++ b/crates/biome_grit_formatter/tests/specs/grit/global_suppression.grit @@ -0,0 +1,9 @@ +// biome-ignore-all format:test + + +`const names = [$names]` where { + if ($names[0] <: $names[-1]) { + $names => + $names[-1] + } +} diff --git a/crates/biome_grit_formatter/tests/specs/grit/global_suppression.grit.snap b/crates/biome_grit_formatter/tests/specs/grit/global_suppression.grit.snap new file mode 100644 index 000000000000..e79d1fb8ad56 --- /dev/null +++ b/crates/biome_grit_formatter/tests/specs/grit/global_suppression.grit.snap @@ -0,0 +1,43 @@ +--- +source: crates/biome_formatter_test/src/snapshot_builder.rs +info: grit/global_suppression.grit +--- +# Input + +```grit +// biome-ignore-all format:test + + +`const names = [$names]` where { + if ($names[0] <: $names[-1]) { + $names => + $names[-1] + } +} + +``` + + +============================= + +# Outputs + +## Output 1 + +----- +Indent style: Tab +Indent width: 2 +Line ending: LF +Line width: 80 +Attribute Position: Auto +----- + +```grit +// biome-ignore-all format:test + +`const names = [$names]` where { + if ($names[0] <: $names[-1]) { + $names => + $names[-1] + } +}``` diff --git a/crates/biome_html_formatter/src/comments.rs b/crates/biome_html_formatter/src/comments.rs index fe0ac13a3c74..1b736d53ccab 100644 --- a/crates/biome_html_formatter/src/comments.rs +++ b/crates/biome_html_formatter/src/comments.rs @@ -7,9 +7,11 @@ use biome_formatter::{ prelude::*, write, }; -use biome_html_syntax::{HtmlClosingElement, HtmlLanguage, HtmlOpeningElement, HtmlSyntaxKind}; -use biome_rowan::{SyntaxNodeCast, SyntaxTriviaPieceComments}; -use biome_suppression::parse_suppression_comment; +use biome_html_syntax::{ + HtmlClosingElement, HtmlLanguage, HtmlOpeningElement, HtmlRoot, HtmlSyntaxKind, +}; +use biome_rowan::{AstNode, SyntaxNodeCast, SyntaxTriviaPieceComments, TextSize}; +use biome_suppression::{SuppressionKind, parse_suppression_comment}; use crate::context::HtmlFormatContext; @@ -39,6 +41,15 @@ impl CommentStyle for HtmlCommentStyle { fn is_suppression(text: &str) -> bool { parse_suppression_comment(text) .filter_map(Result::ok) + .filter(|suppression| suppression.kind == SuppressionKind::Classic) + .flat_map(|suppression| suppression.categories) + .any(|(key, ..)| key == category!("format")) + } + + fn is_global_suppression(text: &str) -> bool { + parse_suppression_comment(text) + .filter_map(Result::ok) + .filter(|suppression| suppression.kind == SuppressionKind::All) .flat_map(|suppression| suppression.categories) .any(|(key, ..)| key == category!("format")) } @@ -56,72 +67,76 @@ impl CommentStyle for HtmlCommentStyle { &self, comment: DecoratedComment, ) -> CommentPlacement { - // Fix trailing comments that are right before EOF being assigned to the wrong node. - // - // The issue is demonstrated in the example below. - // ```html - // Foo - // - // - // ``` - if let Some(token) = comment.following_token() - && token.kind() == HtmlSyntaxKind::EOF - { - return CommentPlacement::trailing(comment.enclosing_node().clone(), comment); - } + handle_global_suppression(comment).or_else(|comment| { + // Fix trailing comments that are right before EOF being assigned to the wrong node. + // + // The issue is demonstrated in the example below. + // ```html + // Foo + // + // + // ``` + if let Some(token) = comment.following_token() + && token.kind() == HtmlSyntaxKind::EOF + { + return CommentPlacement::trailing(comment.enclosing_node().clone(), comment); + } - // Fix trailing comments that should actually be leading comments for the next node. - // ```html - // 123456 - // ``` - // This fix will ensure that the ignore comment is assigned to the 456 node instead of the 123 node. - if let Some(following_node) = comment.following_node() - && comment.text_position().is_same_line() - { - return CommentPlacement::leading(following_node.clone(), comment); - } - // match (comment.preceding_node(), comment.following_node()) { - // (Some(preceding_node), Some(following_node)) => { - // if preceding_node.kind() == HtmlSyntaxKind::HTML_CONTENT - // && following_node.kind() == HtmlSyntaxKind::HTML_CONTENT - // { - // return CommentPlacement::leading(following_node.clone(), comment); - // } - - // if matches!( - // following_node.kind(), - // HtmlSyntaxKind::HTML_CONTENT - // | HtmlSyntaxKind::HTML_ELEMENT - // | HtmlSyntaxKind::HTML_SELF_CLOSING_ELEMENT - // | HtmlSyntaxKind::HTML_BOGUS_ELEMENT - // ) { - // return CommentPlacement::leading(following_node.clone(), comment); - // } - // } - // _ => {} - // } - - // move leading comments placed on closing tags to trailing tags of previous siblings, or to be dangling if no siblings are present. - if let Some(_closing_tag) = comment - .following_node() - .and_then(|node| node.clone().cast::()) - { - if let Some(_preceding_opening_tag) = comment - .preceding_node() - .and_then(|node| node.clone().cast::()) + // Fix trailing comments that should actually be leading comments for the next node. + // ```html + // 123456 + // ``` + // This fix will ensure that the ignore comment is assigned to the 456 node instead of the 123 node. + if let Some(following_node) = comment.following_node() + && comment.text_position().is_same_line() { - return CommentPlacement::dangling( - comment.preceding_node().unwrap().clone(), - comment, - ); - } else { - return CommentPlacement::trailing( - comment.preceding_node().unwrap().clone(), - comment, - ); + return CommentPlacement::leading(following_node.clone(), comment); + } + + // move leading comments placed on closing tags to trailing tags of previous siblings, or to be dangling if no siblings are present. + if let Some(_closing_tag) = comment + .following_node() + .and_then(|node| node.clone().cast::()) + { + if let Some(_preceding_opening_tag) = comment + .preceding_node() + .and_then(|node| node.clone().cast::()) + { + return CommentPlacement::dangling( + comment.preceding_node().unwrap().clone(), + comment, + ); + } else { + return CommentPlacement::trailing( + comment.preceding_node().unwrap().clone(), + comment, + ); + } } - } - CommentPlacement::Default(comment) + CommentPlacement::Default(comment) + }) } } +fn handle_global_suppression( + comment: DecoratedComment, +) -> CommentPlacement { + let node = comment.enclosing_node(); + + if node.text_range_with_trivia().start() == TextSize::from(0) { + let has_global_suppression = node.first_leading_trivia().is_some_and(|trivia| { + trivia + .pieces() + .filter(|piece| piece.is_comments()) + .any(|piece| HtmlCommentStyle::is_global_suppression(piece.text())) + }); + let root = node.ancestors().find_map(HtmlRoot::cast); + if let Some(root) = root + && has_global_suppression + { + return CommentPlacement::leading(root.syntax().clone(), comment); + } + } + + CommentPlacement::Default(comment) +} diff --git a/crates/biome_html_formatter/src/lib.rs b/crates/biome_html_formatter/src/lib.rs index 7398fbdba52b..515cc971a19a 100644 --- a/crates/biome_html_formatter/src/lib.rs +++ b/crates/biome_html_formatter/src/lib.rs @@ -208,7 +208,7 @@ where N: AstNode, { fn fmt(&self, node: &N, f: &mut HtmlFormatter) -> FormatResult<()> { - if self.is_suppressed(node, f) { + if self.is_suppressed(node, f) || self.is_global_suppressed(node, f) { return write!(f, [format_suppressed_node(node.syntax())]); } @@ -252,6 +252,11 @@ where f.context().comments().is_suppressed(node.syntax()) } + /// Returns `true` if the node has a global suppression comment and should use the same formatting as in the source document. + fn is_global_suppressed(&self, node: &N, f: &HtmlFormatter) -> bool { + f.context().comments().is_global_suppressed(node.syntax()) + } + /// Formats the [leading comments](biome_formatter::comments#leading-comments) of the node. /// /// You may want to override this method if you want to manually handle the formatting of comments diff --git a/crates/biome_html_formatter/tests/specs/html/suppressions/global_suppression.html b/crates/biome_html_formatter/tests/specs/html/suppressions/global_suppression.html new file mode 100644 index 000000000000..c8841f92d607 --- /dev/null +++ b/crates/biome_html_formatter/tests/specs/html/suppressions/global_suppression.html @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/crates/biome_html_formatter/tests/specs/html/suppressions/global_suppression.html.snap b/crates/biome_html_formatter/tests/specs/html/suppressions/global_suppression.html.snap new file mode 100644 index 000000000000..2b4005916498 --- /dev/null +++ b/crates/biome_html_formatter/tests/specs/html/suppressions/global_suppression.html.snap @@ -0,0 +1,54 @@ +--- +source: crates/biome_formatter_test/src/snapshot_builder.rs +info: suppressions/global_suppression.html +--- +# Input + +```html + + + + + + + + + + + + + +``` + + +============================= + +# Outputs + +## Output 1 + +----- +Indent style: Tab +Indent width: 2 +Line ending: LF +Line width: 80 +Attribute Position: Auto +Bracket same line: false +Whitespace sensitivity: css +Indent script and style: false +Self close void elements: never +----- + +```html + + + + + + + + + + + + ``` diff --git a/crates/biome_js_formatter/src/comments.rs b/crates/biome_js_formatter/src/comments.rs index 396517f201f0..0509d216a875 100644 --- a/crates/biome_js_formatter/src/comments.rs +++ b/crates/biome_js_formatter/src/comments.rs @@ -18,7 +18,8 @@ use biome_js_syntax::{ JsVariableDeclarator, JsWhileStatement, TsInterfaceDeclaration, TsMappedType, }; use biome_rowan::{AstNode, SyntaxNodeOptionExt, SyntaxTriviaPieceComments, TextLen}; -use biome_suppression::parse_suppression_comment; +use biome_suppression::{SuppressionKind, parse_suppression_comment}; +use biome_text_size::TextSize; pub type JsComments = Comments; @@ -79,6 +80,15 @@ impl CommentStyle for JsCommentStyle { fn is_suppression(text: &str) -> bool { parse_suppression_comment(text) .filter_map(Result::ok) + .filter(|suppression| suppression.kind == SuppressionKind::Classic) + .flat_map(|suppression| suppression.categories) + .any(|(key, ..)| key == category!("format")) + } + + fn is_global_suppression(text: &str) -> bool { + parse_suppression_comment(text) + .filter_map(Result::ok) + .filter(|suppression| suppression.kind == SuppressionKind::All) .flat_map(|suppression| suppression.categories) .any(|(key, ..)| key == category!("format")) } @@ -100,7 +110,8 @@ impl CommentStyle for JsCommentStyle { comment: DecoratedComment, ) -> CommentPlacement { match comment.text_position() { - CommentTextPosition::EndOfLine => handle_typecast_comment(comment) + CommentTextPosition::EndOfLine => handle_global_suppression(comment) + .or_else(handle_typecast_comment) .or_else(handle_function_comment) .or_else(handle_conditional_comment) .or_else(handle_if_statement_comment) @@ -121,7 +132,8 @@ impl CommentStyle for JsCommentStyle { .or_else(handle_import_export_specifier_comment) .or_else(handle_import_named_clause_comments) .or_else(handle_array_expression), - CommentTextPosition::OwnLine => handle_member_expression_comment(comment) + CommentTextPosition::OwnLine => handle_global_suppression(comment) + .or_else(handle_member_expression_comment) .or_else(handle_function_comment) .or_else(handle_if_statement_comment) .or_else(handle_while_comment) @@ -1363,6 +1375,29 @@ fn handle_array_expression(comment: DecoratedComment) -> CommentPlac } } +fn handle_global_suppression( + comment: DecoratedComment, +) -> CommentPlacement { + let node = comment.enclosing_node(); + + if node.text_range_with_trivia().start() == TextSize::from(0) { + let has_global_suppression = node.first_leading_trivia().is_some_and(|trivia| { + trivia + .pieces() + .filter(|piece| piece.is_comments()) + .any(|piece| JsCommentStyle::is_global_suppression(piece.text())) + }); + let root = node.ancestors().find_map(AnyJsRoot::cast); + if let Some(root) = root + && has_global_suppression + { + return CommentPlacement::leading(root.syntax().clone(), comment); + } + } + + CommentPlacement::Default(comment) +} + fn place_leading_statement_comment( statement: AnyJsStatement, comment: DecoratedComment, diff --git a/crates/biome_js_formatter/src/lib.rs b/crates/biome_js_formatter/src/lib.rs index cc30328422e7..981c870bb229 100644 --- a/crates/biome_js_formatter/src/lib.rs +++ b/crates/biome_js_formatter/src/lib.rs @@ -359,7 +359,7 @@ where N: AstNode + std::fmt::Debug, { fn fmt(&self, node: &N, f: &mut JsFormatter) -> FormatResult<()> { - if self.is_suppressed(node, f) { + if self.is_suppressed(node, f) || self.is_global_suppressed(node, f) { return write!(f, [format_suppressed_node(node.syntax())]); } @@ -414,6 +414,11 @@ where f.context().comments().is_suppressed(node.syntax()) } + /// Returns `true` if the node has a global suppression comment and should use the same formatting as in the source document. + fn is_global_suppressed(&self, node: &N, f: &JsFormatter) -> bool { + f.context().comments().is_global_suppressed(node.syntax()) + } + /// Formats the [leading comments](biome_formatter::comments#leading-comments) of the node. /// /// You may want to override this method if you want to manually handle the formatting of comments diff --git a/crates/biome_js_formatter/tests/specs/js/module/global_suppression.js b/crates/biome_js_formatter/tests/specs/js/module/global_suppression.js new file mode 100644 index 000000000000..eded3e9b1a6b --- /dev/null +++ b/crates/biome_js_formatter/tests/specs/js/module/global_suppression.js @@ -0,0 +1,5 @@ +// biome-ignore-all format: test + +let a = 1 ; + +function name() { return " " }; diff --git a/crates/biome_js_formatter/tests/specs/js/module/global_suppression.js.snap b/crates/biome_js_formatter/tests/specs/js/module/global_suppression.js.snap new file mode 100644 index 000000000000..b8faf260b4b5 --- /dev/null +++ b/crates/biome_js_formatter/tests/specs/js/module/global_suppression.js.snap @@ -0,0 +1,46 @@ +--- +source: crates/biome_formatter_test/src/snapshot_builder.rs +info: js/module/global_suppression.js +--- +# Input + +```js +// biome-ignore-all format: test + +let a = 1 ; + +function name() { return " " }; + +``` + + +============================= + +# Outputs + +## Output 1 + +----- +Indent style: Tab +Indent width: 2 +Line ending: LF +Line width: 80 +Quote style: Double Quotes +JSX quote style: Double Quotes +Quote properties: As needed +Trailing commas: All +Semicolons: Always +Arrow parentheses: Always +Bracket spacing: true +Bracket same line: false +Attribute Position: Auto +Expand lists: Auto +Operator linebreak: After +----- + +```js +// biome-ignore-all format: test + +let a = 1 ; + +function name() { return " " };``` diff --git a/crates/biome_json_formatter/src/comments.rs b/crates/biome_json_formatter/src/comments.rs index f3adba00227e..c247e88bf822 100644 --- a/crates/biome_json_formatter/src/comments.rs +++ b/crates/biome_json_formatter/src/comments.rs @@ -6,9 +6,11 @@ use biome_formatter::comments::{ }; use biome_formatter::formatter::Formatter; use biome_formatter::{FormatResult, FormatRule, write}; -use biome_json_syntax::{JsonArrayValue, JsonLanguage, JsonObjectValue, JsonSyntaxKind, TextLen}; +use biome_json_syntax::{ + JsonArrayValue, JsonLanguage, JsonObjectValue, JsonRoot, JsonSyntaxKind, TextLen, TextSize, +}; use biome_rowan::SyntaxTriviaPieceComments; -use biome_suppression::parse_suppression_comment; +use biome_suppression::{SuppressionKind, parse_suppression_comment}; pub type JsonComments = Comments; @@ -65,6 +67,15 @@ impl CommentStyle for JsonCommentStyle { fn is_suppression(text: &str) -> bool { parse_suppression_comment(text) .filter_map(Result::ok) + .filter(|suppression| suppression.kind == SuppressionKind::Classic) + .flat_map(|suppression| suppression.categories) + .any(|(key, ..)| key == category!("format")) + } + + fn is_global_suppression(text: &str) -> bool { + parse_suppression_comment(text) + .filter_map(Result::ok) + .filter(|suppression| suppression.kind == SuppressionKind::All) .flat_map(|suppression| suppression.categories) .any(|(key, ..)| key == category!("format")) } @@ -85,7 +96,7 @@ impl CommentStyle for JsonCommentStyle { &self, comment: biome_formatter::comments::DecoratedComment, ) -> biome_formatter::comments::CommentPlacement { - handle_empty_list_comment(comment) + handle_empty_list_comment(comment).or_else(handle_global_suppression) } } @@ -112,3 +123,26 @@ fn handle_empty_list_comment( CommentPlacement::Default(comment) } + +fn handle_global_suppression( + comment: DecoratedComment, +) -> CommentPlacement { + let node = comment.enclosing_node(); + + if node.text_range_with_trivia().start() == TextSize::from(0) { + let has_global_suppression = node.first_leading_trivia().is_some_and(|trivia| { + trivia + .pieces() + .filter(|piece| piece.is_comments()) + .any(|piece| JsonCommentStyle::is_global_suppression(piece.text())) + }); + let root = node.ancestors().find_map(JsonRoot::cast); + if let Some(root) = root + && has_global_suppression + { + return CommentPlacement::leading(root.syntax().clone(), comment); + } + } + + CommentPlacement::Default(comment) +} diff --git a/crates/biome_json_formatter/src/lib.rs b/crates/biome_json_formatter/src/lib.rs index 0f9c7395f3d1..27b6efaf2437 100644 --- a/crates/biome_json_formatter/src/lib.rs +++ b/crates/biome_json_formatter/src/lib.rs @@ -186,7 +186,7 @@ where N: AstNode, { fn fmt(&self, node: &N, f: &mut JsonFormatter) -> FormatResult<()> { - if self.is_suppressed(node, f) { + if self.is_suppressed(node, f) || self.is_global_suppressed(node, f) { return write!(f, [format_suppressed_node(node.syntax())]); } @@ -203,6 +203,11 @@ where f.context().comments().is_suppressed(node.syntax()) } + /// Returns `true` if the node has a global suppression comment and should use the same formatting as in the source document. + fn is_global_suppressed(&self, node: &N, f: &JsonFormatter) -> bool { + f.context().comments().is_global_suppressed(node.syntax()) + } + /// Formats the [leading comments](biome_formatter::comments#leading-comments) of the node. /// /// You may want to override this method if you want to manually handle the formatting of comments diff --git a/crates/biome_json_formatter/tests/prettier_tests.rs b/crates/biome_json_formatter/tests/prettier_tests.rs index a995013e656c..506895f745d6 100644 --- a/crates/biome_json_formatter/tests/prettier_tests.rs +++ b/crates/biome_json_formatter/tests/prettier_tests.rs @@ -6,7 +6,7 @@ use std::env; mod language; -tests_macros::gen_tests! {"tests/specs/prettier/{json}/**/*.{json}", crate::test_snapshot, ""} +tests_macros::gen_tests! {"tests/specs/prettier/{json}/**/*.{json,jsonc}", crate::test_snapshot, ""} fn test_snapshot(input: &'static str, _: &str, _: &str, _: &str) { countme::enable(true); diff --git a/crates/biome_json_formatter/tests/specs/json/global_suppression.jsonc b/crates/biome_json_formatter/tests/specs/json/global_suppression.jsonc new file mode 100644 index 000000000000..10fa97d7ca9a --- /dev/null +++ b/crates/biome_json_formatter/tests/specs/json/global_suppression.jsonc @@ -0,0 +1,8 @@ +// biome-ignore-all format: test + + +{ + + + "a": "b", "c": "d", "e": "f" +} diff --git a/crates/biome_json_formatter/tests/specs/json/global_suppression.jsonc.snap b/crates/biome_json_formatter/tests/specs/json/global_suppression.jsonc.snap new file mode 100644 index 000000000000..ee8a988cee7c --- /dev/null +++ b/crates/biome_json_formatter/tests/specs/json/global_suppression.jsonc.snap @@ -0,0 +1,43 @@ +--- +source: crates/biome_formatter_test/src/snapshot_builder.rs +info: json/global_suppression.jsonc +--- +# Input + +```jsonc +// biome-ignore-all format: test + + +{ + + + "a": "b", "c": "d", "e": "f" +} + +``` + + +============================= + +# Outputs + +## Output 1 + +----- +Indent style: Tab +Indent width: 2 +Line ending: LF +Line width: 80 +Trailing commas: None +Expand: Auto +Bracket spacing: true +----- + +```jsonc +// biome-ignore-all format: test + +{ + + + "a": "b", "c": "d", "e": "f" +}``` diff --git a/crates/biome_yaml_formatter/src/comments.rs b/crates/biome_yaml_formatter/src/comments.rs index 4b6af3a983f5..4d07c8abbfc8 100644 --- a/crates/biome_yaml_formatter/src/comments.rs +++ b/crates/biome_yaml_formatter/src/comments.rs @@ -4,9 +4,9 @@ use biome_formatter::comments::{ }; use biome_formatter::formatter::Formatter; use biome_formatter::{FormatResult, FormatRule, write}; -use biome_rowan::SyntaxTriviaPieceComments; -use biome_suppression::parse_suppression_comment; -use biome_yaml_syntax::YamlLanguage; +use biome_rowan::{SyntaxTriviaPieceComments, TextSize}; +use biome_suppression::{SuppressionKind, parse_suppression_comment}; +use biome_yaml_syntax::{YamlLanguage, YamlRoot}; use crate::prelude::*; @@ -36,6 +36,15 @@ impl CommentStyle for YamlCommentStyle { fn is_suppression(text: &str) -> bool { parse_suppression_comment(text) .filter_map(Result::ok) + .filter(|suppression| suppression.kind == SuppressionKind::Classic) + .flat_map(|suppression| suppression.categories) + .any(|(key, ..)| key == category!("format")) + } + + fn is_global_suppression(text: &str) -> bool { + parse_suppression_comment(text) + .filter_map(Result::ok) + .filter(|suppression| suppression.kind == SuppressionKind::All) .flat_map(|suppression| suppression.categories) .any(|(key, ..)| key == category!("format")) } @@ -48,6 +57,29 @@ impl CommentStyle for YamlCommentStyle { &self, comment: DecoratedComment, ) -> CommentPlacement { - CommentPlacement::Default(comment) + handle_global_suppression(comment) } } + +fn handle_global_suppression( + comment: DecoratedComment, +) -> CommentPlacement { + let node = comment.enclosing_node(); + + if node.text_range_with_trivia().start() == TextSize::from(0) { + let has_global_suppression = node.first_leading_trivia().is_some_and(|trivia| { + trivia + .pieces() + .filter(|piece| piece.is_comments()) + .any(|piece| YamlCommentStyle::is_global_suppression(piece.text())) + }); + let root = node.ancestors().find_map(YamlRoot::cast); + if let Some(root) = root + && has_global_suppression + { + return CommentPlacement::leading(root.syntax().clone(), comment); + } + } + + CommentPlacement::Default(comment) +} diff --git a/crates/biome_yaml_formatter/src/lib.rs b/crates/biome_yaml_formatter/src/lib.rs index b4012fe27d1c..b68187e87b51 100644 --- a/crates/biome_yaml_formatter/src/lib.rs +++ b/crates/biome_yaml_formatter/src/lib.rs @@ -184,7 +184,7 @@ where N: AstNode, { fn fmt(&self, node: &N, f: &mut YamlFormatter) -> FormatResult<()> { - if self.is_suppressed(node, f) { + if self.is_suppressed(node, f) || self.is_global_suppressed(node, f) { return biome_formatter::write!(f, [format_suppressed_node(node.syntax())]); } @@ -201,6 +201,11 @@ where f.context().comments().is_suppressed(node.syntax()) } + /// Returns `true` if the node has a global suppression comment and should use the same formatting as in the source document. + fn is_global_suppressed(&self, node: &N, f: &YamlFormatter) -> bool { + f.context().comments().is_global_suppressed(node.syntax()) + } + /// Formats the [leading comments](biome_formatter::comments#leading-comments) of the node. /// /// You may want to override this method if you want to manually handle the formatting of comments