diff --git a/apps/oxlint/test/fixtures/basic_with_disable_directive_file_start/.oxlintrc.json b/apps/oxlint/test/fixtures/basic_with_disable_directive_file_start/.oxlintrc.json new file mode 100644 index 0000000000000..1bb5d4b91683d --- /dev/null +++ b/apps/oxlint/test/fixtures/basic_with_disable_directive_file_start/.oxlintrc.json @@ -0,0 +1,9 @@ +{ + "jsPlugins": ["./plugin.ts"], + "categories": { "correctness": "off" }, + "rules": { + "basic-custom-plugin/rule-point": "error", + "basic-custom-plugin/rule-wide": "error", + "no-console": "error" + } +} diff --git a/apps/oxlint/test/fixtures/basic_with_disable_directive_file_start/files/index.js b/apps/oxlint/test/fixtures/basic_with_disable_directive_file_start/files/index.js new file mode 100644 index 0000000000000..44b1a1a16f110 --- /dev/null +++ b/apps/oxlint/test/fixtures/basic_with_disable_directive_file_start/files/index.js @@ -0,0 +1 @@ +/* oxlint-disable basic-custom-plugin/rule-point, basic-custom-plugin/rule-wide */ diff --git a/apps/oxlint/test/fixtures/basic_with_disable_directive_file_start/files/index2.js b/apps/oxlint/test/fixtures/basic_with_disable_directive_file_start/files/index2.js new file mode 100644 index 0000000000000..9c8d09f0153b5 --- /dev/null +++ b/apps/oxlint/test/fixtures/basic_with_disable_directive_file_start/files/index2.js @@ -0,0 +1,3 @@ +/* oxlint-disable-file no-console */ + +console.log("hello world!"); diff --git a/apps/oxlint/test/fixtures/basic_with_disable_directive_file_start/options.json b/apps/oxlint/test/fixtures/basic_with_disable_directive_file_start/options.json new file mode 100644 index 0000000000000..caf91123ca548 --- /dev/null +++ b/apps/oxlint/test/fixtures/basic_with_disable_directive_file_start/options.json @@ -0,0 +1,3 @@ +{ + "args": ["--report-unused-disable-directives"] +} diff --git a/apps/oxlint/test/fixtures/basic_with_disable_directive_file_start/output.snap.md b/apps/oxlint/test/fixtures/basic_with_disable_directive_file_start/output.snap.md new file mode 100644 index 0000000000000..4aa93a6299b58 --- /dev/null +++ b/apps/oxlint/test/fixtures/basic_with_disable_directive_file_start/output.snap.md @@ -0,0 +1,12 @@ +# Exit code +0 + +# stdout +``` +Found 0 warnings and 0 errors. +Finished in Xms on 1 file with 2 rules using X threads. +``` + +# stderr +``` +``` diff --git a/apps/oxlint/test/fixtures/basic_with_disable_directive_file_start/plugin.ts b/apps/oxlint/test/fixtures/basic_with_disable_directive_file_start/plugin.ts new file mode 100644 index 0000000000000..131a36abdc05c --- /dev/null +++ b/apps/oxlint/test/fixtures/basic_with_disable_directive_file_start/plugin.ts @@ -0,0 +1,40 @@ +import type { Plugin } from "#oxlint/plugins"; + +const plugin: Plugin = { + meta: { + name: "basic-custom-plugin", + }, + rules: { + "rule-point": { + create(_context) { + return { + Program(_program) { + _context.report({ + message: "oops", + loc: { + start: { line: 0, column: 0 }, + }, + }); + }, + }; + }, + }, + "rule-wide": { + create(_context) { + return { + Program(_program) { + _context.report({ + message: "oops (wide)", + loc: { + start: { line: 1, column: 0 }, + end: { line: 1, column: 1 }, + }, + }); + }, + }; + }, + }, + }, +}; + +export default plugin; diff --git a/crates/oxc_linter/src/disable_directives.rs b/crates/oxc_linter/src/disable_directives.rs index 523669d4c1db0..d3bb7d87de9be 100644 --- a/crates/oxc_linter/src/disable_directives.rs +++ b/crates/oxc_linter/src/disable_directives.rs @@ -182,6 +182,11 @@ impl DisableDirectives { } pub fn contains(&self, rule_name: &str, span: Span) -> bool { + // Some diagnostics are point locations (start == end). Treat them as + // 1-byte ranges for interval lookup/overlap checks. + let diagnostic_end = + if span.start == span.end { span.end.saturating_add(1) } else { span.end }; + // For `eslint-disable-next-line` and `eslint-disable-line` directives, we only check // if the diagnostic's starting position falls within the disabled interval. // This prevents suppressing diagnostics for larger constructs (like functions) that @@ -193,7 +198,7 @@ impl DisableDirectives { // are still suppressed. let matched_intervals = self .intervals - .find(span.start, span.end) + .find(span.start, diagnostic_end) .filter(|interval| { // Check if this rule should be disabled let rule_matches = match &interval.val { @@ -230,7 +235,7 @@ impl DisableDirectives { } } else { // For regular disable directives, check if there's any overlap - span.start < interval.stop && span.end > interval.start + span.start < interval.stop && diagnostic_end > interval.start } }) .map(|interval| interval.val.clone()) @@ -377,7 +382,9 @@ impl DisableDirectivesBuilder { // `eslint-disable` if text.trim().is_empty() { if self.disable_all_start.is_none() { - self.disable_all_start = Some((comment_span.end, comment_span)); + // Start coverage at the beginning of the directive comment so + // top-of-file headers can suppress file-start diagnostics. + self.disable_all_start = Some((comment.span.start, comment_span)); } self.disable_rule_comments.push(DisableRuleComment { span: comment_span, @@ -493,7 +500,7 @@ impl DisableDirectivesBuilder { let mut rules = vec![]; Self::get_rule_names(text, rule_name_start, |rule_name, name_span| { self.disable_start_map.entry(rule_name.to_string()).or_insert(( - comment_span.end, + comment.span.start, name_span, comment_span, )); @@ -1391,4 +1398,22 @@ function test() { "eslint-disable-next-line should NOT suppress diagnostics on lines after the next line" ); } + + #[test] + fn test_disable_file_header_suppresses_file_start_diagnostic() { + test_directives( + |prefix| { + format!( + "/* {prefix}-disable no-console */\nconsole.log('still disabled by header');\n" + ) + }, + |_, directives| { + // Some rules report at file start (line 1, column 0), e.g. Program-level diagnostics. + // A header disable should still suppress those diagnostics. + assert!(directives.contains("no-console", Span::new(0, 1))); + // Program-level diagnostics can also be reported as zero-width points. + assert!(directives.contains("no-console", Span::new(0, 0))); + }, + ); + } }