Skip to content

Commit 8e3d9be

Browse files
1zumiiBoshen
andauthoredMar 19, 2025··
feat(linter): support --report-unused-disable-directive (#9223)
and `--report-unused-disable-directives-severity` closed #7544 --------- Co-authored-by: Boshen <boshenc@gmail.com>
1 parent 38ad787 commit 8e3d9be

File tree

12 files changed

+589
-44
lines changed

12 files changed

+589
-44
lines changed
 
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"rules": {
3+
"no-console": "warn",
4+
"no-debugger": "warn"
5+
}
6+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// eslint-disable
2+
const unusedVariable1 = 42;
3+
4+
// eslint-disable-next-line no-debugger
5+
console.log('This is a test');
6+
7+
// eslint-enable
8+
9+
// eslint-disable-next-line no-console
10+
debugger;
11+
12+
// eslint-disable-next-line no-unused-vars
13+
const unusedVariable2 = 100;
14+
15+
function testFunction() {
16+
// eslint-disable-next-line no-console
17+
console.log('Inside test function');
18+
}
19+
20+
testFunction();
21+
22+
// eslint-enable

‎apps/oxlint/src/command/lint.rs

+81
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ pub struct LintCommand {
4646
#[bpaf(switch, hide_usage)]
4747
pub disable_nested_config: bool,
4848

49+
#[bpaf(external)]
50+
pub inline_config_options: InlineConfigOptions,
51+
4952
/// Single file, single path or list of paths
5053
#[bpaf(positional("PATH"), many, guard(validate_paths, PATHS_ERROR_MESSAGE))]
5154
pub paths: Vec<PathBuf>,
@@ -349,6 +352,36 @@ impl EnablePlugins {
349352
}
350353
}
351354

355+
#[derive(Debug, Clone, PartialEq, Eq, Bpaf)]
356+
pub enum ReportUnusedDirectives {
357+
WithoutSeverity(
358+
/// Report directive comments like `// eslint-disable-line` when no errors would have been reported on that line anyway.
359+
// More information at <https://eslint.org/docs/latest/use/command-line-interface#--report-unused-disable-directives>
360+
#[bpaf(long("report-unused-disable-directives"), switch, hide_usage)]
361+
bool,
362+
),
363+
WithSeverity(
364+
/// Same as `--report-unused-disable-directives`, but allows you to specify the severity level of the reported errors.
365+
/// Only one of these two options can be used at a time.
366+
#[bpaf(
367+
long("report-unused-disable-directives-severity"),
368+
argument::<String>("SEVERITY"),
369+
guard(|s| AllowWarnDeny::try_from(s.as_str()).is_ok(), "Invalid severity value"),
370+
map(|s| AllowWarnDeny::try_from(s.as_str()).unwrap()), // guard ensures try_from will be Ok
371+
optional,
372+
hide_usage
373+
)]
374+
Option<AllowWarnDeny>,
375+
),
376+
}
377+
378+
/// Inline Configuration Comments
379+
#[derive(Debug, Clone, Bpaf)]
380+
pub struct InlineConfigOptions {
381+
#[bpaf(external)]
382+
pub report_unused_directives: ReportUnusedDirectives,
383+
}
384+
352385
#[cfg(test)]
353386
mod plugins {
354387
use oxc_linter::LintPlugins;
@@ -526,3 +559,51 @@ mod lint_options {
526559
assert!(!options.disable_nested_config);
527560
}
528561
}
562+
563+
#[cfg(test)]
564+
mod inline_config_options {
565+
use oxc_linter::AllowWarnDeny;
566+
567+
use super::{LintCommand, ReportUnusedDirectives, lint_command};
568+
569+
fn get_lint_options(arg: &str) -> LintCommand {
570+
let args = arg.split(' ').map(std::string::ToString::to_string).collect::<Vec<_>>();
571+
lint_command().run_inner(args.as_slice()).unwrap()
572+
}
573+
574+
#[test]
575+
fn default() {
576+
let options = get_lint_options(".");
577+
assert_eq!(
578+
options.inline_config_options.report_unused_directives,
579+
ReportUnusedDirectives::WithoutSeverity(false)
580+
);
581+
}
582+
583+
#[test]
584+
fn without_severity() {
585+
let options = get_lint_options("--report-unused-disable-directives");
586+
assert_eq!(
587+
options.inline_config_options.report_unused_directives,
588+
ReportUnusedDirectives::WithoutSeverity(true)
589+
);
590+
}
591+
592+
#[test]
593+
fn with_severity_warn() {
594+
let options = get_lint_options("--report-unused-disable-directives-severity=warn");
595+
assert_eq!(
596+
options.inline_config_options.report_unused_directives,
597+
ReportUnusedDirectives::WithSeverity(Some(AllowWarnDeny::Warn))
598+
);
599+
}
600+
601+
#[test]
602+
fn with_severity_error() {
603+
let options = get_lint_options("--report-unused-disable-directives-severity error");
604+
assert_eq!(
605+
options.inline_config_options.report_unused_directives,
606+
ReportUnusedDirectives::WithSeverity(Some(AllowWarnDeny::Deny))
607+
);
608+
}
609+
}

