Skip to content

Commit 2649ac6

Browse files
sterliakovarendjr
andauthored
feat(biome_analyzer): support shebang together with // biome-ignore-all file-level suppressions (#6712)
Co-authored-by: Arend van Beelen jr. <[email protected]>
1 parent 90aeead commit 2649ac6

6 files changed

Lines changed: 130 additions & 23 deletions

File tree

.changeset/afraid-coats-post.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 [#6595](https://github.com/biomejs/biome/issues/6595): Biome now supports `// biome-ignore-all` file-level suppressions in files that start with a shebang (`#!`).

crates/biome_analyze/src/lib.rs

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,8 @@ pub use crate::syntax::{Ast, SyntaxVisitor};
5757
pub use crate::visitor::{NodeVisitor, Visitor, VisitorContext, VisitorFinishContext};
5858
use biome_diagnostics::{Diagnostic, DiagnosticExt, category};
5959
use biome_rowan::{
60-
AstNode, BatchMutation, Direction, Language, SyntaxToken, TextRange, TextSize, TokenAtOffset,
61-
TriviaPieceKind,
60+
AstNode, BatchMutation, Direction, Language, SyntaxKind as _, SyntaxToken, TextRange, TextSize,
61+
TokenAtOffset, TriviaPieceKind,
6262
};
6363
use biome_suppression::{Suppression, SuppressionKind};
6464
pub use suppression_action::{ApplySuppression, SuppressionAction};
@@ -172,6 +172,7 @@ where
172172
options: ctx.options,
173173
suppressions: &mut suppressions,
174174
categories,
175+
deny_top_level_suppressions: false,
175176
};
176177

177178
// The first phase being run will inspect the tokens and parse the
@@ -330,6 +331,8 @@ struct PhaseRunner<'analyzer, 'phase, L: Language, Matcher, Break, Diag> {
330331
suppressions: &'phase mut Suppressions<'analyzer>,
331332
/// The current categories
332333
categories: RuleCategories,
334+
/// Whether we have already encountered a token that can't precede top level suppressions
335+
deny_top_level_suppressions: bool,
333336
}
334337

