Skip to content

Commit c8c738e

Browse files
wanghaoPolarWang Haoarendjr
authored
feat: add plugin suppression (#5139)
Co-authored-by: Wang Hao <[email protected]> Co-authored-by: Arend van Beelen jr. <[email protected]>
1 parent 79a7d5c commit c8c738e

20 files changed

Lines changed: 444 additions & 66 deletions

File tree

crates/biome_analyze/src/diagnostics.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ pub struct AnalyzerDiagnostic {
2626
impl From<RuleDiagnostic> for AnalyzerDiagnostic {
2727
fn from(rule_diagnostic: RuleDiagnostic) -> Self {
2828
Self {
29-
kind: DiagnosticKind::Rule(rule_diagnostic),
29+
kind: DiagnosticKind::Rule(Box::new(rule_diagnostic)),
3030
code_suggestion_list: vec![],
3131
}
3232
}
@@ -35,7 +35,7 @@ impl From<RuleDiagnostic> for AnalyzerDiagnostic {
3535
#[derive(Debug)]
3636
enum DiagnosticKind {
3737
/// It holds various info related to diagnostics emitted by the rules
38-
Rule(RuleDiagnostic),
38+
Rule(Box<RuleDiagnostic>),
3939
/// We have raw information to create a basic [Diagnostic]
4040
Raw(Error),
4141
}

crates/biome_analyze/src/lib.rs

Lines changed: 62 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
use biome_console::markup;
44
use biome_parser::AnyParse;
5+
use std::cmp::Ordering;
56
use std::collections::{BTreeMap, BinaryHeap};
67
use std::fmt::{Debug, Display, Formatter};
78
use std::ops;
@@ -190,10 +191,52 @@ where
190191
for plugin in plugins {
191192
let root: AnyParse = ctx.root.syntax().as_send().expect("not a root node").into();
192193
for diagnostic in plugin.evaluate(root, ctx.options.file_path.clone()) {
193-
let signal = DiagnosticSignal::new(|| diagnostic.clone());
194+
let name = diagnostic.subcategory.clone().expect("");
195+
let text_range = diagnostic.span.expect("");
194196

195-
if let ControlFlow::Break(br) = (emit_signal)(&signal) {
196-
return Some(br);
197+
// 1. check for top level suppression
198+
if suppressions.top_level_suppression.suppressed_plugin(&name)
199+
|| suppressions.top_level_suppression.suppress_all
200+
{
201+
break;
202+
}
203+
204+
// 2. check for range suppression is not supprted because:
205+
// plugin is handled separately after basic analyze phases
206+
// at this point, we have read to the end of file, all `// biome-ignore-end` is processed, thus all range suppressions is cleared
207+
208+
// 3. check for line suppression
209+
let suppression = {
210+
let index = suppressions
211+
.line_suppressions
212+
.binary_search_by(|suppression| {
213+
if suppression.text_range.end() < text_range.start() {
214+
Ordering::Less
215+
} else if text_range.end() < suppression.text_range.start() {
216+
Ordering::Greater
217+
} else {
218+
Ordering::Equal
219+
}
220+
});
221+
222+
index
223+
.ok()
224+
.map(|index| &mut suppressions.line_suppressions[index])
225+
};
226+
227+
let suppression = suppression.filter(|suppression| {
228+
suppression.suppress_all
229+
|| suppression.suppress_all_plugins
230+
|| suppression.suppressed_plugins.contains(&name)
231+
});
232+
233+
if let Some(suppression) = suppression {
234+
suppression.did_suppress_signal = true;
235+
} else {
236+
let signal = DiagnosticSignal::new(|| diagnostic.clone());
237+
if let ControlFlow::Break(br) = (emit_signal)(&signal) {
238+
return Some(br);
239+
}
197240
}
198241
}
199242
}
@@ -650,6 +693,14 @@ impl<'a> AnalyzerSuppression<'a> {
650693
}
651694
}
652695

696+
pub fn plugin(plugin_name: Option<&'a str>) -> Self {
697+
Self {
698+
kind: AnalyzerSuppressionKind::Plugin(plugin_name),
699+
ignore_range: None,
700+
variant: AnalyzerSuppressionVariant::Line,
701+
}
702+
}
703+
653704
#[must_use]
654705
pub fn with_ignore_range(mut self, ignore_range: TextRange) -> Self {
655706
self.ignore_range = Some(ignore_range);
@@ -670,6 +721,8 @@ pub enum AnalyzerSuppressionKind<'a> {
670721
Rule(&'a str),
671722
/// A suppression to be evaluated by a specific rule eg. `// biome-ignore lint/correctness/useExhaustiveDependencies(foo)`
672723
RuleInstance(&'a str, &'a str),
724+
/// A suppression disabling a plugin eg. `// lint/biome-ignore plugin/my-plugin`
725+
Plugin(Option<&'a str>),
673726
}
674727

675728
/// Takes a [Suppression] and returns a [AnalyzerSuppression]
@@ -682,9 +735,14 @@ pub fn to_analyzer_suppressions(
682735
piece_range.add_start(suppression.range().start()).start(),
683736
piece_range.add_start(suppression.range().end()).start(),
684737
);
685-
for (key, value) in suppression.categories {
738+
for (key, subcategory, value) in suppression.categories {
686739
if key == category!("lint") {
687740
result.push(AnalyzerSuppression::everything().with_variant(&suppression.kind));
741+
} else if key == category!("lint/plugin") {
742+
let suppression = AnalyzerSuppression::plugin(subcategory)
743+
.with_ignore_range(ignore_range)
744+
.with_variant(&suppression.kind);
745+
result.push(suppression);
688746
} else {
689747
let category = key.name();
690748
if let Some(rule) = category.strip_prefix("lint/") {

crates/biome_analyze/src/rule.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1231,6 +1231,7 @@ pub trait Rule: RuleMeta + Sized {
12311231
pub struct RuleDiagnostic {
12321232
#[category]
12331233
pub(crate) category: &'static Category,
1234+
pub(crate) subcategory: Option<String>,
12341235
#[location(span)]
12351236
pub(crate) span: Option<TextRange>,
12361237
#[message]
@@ -1309,6 +1310,7 @@ impl RuleDiagnostic {
13091310
let message = markup!({ title }).to_owned();
13101311
Self {
13111312
category,
1313+
subcategory: None,
13121314
span: span.as_span(),
13131315
message: MessageAndDescription::from(message),
13141316
tags: DiagnosticTags::empty(),
@@ -1408,6 +1410,11 @@ impl RuleDiagnostic {
14081410
pub fn advices(&self) -> &RuleAdvice {
14091411
&self.rule_advice
14101412
}
1413+
1414+
pub fn subcategory(mut self, subcategory: String) -> Self {
1415+
self.subcategory = Some(subcategory);
1416+
self
1417+
}
14111418
}
14121419

14131420
/// Code Action object returned by a single analysis rule

crates/biome_analyze/src/suppressions.rs

Lines changed: 73 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,18 @@ use biome_diagnostics::category;
77
use biome_rowan::{TextRange, TextSize};
88
use rustc_hash::{FxHashMap, FxHashSet};
99

10+
const PLUGIN_LINT_RULE_FILTER: RuleFilter<'static> = RuleFilter::Group("lint/plugin");
11+
1012
#[derive(Debug, Default)]
1113
pub struct TopLevelSuppression {
1214
/// Whether this suppression suppresses all filters
1315
pub(crate) suppress_all: bool,
1416
/// Filters for the current suppression
1517
pub(crate) filters: FxHashSet<RuleFilter<'static>>,
18+
/// Whether this suppression suppresses all plugins
19+
pub(crate) suppress_all_plugins: bool,
20+
/// Current suppressed plugins
21+
pub(crate) plugins: FxHashSet<String>,
1622
/// The range of the comment
1723
pub(crate) comment_range: TextRange,
1824

@@ -48,6 +54,7 @@ impl TopLevelSuppression {
4854
// The absence of a filter means that it's a suppression all
4955
match filter {
5056
None => self.suppress_all = true,
57+
Some(PLUGIN_LINT_RULE_FILTER) => self.insert_plugin(&suppression.kind),
5158
Some(filter) => self.insert(filter),
5259
}
5360
self.comment_range = comment_range;
@@ -59,10 +66,26 @@ impl TopLevelSuppression {
5966
self.filters.insert(filter);
6067
}
6168

69+
pub(crate) fn insert_plugin(&mut self, kind: &AnalyzerSuppressionKind) {
70+
match kind {
71+
AnalyzerSuppressionKind::Plugin(Some(name)) => {
72+
self.plugins.insert((*name).to_string());
73+
}
74+
AnalyzerSuppressionKind::Plugin(None) => {
75+
self.suppress_all_plugins = true;
76+
}
77+
_ => {}
78+
}
79+
}
80+
6281
pub(crate) fn suppressed_rule(&self, filter: &RuleKey) -> bool {
6382
self.filters.iter().any(|f| f == filter)
6483
}
6584

85+
pub(crate) fn suppressed_plugin(&self, plugin_name: &str) -> bool {
86+
self.suppress_all_plugins || self.plugins.contains(plugin_name)
87+
}
88+
6689
pub(crate) fn expand_range(&mut self, range: TextRange) {
6790
self.range.cover(range);
6891
}
@@ -89,6 +112,10 @@ pub(crate) struct LineSuppression {
89112
pub(crate) suppressed_rules: FxHashSet<RuleFilter<'static>>,
90113
/// List of all the rule instances this comment has started suppressing.
91114
pub(crate) suppressed_instances: FxHashMap<String, RuleFilter<'static>>,
115+
/// List of plugins this comment has started suppressing
116+
pub(crate) suppressed_plugins: FxHashSet<String>,
117+
/// Set to true if this comment suppress all plugins
118+
pub(crate) suppress_all_plugins: bool,
92119
/// Set to `true` when a signal matching this suppression was emitted and
93120
/// suppressed
94121
pub(crate) did_suppress_signal: bool,
@@ -139,6 +166,15 @@ impl RangeSuppressions {
139166
text_range: TextRange,
140167
already_suppressed: Option<TextRange>,
141168
) -> Result<(), AnalyzerSuppressionDiagnostic> {
169+
if let Some(PLUGIN_LINT_RULE_FILTER) = filter {
170+
return Err(AnalyzerSuppressionDiagnostic::new(
171+
category!("suppressions/incorrect"),
172+
text_range,
173+
markup!{"Found a "<Emphasis>"biome-ignore-<range>"</Emphasis>" suppression on plugin. This is not supported. See https://github.com/biomejs/biome/issues/5175"}
174+
).hint(markup!{
175+
"Remove this suppression."
176+
}.to_owned()));
177+
}
142178
if suppression.is_range_start() {
143179
if let Some(range_suppression) = self.suppressions.last_mut() {
144180
match filter {
@@ -270,6 +306,7 @@ impl<'analyzer> Suppressions<'analyzer> {
270306
fn push_line_suppression(
271307
&mut self,
272308
filter: Option<RuleFilter<'static>>,
309+
plugin_name: Option<String>,
273310
instance: Option<String>,
274311
current_range: TextRange,
275312
already_suppressed: Option<TextRange>,
@@ -283,6 +320,16 @@ impl<'analyzer> Suppressions<'analyzer> {
283320
suppression.suppress_all = true;
284321
suppression.suppressed_rules.clear();
285322
suppression.suppressed_instances.clear();
323+
suppression.suppressed_plugins.clear();
324+
}
325+
Some(PLUGIN_LINT_RULE_FILTER) => {
326+
if let Some(plugin_name) = plugin_name {
327+
suppression.suppressed_plugins.insert(plugin_name);
328+
suppression.suppress_all_plugins = false;
329+
} else {
330+
suppression.suppress_all_plugins = true;
331+
}
332+
suppression.suppress_all = false;
286333
}
287334
Some(filter) => {
288335
suppression.suppressed_rules.insert(filter);
@@ -307,6 +354,13 @@ impl<'analyzer> Suppressions<'analyzer> {
307354
None => {
308355
suppression.suppress_all = true;
309356
}
357+
Some(PLUGIN_LINT_RULE_FILTER) => {
358+
if let Some(plugin_name) = plugin_name {
359+
suppression.suppressed_plugins.insert(plugin_name);
360+
} else {
361+
suppression.suppress_all_plugins = true;
362+
}
363+
}
310364
Some(filter) => {
311365
suppression.suppressed_rules.insert(filter);
312366
if let Some(instance) = instance {
@@ -329,6 +383,7 @@ impl<'analyzer> Suppressions<'analyzer> {
329383
AnalyzerSuppressionKind::Everything => return Ok(None),
330384
AnalyzerSuppressionKind::Rule(rule) => rule,
331385
AnalyzerSuppressionKind::RuleInstance(rule, _) => rule,
386+
AnalyzerSuppressionKind::Plugin(_) => return Ok(Some(PLUGIN_LINT_RULE_FILTER)),
332387
};
333388

334389
let group_rule = rule.split_once('/');
@@ -357,11 +412,20 @@ impl<'analyzer> Suppressions<'analyzer> {
357412

358413
fn map_to_rule_instances(&self, suppression_kind: &AnalyzerSuppressionKind) -> Option<String> {
359414
match suppression_kind {
360-
AnalyzerSuppressionKind::Everything | AnalyzerSuppressionKind::Rule(_) => None,
415+
AnalyzerSuppressionKind::Everything
416+
| AnalyzerSuppressionKind::Rule(_)
417+
| AnalyzerSuppressionKind::Plugin(_) => None,
361418
AnalyzerSuppressionKind::RuleInstance(_, instances) => Some((*instances).to_string()),
362419
}
363420
}
364421

422+
fn map_to_plugin_name(&self, suppression_kind: &AnalyzerSuppressionKind) -> Option<String> {
423+
match suppression_kind {
424+
AnalyzerSuppressionKind::Plugin(Some(plugin_name)) => Some((*plugin_name).to_string()),
425+
_ => None,
426+
}
427+
}
428+
365429
pub(crate) fn push_suppression(
366430
&mut self,
367431
suppression: &AnalyzerSuppression,
@@ -370,12 +434,17 @@ impl<'analyzer> Suppressions<'analyzer> {
370434
) -> Result<(), AnalyzerSuppressionDiagnostic> {
371435
let filter = self.map_to_rule_filter(&suppression.kind, comment_range)?;
372436
let instances = self.map_to_rule_instances(&suppression.kind);
437+
let plugin_name: Option<String> = self.map_to_plugin_name(&suppression.kind);
373438
self.last_suppression = Some(suppression.variant.clone());
374439
let already_suppressed = self.already_suppressed(filter.as_ref(), &comment_range);
375440
match suppression.variant {
376-
AnalyzerSuppressionVariant::Line => {
377-
self.push_line_suppression(filter, instances, comment_range, already_suppressed)
378-
}
441+
AnalyzerSuppressionVariant::Line => self.push_line_suppression(
442+
filter,
443+
plugin_name,
444+
instances,
445+
comment_range,
446+
already_suppressed,
447+
),
379448
AnalyzerSuppressionVariant::TopLevel => self.top_level_suppression.push_suppression(
380449
suppression,
381450
filter,

crates/biome_css_formatter/src/comments.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ impl CommentStyle for CssCommentStyle {
7272
parse_suppression_comment(text)
7373
.filter_map(Result::ok)
7474
.flat_map(|suppression| suppression.categories)
75-
.any(|(key, _)| key == category!("format"))
75+
.any(|(key, ..)| key == category!("format"))
7676
}
7777

7878
fn get_comment_kind(comment: &SyntaxTriviaPieceComments<Self::Language>) -> CommentKind {

crates/biome_diagnostics_categories/src/categories.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -388,6 +388,7 @@ define_categories! {
388388
"lint/security",
389389
"lint/style",
390390
"lint/suspicious",
391+
"lint/plugin",
391392

392393
// Suppression comments
393394
"suppressions/parse",

crates/biome_graphql_formatter/src/comments.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ impl CommentStyle for GraphqlCommentStyle {
6868
parse_suppression_comment(text)
6969
.filter_map(Result::ok)
7070
.flat_map(|suppression| suppression.categories)
71-
.any(|(key, _)| key == category!("format"))
71+
.any(|(key, ..)| key == category!("format"))
7272
}
7373

7474
fn get_comment_kind(_comment: &SyntaxTriviaPieceComments<Self::Language>) -> CommentKind {

crates/biome_html_formatter/src/comments.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ impl CommentStyle for HtmlCommentStyle {
8888
parse_suppression_comment(text)
8989
.filter_map(Result::ok)
9090
.flat_map(|suppression| suppression.categories)
91-
.any(|(key, _)| key == category!("format"))
91+
.any(|(key, ..)| key == category!("format"))
9292
}
9393

9494
fn get_comment_kind(_comment: &SyntaxTriviaPieceComments<HtmlLanguage>) -> CommentKind {
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
`Object.assign($args)` where {
2+
register_diagnostic(
3+
span = $args,
4+
message = "Prefer object spread instead of `Object.assign()`"
5+
)
6+
}

0 commit comments

Comments
 (0)