‎apps/oxlint/src/command/mod.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use bpaf::Bpaf;
77

88
pub use self::{
99
ignore::IgnoreOptions,
10-
lint::{LintCommand, OutputOptions, WarningOptions, lint_command},
10+
lint::{LintCommand, OutputOptions, ReportUnusedDirectives, WarningOptions, lint_command},
1111
};
1212

1313
const VERSION: &str = match option_env!("OXC_VERSION") {

‎apps/oxlint/src/lint.rs

+19-2
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ use rustc_hash::{FxHashMap, FxHashSet};
1717
use serde_json::Value;
1818

1919
use crate::{
20-
cli::{CliRunResult, LintCommand, MiscOptions, Runner, WarningOptions},
20+
cli::{CliRunResult, LintCommand, MiscOptions, ReportUnusedDirectives, Runner, WarningOptions},
2121
output_formatter::{LintCommandInfo, OutputFormatter},
2222
walk::{Extensions, Walk},
2323
};
@@ -57,6 +57,7 @@ impl Runner for LintRunner {
5757
enable_plugins,
5858
misc_options,
5959
disable_nested_config,
60+
inline_config_options,
6061
..
6162
} = self.options;
6263

@@ -334,11 +335,20 @@ impl Runner for LintRunner {
334335
}
335336
};
336337

338+
let report_unused_directives = match inline_config_options.report_unused_directives {
339+
ReportUnusedDirectives::WithoutSeverity(true) => Some(AllowWarnDeny::Warn),
340+
ReportUnusedDirectives::WithSeverity(Some(severity)) => Some(severity),
341+
_ => None,
342+
};
343+
337344
let linter = if use_nested_config {
338345
Linter::new_with_nested_configs(LintOptions::default(), lint_config, nested_configs)
339346
.with_fix(fix_options.fix_kind())
347+
.with_report_unused_directives(report_unused_directives)
340348
} else {
341-
Linter::new(LintOptions::default(), lint_config).with_fix(fix_options.fix_kind())
349+
Linter::new(LintOptions::default(), lint_config)
350+
.with_fix(fix_options.fix_kind())
351+
.with_report_unused_directives(report_unused_directives)
342352
};
343353

344354
let tsconfig = basic_options.tsconfig;
@@ -968,6 +978,13 @@ mod test {
968978
.test_and_snapshot(args);
969979
}
970980

