diff --git a/apps/oxlint/fixtures/max_warnings_config/.oxlintrc.json b/apps/oxlint/fixtures/max_warnings_config/.oxlintrc.json new file mode 100644 index 0000000000000..7473a4ba2cb5a --- /dev/null +++ b/apps/oxlint/fixtures/max_warnings_config/.oxlintrc.json @@ -0,0 +1,7 @@ +{ + "maxWarnings": 0, + "rules": { + "no-debugger": "warn", + "no-console": "warn" + } +} diff --git a/apps/oxlint/fixtures/max_warnings_config/many-warnings.js b/apps/oxlint/fixtures/max_warnings_config/many-warnings.js new file mode 100644 index 0000000000000..26d517eb17dbf --- /dev/null +++ b/apps/oxlint/fixtures/max_warnings_config/many-warnings.js @@ -0,0 +1,7 @@ +// This file has 6 warnings +debugger; +console.log("1"); +debugger; +console.log("2"); +debugger; +console.log("3"); diff --git a/apps/oxlint/fixtures/max_warnings_config/oxlintrc-5-warnings.json b/apps/oxlint/fixtures/max_warnings_config/oxlintrc-5-warnings.json new file mode 100644 index 0000000000000..387692b676d7d --- /dev/null +++ b/apps/oxlint/fixtures/max_warnings_config/oxlintrc-5-warnings.json @@ -0,0 +1,7 @@ +{ + "maxWarnings": 5, + "rules": { + "no-debugger": "warn", + "no-console": "warn" + } +} diff --git a/apps/oxlint/fixtures/max_warnings_config/test.js b/apps/oxlint/fixtures/max_warnings_config/test.js new file mode 100644 index 0000000000000..bbe7b20b15c5d --- /dev/null +++ b/apps/oxlint/fixtures/max_warnings_config/test.js @@ -0,0 +1,3 @@ +// This file has 2 warnings +debugger; +console.log("test"); diff --git a/apps/oxlint/fixtures/max_warnings_nested_error/.oxlintrc.json b/apps/oxlint/fixtures/max_warnings_nested_error/.oxlintrc.json new file mode 100644 index 0000000000000..cfc6c7217e7d1 --- /dev/null +++ b/apps/oxlint/fixtures/max_warnings_nested_error/.oxlintrc.json @@ -0,0 +1,5 @@ +{ + "rules": { + "no-debugger": "warn" + } +} diff --git a/apps/oxlint/fixtures/max_warnings_nested_error/subdir/.oxlintrc.json b/apps/oxlint/fixtures/max_warnings_nested_error/subdir/.oxlintrc.json new file mode 100644 index 0000000000000..3963430fd8d94 --- /dev/null +++ b/apps/oxlint/fixtures/max_warnings_nested_error/subdir/.oxlintrc.json @@ -0,0 +1,6 @@ +{ + "maxWarnings": 5, + "rules": { + "no-console": "warn" + } +} diff --git a/apps/oxlint/fixtures/max_warnings_nested_error/subdir/test.js b/apps/oxlint/fixtures/max_warnings_nested_error/subdir/test.js new file mode 100644 index 0000000000000..14c198cadb60d --- /dev/null +++ b/apps/oxlint/fixtures/max_warnings_nested_error/subdir/test.js @@ -0,0 +1 @@ +console.log("test"); diff --git a/apps/oxlint/src/lint.rs b/apps/oxlint/src/lint.rs index d7583f3208e2b..c004f21964e0d 100644 --- a/apps/oxlint/src/lint.rs +++ b/apps/oxlint/src/lint.rs @@ -213,6 +213,9 @@ impl CliRunner { None }; + // Store max_warnings from config before oxlintrc is consumed + let config_max_warnings = oxlintrc.max_warnings; + let config_builder = match ConfigStoreBuilder::from_oxlintrc( false, oxlintrc, @@ -301,7 +304,7 @@ impl CliRunner { _ => None, }; let (mut diagnostic_service, tx_error) = - Self::get_diagnostic_service(&output_formatter, &warning_options, &misc_options); + Self::get_diagnostic_service(&output_formatter, &warning_options, &misc_options, config_max_warnings); let config_store = ConfigStore::new(lint_config, nested_configs, external_plugin_store); @@ -421,13 +424,17 @@ impl CliRunner { reporter: &OutputFormatter, warning_options: &WarningOptions, misc_options: &MiscOptions, + config_max_warnings: Option, ) -> (DiagnosticService, DiagnosticSender) { + // CLI flag takes precedence over config file + let max_warnings = warning_options.max_warnings.or(config_max_warnings); + let (service, sender) = DiagnosticService::new(reporter.get_diagnostic_reporter()); ( service .with_quiet(warning_options.quiet) .with_silent(misc_options.silent) - .with_max_warnings(warning_options.max_warnings), + .with_max_warnings(max_warnings), sender, ) } @@ -519,6 +526,19 @@ impl CliRunner { // iterate over each config and build the ConfigStore for (dir, oxlintrc) in nested_oxlintrc { + // Validate that nested configs don't have maxWarnings + if oxlintrc.max_warnings.is_some() { + let config_path = oxlintrc.path.display(); + print_and_flush_stdout( + stdout, + &format!( + "Error: 'maxWarnings' option is only valid in the root configuration file.\n\ + Found in nested config: {config_path}\n" + ), + ); + return Err(CliRunResult::InvalidOptionConfig); + } + // Collect ignore patterns and their root nested_ignore_patterns.push(( oxlintrc.ignore_patterns.clone(), @@ -1330,4 +1350,41 @@ mod test { let args = &["--type-aware"]; Tester::new().with_cwd("fixtures/tsgolint_config_error".into()).test_and_snapshot(args); } + + #[test] + fn test_max_warnings_config_zero() { + // maxWarnings: 0 should fail when there are warnings + let args = &["-c", ".oxlintrc.json", "test.js"]; + Tester::new().with_cwd("fixtures/max_warnings_config".into()).test_and_snapshot(args); + } + + #[test] + fn test_max_warnings_config_threshold_exceeded() { + // maxWarnings: 5 should fail when there are 6 warnings + let args = &["-c", "oxlintrc-5-warnings.json", "many-warnings.js"]; + Tester::new().with_cwd("fixtures/max_warnings_config".into()).test_and_snapshot(args); + } + + #[test] + fn test_max_warnings_config_threshold_not_exceeded() { + // maxWarnings: 5 should pass when there are 2 warnings + let args = &["-c", "oxlintrc-5-warnings.json", "test.js"]; + Tester::new().with_cwd("fixtures/max_warnings_config".into()).test_and_snapshot(args); + } + + #[test] + fn test_max_warnings_cli_override() { + // CLI --max-warnings should override config file + let args = &["-c", ".oxlintrc.json", "--max-warnings", "10", "test.js"]; + Tester::new().with_cwd("fixtures/max_warnings_config".into()).test_and_snapshot(args); + } + + #[test] + fn test_max_warnings_nested_config_error() { + // maxWarnings in nested config should produce an error + let args = &["subdir/test.js"]; + Tester::new() + .with_cwd("fixtures/max_warnings_nested_error".into()) + .test_and_snapshot(args); + } } diff --git a/apps/oxlint/src/snapshots/fixtures__max_warnings_config_-c .oxlintrc.json --max-warnings 10 test.js@oxlint.snap b/apps/oxlint/src/snapshots/fixtures__max_warnings_config_-c .oxlintrc.json --max-warnings 10 test.js@oxlint.snap new file mode 100644 index 0000000000000..c7564d3259dad --- /dev/null +++ b/apps/oxlint/src/snapshots/fixtures__max_warnings_config_-c .oxlintrc.json --max-warnings 10 test.js@oxlint.snap @@ -0,0 +1,30 @@ +--- +source: apps/oxlint/src/tester.rs +--- +########## +arguments: -c .oxlintrc.json --max-warnings 10 test.js +working directory: fixtures/max_warnings_config +---------- + + ! ]8;;https://oxc.rs/docs/guide/usage/linter/rules/eslint/no-debugger.html\eslint(no-debugger)]8;;\: `debugger` statement is not allowed + ,-[test.js:2:1] + 1 | // This file has 2 warnings + 2 | debugger; + : ^^^^^^^^^ + 3 | console.log("test"); + `---- + help: Remove the debugger statement + + ! ]8;;https://oxc.rs/docs/guide/usage/linter/rules/eslint/no-console.html\eslint(no-console)]8;;\: Unexpected console statement. + ,-[test.js:3:1] + 2 | debugger; + 3 | console.log("test"); + : ^^^^^^^^^^^ + `---- + help: Delete this console statement. + +Found 2 warnings and 0 errors. +Finished in ms on 1 file with 90 rules using 1 threads. +---------- +CLI result: LintSucceeded +---------- diff --git a/apps/oxlint/src/snapshots/fixtures__max_warnings_config_-c .oxlintrc.json test.js@oxlint.snap b/apps/oxlint/src/snapshots/fixtures__max_warnings_config_-c .oxlintrc.json test.js@oxlint.snap new file mode 100644 index 0000000000000..4cc64e1859e16 --- /dev/null +++ b/apps/oxlint/src/snapshots/fixtures__max_warnings_config_-c .oxlintrc.json test.js@oxlint.snap @@ -0,0 +1,31 @@ +--- +source: apps/oxlint/src/tester.rs +--- +########## +arguments: -c .oxlintrc.json test.js +working directory: fixtures/max_warnings_config +---------- + + ! ]8;;https://oxc.rs/docs/guide/usage/linter/rules/eslint/no-debugger.html\eslint(no-debugger)]8;;\: `debugger` statement is not allowed + ,-[test.js:2:1] + 1 | // This file has 2 warnings + 2 | debugger; + : ^^^^^^^^^ + 3 | console.log("test"); + `---- + help: Remove the debugger statement + + ! ]8;;https://oxc.rs/docs/guide/usage/linter/rules/eslint/no-console.html\eslint(no-console)]8;;\: Unexpected console statement. + ,-[test.js:3:1] + 2 | debugger; + 3 | console.log("test"); + : ^^^^^^^^^^^ + `---- + help: Delete this console statement. + +Found 2 warnings and 0 errors. +Exceeded maximum number of warnings. Found 2. +Finished in ms on 1 file with 90 rules using 1 threads. +---------- +CLI result: LintMaxWarningsExceeded +---------- diff --git a/apps/oxlint/src/snapshots/fixtures__max_warnings_config_-c oxlintrc-5-warnings.json many-warnings.js@oxlint.snap b/apps/oxlint/src/snapshots/fixtures__max_warnings_config_-c oxlintrc-5-warnings.json many-warnings.js@oxlint.snap new file mode 100644 index 0000000000000..ec0375f499bde --- /dev/null +++ b/apps/oxlint/src/snapshots/fixtures__max_warnings_config_-c oxlintrc-5-warnings.json many-warnings.js@oxlint.snap @@ -0,0 +1,67 @@ +--- +source: apps/oxlint/src/tester.rs +--- +########## +arguments: -c oxlintrc-5-warnings.json many-warnings.js +working directory: fixtures/max_warnings_config +---------- + + ! ]8;;https://oxc.rs/docs/guide/usage/linter/rules/eslint/no-debugger.html\eslint(no-debugger)]8;;\: `debugger` statement is not allowed + ,-[many-warnings.js:2:1] + 1 | // This file has 6 warnings + 2 | debugger; + : ^^^^^^^^^ + 3 | console.log("1"); + `---- + help: Remove the debugger statement + + ! ]8;;https://oxc.rs/docs/guide/usage/linter/rules/eslint/no-console.html\eslint(no-console)]8;;\: Unexpected console statement. + ,-[many-warnings.js:3:1] + 2 | debugger; + 3 | console.log("1"); + : ^^^^^^^^^^^ + 4 | debugger; + `---- + help: Delete this console statement. + + ! ]8;;https://oxc.rs/docs/guide/usage/linter/rules/eslint/no-debugger.html\eslint(no-debugger)]8;;\: `debugger` statement is not allowed + ,-[many-warnings.js:4:1] + 3 | console.log("1"); + 4 | debugger; + : ^^^^^^^^^ + 5 | console.log("2"); + `---- + help: Remove the debugger statement + + ! ]8;;https://oxc.rs/docs/guide/usage/linter/rules/eslint/no-console.html\eslint(no-console)]8;;\: Unexpected console statement. + ,-[many-warnings.js:5:1] + 4 | debugger; + 5 | console.log("2"); + : ^^^^^^^^^^^ + 6 | debugger; + `---- + help: Delete this console statement. + + ! ]8;;https://oxc.rs/docs/guide/usage/linter/rules/eslint/no-debugger.html\eslint(no-debugger)]8;;\: `debugger` statement is not allowed + ,-[many-warnings.js:6:1] + 5 | console.log("2"); + 6 | debugger; + : ^^^^^^^^^ + 7 | console.log("3"); + `---- + help: Remove the debugger statement + + ! ]8;;https://oxc.rs/docs/guide/usage/linter/rules/eslint/no-console.html\eslint(no-console)]8;;\: Unexpected console statement. + ,-[many-warnings.js:7:1] + 6 | debugger; + 7 | console.log("3"); + : ^^^^^^^^^^^ + `---- + help: Delete this console statement. + +Found 6 warnings and 0 errors. +Exceeded maximum number of warnings. Found 6. +Finished in ms on 1 file with 90 rules using 1 threads. +---------- +CLI result: LintMaxWarningsExceeded +---------- diff --git a/apps/oxlint/src/snapshots/fixtures__max_warnings_config_-c oxlintrc-5-warnings.json test.js@oxlint.snap b/apps/oxlint/src/snapshots/fixtures__max_warnings_config_-c oxlintrc-5-warnings.json test.js@oxlint.snap new file mode 100644 index 0000000000000..d803a9c5eed21 --- /dev/null +++ b/apps/oxlint/src/snapshots/fixtures__max_warnings_config_-c oxlintrc-5-warnings.json test.js@oxlint.snap @@ -0,0 +1,30 @@ +--- +source: apps/oxlint/src/tester.rs +--- +########## +arguments: -c oxlintrc-5-warnings.json test.js +working directory: fixtures/max_warnings_config +---------- + + ! ]8;;https://oxc.rs/docs/guide/usage/linter/rules/eslint/no-debugger.html\eslint(no-debugger)]8;;\: `debugger` statement is not allowed + ,-[test.js:2:1] + 1 | // This file has 2 warnings + 2 | debugger; + : ^^^^^^^^^ + 3 | console.log("test"); + `---- + help: Remove the debugger statement + + ! ]8;;https://oxc.rs/docs/guide/usage/linter/rules/eslint/no-console.html\eslint(no-console)]8;;\: Unexpected console statement. + ,-[test.js:3:1] + 2 | debugger; + 3 | console.log("test"); + : ^^^^^^^^^^^ + `---- + help: Delete this console statement. + +Found 2 warnings and 0 errors. +Finished in ms on 1 file with 90 rules using 1 threads. +---------- +CLI result: LintSucceeded +---------- diff --git a/apps/oxlint/src/snapshots/fixtures__max_warnings_nested_error_subdir__test.js@oxlint.snap b/apps/oxlint/src/snapshots/fixtures__max_warnings_nested_error_subdir__test.js@oxlint.snap new file mode 100644 index 0000000000000..c6a3d94eb3d16 --- /dev/null +++ b/apps/oxlint/src/snapshots/fixtures__max_warnings_nested_error_subdir__test.js@oxlint.snap @@ -0,0 +1,12 @@ +--- +source: apps/oxlint/src/tester.rs +--- +########## +arguments: subdir/test.js +working directory: fixtures/max_warnings_nested_error +---------- +Error: 'maxWarnings' option is only valid in the root configuration file. +Found in nested config: /fixtures/max_warnings_nested_error/subdir/.oxlintrc.json +---------- +CLI result: InvalidOptionConfig +---------- diff --git a/crates/oxc_linter/src/config/oxlintrc.rs b/crates/oxc_linter/src/config/oxlintrc.rs index 30dfe804a7f03..d6d4a8ea270e3 100644 --- a/crates/oxc_linter/src/config/oxlintrc.rs +++ b/crates/oxc_linter/src/config/oxlintrc.rs @@ -116,6 +116,10 @@ pub struct Oxlintrc { /// overriding the previous ones. #[serde(skip_serializing_if = "Vec::is_empty")] pub extends: Vec, + /// Specify a warning threshold, which can be used to force exit with an error status if there + /// are too many warning-level rule violations in your project. + #[serde(rename = "maxWarnings", skip_serializing_if = "Option::is_none")] + pub max_warnings: Option, } impl Oxlintrc { @@ -237,6 +241,8 @@ impl Oxlintrc { (None, None) => None, }; + let max_warnings = self.max_warnings.or(other.max_warnings); + Oxlintrc { plugins, external_plugins, @@ -249,6 +255,7 @@ impl Oxlintrc { path: self.path.clone(), ignore_patterns: self.ignore_patterns.clone(), extends: self.extends.clone(), + max_warnings, } } } @@ -350,4 +357,31 @@ mod test { let config: Oxlintrc = serde_json::from_str(r#"{"extends": []}"#).unwrap(); assert_eq!(0, config.extends.len()); } + + #[test] + fn test_oxlintrc_max_warnings() { + let config: Oxlintrc = serde_json::from_str(r#"{"maxWarnings": 0}"#).unwrap(); + assert_eq!(config.max_warnings, Some(0)); + + let config: Oxlintrc = serde_json::from_str(r#"{"maxWarnings": 10}"#).unwrap(); + assert_eq!(config.max_warnings, Some(10)); + + let config: Oxlintrc = serde_json::from_str(r#"{}"#).unwrap(); + assert_eq!(config.max_warnings, None); + } + + #[test] + fn test_oxlintrc_merge_max_warnings() { + let config1: Oxlintrc = serde_json::from_str(r#"{"maxWarnings": 5}"#).unwrap(); + let config2: Oxlintrc = serde_json::from_str(r#"{"maxWarnings": 10}"#).unwrap(); + let merged = config1.merge(config2); + // config1 takes precedence + assert_eq!(merged.max_warnings, Some(5)); + + let config1: Oxlintrc = serde_json::from_str(r#"{}"#).unwrap(); + let config2: Oxlintrc = serde_json::from_str(r#"{"maxWarnings": 10}"#).unwrap(); + let merged = config1.merge(config2); + // config2's value is used when config1 doesn't have it + assert_eq!(merged.max_warnings, Some(10)); + } }