diff --git a/crates/biome_cli/src/commands/check.rs b/crates/biome_cli/src/commands/check.rs index 616f9013e7f6..74ba7472683f 100644 --- a/crates/biome_cli/src/commands/check.rs +++ b/crates/biome_cli/src/commands/check.rs @@ -141,6 +141,7 @@ impl CommandRunner for CheckCommandPayload { apply: self.apply, apply_unsafe: self.apply_unsafe, write: self.write, + suppress: false, fix: self.fix, unsafe_: self.unsafe_, }, diff --git a/crates/biome_cli/src/commands/lint.rs b/crates/biome_cli/src/commands/lint.rs index 065674b2e4c8..32957c11e542 100644 --- a/crates/biome_cli/src/commands/lint.rs +++ b/crates/biome_cli/src/commands/lint.rs @@ -24,6 +24,7 @@ pub(crate) struct LintCommandPayload { pub(crate) write: bool, pub(crate) fix: bool, pub(crate) unsafe_: bool, + pub(crate) suppress: bool, pub(crate) linter_configuration: Option, pub(crate) vcs_configuration: Option, pub(crate) files_configuration: Option, @@ -136,6 +137,7 @@ impl CommandRunner for LintCommandPayload { write: self.write, fix: self.fix, unsafe_: self.unsafe_, + suppress: self.suppress, }, console, )?; @@ -145,6 +147,7 @@ impl CommandRunner for LintCommandPayload { only: self.only.clone(), skip: self.skip.clone(), vcs_targeted: (self.staged, self.changed).into(), + suppress: self.suppress, }) .set_report(cli_options)) } diff --git a/crates/biome_cli/src/commands/migrate.rs b/crates/biome_cli/src/commands/migrate.rs index ed29e23eb737..85f3457141ea 100644 --- a/crates/biome_cli/src/commands/migrate.rs +++ b/crates/biome_cli/src/commands/migrate.rs @@ -84,6 +84,7 @@ impl CommandRunner for MigrateCommandPayload { write: self.write, fix: self.fix, unsafe_: false, + suppress: false, }) } diff --git a/crates/biome_cli/src/commands/mod.rs b/crates/biome_cli/src/commands/mod.rs index 0101d9545236..bf9aa02dec16 100644 --- a/crates/biome_cli/src/commands/mod.rs +++ b/crates/biome_cli/src/commands/mod.rs @@ -187,6 +187,10 @@ pub enum BiomeCommand { #[bpaf(long("write"), switch)] write: bool, + /// Fix diagnostics with suppression comments if the language supports it. + #[bpaf(long("suppress"))] + suppress: bool, + /// Allow to do unsafe fixes, should be used with `--write` or `--fix` #[bpaf(long("unsafe"), switch)] unsafe_: bool, @@ -703,6 +707,7 @@ pub(crate) struct FixFileModeOptions { apply: bool, apply_unsafe: bool, write: bool, + suppress: bool, fix: bool, unsafe_: bool, } @@ -719,6 +724,7 @@ pub(crate) fn determine_fix_file_mode( apply_unsafe, write, fix, + suppress, unsafe_, } = options; @@ -743,6 +749,8 @@ pub(crate) fn determine_fix_file_mode( Ok(Some(FixFileMode::SafeAndUnsafeFixes)) } else if safe_fixes { Ok(Some(FixFileMode::SafeFixes)) + } else if suppress { + Ok(Some(FixFileMode::ApplySuppressions)) } else { Ok(None) } @@ -754,6 +762,7 @@ fn check_fix_incompatible_arguments(options: FixFileModeOptions) -> Result<(), C apply, apply_unsafe, write, + suppress, fix, unsafe_, } = options; @@ -779,6 +788,13 @@ fn check_fix_incompatible_arguments(options: FixFileModeOptions) -> Result<(), C )); } else if write && fix { return Err(CliDiagnostic::incompatible_arguments("--write", "--fix")); + } else if suppress && write { + return Err(CliDiagnostic::incompatible_arguments( + "--suppress", + "--write", + )); + } else if suppress && fix { + return Err(CliDiagnostic::incompatible_arguments("--suppress", "--fix")); } Ok(()) } @@ -967,19 +983,20 @@ mod tests { #[test] fn incompatible_arguments() { - for (apply, apply_unsafe, write, fix, unsafe_) in [ - (true, true, false, false, false), // --apply --apply-unsafe - (true, false, true, false, false), // --apply --write - (true, false, false, true, false), // --apply --fix - (false, true, false, false, true), // --apply-unsafe --unsafe - (false, true, true, false, false), // --apply-unsafe --write - (false, true, false, true, false), // --apply-unsafe --fix - (false, false, true, true, false), // --write --fix + for (apply, apply_unsafe, write, suppress, fix, unsafe_) in [ + (true, true, false, false, false, false), // --apply --apply-unsafe + (true, false, true, false, false, false), // --apply --write + (true, false, false, false, true, false), // --apply --fix + (false, true, false, false, false, true), // --apply-unsafe --unsafe + (false, true, true, false, false, false), // --apply-unsafe --write + (false, true, false, false, true, false), // --apply-unsafe --fix + (false, false, true, false, true, false), // --write --fix ] { assert!(check_fix_incompatible_arguments(FixFileModeOptions { apply, apply_unsafe, write, + suppress, fix, unsafe_ }) @@ -991,10 +1008,10 @@ mod tests { fn safe_fixes() { let mut console = BufferConsole::default(); - for (apply, apply_unsafe, write, fix, unsafe_) in [ - (true, false, false, false, false), // --apply - (false, false, true, false, false), // --write - (false, false, false, true, false), // --fix + for (apply, apply_unsafe, write, suppress, fix, unsafe_) in [ + (true, false, false, false, false, false), // --apply + (false, false, true, false, false, false), // --write + (false, false, false, false, true, false), // --fix ] { assert_eq!( determine_fix_file_mode( @@ -1002,6 +1019,7 @@ mod tests { apply, apply_unsafe, write, + suppress, fix, unsafe_ }, @@ -1017,10 +1035,10 @@ mod tests { fn safe_and_unsafe_fixes() { let mut console = BufferConsole::default(); - for (apply, apply_unsafe, write, fix, unsafe_) in [ - (false, true, false, false, false), // --apply-unsafe - (false, false, true, false, true), // --write --unsafe - (false, false, false, true, true), // --fix --unsafe + for (apply, apply_unsafe, write, suppress, fix, unsafe_) in [ + (false, true, false, false, false, false), // --apply-unsafe + (false, false, true, false, false, true), // --write --unsafe + (false, false, false, false, true, true), // --fix --unsafe ] { assert_eq!( determine_fix_file_mode( @@ -1028,6 +1046,7 @@ mod tests { apply, apply_unsafe, write, + suppress, fix, unsafe_ }, @@ -1043,13 +1062,15 @@ mod tests { fn no_fix() { let mut console = BufferConsole::default(); - let (apply, apply_unsafe, write, fix, unsafe_) = (false, false, false, false, false); + let (apply, apply_unsafe, write, suppress, fix, unsafe_) = + (false, false, false, false, false, false); assert_eq!( determine_fix_file_mode( FixFileModeOptions { apply, apply_unsafe, write, + suppress, fix, unsafe_ }, diff --git a/crates/biome_cli/src/execute/mod.rs b/crates/biome_cli/src/execute/mod.rs index 3cd4a8eb97a9..1762c920b1cb 100644 --- a/crates/biome_cli/src/execute/mod.rs +++ b/crates/biome_cli/src/execute/mod.rs @@ -157,6 +157,8 @@ pub enum TraversalMode { skip: Vec, /// A flag to know vcs integrated options such as `--staged` or `--changed` are enabled vcs_targeted: VcsTargeted, + /// Supress existing diagnostics with a `// biome-ignore` comment + suppress: bool, }, /// This mode is enabled when running the command `biome ci` CI { diff --git a/crates/biome_cli/src/lib.rs b/crates/biome_cli/src/lib.rs index ca7fff6a4ab6..fab1c79db545 100644 --- a/crates/biome_cli/src/lib.rs +++ b/crates/biome_cli/src/lib.rs @@ -130,6 +130,7 @@ impl<'app> CliSession<'app> { apply, apply_unsafe, write, + suppress, fix, unsafe_, cli_options, @@ -154,6 +155,7 @@ impl<'app> CliSession<'app> { apply_unsafe, apply, write, + suppress, fix, unsafe_, linter_configuration, diff --git a/crates/biome_cli/tests/cases/mod.rs b/crates/biome_cli/tests/cases/mod.rs index dfb7f5de94ce..a54d4f7a6b90 100644 --- a/crates/biome_cli/tests/cases/mod.rs +++ b/crates/biome_cli/tests/cases/mod.rs @@ -22,4 +22,5 @@ mod reporter_github; mod reporter_gitlab; mod reporter_junit; mod reporter_summary; +mod suppressions; mod unknown_files; diff --git a/crates/biome_cli/tests/cases/suppressions.rs b/crates/biome_cli/tests/cases/suppressions.rs new file mode 100644 index 000000000000..1c861b0468b0 --- /dev/null +++ b/crates/biome_cli/tests/cases/suppressions.rs @@ -0,0 +1,233 @@ +use bpaf::Args; +use std::path::Path; + +use crate::snap_test::SnapshotPayload; +use crate::{assert_cli_snapshot, run_cli, FORMATTED}; +use biome_console::BufferConsole; +use biome_fs::{FileSystemExt, MemoryFileSystem}; +use biome_service::DynRef; + +const SUPPRESS_BEFORE: &str = "(1 >= -0)"; +const SUPPRESS_AFTER: &str = + "// biome-ignore lint/suspicious/noCompareNegZero: \n(1 >= -0)"; + +#[test] +fn ok() { + let mut fs = MemoryFileSystem::default(); + let mut console = BufferConsole::default(); + + let file_path = Path::new("check.js"); + fs.insert(file_path.into(), FORMATTED.as_bytes()); + + let result = run_cli( + DynRef::Borrowed(&mut fs), + &mut console, + Args::from( + [ + ("lint"), + ("--suppress"), + file_path.as_os_str().to_str().unwrap(), + ] + .as_slice(), + ), + ); + + assert!(result.is_ok(), "run_cli returned {result:?}"); +} + +#[test] +fn err_when_both_write_and_suppress_are_passed() { + let mut fs = MemoryFileSystem::new_read_only(); + let mut console = BufferConsole::default(); + + let file_path = Path::new("check.js"); + fs.insert(file_path.into(), FORMATTED.as_bytes()); + + let result = run_cli( + DynRef::Borrowed(&mut fs), + &mut console, + Args::from( + [ + ("lint"), + ("--write"), + ("--suppress"), + file_path.as_os_str().to_str().unwrap(), + ] + .as_slice(), + ), + ); + + assert!(result.is_err(), "run_cli returned {result:?}"); + assert_cli_snapshot(SnapshotPayload::new( + module_path!(), + "err_when_both_write_and_suppress_are_passed", + fs, + console, + result, + )); +} + +#[test] +fn suppress_ok() { + let mut fs = MemoryFileSystem::default(); + let mut console = BufferConsole::default(); + + let file_path = Path::new("fix.js"); + fs.insert(file_path.into(), SUPPRESS_BEFORE.as_bytes()); + + let result = run_cli( + DynRef::Borrowed(&mut fs), + &mut console, + Args::from( + [ + ("lint"), + ("--suppress"), + file_path.as_os_str().to_str().unwrap(), + ] + .as_slice(), + ), + ); + + assert!(result.is_ok(), "run_cli returned {result:?}"); + + let mut buffer = String::new(); + fs.open(file_path) + .unwrap() + .read_to_string(&mut buffer) + .unwrap(); + + assert_eq!(buffer, SUPPRESS_AFTER); + + assert_cli_snapshot(SnapshotPayload::new( + module_path!(), + "suppress_ok", + fs, + console, + result, + )); +} + +#[test] +fn suppress_multiple_ok() { + let mut fs = MemoryFileSystem::default(); + let mut console = BufferConsole::default(); + + let file_path = Path::new("fix.js"); + fs.insert( + file_path.into(), + [SUPPRESS_BEFORE, SUPPRESS_BEFORE].join("\n").as_bytes(), + ); + + let result = run_cli( + DynRef::Borrowed(&mut fs), + &mut console, + Args::from( + [ + ("lint"), + ("--suppress"), + file_path.as_os_str().to_str().unwrap(), + ] + .as_slice(), + ), + ); + + assert!(result.is_ok(), "run_cli returned {result:?}"); + + let mut buffer = String::new(); + fs.open(file_path) + .unwrap() + .read_to_string(&mut buffer) + .unwrap(); + + assert_eq!(buffer, [SUPPRESS_AFTER, SUPPRESS_AFTER].join("\n")); + + assert_cli_snapshot(SnapshotPayload::new( + module_path!(), + "suppress_multiple_ok", + fs, + console, + result, + )); +} + +#[test] +fn suppress_only_ok() { + let mut fs = MemoryFileSystem::default(); + let mut console = BufferConsole::default(); + + let file_path = Path::new("fix.js"); + fs.insert(file_path.into(), SUPPRESS_BEFORE.as_bytes()); + + let result = run_cli( + DynRef::Borrowed(&mut fs), + &mut console, + Args::from( + [ + ("lint"), + ("--suppress"), + ("--only=lint/suspicious/noCompareNegZero"), + file_path.as_os_str().to_str().unwrap(), + ] + .as_slice(), + ), + ); + + assert!(result.is_ok(), "run_cli returned {result:?}"); + + let mut buffer = String::new(); + fs.open(file_path) + .unwrap() + .read_to_string(&mut buffer) + .unwrap(); + + assert_eq!(buffer, SUPPRESS_AFTER); + + assert_cli_snapshot(SnapshotPayload::new( + module_path!(), + "suppress_only_ok", + fs, + console, + result, + )); +} + +#[test] +fn suppress_skip_ok() { + let mut fs = MemoryFileSystem::default(); + let mut console = BufferConsole::default(); + + let file_path = Path::new("fix.js"); + fs.insert(file_path.into(), SUPPRESS_BEFORE.as_bytes()); + + let result = run_cli( + DynRef::Borrowed(&mut fs), + &mut console, + Args::from( + [ + ("lint"), + ("--suppress"), + ("--skip=lint/suspicious/noCompareNegZero"), + file_path.as_os_str().to_str().unwrap(), + ] + .as_slice(), + ), + ); + + assert!(result.is_ok(), "run_cli returned {result:?}"); + + let mut buffer = String::new(); + fs.open(file_path) + .unwrap() + .read_to_string(&mut buffer) + .unwrap(); + + assert_eq!(buffer, SUPPRESS_BEFORE); + + assert_cli_snapshot(SnapshotPayload::new( + module_path!(), + "suppress_skip_ok", + fs, + console, + result, + )); +} diff --git a/crates/biome_cli/tests/snapshots/main_cases_suppressions/err_when_both_write_and_suppress_are_passed.snap b/crates/biome_cli/tests/snapshots/main_cases_suppressions/err_when_both_write_and_suppress_are_passed.snap new file mode 100644 index 000000000000..ac2d014c70d4 --- /dev/null +++ b/crates/biome_cli/tests/snapshots/main_cases_suppressions/err_when_both_write_and_suppress_are_passed.snap @@ -0,0 +1,22 @@ +--- +source: crates/biome_cli/tests/snap_test.rs +assertion_line: 423 +expression: content +--- +## `check.js` + +```js +statement(); + +``` + +# Termination Message + +```block +flags/invalid ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Incompatible arguments --suppress and --write + + + +``` diff --git a/crates/biome_cli/tests/snapshots/main_cases_suppressions/suppress_multiple_ok.snap b/crates/biome_cli/tests/snapshots/main_cases_suppressions/suppress_multiple_ok.snap new file mode 100644 index 000000000000..9228d691158a --- /dev/null +++ b/crates/biome_cli/tests/snapshots/main_cases_suppressions/suppress_multiple_ok.snap @@ -0,0 +1,19 @@ +--- +source: crates/biome_cli/tests/snap_test.rs +assertion_line: 423 +expression: content +--- +## `fix.js` + +```js +// biome-ignore lint/suspicious/noCompareNegZero: +(1 >= -0) +// biome-ignore lint/suspicious/noCompareNegZero: +(1 >= -0) +``` + +# Emitted Messages + +```block +Checked 1 file in