981+
#[test]
982+
fn test_report_unused_directives() {
983+
let args = &["-c", ".oxlintrc.json", "--report-unused-disable-directives", "test.js"];
984+
985+
Tester::new().with_cwd("fixtures/report_unused_directives".into()).test_and_snapshot(args);
986+
}
987+
971988
#[test]
972989
fn test_adjust_ignore_patterns() {
973990
let base = PathBuf::from("/project/root");
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
---
2+
source: apps/oxlint/src/tester.rs
3+
---
4+
##########
5+
arguments: -c .oxlintrc.json --report-unused-disable-directives test.js
6+
working directory: fixtures/report_unused_directives
7+
----------
8+
9+
! Unused eslint-disable directive (no problems were reported from no-debugger).
10+
,-[test.js:4:3]
11+
3 |
12+
4 | // eslint-disable-next-line no-debugger
13+
: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
14+
5 | console.log('This is a test');
15+
`----
16+
17+
! Unused eslint-disable directive (no problems were reported from no-console).
18+
,-[test.js:9:3]
19+
8 |
20+
9 | // eslint-disable-next-line no-console
21+
: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
22+
10 | debugger;
23+
`----
24+
25+
! ]8;;https://oxc.rs/docs/guide/usage/linter/rules/eslint/no-debugger.html\eslint(no-debugger)]8;;\: `debugger` statement is not allowed
26+
,-[test.js:10:1]
27+
9 | // eslint-disable-next-line no-console
28+
10 | debugger;
29+
: ^^^^^^^^^
30+
11 |
31+
`----
32+
help: Delete this code.
33+
34+
! Unused eslint-enable directive (no matching eslint-disable directives were found).
35+
,-[test.js:22:3]
36+
21 |
37+
22 | // eslint-enable
38+
: ^^^^^^^^^^^^^^
39+
`----
40+
41+
Found 4 warnings and 0 errors.
42+
Finished in <variable>ms on 1 file with 101 rules using 1 threads.
43+
----------
44+
CLI result: LintSucceeded
45+
----------

‎crates/oxc_linter/src/context/host.rs

+61-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
use std::{cell::RefCell, path::Path, rc::Rc, sync::Arc};
1+
use std::{borrow::Cow, cell::RefCell, path::Path, rc::Rc, sync::Arc};
22

3+
use oxc_diagnostics::{OxcDiagnostic, Severity};
34
use oxc_semantic::Semantic;
4-
use oxc_span::SourceType;
5+
use oxc_span::{SourceType, Span};
56

67
use crate::{
78
FrameworkFlags, RuleWithSeverity,
@@ -158,9 +159,66 @@ impl<'a> ContextHost<'a> {
158159
self.diagnostics.borrow_mut().push(diagnostic);
159160
}
160161

162+
// Append a list of diagnostics. Only used in report_unused_directives.
163+
fn append_diagnostics(&self, diagnostics: Vec<Message<'a>>) {
164+
self.diagnostics.borrow_mut().extend(diagnostics);
165+
}
166+
167+
/// report unused enable/disable directives, add these as Messages to diagnostics
168+
pub fn report_unused_directives(&self, rule_severity: Severity) {
169+
let unused_enable_comments = self.disable_directives.unused_enable_comments();
170+
let unused_disable_comments = self.disable_directives.collect_unused_disable_comments();
171+
let mut unused_directive_diagnostics: Vec<(Cow<str>, Span)> =
172+
Vec::with_capacity(unused_enable_comments.len() + unused_disable_comments.len());
173+
174+
// report unused disable
175+
// relate to lint result, check after linter run finish
176+
let unused_disable_comments = self.disable_directives.collect_unused_disable_comments();
177+
let message_for_disable = "Unused eslint-disable directive (no problems were reported).";
178+
for (rule_name, disable_comment_span) in unused_disable_comments {
179+
unused_directive_diagnostics.push((
180+
rule_name.map_or(Cow::Borrowed(message_for_disable), |name| {
181+
Cow::Owned(format!(
182+
"Unused eslint-disable directive (no problems were reported from {name})."
183+
))
184+
}),
185+
disable_comment_span,
186+
));
187+
}
188+
189+
// report unused enable
190+
// not relate to lint result, check during comment directives' construction
191+
let message_for_enable =
192+
"Unused eslint-enable directive (no matching eslint-disable directives were found).";
193+
for (rule_name, enable_comment_span) in self.disable_directives.unused_enable_comments() {
194+
unused_directive_diagnostics.push((
195+
rule_name.map_or(Cow::Borrowed(message_for_enable), |name| {
196+
Cow::Owned(format!(
197+
"Unused eslint-enable directive (no matching eslint-disable directives were found for {name})."
198+
))
199+
}),
200+
*enable_comment_span,
201+
));
202+
}
203+
204+
self.append_diagnostics(
205+
unused_directive_diagnostics
206+
.into_iter()
207+
.map(|(message, span)| {
208+
Message::new(
209+
OxcDiagnostic::error(message).with_label(span).with_severity(rule_severity),
210+
// TODO: fixer
211+
// if all rules in the same directive are unused, fixer should remove the entire comment
212+
None,
213+
)
214+
})
215+
.collect(),
216+
);
217+
}
218+
161219
/// Take ownership of all diagnostics collected during linting.
162220
pub fn take_diagnostics(&self) -> Vec<Message<'a>> {
163-
// NOTE: diagnostics are only ever borrowed here and in push_diagnostic.
221+
// NOTE: diagnostics are only ever borrowed here and in push_diagnostic, append_diagnostics.
164222
// The latter drops the reference as soon as the function returns, so
165223
// this should never panic.
166224
let mut messages = self.diagnostics.borrow_mut();

‎crates/oxc_linter/src/disable_directives.rs

+325-38
Large diffs are not rendered by default.

‎crates/oxc_linter/src/lib.rs

+12
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,12 @@ impl Linter {
102102
self
103103
}
104104

105+
#[must_use]
106+
pub fn with_report_unused_directives(mut self, report_config: Option<AllowWarnDeny>) -> Self {
107+
self.options.report_unused_directive = report_config;
108+
self
109+
}
110+
105111
pub(crate) fn options(&self) -> &LintOptions {
106112
&self.options
107113
}
@@ -206,6 +212,12 @@ impl Linter {
206212
}
207213
}
208214

