diff --git a/.changeset/busy-olives-hang.md b/.changeset/busy-olives-hang.md new file mode 100644 index 000000000000..a28532d1f0b6 --- /dev/null +++ b/.changeset/busy-olives-hang.md @@ -0,0 +1,5 @@ +--- +"@biomejs/biome": patch +--- + +Fixed #7772: `--fix` now respects `--diagnostic-level` diff --git a/crates/biome_cli/src/commands/check.rs b/crates/biome_cli/src/commands/check.rs index 56e4e0c56530..573a8ef9d56d 100644 --- a/crates/biome_cli/src/commands/check.rs +++ b/crates/biome_cli/src/commands/check.rs @@ -8,6 +8,7 @@ use biome_configuration::formatter::FormatterEnabled; use biome_configuration::{Configuration, FormatterConfiguration, LinterConfiguration}; use biome_console::Console; use biome_deserialize::Merge; +use biome_diagnostics::Severity; use biome_fs::FileSystem; use biome_service::{Workspace, WorkspaceError, configuration::LoadedConfiguration}; use std::ffi::OsString; @@ -26,6 +27,7 @@ pub(crate) struct CheckCommandPayload { pub(crate) staged: bool, pub(crate) changed: bool, pub(crate) since: Option, + pub(crate) fix_level: Severity, } impl LoadEditorConfig for CheckCommandPayload { @@ -144,6 +146,7 @@ impl CommandRunner for CheckCommandPayload { vcs_targeted: (self.staged, self.changed).into(), enforce_assist: self.enforce_assist, skip_parse_errors: cli_options.skip_parse_errors, + fix_level: self.fix_level, }) .set_report(cli_options)) } diff --git a/crates/biome_cli/src/commands/lint.rs b/crates/biome_cli/src/commands/lint.rs index df20df8bd9da..0c63139ab4da 100644 --- a/crates/biome_cli/src/commands/lint.rs +++ b/crates/biome_cli/src/commands/lint.rs @@ -11,6 +11,7 @@ use biome_configuration::vcs::VcsConfiguration; use biome_configuration::{Configuration, FilesConfiguration, LinterConfiguration}; use biome_console::Console; use biome_deserialize::Merge; +use biome_diagnostics::Severity; use biome_fs::FileSystem; use biome_service::configuration::LoadedConfiguration; use biome_service::{Workspace, WorkspaceError}; @@ -36,6 +37,7 @@ pub(crate) struct LintCommandPayload { pub(crate) json_linter: Option, pub(crate) css_linter: Option, pub(crate) graphql_linter: Option, + pub(crate) fix_level: Severity, } impl CommandRunner for LintCommandPayload { @@ -143,6 +145,7 @@ impl CommandRunner for LintCommandPayload { suppress: self.suppress, suppression_reason: self.suppression_reason.clone(), skip_parse_errors: cli_options.skip_parse_errors, + fix_level: self.fix_level, }) .set_report(cli_options)) } diff --git a/crates/biome_cli/src/commands/mod.rs b/crates/biome_cli/src/commands/mod.rs index 5c41811dc598..930144f4111e 100644 --- a/crates/biome_cli/src/commands/mod.rs +++ b/crates/biome_cli/src/commands/mod.rs @@ -126,6 +126,15 @@ pub enum BiomeCommand { #[bpaf(long("fix"), switch, hide_usage)] fix: bool, + /// The level of diagnostics to fix. In order, from the lowest to the most important: info, warn, error. Passing `--fix-level=error` will cause Biome only to fix error-level diagnostics contain only errors. + #[bpaf( + long("fix-level"), + argument("info|warn|error"), + fallback(Severity::default()), + display_fallback + )] + fix_level: Severity, + /// Allow enabling or disabling the formatter check. #[bpaf( long("formatter-enabled"), @@ -273,6 +282,15 @@ pub enum BiomeCommand { /// Single file, single path or list of paths #[bpaf(positional("PATH"), many)] paths: Vec, + + /// The level of diagnostics to fix. In order, from the lowest to the most important: info, warn, error. Passing `--fix-level=error` will cause Biome only to fix error-level diagnostics contain only errors. + #[bpaf( + long("fix-level"), + argument("info|warn|error"), + fallback(Severity::default()), + display_fallback + )] + fix_level: Severity, }, /// Run the formatter on a set of files. #[bpaf(command)] diff --git a/crates/biome_cli/src/commands/scan_kind.rs b/crates/biome_cli/src/commands/scan_kind.rs index 8cb1fa62a0a7..2d10b2099b31 100644 --- a/crates/biome_cli/src/commands/scan_kind.rs +++ b/crates/biome_cli/src/commands/scan_kind.rs @@ -94,6 +94,7 @@ mod tests { suppress: false, suppression_reason: None, skip_parse_errors: false, + fix_level: biome_diagnostics::Severity::default(), }); let root_dir = Utf8Path::new("/"); @@ -125,6 +126,7 @@ mod tests { vcs_targeted: VcsTargeted::default(), skip_parse_errors: false, enforce_assist: true, + fix_level: biome_diagnostics::Severity::default(), }); let config = Configuration { diff --git a/crates/biome_cli/src/execute/mod.rs b/crates/biome_cli/src/execute/mod.rs index 41d5adf90e7a..b52485624c7d 100644 --- a/crates/biome_cli/src/execute/mod.rs +++ b/crates/biome_cli/src/execute/mod.rs @@ -111,6 +111,9 @@ pub enum TraversalMode { /// It skips parse errors skip_parse_errors: bool, + + /// The minimum level of diagnostics to fix + fix_level: biome_diagnostics::Severity, }, /// This mode is enabled when running the command `biome lint` Lint { @@ -139,6 +142,9 @@ pub enum TraversalMode { /// It skips parse errors skip_parse_errors: bool, + + /// The minimum level of diagnostics to fix + fix_level: biome_diagnostics::Severity, }, /// This mode is enabled when running the command `biome ci` CI { @@ -510,6 +516,14 @@ impl Execution { _ => false, } } + + pub(crate) fn get_fix_level(&self) -> biome_diagnostics::Severity { + match &self.traversal_mode { + TraversalMode::Check { fix_level, .. } => *fix_level, + TraversalMode::Lint { fix_level, .. } => *fix_level, + _ => biome_diagnostics::Severity::default(), + } + } } /// Based on the [mode](TraversalMode), the function might launch a traversal of the file system diff --git a/crates/biome_cli/src/execute/process_file/lint_and_assist.rs b/crates/biome_cli/src/execute/process_file/lint_and_assist.rs index a025e4fcbeb0..c8d2e5f1caa6 100644 --- a/crates/biome_cli/src/execute/process_file/lint_and_assist.rs +++ b/crates/biome_cli/src/execute/process_file/lint_and_assist.rs @@ -72,6 +72,7 @@ pub(crate) fn analyze_with_guard<'ctx>( only.clone(), skip.clone(), Some(suppression_explanation.to_string()), + Some(ctx.fix_level), ) .with_file_path_and_code( workspace_file.path.to_string(), diff --git a/crates/biome_cli/src/execute/std_in.rs b/crates/biome_cli/src/execute/std_in.rs index 1420358c8828..30447c567fc6 100644 --- a/crates/biome_cli/src/execute/std_in.rs +++ b/crates/biome_cli/src/execute/std_in.rs @@ -172,6 +172,7 @@ pub(crate) fn run<'a>( suppression_reason: None, enabled_rules: vec![], rule_categories: rule_categories.build(), + fix_level: Some(mode.get_fix_level()), })?; let code = fix_file_result.code; let output = match biome_path.extension() { diff --git a/crates/biome_cli/src/execute/traverse.rs b/crates/biome_cli/src/execute/traverse.rs index 28f7512a4f14..7f8fa0b56c86 100644 --- a/crates/biome_cli/src/execute/traverse.rs +++ b/crates/biome_cli/src/execute/traverse.rs @@ -84,6 +84,7 @@ pub(crate) fn traverse( skipped: &skipped, messages: sender, evaluated_paths: RwLock::default(), + fix_level: execution.get_fix_level(), }, ); // wait for the main thread to finish @@ -447,6 +448,8 @@ pub(crate) struct TraversalOptions<'ctx, 'app> { pub(crate) messages: Sender, /// List of paths that should be processed pub(crate) evaluated_paths: RwLock>, + /// The minimum diagnostic level to fix + pub(crate) fix_level: biome_diagnostics::Severity, } impl TraversalOptions<'_, '_> { diff --git a/crates/biome_cli/src/lib.rs b/crates/biome_cli/src/lib.rs index a1edd8296ef0..75c4fe85ba32 100644 --- a/crates/biome_cli/src/lib.rs +++ b/crates/biome_cli/src/lib.rs @@ -91,6 +91,7 @@ impl<'app> CliSession<'app> { staged, changed, since, + fix_level, } => run_command( self, &cli_options, @@ -108,6 +109,7 @@ impl<'app> CliSession<'app> { staged, changed, since, + fix_level, }, ), BiomeCommand::Lint { @@ -131,6 +133,7 @@ impl<'app> CliSession<'app> { javascript_linter, json_linter, graphql_linter, + fix_level, } => run_command( self, &cli_options, @@ -154,6 +157,7 @@ impl<'app> CliSession<'app> { javascript_linter, json_linter, graphql_linter, + fix_level, }, ), BiomeCommand::Ci { diff --git a/crates/biome_cli/tests/commands/check.rs b/crates/biome_cli/tests/commands/check.rs index 6c8e39fc84b2..234b9d6f19b0 100644 --- a/crates/biome_cli/tests/commands/check.rs +++ b/crates/biome_cli/tests/commands/check.rs @@ -54,6 +54,30 @@ const NO_DEBUGGER_AFTER: &str = "debugger;\n"; const UPGRADE_SEVERITY_CODE: &str = r#"if(!cond) { exprA(); } else { exprB() }"#; +// Test code for diagnostic level with fix functionality +const DIAGNOSTIC_LEVEL_TEST_BEFORE: &str = r#"let x: number | undefined = undefined; +if (x == 1) { + console.log(x); +} +"#; + +const DIAGNOSTIC_LEVEL_TEST_CONFIG: &str = r#"{ + "linter": { + "enabled": true, + "rules": { + "recommended": false, + "complexity": { + "noUselessUndefinedInitialization": "info" + }, + "suspicious": { + "noDoubleEquals": "error" + } + } + } +} +"#; + + const NURSERY_UNSTABLE: &str = r#"if(a = b) {}"#; #[test] @@ -3251,3 +3275,185 @@ fn check_does_not_enable_assist() { result, )); } + +#[test] +fn fix_level_error_only_fixes_error_level() { + let fs = MemoryFileSystem::default(); + let mut console = BufferConsole::default(); + + let file_path = Utf8Path::new("test.ts"); + let config_path = Utf8Path::new("biome.json"); + + fs.insert(file_path.into(), DIAGNOSTIC_LEVEL_TEST_BEFORE.as_bytes()); + fs.insert(config_path.into(), DIAGNOSTIC_LEVEL_TEST_CONFIG.as_bytes()); + + let (fs, result) = run_cli( + fs, + &mut console, + Args::from([ + "check", + "--fix-level=error", + "--fix", + "--unsafe", + file_path.as_str(), + ] + .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(); + + // Should only fix the error-level violation (noDoubleEquals), not the info-level one + // Verify that == was changed to === (error-level fix applied) + assert!(buffer.contains("x === 1"), "Error-level fix should be applied: == -> ==="); + // Verify that undefined initialization was NOT removed (info-level fix not applied) + assert!(buffer.contains("= undefined"), "Info-level fix should NOT be applied: = undefined should remain"); + + assert_cli_snapshot(SnapshotPayload::new( + module_path!(), + "fix_level_error_only_fixes_error_level", + fs, + console, + result, + )); +} + +#[test] +fn fix_level_info_fixes_all_levels() { + let fs = MemoryFileSystem::default(); + let mut console = BufferConsole::default(); + + let file_path = Utf8Path::new("test.ts"); + let config_path = Utf8Path::new("biome.json"); + + fs.insert(file_path.into(), DIAGNOSTIC_LEVEL_TEST_BEFORE.as_bytes()); + fs.insert(config_path.into(), DIAGNOSTIC_LEVEL_TEST_CONFIG.as_bytes()); + + let (fs, result) = run_cli( + fs, + &mut console, + Args::from([ + "check", + "--fix-level=info", + "--fix", + "--unsafe", + file_path.as_str(), + ] + .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(); + + // Should fix both info-level and error-level violations + // Verify that == was changed to === (error-level fix applied) + assert!(buffer.contains("x === 1"), "Error-level fix should be applied: == -> ==="); + // Verify that undefined initialization was removed (info-level fix applied) + assert!(!buffer.contains("= undefined"), "Info-level fix should be applied: = undefined should be removed"); + + assert_cli_snapshot(SnapshotPayload::new( + module_path!(), + "fix_level_info_fixes_all_levels", + fs, + console, + result, + )); +} + +#[test] +fn fix_level_warn_fixes_warn_and_error_levels() { + let fs = MemoryFileSystem::default(); + let mut console = BufferConsole::default(); + + let file_path = Utf8Path::new("test.ts"); + let config_path = Utf8Path::new("biome.json"); + + fs.insert(file_path.into(), DIAGNOSTIC_LEVEL_TEST_BEFORE.as_bytes()); + fs.insert(config_path.into(), DIAGNOSTIC_LEVEL_TEST_CONFIG.as_bytes()); + + let (fs, result) = run_cli( + fs, + &mut console, + Args::from([ + "check", + "--fix-level=warn", + "--fix", + "--unsafe", + file_path.as_str(), + ] + .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(); + + // Should only fix the error-level violation (warn < error), not the info-level one + // Verify that == was changed to === (error-level fix applied) + assert!(buffer.contains("x === 1"), "Error-level fix should be applied: == -> ==="); + // Verify that undefined initialization was NOT removed (info-level fix not applied) + assert!(buffer.contains("= undefined"), "Info-level fix should NOT be applied: = undefined should remain"); + + assert_cli_snapshot(SnapshotPayload::new( + module_path!(), + "fix_level_warn_fixes_warn_and_error_levels", + fs, + console, + result, + )); +} + +#[test] +fn diagnostic_level_without_fix_respects_level() { + let fs = MemoryFileSystem::default(); + let mut console = BufferConsole::default(); + + let file_path = Utf8Path::new("test.ts"); + let config_path = Utf8Path::new("biome.json"); + + fs.insert(file_path.into(), DIAGNOSTIC_LEVEL_TEST_BEFORE.as_bytes()); + fs.insert(config_path.into(), DIAGNOSTIC_LEVEL_TEST_CONFIG.as_bytes()); + + let (fs, result) = run_cli( + fs, + &mut console, + Args::from([ + "check", + "--diagnostic-level=error", + file_path.as_str(), + ] + .as_slice()), + ); + + assert!(result.is_err(), "run_cli returned {result:?}"); + + // File should remain unchanged + let mut buffer = String::new(); + fs.open(file_path) + .unwrap() + .read_to_string(&mut buffer) + .unwrap(); + assert_eq!(buffer, DIAGNOSTIC_LEVEL_TEST_BEFORE); + + assert_cli_snapshot(SnapshotPayload::new( + module_path!(), + "diagnostic_level_without_fix_respects_level", + fs, + console, + result, + )); +} diff --git a/crates/biome_cli/tests/commands/lint.rs b/crates/biome_cli/tests/commands/lint.rs index a9a734160715..b51222b3be0e 100644 --- a/crates/biome_cli/tests/commands/lint.rs +++ b/crates/biome_cli/tests/commands/lint.rs @@ -52,6 +52,29 @@ const NO_DEBUGGER_AFTER: &str = "debugger;\n"; const UPGRADE_SEVERITY_CODE: &str = r#"if(!cond) { exprA(); } else { exprB() }"#; +// Test code for diagnostic level with fix functionality +const DIAGNOSTIC_LEVEL_TEST_BEFORE: &str = r#"let x: number | undefined = undefined; +if (x == 1) { + console.log(x); +} +"#; + +const DIAGNOSTIC_LEVEL_TEST_CONFIG: &str = r#"{ + "linter": { + "enabled": true, + "rules": { + "recommended": false, + "complexity": { + "noUselessUndefinedInitialization": "info" + }, + "suspicious": { + "noDoubleEquals": "error" + } + } + } +} +"#; + const NURSERY_UNSTABLE: &str = r#"if(a = b) {}"#; #[test] @@ -4339,3 +4362,138 @@ fn should_not_choke_on_recursive_function_call() { result, )); } + +#[test] +fn lint_fix_level_error_only_fixes_error_level() { + let fs = MemoryFileSystem::default(); + let mut console = BufferConsole::default(); + + let file_path = Utf8Path::new("test.ts"); + let config_path = Utf8Path::new("biome.json"); + + fs.insert(file_path.into(), DIAGNOSTIC_LEVEL_TEST_BEFORE.as_bytes()); + fs.insert(config_path.into(), DIAGNOSTIC_LEVEL_TEST_CONFIG.as_bytes()); + + let (fs, result) = run_cli( + fs, + &mut console, + Args::from([ + "lint", + "--fix-level=error", + "--fix", + "--unsafe", + file_path.as_str(), + ] + .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(); + + // Should only fix the error-level violation (noDoubleEquals), not the info-level one + // Verify that == was changed to === (error-level fix applied) + assert!(buffer.contains("x === 1"), "Error-level fix should be applied: == -> ==="); + // Verify that undefined initialization was NOT removed (info-level fix not applied) + assert!(buffer.contains("= undefined"), "Info-level fix should NOT be applied: = undefined should remain"); + + assert_cli_snapshot(SnapshotPayload::new( + module_path!(), + "lint_fix_level_error_only_fixes_error_level", + fs, + console, + result, + )); +} + +#[test] +fn lint_fix_level_info_fixes_all_levels() { + let fs = MemoryFileSystem::default(); + let mut console = BufferConsole::default(); + + let file_path = Utf8Path::new("test.ts"); + let config_path = Utf8Path::new("biome.json"); + + fs.insert(file_path.into(), DIAGNOSTIC_LEVEL_TEST_BEFORE.as_bytes()); + fs.insert(config_path.into(), DIAGNOSTIC_LEVEL_TEST_CONFIG.as_bytes()); + + let (fs, result) = run_cli( + fs, + &mut console, + Args::from([ + "lint", + "--fix-level=info", + "--fix", + "--unsafe", + file_path.as_str(), + ] + .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(); + + // Should fix both info-level and error-level violations + // Verify that == was changed to === (error-level fix applied) + assert!(buffer.contains("x === 1"), "Error-level fix should be applied: == -> ==="); + // Verify that undefined initialization was removed (info-level fix applied) + assert!(!buffer.contains("= undefined"), "Info-level fix should be applied: = undefined should be removed"); + + assert_cli_snapshot(SnapshotPayload::new( + module_path!(), + "lint_fix_level_info_fixes_all_levels", + fs, + console, + result, + )); +} + +#[test] +fn lint_diagnostic_level_without_fix_respects_level() { + let fs = MemoryFileSystem::default(); + let mut console = BufferConsole::default(); + + let file_path = Utf8Path::new("test.ts"); + let config_path = Utf8Path::new("biome.json"); + + fs.insert(file_path.into(), DIAGNOSTIC_LEVEL_TEST_BEFORE.as_bytes()); + fs.insert(config_path.into(), DIAGNOSTIC_LEVEL_TEST_CONFIG.as_bytes()); + + let (fs, result) = run_cli( + fs, + &mut console, + Args::from([ + "lint", + "--diagnostic-level=error", + file_path.as_str(), + ] + .as_slice()), + ); + + assert!(result.is_err(), "run_cli returned {result:?}"); + + // File should remain unchanged + let mut buffer = String::new(); + fs.open(file_path) + .unwrap() + .read_to_string(&mut buffer) + .unwrap(); + assert_eq!(buffer, DIAGNOSTIC_LEVEL_TEST_BEFORE); + + assert_cli_snapshot(SnapshotPayload::new( + module_path!(), + "lint_diagnostic_level_without_fix_respects_level", + fs, + console, + result, + )); +} diff --git a/crates/biome_cli/tests/snapshots/main_commands_check/diagnostic_level_without_fix_respects_level.snap b/crates/biome_cli/tests/snapshots/main_commands_check/diagnostic_level_without_fix_respects_level.snap new file mode 100644 index 000000000000..370f619adafb --- /dev/null +++ b/crates/biome_cli/tests/snapshots/main_commands_check/diagnostic_level_without_fix_respects_level.snap @@ -0,0 +1,85 @@ +--- +source: crates/biome_cli/tests/snap_test.rs +expression: redactor(content) +--- +## `biome.json` + +```json +{ + "linter": { + "enabled": true, + "rules": { + "recommended": false, + "complexity": { + "noUselessUndefinedInitialization": "info" + }, + "suspicious": { + "noDoubleEquals": "error" + } + } + } +} +``` + +## `test.ts` + +```ts +let x: number | undefined = undefined; +if (x == 1) { + console.log(x); +} + +``` + +# Termination Message + +```block +check ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Some errors were emitted while running checks. + + + +``` + +# Emitted Messages + +```block +test.ts:2:7 lint/suspicious/noDoubleEquals FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Using == may be unsafe if you are relying on type coercion. + + 1 │ let x: number | undefined = undefined; + > 2 │ if (x == 1) { + │ ^^ + 3 │ console.log(x); + 4 │ } + + i == is only allowed when comparing against null. + + i Unsafe fix: Use === instead. + + 2 │ if·(x·===·1)·{ + │ + + +``` + +```block +test.ts format ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Formatter would have printed the following content: + + 1 1 │ let x: number | undefined = undefined; + 2 2 │ if (x == 1) { + 3 │ - ··console.log(x); + 3 │ + → console.log(x); + 4 4 │ } + 5 5 │ + + +``` + +```block +Checked 1 file in