Skip to content

Commit b7e2d4d

Browse files
sterliakovarendjr
andauthored
fix(biome_analyze): restore support for multiple consecutive suppression comments with rule instances. (#6844)
Co-authored-by: Arend van Beelen jr. <[email protected]>
1 parent 4cd62d8 commit b7e2d4d

File tree

8 files changed

+443
-42
lines changed

8 files changed

+443
-42
lines changed

.changeset/silent-suits-judge.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@biomejs/biome": patch
3+
---
4+
5+
Fixed [#6837](https://github.com/biomejs/biome/issues/6837): Fixed regression with multiple consecutive line suppression comments using instances (like `// biome-ignore lint/correctness/useExhaustiveDependencies(depName): reason`).

crates/biome_analyze/src/lib.rs

Lines changed: 38 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@
22

33
use biome_console::markup;
44
use biome_parser::AnyParse;
5+
use rustc_hash::FxHashSet;
56
use std::collections::{BTreeMap, BinaryHeap};
67
use std::fmt::{Debug, Display, Formatter};
78
use std::ops;
89
use std::str::FromStr;
910
use std::sync::Arc;
10-
use suppressions::LineSuppression;
1111

1212
mod analyzer_plugin;
1313
mod categories;
@@ -489,33 +489,49 @@ where
489489
// if it matches the current line index, otherwise perform a binary
490490
// search over all the previously seen suppressions to find one
491491
// with a matching range
492-
let matches_entry = |suppression: &LineSuppression| {
493-
if suppression.suppressed_categories.contains(entry.category) {
494-
return true;
495-
}
496-
if suppression.suppressed_instances.is_empty() {
497-
suppression.matches_rule(&entry.category, &entry.rule)
498-
} else {
499-
entry.instances.iter().all(|value| {
500-
suppression
501-
.suppressed_instances
502-
.iter()
503-
.any(|(v, filter)| *filter == entry.rule && v == value.as_ref())
504-
})
505-
}
506-
};
507-
508-
let suppression = self
492+
let mut is_fully_suppressed = false;
493+
// Check that instance-based comments do indeed suppress all instances
494+
// Every match is discarded from this set. Use `Option` for lazy init,
495+
// because most of the rules do not use instances.
496+
let mut instances: Option<FxHashSet<&Box<str>>> = None;
497+
for suppression in self
509498
.suppressions
510499
.overlapping_line_suppressions(&entry.text_range)
511500
.iter_mut()
512-
.find(|s| s.text_range.contains(start) && matches_entry(s));
501+
{
502+
if !suppression.text_range.contains(start) {
503+
continue;
504+
}
505+
let (is_match, is_exhaustive) =
506+
if suppression.suppressed_categories.contains(entry.category) {
507+
(true, true)
508+
} else if !suppression.matches_rule(&entry.category, &entry.rule) {
509+
(false, false)
510+
} else {
511+
match suppression.suppressed_instance.as_ref() {
512+
None => (true, true),
513+
Some(v) => {
514+
let matches_instance = instances
515+
.get_or_insert_with(|| entry.instances.iter().collect())
516+
.remove(v);
517+
(matches_instance, false)
518+
}
519+
}
520+
};
521+
if is_match {
522+
suppression.did_suppress_signal = true;
523+
is_fully_suppressed =
524+
is_exhaustive || instances.as_ref().is_some_and(|v| v.is_empty());
525+
if is_fully_suppressed {
526+
break;
527+
}
528+
}
529+
}
513530

514531
// If the signal is being suppressed, mark the line suppression as
515532
// hit, otherwise emit the signal
516-
if let Some(suppression) = suppression {
517-
suppression.did_suppress_signal = true;
518-
} else if range_match(self.range, entry.text_range) {
533+
if !is_fully_suppressed && range_match(self.range, entry.text_range) {
534+
// TODO: would be nice to remove suppressed instances, if any, before emitting
519535
(self.emit_signal)(&*entry.signal)?;
520536
}
521537

crates/biome_analyze/src/suppressions.rs

Lines changed: 15 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -130,19 +130,21 @@ pub(crate) struct LineSuppression {
130130
pub(crate) text_range: TextRange,
131131
/// All rules from groups included here are ignored.
132132
pub(crate) suppressed_categories: RuleCategories,
133-
/// List of all the rules this comment has started suppressing (must be
134-
/// removed from the suppressed set on expiration)
135-
pub(crate) suppressed_rules: FxHashMap<RuleCategory, FxHashSet<RuleFilter<'static>>>,
136-
/// List of all the rule instances this comment has started suppressing.
137-
pub(crate) suppressed_instances: FxHashMap<String, RuleFilter<'static>>,
133+
/// The rule this comment should be suppressing.
134+
pub(crate) suppressed_rule: Option<(RuleCategory, RuleFilter<'static>)>,
135+
/// An instance this comment should be suppressing.
136+
///
137+
/// For example, this is `foo` in `// biome-ignore lint/correctness/xxx(foo): ...`
138+
pub(crate) suppressed_instance: Option<Box<str>>,
138139
/// List of plugins this comment has started suppressing
139140
pub(crate) suppressed_plugins: FxHashSet<String>,
140141
/// Set to true if this comment suppress all plugins
141142
pub(crate) suppress_all_plugins: bool,
142143
/// Set to `true` when a signal matching this suppression was emitted and
143144
/// suppressed
144145
pub(crate) did_suppress_signal: bool,
145-
/// Set to `true` when this line suppresses a signal that was already suppressed by another entity e.g. top-level suppression
146+
/// Points to the previous suppression if this line suppresses a signal
147+
/// that was already suppressed by another entity (e.g. top-level suppression)
146148
pub(crate) already_suppressed: Option<TextRange>,
147149
}
148150

@@ -153,8 +155,8 @@ impl Default for LineSuppression {
153155
comment_span: Default::default(),
154156
text_range: Default::default(),
155157
suppressed_categories: RuleCategories::empty(),
156-
suppressed_rules: Default::default(),
157-
suppressed_instances: Default::default(),
158+
suppressed_rule: Default::default(),
159+
suppressed_instance: Default::default(),
158160
suppressed_plugins: Default::default(),
159161
suppress_all_plugins: false,
160162
did_suppress_signal: false,
@@ -165,9 +167,9 @@ impl Default for LineSuppression {
165167

166168
impl LineSuppression {
167169
pub(crate) fn matches_rule(&self, rule_category: &RuleCategory, filter: &RuleKey) -> bool {
168-
self.suppressed_rules
169-
.get(rule_category)
170-
.is_some_and(|filters| filters.iter().any(|f| f == filter))
170+
self.suppressed_rule
171+
.as_ref()
172+
.is_some_and(|(c, f)| c == rule_category && f == filter)
171173
}
172174
}
173175

@@ -431,14 +433,8 @@ impl<'analyzer> Suppressions<'analyzer> {
431433
}
432434
}
433435
Some(filter) => {
434-
let filters = suppression
435-
.suppressed_rules
436-
.entry(rule_category)
437-
.or_default();
438-
filters.insert(filter);
439-
if let Some(instance) = instance {
440-
suppression.suppressed_instances.insert(instance, filter);
441-
}
436+
suppression.suppressed_rule = Some((rule_category, filter));
437+
suppression.suppressed_instance = instance.map(String::into_boxed_str);
442438
}
443439
}
444440
self.line_suppressions.push(suppression);

crates/biome_js_analyze/src/lint/correctness/use_exhaustive_dependencies.rs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,21 @@ declare_lint_rule! {
165165
/// ```
166166
///
167167
/// If you wish to ignore multiple dependencies, you can add multiple
168-
/// comments and add a reason for each.
168+
/// comments and add a reason for each:
169+
///
170+
/// ```js
171+
/// import { useEffect } from "react";
172+
///
173+
/// function component() {
174+
/// let a = 1;
175+
/// let b = 1;
176+
/// // biome-ignore lint/correctness/useExhaustiveDependencies(a): suppress dependency a
177+
/// // biome-ignore lint/correctness/useExhaustiveDependencies(b): suppress dependency b
178+
/// useEffect(() => {
179+
/// console.log(a, b);
180+
/// }, []);
181+
/// }
182+
/// ```
169183
///
170184
/// ## Options
171185
///
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { useEffect } from "react";
2+
3+
function SingleInstanceSuppressionWrong({a, b}) {
4+
// biome-ignore lint/correctness/useExhaustiveDependencies(b): test
5+
useEffect(() => {}, [a]);
6+
}
7+
8+
function SingleInstanceSuppressionDuplicate({a, b}) {
9+
// biome-ignore lint/correctness/useExhaustiveDependencies(b): test
10+
// biome-ignore lint/correctness/useExhaustiveDependencies(b): test
11+
useEffect(() => {}, [b]);
12+
}
13+
14+
function SingleInstanceSuppressionNotEnough({a, b}) {
15+
// biome-ignore lint/correctness/useExhaustiveDependencies(a): test
16+
useEffect(() => {}, [a, b]);
17+
}
18+
19+
function SingleInstanceSuppressionNotEnough2({a, b}) {
20+
// biome-ignore lint/correctness/useExhaustiveDependencies(b): test
21+
useEffect(() => {}, [a, b]);
22+
}
23+
24+
function MultiInstanceSuppressionSomeWrong({a, b, c}) {
25+
// biome-ignore lint/correctness/useExhaustiveDependencies(a): test
26+
// biome-ignore lint/correctness/useExhaustiveDependencies(c): test
27+
useEffect(() => {}, [a, b]);
28+
}
29+
30+
function MultiInstanceSuppressionAllWrong({a, b, c, d}) {
31+
// biome-ignore lint/correctness/useExhaustiveDependencies(c): test
32+
// biome-ignore lint/correctness/useExhaustiveDependencies(d): test
33+
useEffect(() => {}, [a, b]);
34+
}

0 commit comments

Comments
 (0)