From c8c738e1721dae6068b6828e6e09daee799b482c Mon Sep 17 00:00:00 2001 From: wanghao Date: Mon, 24 Feb 2025 16:10:36 +0800 Subject: [PATCH] feat: add plugin suppression (#5139) Co-authored-by: Wang Hao Co-authored-by: Arend van Beelen jr. --- crates/biome_analyze/src/diagnostics.rs | 4 +- crates/biome_analyze/src/lib.rs | 66 +++++- crates/biome_analyze/src/rule.rs | 7 + crates/biome_analyze/src/suppressions.rs | 77 ++++++- crates/biome_css_formatter/src/comments.rs | 2 +- .../src/categories.rs | 1 + .../biome_graphql_formatter/src/comments.rs | 2 +- crates/biome_html_formatter/src/comments.rs | 2 +- .../plugin/preferObjectSpreadSuppression.grit | 6 + .../preferObjectSpreadSuppression.grit.snap | 100 +++++++++ .../plugin/preferObjectSpreadSuppression.js | 14 ++ .../preferObjectSpreadSuppressionAll.grit | 6 + ...preferObjectSpreadSuppressionAll.grit.snap | 14 ++ .../preferObjectSpreadSuppressionAll.js | 5 + crates/biome_js_analyze/tests/spec_tests.rs | 5 +- crates/biome_js_formatter/src/comments.rs | 2 +- crates/biome_json_formatter/src/comments.rs | 2 +- .../src/analyzer_grit_plugin.rs | 1 + crates/biome_suppression/src/lib.rs | 193 +++++++++++++----- .../@biomejs/backend-jsonrpc/src/workspace.ts | 1 + 20 files changed, 444 insertions(+), 66 deletions(-) create mode 100644 crates/biome_js_analyze/tests/plugin/preferObjectSpreadSuppression.grit create mode 100644 crates/biome_js_analyze/tests/plugin/preferObjectSpreadSuppression.grit.snap create mode 100644 crates/biome_js_analyze/tests/plugin/preferObjectSpreadSuppression.js create mode 100644 crates/biome_js_analyze/tests/plugin/preferObjectSpreadSuppressionAll.grit create mode 100644 crates/biome_js_analyze/tests/plugin/preferObjectSpreadSuppressionAll.grit.snap create mode 100644 crates/biome_js_analyze/tests/plugin/preferObjectSpreadSuppressionAll.js diff --git a/crates/biome_analyze/src/diagnostics.rs b/crates/biome_analyze/src/diagnostics.rs index 436653d4c29e..c6692826574a 100644 --- a/crates/biome_analyze/src/diagnostics.rs +++ b/crates/biome_analyze/src/diagnostics.rs @@ -26,7 +26,7 @@ pub struct AnalyzerDiagnostic { impl From for AnalyzerDiagnostic { fn from(rule_diagnostic: RuleDiagnostic) -> Self { Self { - kind: DiagnosticKind::Rule(rule_diagnostic), + kind: DiagnosticKind::Rule(Box::new(rule_diagnostic)), code_suggestion_list: vec![], } } @@ -35,7 +35,7 @@ impl From for AnalyzerDiagnostic { #[derive(Debug)] enum DiagnosticKind { /// It holds various info related to diagnostics emitted by the rules - Rule(RuleDiagnostic), + Rule(Box), /// We have raw information to create a basic [Diagnostic] Raw(Error), } diff --git a/crates/biome_analyze/src/lib.rs b/crates/biome_analyze/src/lib.rs index 21d7831aa0cf..df4c5b235fe1 100644 --- a/crates/biome_analyze/src/lib.rs +++ b/crates/biome_analyze/src/lib.rs @@ -2,6 +2,7 @@ use biome_console::markup; use biome_parser::AnyParse; +use std::cmp::Ordering; use std::collections::{BTreeMap, BinaryHeap}; use std::fmt::{Debug, Display, Formatter}; use std::ops; @@ -190,10 +191,52 @@ where for plugin in plugins { let root: AnyParse = ctx.root.syntax().as_send().expect("not a root node").into(); for diagnostic in plugin.evaluate(root, ctx.options.file_path.clone()) { - let signal = DiagnosticSignal::new(|| diagnostic.clone()); + let name = diagnostic.subcategory.clone().expect(""); + let text_range = diagnostic.span.expect(""); - if let ControlFlow::Break(br) = (emit_signal)(&signal) { - return Some(br); + // 1. check for top level suppression + if suppressions.top_level_suppression.suppressed_plugin(&name) + || suppressions.top_level_suppression.suppress_all + { + break; + } + + // 2. check for range suppression is not supprted because: + // plugin is handled separately after basic analyze phases + // at this point, we have read to the end of file, all `// biome-ignore-end` is processed, thus all range suppressions is cleared + + // 3. check for line suppression + let suppression = { + let index = suppressions + .line_suppressions + .binary_search_by(|suppression| { + if suppression.text_range.end() < text_range.start() { + Ordering::Less + } else if text_range.end() < suppression.text_range.start() { + Ordering::Greater + } else { + Ordering::Equal + } + }); + + index + .ok() + .map(|index| &mut suppressions.line_suppressions[index]) + }; + + let suppression = suppression.filter(|suppression| { + suppression.suppress_all + || suppression.suppress_all_plugins + || suppression.suppressed_plugins.contains(&name) + }); + + if let Some(suppression) = suppression { + suppression.did_suppress_signal = true; + } else { + let signal = DiagnosticSignal::new(|| diagnostic.clone()); + if let ControlFlow::Break(br) = (emit_signal)(&signal) { + return Some(br); + } } } } @@ -650,6 +693,14 @@ impl<'a> AnalyzerSuppression<'a> { } } + pub fn plugin(plugin_name: Option<&'a str>) -> Self { + Self { + kind: AnalyzerSuppressionKind::Plugin(plugin_name), + ignore_range: None, + variant: AnalyzerSuppressionVariant::Line, + } + } + #[must_use] pub fn with_ignore_range(mut self, ignore_range: TextRange) -> Self { self.ignore_range = Some(ignore_range); @@ -670,6 +721,8 @@ pub enum AnalyzerSuppressionKind<'a> { Rule(&'a str), /// A suppression to be evaluated by a specific rule eg. `// biome-ignore lint/correctness/useExhaustiveDependencies(foo)` RuleInstance(&'a str, &'a str), + /// A suppression disabling a plugin eg. `// lint/biome-ignore plugin/my-plugin` + Plugin(Option<&'a str>), } /// Takes a [Suppression] and returns a [AnalyzerSuppression] @@ -682,9 +735,14 @@ pub fn to_analyzer_suppressions( piece_range.add_start(suppression.range().start()).start(), piece_range.add_start(suppression.range().end()).start(), ); - for (key, value) in suppression.categories { + for (key, subcategory, value) in suppression.categories { if key == category!("lint") { result.push(AnalyzerSuppression::everything().with_variant(&suppression.kind)); + } else if key == category!("lint/plugin") { + let suppression = AnalyzerSuppression::plugin(subcategory) + .with_ignore_range(ignore_range) + .with_variant(&suppression.kind); + result.push(suppression); } else { let category = key.name(); if let Some(rule) = category.strip_prefix("lint/") { diff --git a/crates/biome_analyze/src/rule.rs b/crates/biome_analyze/src/rule.rs index acc1a8f0f282..2af48bef2ac9 100644 --- a/crates/biome_analyze/src/rule.rs +++ b/crates/biome_analyze/src/rule.rs @@ -1231,6 +1231,7 @@ pub trait Rule: RuleMeta + Sized { pub struct RuleDiagnostic { #[category] pub(crate) category: &'static Category, + pub(crate) subcategory: Option, #[location(span)] pub(crate) span: Option, #[message] @@ -1309,6 +1310,7 @@ impl RuleDiagnostic { let message = markup!({ title }).to_owned(); Self { category, + subcategory: None, span: span.as_span(), message: MessageAndDescription::from(message), tags: DiagnosticTags::empty(), @@ -1408,6 +1410,11 @@ impl RuleDiagnostic { pub fn advices(&self) -> &RuleAdvice { &self.rule_advice } + + pub fn subcategory(mut self, subcategory: String) -> Self { + self.subcategory = Some(subcategory); + self + } } /// Code Action object returned by a single analysis rule diff --git a/crates/biome_analyze/src/suppressions.rs b/crates/biome_analyze/src/suppressions.rs index c8f14fb08f06..28375a116547 100644 --- a/crates/biome_analyze/src/suppressions.rs +++ b/crates/biome_analyze/src/suppressions.rs @@ -7,12 +7,18 @@ use biome_diagnostics::category; use biome_rowan::{TextRange, TextSize}; use rustc_hash::{FxHashMap, FxHashSet}; +const PLUGIN_LINT_RULE_FILTER: RuleFilter<'static> = RuleFilter::Group("lint/plugin"); + #[derive(Debug, Default)] pub struct TopLevelSuppression { /// Whether this suppression suppresses all filters pub(crate) suppress_all: bool, /// Filters for the current suppression pub(crate) filters: FxHashSet>, + /// Whether this suppression suppresses all plugins + pub(crate) suppress_all_plugins: bool, + /// Current suppressed plugins + pub(crate) plugins: FxHashSet, /// The range of the comment pub(crate) comment_range: TextRange, @@ -48,6 +54,7 @@ impl TopLevelSuppression { // The absence of a filter means that it's a suppression all match filter { None => self.suppress_all = true, + Some(PLUGIN_LINT_RULE_FILTER) => self.insert_plugin(&suppression.kind), Some(filter) => self.insert(filter), } self.comment_range = comment_range; @@ -59,10 +66,26 @@ impl TopLevelSuppression { self.filters.insert(filter); } + pub(crate) fn insert_plugin(&mut self, kind: &AnalyzerSuppressionKind) { + match kind { + AnalyzerSuppressionKind::Plugin(Some(name)) => { + self.plugins.insert((*name).to_string()); + } + AnalyzerSuppressionKind::Plugin(None) => { + self.suppress_all_plugins = true; + } + _ => {} + } + } + pub(crate) fn suppressed_rule(&self, filter: &RuleKey) -> bool { self.filters.iter().any(|f| f == filter) } + pub(crate) fn suppressed_plugin(&self, plugin_name: &str) -> bool { + self.suppress_all_plugins || self.plugins.contains(plugin_name) + } + pub(crate) fn expand_range(&mut self, range: TextRange) { self.range.cover(range); } @@ -89,6 +112,10 @@ pub(crate) struct LineSuppression { pub(crate) suppressed_rules: FxHashSet>, /// List of all the rule instances this comment has started suppressing. pub(crate) suppressed_instances: FxHashMap>, + /// List of plugins this comment has started suppressing + pub(crate) suppressed_plugins: FxHashSet, + /// Set to true if this comment suppress all plugins + pub(crate) suppress_all_plugins: bool, /// Set to `true` when a signal matching this suppression was emitted and /// suppressed pub(crate) did_suppress_signal: bool, @@ -139,6 +166,15 @@ impl RangeSuppressions { text_range: TextRange, already_suppressed: Option, ) -> Result<(), AnalyzerSuppressionDiagnostic> { + if let Some(PLUGIN_LINT_RULE_FILTER) = filter { + return Err(AnalyzerSuppressionDiagnostic::new( + category!("suppressions/incorrect"), + text_range, + markup!{"Found a ""biome-ignore-"" suppression on plugin. This is not supported. See https://github.com/biomejs/biome/issues/5175"} + ).hint(markup!{ + "Remove this suppression." + }.to_owned())); + } if suppression.is_range_start() { if let Some(range_suppression) = self.suppressions.last_mut() { match filter { @@ -270,6 +306,7 @@ impl<'analyzer> Suppressions<'analyzer> { fn push_line_suppression( &mut self, filter: Option>, + plugin_name: Option, instance: Option, current_range: TextRange, already_suppressed: Option, @@ -283,6 +320,16 @@ impl<'analyzer> Suppressions<'analyzer> { suppression.suppress_all = true; suppression.suppressed_rules.clear(); suppression.suppressed_instances.clear(); + suppression.suppressed_plugins.clear(); + } + Some(PLUGIN_LINT_RULE_FILTER) => { + if let Some(plugin_name) = plugin_name { + suppression.suppressed_plugins.insert(plugin_name); + suppression.suppress_all_plugins = false; + } else { + suppression.suppress_all_plugins = true; + } + suppression.suppress_all = false; } Some(filter) => { suppression.suppressed_rules.insert(filter); @@ -307,6 +354,13 @@ impl<'analyzer> Suppressions<'analyzer> { None => { suppression.suppress_all = true; } + Some(PLUGIN_LINT_RULE_FILTER) => { + if let Some(plugin_name) = plugin_name { + suppression.suppressed_plugins.insert(plugin_name); + } else { + suppression.suppress_all_plugins = true; + } + } Some(filter) => { suppression.suppressed_rules.insert(filter); if let Some(instance) = instance { @@ -329,6 +383,7 @@ impl<'analyzer> Suppressions<'analyzer> { AnalyzerSuppressionKind::Everything => return Ok(None), AnalyzerSuppressionKind::Rule(rule) => rule, AnalyzerSuppressionKind::RuleInstance(rule, _) => rule, + AnalyzerSuppressionKind::Plugin(_) => return Ok(Some(PLUGIN_LINT_RULE_FILTER)), }; let group_rule = rule.split_once('/'); @@ -357,11 +412,20 @@ impl<'analyzer> Suppressions<'analyzer> { fn map_to_rule_instances(&self, suppression_kind: &AnalyzerSuppressionKind) -> Option { match suppression_kind { - AnalyzerSuppressionKind::Everything | AnalyzerSuppressionKind::Rule(_) => None, + AnalyzerSuppressionKind::Everything + | AnalyzerSuppressionKind::Rule(_) + | AnalyzerSuppressionKind::Plugin(_) => None, AnalyzerSuppressionKind::RuleInstance(_, instances) => Some((*instances).to_string()), } } + fn map_to_plugin_name(&self, suppression_kind: &AnalyzerSuppressionKind) -> Option { + match suppression_kind { + AnalyzerSuppressionKind::Plugin(Some(plugin_name)) => Some((*plugin_name).to_string()), + _ => None, + } + } + pub(crate) fn push_suppression( &mut self, suppression: &AnalyzerSuppression, @@ -370,12 +434,17 @@ impl<'analyzer> Suppressions<'analyzer> { ) -> Result<(), AnalyzerSuppressionDiagnostic> { let filter = self.map_to_rule_filter(&suppression.kind, comment_range)?; let instances = self.map_to_rule_instances(&suppression.kind); + let plugin_name: Option = self.map_to_plugin_name(&suppression.kind); self.last_suppression = Some(suppression.variant.clone()); let already_suppressed = self.already_suppressed(filter.as_ref(), &comment_range); match suppression.variant { - AnalyzerSuppressionVariant::Line => { - self.push_line_suppression(filter, instances, comment_range, already_suppressed) - } + AnalyzerSuppressionVariant::Line => self.push_line_suppression( + filter, + plugin_name, + instances, + comment_range, + already_suppressed, + ), AnalyzerSuppressionVariant::TopLevel => self.top_level_suppression.push_suppression( suppression, filter, diff --git a/crates/biome_css_formatter/src/comments.rs b/crates/biome_css_formatter/src/comments.rs index 51e734ec5cd9..90d6dca52ef2 100644 --- a/crates/biome_css_formatter/src/comments.rs +++ b/crates/biome_css_formatter/src/comments.rs @@ -72,7 +72,7 @@ impl CommentStyle for CssCommentStyle { parse_suppression_comment(text) .filter_map(Result::ok) .flat_map(|suppression| suppression.categories) - .any(|(key, _)| key == category!("format")) + .any(|(key, ..)| key == category!("format")) } fn get_comment_kind(comment: &SyntaxTriviaPieceComments) -> CommentKind { diff --git a/crates/biome_diagnostics_categories/src/categories.rs b/crates/biome_diagnostics_categories/src/categories.rs index 696cd80549ea..4b9a71066a09 100644 --- a/crates/biome_diagnostics_categories/src/categories.rs +++ b/crates/biome_diagnostics_categories/src/categories.rs @@ -388,6 +388,7 @@ define_categories! { "lint/security", "lint/style", "lint/suspicious", + "lint/plugin", // Suppression comments "suppressions/parse", diff --git a/crates/biome_graphql_formatter/src/comments.rs b/crates/biome_graphql_formatter/src/comments.rs index 807db2c4cfc4..76ba9d05e1b5 100644 --- a/crates/biome_graphql_formatter/src/comments.rs +++ b/crates/biome_graphql_formatter/src/comments.rs @@ -68,7 +68,7 @@ impl CommentStyle for GraphqlCommentStyle { parse_suppression_comment(text) .filter_map(Result::ok) .flat_map(|suppression| suppression.categories) - .any(|(key, _)| key == category!("format")) + .any(|(key, ..)| key == category!("format")) } fn get_comment_kind(_comment: &SyntaxTriviaPieceComments) -> CommentKind { diff --git a/crates/biome_html_formatter/src/comments.rs b/crates/biome_html_formatter/src/comments.rs index fec2bc969ebd..e713fe66c692 100644 --- a/crates/biome_html_formatter/src/comments.rs +++ b/crates/biome_html_formatter/src/comments.rs @@ -88,7 +88,7 @@ impl CommentStyle for HtmlCommentStyle { parse_suppression_comment(text) .filter_map(Result::ok) .flat_map(|suppression| suppression.categories) - .any(|(key, _)| key == category!("format")) + .any(|(key, ..)| key == category!("format")) } fn get_comment_kind(_comment: &SyntaxTriviaPieceComments) -> CommentKind { diff --git a/crates/biome_js_analyze/tests/plugin/preferObjectSpreadSuppression.grit b/crates/biome_js_analyze/tests/plugin/preferObjectSpreadSuppression.grit new file mode 100644 index 000000000000..82a21aa299fc --- /dev/null +++ b/crates/biome_js_analyze/tests/plugin/preferObjectSpreadSuppression.grit @@ -0,0 +1,6 @@ +`Object.assign($args)` where { + register_diagnostic( + span = $args, + message = "Prefer object spread instead of `Object.assign()`" + ) +} diff --git a/crates/biome_js_analyze/tests/plugin/preferObjectSpreadSuppression.grit.snap b/crates/biome_js_analyze/tests/plugin/preferObjectSpreadSuppression.grit.snap new file mode 100644 index 000000000000..a9ec8ef125a9 --- /dev/null +++ b/crates/biome_js_analyze/tests/plugin/preferObjectSpreadSuppression.grit.snap @@ -0,0 +1,100 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +assertion_line: 366 +expression: preferObjectSpreadSuppression.grit +--- +# Input +```js +// biome-ignore lint/plugin/preferObjectSpreadSuppression: reason +Object.assign({ foo: 'bar'}, baz); + +// biome-ignore-start lint/plugin/preferObjectSpreadSuppression: reason +Object.assign({}, {foo: 'bar'}); +// biome-ignore-end lint/plugin/preferObjectSpreadSuppression: reason + +// if no name is specified, should suppress all plugins +// biome-ignore lint/plugin: reason +Object.assign({}, foo); + +// only suppress specified plugin +// biome-ignore lint/plugin/anotherPlugin: reason +Object.assign({ foo: 'bar'}, baz); + +``` + +# Diagnostics +``` +preferObjectSpreadSuppression.grit:4:1 suppressions/incorrect ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Found a biome-ignore- suppression on plugin. This is not supported. See https://github.com/biomejs/biome/issues/5175 + + 2 │ Object.assign({ foo: 'bar'}, baz); + 3 │ + > 4 │ // biome-ignore-start lint/plugin/preferObjectSpreadSuppression: reason + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 5 │ Object.assign({}, {foo: 'bar'}); + 6 │ // biome-ignore-end lint/plugin/preferObjectSpreadSuppression: reason + + i Remove this suppression. + + +``` + +``` +preferObjectSpreadSuppression.grit:6:1 suppressions/incorrect ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Found a biome-ignore- suppression on plugin. This is not supported. See https://github.com/biomejs/biome/issues/5175 + + 4 │ // biome-ignore-start lint/plugin/preferObjectSpreadSuppression: reason + 5 │ Object.assign({}, {foo: 'bar'}); + > 6 │ // biome-ignore-end lint/plugin/preferObjectSpreadSuppression: reason + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 7 │ + 8 │ // if no name is specified, should suppress all plugins + + i Remove this suppression. + + +``` + +``` +preferObjectSpreadSuppression.grit:5:15 plugin ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Prefer object spread instead of `Object.assign()` + + 4 │ // biome-ignore-start lint/plugin/preferObjectSpreadSuppression: reason + > 5 │ Object.assign({}, {foo: 'bar'}); + │ ^^^^^^^^^^^^^^^^ + 6 │ // biome-ignore-end lint/plugin/preferObjectSpreadSuppression: reason + 7 │ + + +``` + +``` +preferObjectSpreadSuppression.grit:14:15 plugin ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Prefer object spread instead of `Object.assign()` + + 12 │ // only suppress specified plugin + 13 │ // biome-ignore lint/plugin/anotherPlugin: reason + > 14 │ Object.assign({ foo: 'bar'}, baz); + │ ^^^^^^^^^^^^^^^^^^ + 15 │ + + +``` + +``` +preferObjectSpreadSuppression.grit:13:1 suppressions/unused ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Suppression comment has no effect. Remove the suppression or make sure you are suppressing the correct rule. + + 12 │ // only suppress specified plugin + > 13 │ // biome-ignore lint/plugin/anotherPlugin: reason + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 14 │ Object.assign({ foo: 'bar'}, baz); + 15 │ + + +``` diff --git a/crates/biome_js_analyze/tests/plugin/preferObjectSpreadSuppression.js b/crates/biome_js_analyze/tests/plugin/preferObjectSpreadSuppression.js new file mode 100644 index 000000000000..254523fd786c --- /dev/null +++ b/crates/biome_js_analyze/tests/plugin/preferObjectSpreadSuppression.js @@ -0,0 +1,14 @@ +// biome-ignore lint/plugin/preferObjectSpreadSuppression: reason +Object.assign({ foo: 'bar'}, baz); + +// biome-ignore-start lint/plugin/preferObjectSpreadSuppression: reason +Object.assign({}, {foo: 'bar'}); +// biome-ignore-end lint/plugin/preferObjectSpreadSuppression: reason + +// if no name is specified, should suppress all plugins +// biome-ignore lint/plugin: reason +Object.assign({}, foo); + +// only suppress specified plugin +// biome-ignore lint/plugin/anotherPlugin: reason +Object.assign({ foo: 'bar'}, baz); diff --git a/crates/biome_js_analyze/tests/plugin/preferObjectSpreadSuppressionAll.grit b/crates/biome_js_analyze/tests/plugin/preferObjectSpreadSuppressionAll.grit new file mode 100644 index 000000000000..82a21aa299fc --- /dev/null +++ b/crates/biome_js_analyze/tests/plugin/preferObjectSpreadSuppressionAll.grit @@ -0,0 +1,6 @@ +`Object.assign($args)` where { + register_diagnostic( + span = $args, + message = "Prefer object spread instead of `Object.assign()`" + ) +} diff --git a/crates/biome_js_analyze/tests/plugin/preferObjectSpreadSuppressionAll.grit.snap b/crates/biome_js_analyze/tests/plugin/preferObjectSpreadSuppressionAll.grit.snap new file mode 100644 index 000000000000..a68e01879a0e --- /dev/null +++ b/crates/biome_js_analyze/tests/plugin/preferObjectSpreadSuppressionAll.grit.snap @@ -0,0 +1,14 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +assertion_line: 366 +expression: preferObjectSpreadSuppressionAll.grit +--- +# Input +```js +// biome-ignore-all lint/plugin/preferObjectSpreadSuppressionAll: reason + +console.log("foo"); + +Object.assign({ foo: 'bar'}, baz); + +``` diff --git a/crates/biome_js_analyze/tests/plugin/preferObjectSpreadSuppressionAll.js b/crates/biome_js_analyze/tests/plugin/preferObjectSpreadSuppressionAll.js new file mode 100644 index 000000000000..9e733bbc82ed --- /dev/null +++ b/crates/biome_js_analyze/tests/plugin/preferObjectSpreadSuppressionAll.js @@ -0,0 +1,5 @@ +// biome-ignore-all lint/plugin/preferObjectSpreadSuppressionAll: reason + +console.log("foo"); + +Object.assign({ foo: 'bar'}, baz); diff --git a/crates/biome_js_analyze/tests/spec_tests.rs b/crates/biome_js_analyze/tests/spec_tests.rs index c83004ef4957..9b34d3d31c15 100644 --- a/crates/biome_js_analyze/tests/spec_tests.rs +++ b/crates/biome_js_analyze/tests/spec_tests.rs @@ -332,8 +332,11 @@ fn run_plugin_test(input: &'static str, _: &str, _: &str, _: &str) { Err(err) => panic!("Cannot load plugin: {err:?}"), }; + // Enable at least 1 rule so that PhaseRunner will be called + // which is necessary to parse and store supression comments + let rule_filter = RuleFilter::Rule("nursery", "noCommonJs"); let filter = AnalysisFilter { - enabled_rules: Some(&[]), + enabled_rules: Some(slice::from_ref(&rule_filter)), ..AnalysisFilter::default() }; diff --git a/crates/biome_js_formatter/src/comments.rs b/crates/biome_js_formatter/src/comments.rs index d20e546c7a4e..31ee76172151 100644 --- a/crates/biome_js_formatter/src/comments.rs +++ b/crates/biome_js_formatter/src/comments.rs @@ -80,7 +80,7 @@ impl CommentStyle for JsCommentStyle { parse_suppression_comment(text) .filter_map(Result::ok) .flat_map(|suppression| suppression.categories) - .any(|(key, _)| key == category!("format")) + .any(|(key, ..)| key == category!("format")) } fn get_comment_kind(comment: &SyntaxTriviaPieceComments) -> CommentKind { diff --git a/crates/biome_json_formatter/src/comments.rs b/crates/biome_json_formatter/src/comments.rs index 88555af9753d..790f425d73c4 100644 --- a/crates/biome_json_formatter/src/comments.rs +++ b/crates/biome_json_formatter/src/comments.rs @@ -69,7 +69,7 @@ impl CommentStyle for JsonCommentStyle { parse_suppression_comment(text) .filter_map(Result::ok) .flat_map(|suppression| suppression.categories) - .any(|(key, _)| key == category!("format")) + .any(|(key, ..)| key == category!("format")) } fn get_comment_kind(comment: &SyntaxTriviaPieceComments) -> CommentKind { diff --git a/crates/biome_plugin_loader/src/analyzer_grit_plugin.rs b/crates/biome_plugin_loader/src/analyzer_grit_plugin.rs index d71af3a18263..20620590b62e 100644 --- a/crates/biome_plugin_loader/src/analyzer_grit_plugin.rs +++ b/crates/biome_plugin_loader/src/analyzer_grit_plugin.rs @@ -65,6 +65,7 @@ impl AnalyzerPlugin for AnalyzerGritPlugin { .verbose() }) .chain(result.diagnostics) + .map(|diagnos| diagnos.subcategory(name.to_string())) .collect(), Err(error) => vec![RuleDiagnostic::new( category!("plugin"), diff --git a/crates/biome_suppression/src/lib.rs b/crates/biome_suppression/src/lib.rs index 683979ab632a..1e67f3b28f43 100644 --- a/crates/biome_suppression/src/lib.rs +++ b/crates/biome_suppression/src/lib.rs @@ -20,8 +20,9 @@ pub struct Suppression<'a> { /// List of categories for this suppression /// /// Categories are a pair of the category name + + /// an optional dynamic subcategory name + /// an optional category value - pub categories: Vec<(&'a Category, Option<&'a str>)>, + pub categories: Vec<(&'a Category, Option<&'a str>, Option<&'a str>)>, /// Reason for this suppression comment to exist pub reason: &'a str, @@ -252,15 +253,7 @@ fn parse_suppression_line( let (category, rest) = line.split_at(separator); let category = category.trim_end(); - let category: Option<&'static Category> = if !category.is_empty() { - let category = category.parse().map_err(|()| SuppressionDiagnostic { - message: SuppressionDiagnosticKind::ParseCategory(category.into()), - span: TextRange::at(offset_from(base, category), TextSize::of(category)), - })?; - Some(category) - } else { - None - }; + let (category, subcategory) = parse_category(base, category)?; // Skip over and match the separator let (separator, rest) = rest.split_at(1); @@ -269,7 +262,7 @@ fn parse_suppression_line( // Colon token: stop parsing categories ":" => { if let Some(category) = category { - categories.push((category, None)); + categories.push((category, subcategory, None)); } line = rest.trim_start(); @@ -292,14 +285,14 @@ fn parse_suppression_line( let (value, rest) = rest.split_at(paren); let value = value.trim(); - categories.push((category, Some(value))); + categories.push((category, subcategory, Some(value))); line = rest.strip_prefix(')').unwrap().trim_start(); } // Whitespace: push a category without value _ => { if let Some(category) = category { - categories.push((category, None)); + categories.push((category, subcategory, None)); } line = rest.trim_start(); @@ -316,6 +309,35 @@ fn parse_suppression_line( }) } +/// Parse the comment's category part into (category, subcategory) +/// +/// category is static, predefined in crates/biome_diagnostics_categories/src/categories.rs +/// subcategory is dynamic (e.g user defined plugin name) +/// +/// # Example +/// - No category: `// biome-ignore` -> `(None, None)` +/// - Custom Plugin: `// biome-ignore lint/plugin/myPlugin` -> `("lint/plugin", "myPlugin")` +/// - Valid category: `// biome-ignore lint/complexity` -> `("lint/complexity", None)` +/// - Invalid category: `// biome-ignore linx` -> `Err(SuppressionDiagnostic)` +fn parse_category<'a>( + base: &'a str, + category: &'a str, +) -> Result<(Option<&'static Category>, Option<&'a str>), SuppressionDiagnostic> { + if category.is_empty() { + return Ok((None, None)); + } + if let Some(rest) = category.strip_prefix("lint/plugin/") { + return Ok(("lint/plugin".parse().ok(), Some(rest))); + // if user doesn't specify plugin name: e.g. `// biome-ignore lint/plugin: reason` + // will return ("lint/plugin", None) and treat as `suppress all plugins linting` + } + let category: &'static Category = category.parse().map_err(|()| SuppressionDiagnostic { + message: SuppressionDiagnosticKind::ParseCategory(category.into()), + span: TextRange::at(offset_from(base, category), TextSize::of(category)), + })?; + Ok((Some(category), None)) +} + /// Returns the byte offset of `substr` within `base` /// /// # Safety @@ -353,7 +375,10 @@ mod tests_suppression_kinds { parse_suppression_comment("// biome-ignore format lint: explanation") .collect::>(), vec![Ok(Suppression { - categories: vec![(category!("format"), None), (category!("lint"), None)], + categories: vec![ + (category!("format"), None, None), + (category!("lint"), None, None) + ], reason: "explanation", kind: SuppressionKind::Classic, range: TextRange::new(TextSize::from(3), TextSize::from(15)) @@ -367,7 +392,10 @@ mod tests_suppression_kinds { parse_suppression_comment("// biome-ignore-all format lint: explanation") .collect::>(), vec![Ok(Suppression { - categories: vec![(category!("format"), None), (category!("lint"), None)], + categories: vec![ + (category!("format"), None, None), + (category!("lint"), None, None) + ], reason: "explanation", kind: SuppressionKind::All, range: TextRange::new(TextSize::from(3), TextSize::from(19)) @@ -381,7 +409,10 @@ mod tests_suppression_kinds { parse_suppression_comment("// biome-ignore-start format lint: explanation") .collect::>(), vec![Ok(Suppression { - categories: vec![(category!("format"), None), (category!("lint"), None)], + categories: vec![ + (category!("format"), None, None), + (category!("lint"), None, None) + ], reason: "explanation", kind: SuppressionKind::RangeStart, range: TextRange::new(TextSize::from(3), TextSize::from(21)) @@ -395,7 +426,10 @@ mod tests_suppression_kinds { parse_suppression_comment("// biome-ignore-end format lint: explanation") .collect::>(), vec![Ok(Suppression { - categories: vec![(category!("format"), None), (category!("lint"), None)], + categories: vec![ + (category!("format"), None, None), + (category!("lint"), None, None) + ], reason: "explanation", kind: SuppressionKind::RangeEnd, range: TextRange::new(TextSize::from(3), TextSize::from(19)) @@ -409,7 +443,10 @@ mod tests_biome_ignore_inline { use biome_diagnostics::category; use biome_rowan::{TextRange, TextSize}; - use crate::{offset_from, SuppressionDiagnostic, SuppressionDiagnosticKind, SuppressionKind}; + use crate::{ + offset_from, parse_category, SuppressionDiagnostic, SuppressionDiagnosticKind, + SuppressionKind, + }; use super::{parse_suppression_comment, Suppression}; @@ -418,7 +455,7 @@ mod tests_biome_ignore_inline { assert_eq!( parse_suppression_comment("// biome-ignore parse: explanation1").collect::>(), vec![Ok(Suppression { - categories: vec![(category!("parse"), None)], + categories: vec![(category!("parse"), None, None)], reason: "explanation1", kind: SuppressionKind::Classic, range: TextRange::new(TextSize::from(3), TextSize::from(15)) @@ -429,7 +466,7 @@ mod tests_biome_ignore_inline { parse_suppression_comment("/** biome-ignore parse: explanation2 */") .collect::>(), vec![Ok(Suppression { - categories: vec![(category!("parse"), None)], + categories: vec![(category!("parse"), None, None)], reason: "explanation2", kind: SuppressionKind::Classic, range: TextRange::new(TextSize::from(4), TextSize::from(16)) @@ -444,7 +481,7 @@ mod tests_biome_ignore_inline { ) .collect::>(), vec![Ok(Suppression { - categories: vec![(category!("parse"), None)], + categories: vec![(category!("parse"), None, None)], reason: "explanation3", kind: SuppressionKind::Classic, range: TextRange::new(TextSize::from(24), TextSize::from(36)) @@ -460,19 +497,41 @@ mod tests_biome_ignore_inline { ) .collect::>(), vec![Ok(Suppression { - categories: vec![(category!("parse"), None)], + categories: vec![(category!("parse"), None, None)], reason: "explanation4", kind: SuppressionKind::Classic, range: TextRange::new(TextSize::from(50), TextSize::from(62)) })], ); + + assert_eq!( + parse_suppression_comment("// biome-ignore lint/plugin: explanation5") + .collect::>(), + vec![Ok(Suppression { + categories: vec![(category!("lint/plugin"), None, None)], + reason: "explanation5", + kind: SuppressionKind::Classic, + range: TextRange::new(TextSize::from(3), TextSize::from(15)) + })], + ); + + assert_eq!( + parse_suppression_comment("// biome-ignore lint/plugin/myPlugin: explanation6") + .collect::>(), + vec![Ok(Suppression { + categories: vec![(category!("lint/plugin"), Some("myPlugin"), None)], + reason: "explanation6", + kind: SuppressionKind::Classic, + range: TextRange::new(TextSize::from(3), TextSize::from(15)) + })], + ); } #[test] fn parse_unclosed_block_comment_suppressions() { assert_eq!( parse_suppression_comment("/* biome-ignore format: explanation").collect::>(), vec![Ok(Suppression { - categories: vec![(category!("format"), None)], + categories: vec![(category!("format"), None, None)], reason: "explanation", kind: SuppressionKind::Classic, range: TextRange::new(TextSize::from(3), TextSize::from(15)) @@ -482,7 +541,7 @@ mod tests_biome_ignore_inline { assert_eq!( parse_suppression_comment("/* biome-ignore format: explanation *").collect::>(), vec![Ok(Suppression { - categories: vec![(category!("format"), None)], + categories: vec![(category!("format"), None, None)], reason: "explanation", kind: SuppressionKind::Classic, range: TextRange::new(TextSize::from(3), TextSize::from(15)) @@ -492,7 +551,7 @@ mod tests_biome_ignore_inline { assert_eq!( parse_suppression_comment("/* biome-ignore format: explanation /").collect::>(), vec![Ok(Suppression { - categories: vec![(category!("format"), None)], + categories: vec![(category!("format"), None, None)], reason: "explanation", kind: SuppressionKind::Classic, range: TextRange::new(TextSize::from(3), TextSize::from(15)) @@ -507,8 +566,8 @@ mod tests_biome_ignore_inline { .collect::>(), vec![Ok(Suppression { categories: vec![ - (category!("parse"), Some("foo")), - (category!("parse"), Some("dog")) + (category!("parse"), None, Some("foo")), + (category!("parse"), None, Some("dog")) ], reason: "explanation", kind: SuppressionKind::Classic, @@ -521,8 +580,8 @@ mod tests_biome_ignore_inline { .collect::>(), vec![Ok(Suppression { categories: vec![ - (category!("parse"), Some("bar")), - (category!("parse"), Some("cat")) + (category!("parse"), None, Some("bar")), + (category!("parse"), None, Some("cat")) ], reason: "explanation", kind: SuppressionKind::Classic, @@ -539,8 +598,8 @@ mod tests_biome_ignore_inline { .collect::>(), vec![Ok(Suppression { categories: vec![ - (category!("parse"), Some("yes")), - (category!("parse"), Some("frog")) + (category!("parse"), None, Some("yes")), + (category!("parse"), None, Some("frog")) ], reason: "explanation", kind: SuppressionKind::Classic, @@ -558,8 +617,8 @@ mod tests_biome_ignore_inline { .collect::>(), vec![Ok(Suppression { categories: vec![ - (category!("parse"), Some("wow")), - (category!("parse"), Some("fish")) + (category!("parse"), None, Some("wow")), + (category!("parse"), None, Some("fish")) ], reason: "explanation", kind: SuppressionKind::Classic, @@ -574,7 +633,10 @@ mod tests_biome_ignore_inline { parse_suppression_comment("// biome-ignore format lint: explanation") .collect::>(), vec![Ok(Suppression { - categories: vec![(category!("format"), None), (category!("lint"), None)], + categories: vec![ + (category!("format"), None, None), + (category!("lint"), None, None), + ], reason: "explanation", kind: SuppressionKind::Classic, range: TextRange::new(TextSize::from(3), TextSize::from(15)) @@ -582,6 +644,34 @@ mod tests_biome_ignore_inline { ); } + #[test] + fn check_parse_category() { + assert_eq!( + parse_category("// biome-ignore: reason", ""), + Ok((None, None)) + ); + + assert_eq!( + parse_category( + "// biome-ignore lint/plugin/myPlugin: reason", + "lint/plugin/myPlugin" + ), + Ok((Some(category!("lint/plugin")), Some("myPlugin"))) + ); + + assert_eq!( + parse_category("// biome-ignore lint/complexity: reason", "lint/complexity"), + Ok((Some(category!("lint/complexity")), None)) + ); + + let base = "// biome-ignore linx: reason"; + let category = &base[16..20]; + assert!(matches!( + parse_category(base, category), + Err(SuppressionDiagnostic { .. }) + )); + } + #[test] fn check_offset_from() { const BASE: &str = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua"; @@ -655,7 +745,7 @@ mod tests_biome_ignore_toplevel { parse_suppression_comment("// biome-ignore-all parse: explanation1") .collect::>(), vec![Ok(Suppression { - categories: vec![(category!("parse"), None)], + categories: vec![(category!("parse"), None, None)], reason: "explanation1", kind: SuppressionKind::All, range: TextRange::new(TextSize::from(3), TextSize::from(19)) @@ -666,7 +756,7 @@ mod tests_biome_ignore_toplevel { parse_suppression_comment("/** biome-ignore-all parse: explanation2 */") .collect::>(), vec![Ok(Suppression { - categories: vec![(category!("parse"), None)], + categories: vec![(category!("parse"), None, None)], reason: "explanation2", kind: SuppressionKind::All, range: TextRange::new(TextSize::from(4), TextSize::from(20)) @@ -681,7 +771,7 @@ mod tests_biome_ignore_toplevel { ) .collect::>(), vec![Ok(Suppression { - categories: vec![(category!("parse"), None)], + categories: vec![(category!("parse"), None, None)], reason: "explanation3", kind: SuppressionKind::All, range: TextRange::new(TextSize::from(24), TextSize::from(40)) @@ -697,7 +787,7 @@ mod tests_biome_ignore_toplevel { ) .collect::>(), vec![Ok(Suppression { - categories: vec![(category!("parse"), None)], + categories: vec![(category!("parse"), None, None)], reason: "explanation4", kind: SuppressionKind::All, range: TextRange::new(TextSize::from(50), TextSize::from(66)) @@ -710,7 +800,7 @@ mod tests_biome_ignore_toplevel { parse_suppression_comment("/* biome-ignore-all format: explanation") .collect::>(), vec![Ok(Suppression { - categories: vec![(category!("format"), None)], + categories: vec![(category!("format"), None, None)], reason: "explanation", kind: SuppressionKind::All, range: TextRange::new(TextSize::from(3), TextSize::from(19)) @@ -721,7 +811,7 @@ mod tests_biome_ignore_toplevel { parse_suppression_comment("/* biome-ignore-all format: explanation *") .collect::>(), vec![Ok(Suppression { - categories: vec![(category!("format"), None)], + categories: vec![(category!("format"), None, None)], reason: "explanation", kind: SuppressionKind::All, range: TextRange::new(TextSize::from(3), TextSize::from(19)) @@ -732,7 +822,7 @@ mod tests_biome_ignore_toplevel { parse_suppression_comment("/* biome-ignore-all format: explanation /") .collect::>(), vec![Ok(Suppression { - categories: vec![(category!("format"), None)], + categories: vec![(category!("format"), None, None)], reason: "explanation", kind: SuppressionKind::All, range: TextRange::new(TextSize::from(3), TextSize::from(19)) @@ -747,8 +837,8 @@ mod tests_biome_ignore_toplevel { .collect::>(), vec![Ok(Suppression { categories: vec![ - (category!("parse"), Some("foo")), - (category!("parse"), Some("dog")) + (category!("parse"), None, Some("foo")), + (category!("parse"), None, Some("dog")) ], reason: "explanation", kind: SuppressionKind::All, @@ -761,8 +851,8 @@ mod tests_biome_ignore_toplevel { .collect::>(), vec![Ok(Suppression { categories: vec![ - (category!("parse"), Some("bar")), - (category!("parse"), Some("cat")) + (category!("parse"), None, Some("bar")), + (category!("parse"), None, Some("cat")) ], reason: "explanation", kind: SuppressionKind::All, @@ -779,8 +869,8 @@ mod tests_biome_ignore_toplevel { .collect::>(), vec![Ok(Suppression { categories: vec![ - (category!("parse"), Some("yes")), - (category!("parse"), Some("frog")) + (category!("parse"), None, Some("yes")), + (category!("parse"), None, Some("frog")) ], reason: "explanation", kind: SuppressionKind::All, @@ -798,8 +888,8 @@ mod tests_biome_ignore_toplevel { .collect::>(), vec![Ok(Suppression { categories: vec![ - (category!("parse"), Some("wow")), - (category!("parse"), Some("fish")) + (category!("parse"), None, Some("wow")), + (category!("parse"), None, Some("fish")) ], reason: "explanation", kind: SuppressionKind::All, @@ -814,7 +904,10 @@ mod tests_biome_ignore_toplevel { parse_suppression_comment("// biome-ignore-all format lint: explanation") .collect::>(), vec![Ok(Suppression { - categories: vec![(category!("format"), None), (category!("lint"), None)], + categories: vec![ + (category!("format"), None, None), + (category!("lint"), None, None) + ], reason: "explanation", kind: SuppressionKind::All, range: TextRange::new(TextSize::from(3), TextSize::from(19)) diff --git a/packages/@biomejs/backend-jsonrpc/src/workspace.ts b/packages/@biomejs/backend-jsonrpc/src/workspace.ts index c4f88cbb799c..f194c0d5942f 100644 --- a/packages/@biomejs/backend-jsonrpc/src/workspace.ts +++ b/packages/@biomejs/backend-jsonrpc/src/workspace.ts @@ -3547,6 +3547,7 @@ export type Category = | "lint/security" | "lint/style" | "lint/suspicious" + | "lint/plugin" | "suppressions/parse" | "suppressions/unknownGroup" | "suppressions/unknownRule"