diff --git a/apps/oxlint/fixtures/report_unused_directives/.oxlintrc-with-rudd.json b/apps/oxlint/fixtures/report_unused_directives/.oxlintrc-with-rudd.json new file mode 100644 index 0000000000000..58647de4d5e63 --- /dev/null +++ b/apps/oxlint/fixtures/report_unused_directives/.oxlintrc-with-rudd.json @@ -0,0 +1,9 @@ +{ + "options": { + "reportUnusedDisableDirectives": "warn" + }, + "rules": { + "no-console": "warn", + "no-debugger": "warn" + } +} diff --git a/apps/oxlint/src-js/package/config.generated.ts b/apps/oxlint/src-js/package/config.generated.ts index d10e0cfb1b95d..8814016009477 100644 --- a/apps/oxlint/src-js/package/config.generated.ts +++ b/apps/oxlint/src-js/package/config.generated.ts @@ -312,6 +312,14 @@ export interface OxlintGlobals { * Options for the linter. */ export interface OxlintOptions { + /** + * Report unused disable directives (e.g. `// oxlint-disable-line` or `// eslint-disable-line`). + * + * Equivalent to passing `--report-unused-disable-directives-severity` on the CLI. + * CLI flags take precedence over this value when both are set. + * Only supported in the root configuration file. + */ + reportUnusedDisableDirectives?: AllowWarnDeny | null; /** * Ensure warnings produce a non-zero exit code. * diff --git a/apps/oxlint/src/config_loader.rs b/apps/oxlint/src/config_loader.rs index 8eaaaec56d411..f761030ef2f5c 100644 --- a/apps/oxlint/src/config_loader.rs +++ b/apps/oxlint/src/config_loader.rs @@ -399,6 +399,12 @@ impl<'a> ConfigLoader<'a> { ))); continue; } + if builder.report_unused_disable_directives().is_some() { + errors.push(ConfigLoadError::Diagnostic( + nested_report_unused_disable_directives_not_supported(&path), + )); + continue; + } } let extended_paths = builder.extended_paths.clone(); @@ -677,6 +683,14 @@ fn nested_max_warnings_not_supported(path: &Path) -> OxcDiagnostic { .with_help("Move `options.maxWarnings` to the root configuration file.") } +fn nested_report_unused_disable_directives_not_supported(path: &Path) -> OxcDiagnostic { + OxcDiagnostic::error(format!( + "The `options.reportUnusedDisableDirectives` option is only supported in the root config, but it was found in {}.", + path.display() + )) + .with_help("Move `options.reportUnusedDisableDirectives` to the root configuration file.") +} + #[cfg(test)] mod test { use std::path::{Path, PathBuf}; @@ -815,6 +829,24 @@ mod test { let nested_path = root_dir.path().join("nested/.oxlintrc.json"); std::fs::create_dir_all(nested_path.parent().unwrap()).unwrap(); std::fs::write(&nested_path, r#"{ "options": { "denyWarnings": true } }"#).unwrap(); + let mut external_plugin_store = ExternalPluginStore::new(false); + let mut loader = ConfigLoader::new(None, &mut external_plugin_store, &[], None); + let (_configs, errors) = loader + .load_discovered_with_root_dir(root_dir.path(), [DiscoveredConfig::Json(nested_path)]); + assert_eq!(errors.len(), 1); + assert!(matches!(errors[0], ConfigLoadError::Diagnostic(_))); + } + + #[test] + fn test_nested_json_config_rejects_report_unused_disable_directives() { + let root_dir = tempfile::tempdir().unwrap(); + let nested_path = root_dir.path().join("nested/.oxlintrc.json"); + std::fs::create_dir_all(nested_path.parent().unwrap()).unwrap(); + std::fs::write( + &nested_path, + r#"{ "options": { "reportUnusedDisableDirectives": "warn" } }"#, + ) + .unwrap(); let mut external_plugin_store = ExternalPluginStore::new(false); let mut loader = ConfigLoader::new(None, &mut external_plugin_store, &[], None); diff --git a/apps/oxlint/src/lint.rs b/apps/oxlint/src/lint.rs index d8d72340cddf6..dcd0c442e3fee 100644 --- a/apps/oxlint/src/lint.rs +++ b/apps/oxlint/src/lint.rs @@ -344,10 +344,17 @@ impl CliRunner { let deny_warnings = warning_options.deny_warnings || config_store.deny_warnings(); let max_warnings = warning_options.max_warnings.or(config_store.max_warnings()); + // Only propagate Warn/Deny; treat Allow (off) as disabling reports. let report_unused_directives = match inline_config_options.report_unused_directives { ReportUnusedDirectives::WithoutSeverity(true) => Some(AllowWarnDeny::Warn), - ReportUnusedDirectives::WithSeverity(Some(severity)) => Some(severity), - _ => None, + ReportUnusedDirectives::WithSeverity(Some(severity)) if severity.is_warn_deny() => { + Some(severity) + } + ReportUnusedDirectives::WithSeverity(Some(_)) => None, + _ => match config_store.report_unused_disable_directives() { + Some(severity) if severity.is_warn_deny() => Some(severity), + _ => None, + }, }; let (mut diagnostic_service, tx_error) = Self::get_diagnostic_service( &output_formatter, @@ -1059,6 +1066,26 @@ mod test { Tester::new().with_cwd("fixtures/report_unused_directives".into()).test_and_snapshot(args); } + #[test] + fn test_report_unused_directives_from_config() { + // Verify that `reportUnusedDisableDirectives` in the config file enables reporting + // without needing a CLI flag. + let args = &["-c", ".oxlintrc-with-rudd.json"]; + + Tester::new().with_cwd("fixtures/report_unused_directives".into()).test_and_snapshot(args); + } + + #[test] + fn test_report_unused_directives_cli_overrides_config() { + // Verify that the CLI flag takes precedence over the config file value. + // Config has `reportUnusedDisableDirectives: "warn"`, but CLI passes `off`, + // so no unused-directive diagnostics should be reported. + let args = + &["-c", ".oxlintrc-with-rudd.json", "--report-unused-disable-directives-severity=off"]; + + Tester::new().with_cwd("fixtures/report_unused_directives".into()).test_and_snapshot(args); + } + #[test] fn test_nested_config() { let args = &[]; diff --git a/apps/oxlint/src/snapshots/fixtures__report_unused_directives_-c .oxlintrc-with-rudd.json --report-unused-disable-directives-severity=off@oxlint.snap b/apps/oxlint/src/snapshots/fixtures__report_unused_directives_-c .oxlintrc-with-rudd.json --report-unused-disable-directives-severity=off@oxlint.snap new file mode 100644 index 0000000000000..a6beb55c4e14e --- /dev/null +++ b/apps/oxlint/src/snapshots/fixtures__report_unused_directives_-c .oxlintrc-with-rudd.json --report-unused-disable-directives-severity=off@oxlint.snap @@ -0,0 +1,112 @@ +--- +source: apps/oxlint/src/tester.rs +--- +########## +arguments: -c .oxlintrc-with-rudd.json --report-unused-disable-directives-severity=off +working directory: fixtures/report_unused_directives +---------- + + ! eslint(no-console): Unexpected console statement. + ,-[test-multiple-scripts.vue:7:1] + 6 | // eslint-disable-next-line no-debugger + 7 | console.log('regular script'); + : ^^^^^^^^^^^ + 8 | + `---- + help: Delete this console statement. + + ! eslint(no-debugger): `debugger` statement is not allowed + ,-[test-multiple-scripts.vue:10:1] + 9 | // eslint-disable-next-line no-console + 10 | debugger; + : ^^^^^^^^^ + 11 | + `---- + help: Remove the debugger statement + + ! eslint(no-console): Unexpected console statement. + ,-[test-multiple-scripts.vue:31:1] + 30 | // oxlint-disable-next-line no-debugger, no-for-loop + 31 | console.log("complete line"); + : ^^^^^^^^^^^ + 32 | + `---- + help: Delete this console statement. + + ! eslint(no-debugger): `debugger` statement is not allowed + ,-[test.astro:11:1] + 10 | // eslint-disable-next-line no-console + 11 | debugger; + : ^^^^^^^^^ + 12 | --- + `---- + help: Remove the debugger statement + + ! eslint(no-console): Unexpected console statement. + ,-[test.astro:32:1] + 31 | // oxlint-disable-next-line no-debugger, no-for-loop + 32 | console.log("complete line"); + : ^^^^^^^^^^^ + 33 | + `---- + help: Delete this console statement. + + ! eslint(no-debugger): `debugger` statement is not allowed + ,-[test.js:10:1] + 9 | // eslint-disable-next-line no-console + 10 | debugger; + : ^^^^^^^^^ + 11 | + `---- + help: Remove the debugger statement + + ! eslint(no-console): Unexpected console statement. + ,-[test.js:27:1] + 26 | // oxlint-disable-next-line no-debugger, no-for-loop + 27 | console.log("complete line"); + : ^^^^^^^^^^^ + 28 | + `---- + help: Delete this console statement. + + ! eslint(no-debugger): `debugger` statement is not allowed + ,-[test.svelte:11:1] + 10 | // eslint-disable-next-line no-console + 11 | debugger; + : ^^^^^^^^^ + 12 | + `---- + help: Remove the debugger statement + + ! eslint(no-console): Unexpected console statement. + ,-[test.svelte:28:1] + 27 | // oxlint-disable-next-line no-debugger, no-for-loop + 28 | console.log("complete line"); + : ^^^^^^^^^^^ + 29 | + `---- + help: Delete this console statement. + + ! eslint(no-debugger): `debugger` statement is not allowed + ,-[test.vue:15:1] + 14 | // eslint-disable-next-line no-console + 15 | debugger; + : ^^^^^^^^^ + 16 | + `---- + help: Remove the debugger statement + + ! eslint(no-console): Unexpected console statement. + ,-[test.vue:32:1] + 31 | // oxlint-disable-next-line no-debugger, no-for-loop + 32 | console.log("complete line"); + : ^^^^^^^^^^^ + 33 | + `---- + help: Delete this console statement. + +Found 11 warnings and 0 errors. +Finished in ms on 5 files with 94 rules using 1 threads. +---------- +CLI result: LintSucceeded +---------- diff --git a/apps/oxlint/src/snapshots/fixtures__report_unused_directives_-c .oxlintrc-with-rudd.json@oxlint.snap b/apps/oxlint/src/snapshots/fixtures__report_unused_directives_-c .oxlintrc-with-rudd.json@oxlint.snap new file mode 100644 index 0000000000000..74bafbdac250d --- /dev/null +++ b/apps/oxlint/src/snapshots/fixtures__report_unused_directives_-c .oxlintrc-with-rudd.json@oxlint.snap @@ -0,0 +1,327 @@ +--- +source: apps/oxlint/src/tester.rs +--- +########## +arguments: -c .oxlintrc-with-rudd.json +working directory: fixtures/report_unused_directives +---------- + + ! Unused eslint-disable directive (no problems were reported). + ,-[test-multiple-scripts.vue:6:3] + 5 | + `---- + help: Remove the debugger statement + + ! Unused eslint-disable directive (no problems were reported). + ,-[test-multiple-scripts.vue:17:3] + 16 | + 17 | // eslint-disable-next-line no-unused-vars + : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 18 | const unusedVariable2 = 100; + `---- + + ! Unused eslint-disable directive (no problems were reported). + ,-[test-multiple-scripts.vue:30:3] + 29 | + 30 | // oxlint-disable-next-line no-debugger, no-for-loop + : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 31 | console.log("complete line"); + `---- + + ! eslint(no-console): Unexpected console statement. + ,-[test-multiple-scripts.vue:31:1] + 30 | // oxlint-disable-next-line no-debugger, no-for-loop + 31 | console.log("complete line"); + : ^^^^^^^^^^^ + 32 | + `---- + help: Delete this console statement. + + ! Unused eslint-disable directive (no problems were reported). + ,-[test.astro:5:3] + 4 | + 5 | // eslint-disable-next-line no-debugger + : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 6 | console.log('This is a test'); + `---- + + ! Unused eslint-disable directive (no problems were reported). + ,-[test.astro:10:3] + 9 | + 10 | // eslint-disable-next-line no-console + : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 11 | debugger; + `---- + + ! eslint(no-debugger): `debugger` statement is not allowed + ,-[test.astro:11:1] + 10 | // eslint-disable-next-line no-console + 11 | debugger; + : ^^^^^^^^^ + 12 | --- + `---- + help: Remove the debugger statement + + ! Unused eslint-disable directive (no problems were reported). + ,-[test.astro:17:3] + 16 | + `---- + + ! Unused eslint-disable directive (no problems were reported). + ,-[test.js:4:3] + 3 | + 4 | // eslint-disable-next-line no-debugger + : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 5 | console.log('This is a test'); + `---- + + ! Unused eslint-disable directive (no problems were reported). + ,-[test.js:9:3] + 8 | + 9 | // eslint-disable-next-line no-console + : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 10 | debugger; + `---- + + ! eslint(no-debugger): `debugger` statement is not allowed + ,-[test.js:10:1] + 9 | // eslint-disable-next-line no-console + 10 | debugger; + : ^^^^^^^^^ + 11 | + `---- + help: Remove the debugger statement + + ! Unused eslint-disable directive (no problems were reported from no-debugger). + ,-[test.js:23:41] + 22 | + 23 | // eslint-disable-next-line no-console, no-debugger + : ^^^^^^^^^^^ + 24 | console.log('no'); + `---- + + ! Unused eslint-disable directive (no problems were reported). + ,-[test.js:26:3] + 25 | + 26 | // oxlint-disable-next-line no-debugger, no-for-loop + : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 27 | console.log("complete line"); + `---- + + ! eslint(no-console): Unexpected console statement. + ,-[test.js:27:1] + 26 | // oxlint-disable-next-line no-debugger, no-for-loop + 27 | console.log("complete line"); + : ^^^^^^^^^^^ + 28 | + `---- + help: Delete this console statement. + + ! Unused eslint-enable directive (no matching eslint-disable directives were found). + ,-[test.js:31:3] + 30 | + 31 | // eslint-enable + : ^^^^^^^^^^^^^^ + `---- + + ! Unused eslint-disable directive (no problems were reported). + ,-[test.svelte:5:3] + 4 | + 5 | // eslint-disable-next-line no-debugger + : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 6 | console.log('This is a test'); + `---- + + ! Unused eslint-disable directive (no problems were reported). + ,-[test.svelte:10:3] + 9 | + 10 | // eslint-disable-next-line no-console + : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 11 | debugger; + `---- + + ! eslint(no-debugger): `debugger` statement is not allowed + ,-[test.svelte:11:1] + 10 | // eslint-disable-next-line no-console + 11 | debugger; + : ^^^^^^^^^ + 12 | + `---- + help: Remove the debugger statement + + ! Unused eslint-disable directive (no problems were reported). + ,-[test.svelte:13:3] + 12 | + 13 | // eslint-disable-next-line no-unused-vars + : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 14 | const unusedVariable2 = 100; + `---- + + ! Unused eslint-disable directive (no problems were reported from no-debugger). + ,-[test.svelte:24:41] + 23 | + 24 | // eslint-disable-next-line no-console, no-debugger + : ^^^^^^^^^^^ + 25 | console.log('no'); + `---- + + ! Unused eslint-disable directive (no problems were reported). + ,-[test.svelte:27:3] + 26 | + 27 | // oxlint-disable-next-line no-debugger, no-for-loop + : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 28 | console.log("complete line"); + `---- + + ! eslint(no-console): Unexpected console statement. + ,-[test.svelte:28:1] + 27 | // oxlint-disable-next-line no-debugger, no-for-loop + 28 | console.log("complete line"); + : ^^^^^^^^^^^ + 29 | + `---- + help: Delete this console statement. + + ! Unused eslint-enable directive (no matching eslint-disable directives were found). + ,-[test.svelte:32:3] + 31 | + 32 | // eslint-enable + : ^^^^^^^^^^^^^^ + 33 | + `---- + + ! Unused eslint-disable directive (no problems were reported). + ,-[test.vue:9:3] + 8 | + 9 | // eslint-disable-next-line no-debugger + : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 10 | console.log('This is a test'); + `---- + + ! Unused eslint-disable directive (no problems were reported). + ,-[test.vue:14:3] + 13 | + 14 | // eslint-disable-next-line no-console + : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 15 | debugger; + `---- + + ! eslint(no-debugger): `debugger` statement is not allowed + ,-[test.vue:15:1] + 14 | // eslint-disable-next-line no-console + 15 | debugger; + : ^^^^^^^^^ + 16 | + `---- + help: Remove the debugger statement + + ! Unused eslint-disable directive (no problems were reported). + ,-[test.vue:17:3] + 16 | + 17 | // eslint-disable-next-line no-unused-vars + : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 18 | const unusedVariable2 = 100; + `---- + + ! Unused eslint-disable directive (no problems were reported from no-debugger). + ,-[test.vue:28:41] + 27 | + 28 | // eslint-disable-next-line no-console, no-debugger + : ^^^^^^^^^^^ + 29 | console.log('no'); + `---- + + ! Unused eslint-disable directive (no problems were reported). + ,-[test.vue:31:3] + 30 | + 31 | // oxlint-disable-next-line no-debugger, no-for-loop + : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 32 | console.log("complete line"); + `---- + + ! eslint(no-console): Unexpected console statement. + ,-[test.vue:32:1] + 31 | // oxlint-disable-next-line no-debugger, no-for-loop + 32 | console.log("complete line"); + : ^^^^^^^^^^^ + 33 | + `---- + help: Delete this console statement. + + ! Unused eslint-enable directive (no matching eslint-disable directives were found). + ,-[test.vue:36:3] + 35 | + 36 | // eslint-enable + : ^^^^^^^^^^^^^^ + 37 | + `---- + +Found 38 warnings and 0 errors. +Finished in ms on 5 files with 94 rules using 1 threads. +---------- +CLI result: LintSucceeded +---------- diff --git a/crates/oxc_linter/fixtures/extends_config/options/report_unused_disable_directives_allow.json b/crates/oxc_linter/fixtures/extends_config/options/report_unused_disable_directives_allow.json new file mode 100644 index 0000000000000..5747a8ef1e3eb --- /dev/null +++ b/crates/oxc_linter/fixtures/extends_config/options/report_unused_disable_directives_allow.json @@ -0,0 +1,5 @@ +{ + "options": { + "reportUnusedDisableDirectives": "allow" + } +} diff --git a/crates/oxc_linter/fixtures/extends_config/options/report_unused_disable_directives_error.json b/crates/oxc_linter/fixtures/extends_config/options/report_unused_disable_directives_error.json new file mode 100644 index 0000000000000..96175673f36ff --- /dev/null +++ b/crates/oxc_linter/fixtures/extends_config/options/report_unused_disable_directives_error.json @@ -0,0 +1,5 @@ +{ + "options": { + "reportUnusedDisableDirectives": "error" + } +} diff --git a/crates/oxc_linter/fixtures/extends_config/options/report_unused_disable_directives_warn.json b/crates/oxc_linter/fixtures/extends_config/options/report_unused_disable_directives_warn.json new file mode 100644 index 0000000000000..0d82cbce6d0f8 --- /dev/null +++ b/crates/oxc_linter/fixtures/extends_config/options/report_unused_disable_directives_warn.json @@ -0,0 +1,5 @@ +{ + "options": { + "reportUnusedDisableDirectives": "warn" + } +} diff --git a/crates/oxc_linter/src/config/config_builder.rs b/crates/oxc_linter/src/config/config_builder.rs index 7a4ff3647a07a..4fb42ab7d023f 100644 --- a/crates/oxc_linter/src/config/config_builder.rs +++ b/crates/oxc_linter/src/config/config_builder.rs @@ -352,6 +352,11 @@ impl ConfigStoreBuilder { self.config.options.max_warnings } + #[inline] + pub fn report_unused_disable_directives(&self) -> Option { + self.config.options.report_unused_disable_directives + } + #[cfg(test)] pub(crate) fn with_rule(mut self, rule: RuleEnum, severity: AllowWarnDeny) -> Self { self.rules.insert(rule, severity); @@ -1426,6 +1431,13 @@ mod test { ); assert_eq!(config.base.config.options.type_check, Some(false)); + let config = + config_store_from_str(r#"{ "options": {"reportUnusedDisableDirectives": "error" } }"#); + assert_eq!( + config.base.config.options.report_unused_disable_directives, + Some(AllowWarnDeny::Deny) + ); + let config = config_store_from_str( r#"{ "extends": ["fixtures/extends_config/options/deny_warnings_true.json"] }"#, ); diff --git a/crates/oxc_linter/src/config/config_store.rs b/crates/oxc_linter/src/config/config_store.rs index f2006e4c0803a..f132183ab11a2 100644 --- a/crates/oxc_linter/src/config/config_store.rs +++ b/crates/oxc_linter/src/config/config_store.rs @@ -340,6 +340,11 @@ impl ConfigStore { self.base.base.config.options.max_warnings } + /// The severity for reporting unused disable directives, if set in the root config. + pub fn report_unused_disable_directives(&self) -> Option { + self.base.base.config.options.report_unused_disable_directives + } + pub(crate) fn get_related_config(&self, path: &Path) -> &Config { if self.nested_configs.is_empty() { &self.base @@ -1249,6 +1254,38 @@ mod test { assert!(!store.deny_warnings()); } + #[test] + fn test_report_unused_disable_directives_from_root_config() { + let base = Config::new( + vec![], + vec![], + OxlintCategories::default(), + LintConfig { + options: OxlintOptions { + report_unused_disable_directives: Some(AllowWarnDeny::Warn), + ..OxlintOptions::default() + }, + ..LintConfig::default() + }, + ResolvedOxlintOverrides::new(vec![]), + ); + let store = ConfigStore::new(base, FxHashMap::default(), ExternalPluginStore::default()); + assert_eq!(store.report_unused_disable_directives(), Some(AllowWarnDeny::Warn)); + } + + #[test] + fn test_report_unused_disable_directives_none_by_default() { + let base = Config::new( + vec![], + vec![], + OxlintCategories::default(), + LintConfig::default(), + ResolvedOxlintOverrides::new(vec![]), + ); + let store = ConfigStore::new(base, FxHashMap::default(), ExternalPluginStore::default()); + assert_eq!(store.report_unused_disable_directives(), None); + } + #[test] fn test_max_warnings_from_root_config() { let base = Config::new( diff --git a/crates/oxc_linter/src/config/oxlintrc.rs b/crates/oxc_linter/src/config/oxlintrc.rs index ec84d6b6408d9..7155cfcfa98ca 100644 --- a/crates/oxc_linter/src/config/oxlintrc.rs +++ b/crates/oxc_linter/src/config/oxlintrc.rs @@ -9,7 +9,7 @@ use serde::{Deserialize, Serialize}; use oxc_diagnostics::OxcDiagnostic; -use crate::{LintPlugins, utils::read_to_string}; +use crate::{AllowWarnDeny, LintPlugins, utils::read_to_string}; use super::{ categories::OxlintCategories, @@ -46,6 +46,13 @@ pub struct OxlintOptions { /// Equivalent to passing `--max-warnings` on the CLI. #[serde(skip_serializing_if = "Option::is_none")] pub max_warnings: Option, + /// Report unused disable directives (e.g. `// oxlint-disable-line` or `// eslint-disable-line`). + /// + /// Equivalent to passing `--report-unused-disable-directives-severity` on the CLI. + /// CLI flags take precedence over this value when both are set. + /// Only supported in the root configuration file. + #[serde(skip_serializing_if = "Option::is_none")] + pub report_unused_disable_directives: Option, } impl OxlintOptions { @@ -55,6 +62,7 @@ impl OxlintOptions { && self.type_check.is_none() && self.deny_warnings.is_none() && self.max_warnings.is_none() + && self.report_unused_disable_directives.is_none() } #[must_use] @@ -64,6 +72,9 @@ impl OxlintOptions { type_check: self.type_check.or(other.type_check), deny_warnings: self.deny_warnings.or(other.deny_warnings), max_warnings: self.max_warnings.or(other.max_warnings), + report_unused_disable_directives: self + .report_unused_disable_directives + .or(other.report_unused_disable_directives), } } } @@ -416,6 +427,7 @@ mod test { assert_eq!(config.options.type_check, None); assert_eq!(config.options.deny_warnings, None); assert_eq!(config.options.max_warnings, None); + assert_eq!(config.options.report_unused_disable_directives, None); } #[test] @@ -451,6 +463,24 @@ mod test { let config: Result = serde_json::from_value(json!({ "options": { "maxWarnings": -1 } })); assert!(config.is_err()); + + let config: Oxlintrc = serde_json::from_value( + json!({ "options": { "reportUnusedDisableDirectives": "warn" } }), + ) + .unwrap(); + assert_eq!(config.options.report_unused_disable_directives, Some(AllowWarnDeny::Warn)); + + let config: Oxlintrc = serde_json::from_value( + json!({ "options": { "reportUnusedDisableDirectives": "error" } }), + ) + .unwrap(); + assert_eq!(config.options.report_unused_disable_directives, Some(AllowWarnDeny::Deny)); + + let config: Oxlintrc = serde_json::from_value( + json!({ "options": { "reportUnusedDisableDirectives": "off" } }), + ) + .unwrap(); + assert_eq!(config.options.report_unused_disable_directives, Some(AllowWarnDeny::Allow)); } #[test] @@ -466,6 +496,10 @@ mod test { let config: Result = serde_json::from_value(json!({ "maxWarnings": 1 })); assert!(config.is_err()); + + let config: Result = + serde_json::from_value(json!({ "reportUnusedDisableDirectives": "warn" })); + assert!(config.is_err()); } #[test] @@ -487,6 +521,31 @@ mod test { assert_eq!(merged.options.type_check, Some(false)); assert_eq!(merged.options.deny_warnings, Some(true)); assert_eq!(merged.options.max_warnings, Some(1)); + + // root wins over base for reportUnusedDisableDirectives + let mut root: Oxlintrc = serde_json::from_value( + json!({ "options": { "reportUnusedDisableDirectives": "error" } }), + ) + .unwrap(); + root.path = PathBuf::from("/root/.oxlintrc.json"); + let mut base: Oxlintrc = serde_json::from_value( + json!({ "options": { "reportUnusedDisableDirectives": "warn" } }), + ) + .unwrap(); + base.path = PathBuf::from("/root/base.json"); + let merged = root.merge(base); + assert_eq!(merged.options.report_unused_disable_directives, Some(AllowWarnDeny::Deny)); + + // base value propagates when root does not set the field + let mut root: Oxlintrc = serde_json::from_value(json!({})).unwrap(); + root.path = PathBuf::from("/root/.oxlintrc.json"); + let mut base: Oxlintrc = serde_json::from_value( + json!({ "options": { "reportUnusedDisableDirectives": "warn" } }), + ) + .unwrap(); + base.path = PathBuf::from("/root/base.json"); + let merged = root.merge(base); + assert_eq!(merged.options.report_unused_disable_directives, Some(AllowWarnDeny::Warn)); } #[test] diff --git a/npm/oxlint/configuration_schema.json b/npm/oxlint/configuration_schema.json index dc05e556c3b64..c3adc16c8996b 100644 --- a/npm/oxlint/configuration_schema.json +++ b/npm/oxlint/configuration_schema.json @@ -512,6 +512,18 @@ "minimum": 0.0, "markdownDescription": "Specify a warning threshold. Exits with an error status if warnings exceed this value.\n\nEquivalent to passing `--max-warnings` on the CLI." }, + "reportUnusedDisableDirectives": { + "description": "Report unused disable directives (e.g. `// oxlint-disable-line` or `// eslint-disable-line`).\n\nEquivalent to passing `--report-unused-disable-directives-severity` on the CLI.\nCLI flags take precedence over this value when both are set.\nOnly supported in the root configuration file.", + "anyOf": [ + { + "$ref": "#/definitions/AllowWarnDeny" + }, + { + "type": "null" + } + ], + "markdownDescription": "Report unused disable directives (e.g. `// oxlint-disable-line` or `// eslint-disable-line`).\n\nEquivalent to passing `--report-unused-disable-directives-severity` on the CLI.\nCLI flags take precedence over this value when both are set.\nOnly supported in the root configuration file." + }, "typeAware": { "description": "Enable rules that require type information.\n\nEquivalent to passing `--type-aware` on the CLI.", "type": [ diff --git a/tasks/website_linter/src/snapshots/schema_json.snap b/tasks/website_linter/src/snapshots/schema_json.snap index 1d11aafce5785..b9fc4a4d2b76f 100644 --- a/tasks/website_linter/src/snapshots/schema_json.snap +++ b/tasks/website_linter/src/snapshots/schema_json.snap @@ -516,6 +516,18 @@ expression: json "minimum": 0.0, "markdownDescription": "Specify a warning threshold. Exits with an error status if warnings exceed this value.\n\nEquivalent to passing `--max-warnings` on the CLI." }, + "reportUnusedDisableDirectives": { + "description": "Report unused disable directives (e.g. `// oxlint-disable-line` or `// eslint-disable-line`).\n\nEquivalent to passing `--report-unused-disable-directives-severity` on the CLI.\nCLI flags take precedence over this value when both are set.\nOnly supported in the root configuration file.", + "anyOf": [ + { + "$ref": "#/definitions/AllowWarnDeny" + }, + { + "type": "null" + } + ], + "markdownDescription": "Report unused disable directives (e.g. `// oxlint-disable-line` or `// eslint-disable-line`).\n\nEquivalent to passing `--report-unused-disable-directives-severity` on the CLI.\nCLI flags take precedence over this value when both are set.\nOnly supported in the root configuration file." + }, "typeAware": { "description": "Enable rules that require type information.\n\nEquivalent to passing `--type-aware` on the CLI.", "type": [ diff --git a/tasks/website_linter/src/snapshots/schema_markdown.snap b/tasks/website_linter/src/snapshots/schema_markdown.snap index 412389d5474b6..9ef35c2d06bfd 100644 --- a/tasks/website_linter/src/snapshots/schema_markdown.snap +++ b/tasks/website_linter/src/snapshots/schema_markdown.snap @@ -324,6 +324,17 @@ Specify a warning threshold. Exits with an error status if warnings exceed this Equivalent to passing `--max-warnings` on the CLI. +### options.reportUnusedDisableDirectives + + + +Report unused disable directives (e.g. `// oxlint-disable-line` or `// eslint-disable-line`). + +Equivalent to passing `--report-unused-disable-directives-severity` on the CLI. +CLI flags take precedence over this value when both are set. +Only supported in the root configuration file. + + ### options.typeAware type: `boolean`