335338
impl<L, Matcher, Break, Diag> PhaseRunner<'_, '_, L, Matcher, Break, Diag>
@@ -409,7 +412,7 @@ where
409412
/// whose position is less than the end of the token within the file
410413
fn handle_token(&mut self, token: SyntaxToken<L>) -> ControlFlow<Break> {
411414
// Process the content of the token for comments and newline
412-
for (index, piece) in token.leading_trivia().pieces().enumerate() {
415+
for piece in token.leading_trivia().pieces() {
413416
if matches!(
414417
piece.kind(),
415418
TriviaPieceKind::Newline
@@ -420,13 +423,16 @@ where
420423
}
421424

422425
if let Some(comment) = piece.as_comments() {
423-
self.handle_comment(&token, true, index, comment.text(), piece.text_range())?;
426+
self.handle_comment(comment.text(), piece.text_range())?;
424427
}
425428
}
426429

427430
self.bump_line_index(token.text_trimmed(), token.text_trimmed_range());
431+
if !self.deny_top_level_suppressions {
432+
self.deny_top_level_suppressions = !token.kind().is_allowed_before_suppressions();
433+
}
428434

429-
for (index, piece) in token.trailing_trivia().pieces().enumerate() {
435+
for piece in token.trailing_trivia().pieces() {
430436
if matches!(
431437
piece.kind(),
432438
TriviaPieceKind::Newline
@@ -437,7 +443,7 @@ where
437443
}
438444

439445
if let Some(comment) = piece.as_comments() {
440-
self.handle_comment(&token, false, index, comment.text(), piece.text_range())?;
446+
self.handle_comment(comment.text(), piece.text_range())?;
441447
}
442448
}
443449

@@ -522,14 +528,7 @@ where
522528

523529
/// Parse the text content of a comment trivia piece for suppression
524530
/// comments, and create line suppression entries accordingly
525-
fn handle_comment(
526-
&mut self,
527-
token: &SyntaxToken<L>,
528-
_is_leading: bool,
529-
_index: usize,
530-
text: &str,
531-
range: TextRange,
532-
) -> ControlFlow<Break> {
531+
fn handle_comment(&mut self, text: &str, range: TextRange) -> ControlFlow<Break> {
533532
for result in (self.parse_suppression_comment)(text, range) {
534533
let suppression: AnalyzerSuppression = match result {
535534
Ok(kind) => kind,
@@ -570,10 +569,11 @@ where
570569
continue;
571570
}
572571

573-
if let Err(diagnostic) =
574-
self.suppressions
575-
.push_suppression(&suppression, range, token.text_range())
576-
{
572+
if let Err(diagnostic) = self.suppressions.push_suppression(
573+
&suppression,
574+
range,
575+
!self.deny_top_level_suppressions,
576+
) {
577577
let signal = DiagnosticSignal::new(|| diagnostic.clone());
578578
(self.emit_signal)(&signal)?;
579579
continue;

crates/biome_analyze/src/suppressions.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use crate::{
77
};
88
use biome_console::markup;
99
use biome_diagnostics::category;
10-
use biome_rowan::{TextRange, TextSize};
10+
use biome_rowan::TextRange;
1111
use rustc_hash::{FxHashMap, FxHashSet};
1212

1313
const PLUGIN_LINT_RULE_FILTER: RuleFilter<'static> = RuleFilter::Group("lint/plugin");
@@ -48,10 +48,10 @@ impl TopLevelSuppression {
4848
&mut self,
4949
suppression: &AnalyzerSuppression,
5050
filter: Option<RuleFilter<'static>>,
51-
token_range: TextRange,
5251
comment_range: TextRange,
52+
is_leading_in_file: bool,
5353
) -> Result<(), AnalyzerSuppressionDiagnostic> {
54-
if suppression.is_top_level() && token_range.start() > TextSize::from(0) {
54+
if suppression.is_top_level() && !is_leading_in_file {
5555
let mut diagnostic = AnalyzerSuppressionDiagnostic::new(
5656
category!("suppressions/incorrect"),
5757
comment_range,
@@ -512,7 +512,7 @@ impl<'analyzer> Suppressions<'analyzer> {
512512
&mut self,
513513
suppression: &AnalyzerSuppression,
514514
comment_range: TextRange,
515-
token_range_not_trimmed: TextRange,
515+
is_leading_in_file: bool,
516516
) -> Result<(), AnalyzerSuppressionDiagnostic> {
517517
let filter = self.map_to_rule_filter(suppression, comment_range)?;
518518
let instances = self.map_to_rule_instances(&suppression.kind);
@@ -531,8 +531,8 @@ impl<'analyzer> Suppressions<'analyzer> {
531531
AnalyzerSuppressionVariant::TopLevel => self.top_level_suppression.push_suppression(
532532
suppression,
533533
filter,
534-
token_range_not_trimmed,
535534
comment_range,
535+
is_leading_in_file,
536536
),
537537
AnalyzerSuppressionVariant::RangeStart | AnalyzerSuppressionVariant::RangeEnd => self
538538
.range_suppressions

crates/biome_js_analyze/src/suppressions.tests.rs

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -539,6 +539,99 @@ let bar = 33;
539539
);
540540
}
541541

542+
#[test]
543+
fn top_level_suppression_with_shebang() {
544+
const SOURCE: &str = "#!/usr/bin/env bun
545+
/**
546+
* biome-ignore-all lint/style/useConst: reason
547+
*/
548+
549+
let foo = 2;
550+
let bar = 33;
551+
";
552+
553+
let parsed = parse(
554+
SOURCE,
555+
JsFileSource::js_module(),
556+
JsParserOptions::default(),
557+
);
558+
559+
let filter = AnalysisFilter {
560+
categories: RuleCategoriesBuilder::default().with_lint().build(),
561+
enabled_rules: Some(&[RuleFilter::Rule("style", "useConst")]),
562+
..AnalysisFilter::default()
563+
};
564+
565+
let options = AnalyzerOptions::default();
566+
crate::analyze(
567+
&parsed.tree(),
568+
filter,
569+
&options,
570+
&[],
571+
Default::default(),
572+
|signal| {
573+
if let Some(diag) = signal.diagnostic() {
574+
let error = diag
575+
.with_file_path("dummyFile")
576+
.with_file_source_code(SOURCE);
577+
let text = print_diagnostic_to_string(&error);
578+
eprintln!("{text}");
579+
panic!("Unexpected diagnostic");
580+
}
581+
582+
ControlFlow::<Never>::Continue(())
583+
},
584+
);
585+
}
586+
587+
#[test]
588+
fn top_level_suppression_with_shebang_and_comment() {
589+
const SOURCE: &str = "#!/usr/bin/env bun
590+
591+
/* Arbitrary comment here
592+
*/
593+
/**
594+
* biome-ignore-all lint/style/useConst: reason
595+
*/
596+
597+
let foo = 2;
598+
let bar = 33;
599+
";
600+
601+
let parsed = parse(
602+
SOURCE,
603+
JsFileSource::js_module(),
604+
JsParserOptions::default(),
605+
);
606+
607+
let filter = AnalysisFilter {
608+
categories: RuleCategoriesBuilder::default().with_lint().build(),
609+
enabled_rules: Some(&[RuleFilter::Rule("style", "useConst")]),
610+
..AnalysisFilter::default()
611+
};
612+
613+
let options = AnalyzerOptions::default();
614+
crate::analyze(
615+
&parsed.tree(),
616+
filter,
617+
&options,
618+
&[],
619+
Default::default(),
620+
|signal| {
621+
if let Some(diag) = signal.diagnostic() {
622+
let error = diag
623+
.with_file_path("dummyFile")
624+
.with_file_source_code(SOURCE);
625+
let text = print_diagnostic_to_string(&error);
626+
eprintln!("{text}");
627+
panic!("Unexpected diagnostic");
628+
}
629+
630+
ControlFlow::<Never>::Continue(())
631+
},
632+
);
633+
}
634+
542635
#[test]
543636
fn suppression_range_should_report_after_end() {
544637
const SOURCE: &str = "

crates/biome_js_syntax/src/lib.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,10 @@ impl biome_rowan::SyntaxKind for JsSyntaxKind {
159159
fn to_string(&self) -> Option<&'static str> {
160160
Self::to_string(self)
161161
}
162+
163+
fn is_allowed_before_suppressions(&self) -> bool {
164+
matches!(self, Self::JS_SHEBANG)
165+
}
162166
}
163167

164168
impl TryFrom<JsSyntaxKind> for TriviaPieceKind {

crates/biome_rowan/src/syntax.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,11 @@ pub trait SyntaxKind: fmt::Debug + PartialEq + Copy {
5050
/// Returns a string for keywords, punctuation tokens, and the `EOL` token,
5151
/// or `None` otherwise.
5252
fn to_string(&self) -> Option<&'static str>;
53+
54+
/// Returns `true` if this kind is allowed to precede file suppression comments,
55+
fn is_allowed_before_suppressions(&self) -> bool {
56+
false
57+
}
5358
}
5459

5560
pub trait Language: Sized + Clone + Copy + fmt::Debug + Eq + Ord + std::hash::Hash {

0 commit comments

Comments
 (0)