215+
if let Some(severity) = self.options.report_unused_directive {
216+
if severity.is_warn_deny() {
217+
ctx_host.report_unused_directives(severity.into());
218+
}
219+
}
220+
209221
ctx_host.take_diagnostics()
210222
}
211223

‎crates/oxc_linter/src/options/mod.rs

+1
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,5 @@ use crate::{FrameworkFlags, fixer::FixKind};
1212
pub struct LintOptions {
1313
pub fix: FixKind,
1414
pub framework_hints: FrameworkFlags,
15+
pub report_unused_directive: Option<AllowWarnDeny>,
1516
}

‎tasks/website/src/linter/snapshots/cli.snap

+8
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,14 @@ Arguments:
124124

125125

126126

127+
## Inline Configuration Comments
128+
- **` --report-unused-disable-directives`** &mdash;
129+
Report directive comments like `// eslint-disable-line` when no errors would have been reported on that line anyway.
130+
- **` --report-unused-disable-directives-severity`**=_`SEVERITY`_ &mdash;
131+
Same as `--report-unused-disable-directives`, but allows you to specify the severity level of the reported errors. Only one of these two options can be used at a time.
132+
133+
134+
127135
## Available positional items:
128136
- _`PATH`_ &mdash;
129137
Single file, single path or list of paths

‎tasks/website/src/linter/snapshots/cli_terminal.snap

+8
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,14 @@ Miscellaneous
7676
--print-config This option outputs the configuration to be used. When present, no
7777
linting is performed and only config-related options are valid.
7878

79+
Inline Configuration Comments
80+
--report-unused-disable-directives Report directive comments like `// eslint-disable-line`
81+
when no errors would have been reported on that line anyway.
82+
--report-unused-disable-directives-severity=SEVERITY Same as
83+
`--report-unused-disable-directives`, but allows you to specify the
84+
severity level of the reported errors. Only one of these two options
85+
can be used at a time.
86+
7987
Available positional items:
8088
PATH Single file, single path or list of paths
8189

0 commit comments

Comments
 (0)
Please sign in to comment.