diff --git a/crates/oxc_linter/src/rules.rs b/crates/oxc_linter/src/rules.rs index 08fdf0249bda3..22224a641bc94 100644 --- a/crates/oxc_linter/src/rules.rs +++ b/crates/oxc_linter/src/rules.rs @@ -124,6 +124,7 @@ mod typescript { pub mod no_var_requires; pub mod prefer_as_const; pub mod prefer_function_type; + pub mod prefer_ts_expect_error; pub mod triple_slash_reference; } @@ -422,6 +423,7 @@ oxc_macros::declare_all_lint_rules! { typescript::no_var_requires, typescript::prefer_as_const, typescript::prefer_function_type, + typescript::prefer_ts_expect_error, typescript::triple_slash_reference, jest::expect_expect, jest::max_expects, diff --git a/crates/oxc_linter/src/rules/typescript/prefer_ts_expect_error.rs b/crates/oxc_linter/src/rules/typescript/prefer_ts_expect_error.rs new file mode 100644 index 0000000000000..df2182141cced --- /dev/null +++ b/crates/oxc_linter/src/rules/typescript/prefer_ts_expect_error.rs @@ -0,0 +1,218 @@ +use oxc_ast::Comment; +use oxc_diagnostics::{ + miette::{self, Diagnostic}, + thiserror::Error, +}; +use oxc_macros::declare_oxc_lint; +use oxc_span::Span; + +use crate::fixer::Fix; +use crate::{context::LintContext, rule::Rule}; + +#[derive(Debug, Error, Diagnostic)] +#[error( + "typescript-eslint(prefer-ts-expect-error): Enforce using `@ts-expect-error` over `@ts-ignore`" +)] +#[diagnostic( + severity(warning), + help("Use \"@ts-expect-error\" to ensure an error is actually being suppressed.") +)] +struct PreferTsExpectErrorDiagnostic(#[label] pub Span); + +#[derive(Debug, Default, Clone)] +pub struct PreferTsExpectError; + +declare_oxc_lint!( + /// ### What it does + /// + /// Enforce using @ts-expect-error over @ts-ignore. + /// + /// ### Why is this bad? + /// TypeScript allows you to suppress all errors on a line by placing a comment starting with @ts-ignore or @ts-expect-error immediately before the erroring line. + /// The two directives work the same, except @ts-expect-error causes a type error if placed before a line that's not erroring in the first place. + /// + /// This means it's easy for @ts-ignores to be forgotten about, and remain in code even after the error they were suppressing is fixed. + /// This is dangerous, as if a new error arises on that line it'll be suppressed by the forgotten about @ts-ignore, and so be missed. + /// + /// ### Example + /// ```javascript + /// // @ts-ignore + /// const str: string = 1; + /// + /// /** + /// * Explaining comment + /// * + /// * @ts-ignore */ + /// const multiLine: number = 'value'; + /// ``` + PreferTsExpectError, + pedantic +); + +impl Rule for PreferTsExpectError { + fn run_once(&self, ctx: &LintContext) { + let comments = ctx.semantic().trivias().comments(); + + for (start, &comment) in comments { + let raw = &ctx.semantic().source_text()[*start as usize..comment.end() as usize]; + + if !is_valid_ts_ignore_present(comment, raw) { + continue; + } + + if comment.is_single_line() { + let comment_span = Span::new(*start - 2, comment.end()); + ctx.diagnostic_with_fix(PreferTsExpectErrorDiagnostic(comment_span), || { + Fix::new( + format!("//{}", raw.replace("@ts-ignore", "@ts-expect-error")), + comment_span, + ) + }); + } else { + let comment_span = Span::new(*start - 2, comment.end() + 2); + ctx.diagnostic_with_fix(PreferTsExpectErrorDiagnostic(comment_span), || { + Fix::new( + format!("/*{}*/", raw.replace("@ts-ignore", "@ts-expect-error")), + comment_span, + ) + }); + } + } + } +} + +fn get_last_comment_line(comment: Comment, raw: &str) -> String { + if comment.is_single_line() { + return String::from(raw); + } + + return String::from(raw.lines().last().unwrap_or(raw)); +} + +fn is_valid_ts_ignore_present(comment: Comment, raw: &str) -> bool { + let line = get_last_comment_line(comment, raw); + + if comment.is_single_line() { + test_single_line_comment(&line) + } else { + test_multi_line_comment(&line) + } +} + +fn test_single_line_comment(line: &str) -> bool { + line.trim_start_matches(|c: char| c.is_whitespace() || c == '/').starts_with("@ts-ignore") +} + +fn test_multi_line_comment(line: &str) -> bool { + line.trim_start_matches(|c: char| c.is_whitespace() || c == '/' || c == '*') + .starts_with("@ts-ignore") +} + +#[test] +fn test() { + use crate::tester::Tester; + + let pass = vec![ + "// @ts-nocheck", + "// @ts-check", + "// just a comment containing @ts-ignore somewhere", + " +{ +/* +just a comment containing @ts-ignore somewhere in a block +*/ +} + ", + "// @ts-expect-error", + " +if (false) { +// @ts-expect-error: Unreachable code error +console.log('hello'); +} + ", + " +/** +* Explaining comment +* +* @ts-expect-error +* +* Not last line +* */ + ", + ]; + + let fail = vec![ + "// @ts-ignore", + "// @ts-ignore: Suppress next line", + "///@ts-ignore: Suppress next line", + " +if (false) { +// @ts-ignore: Unreachable code error +console.log('hello'); +} + ", + "/* @ts-ignore */", + " +/** +* Explaining comment +* +* @ts-ignore */ + ", + "/* @ts-ignore in a single block */", + " +/* +// @ts-ignore in a block with single line comments */ + ", + ]; + + let fix = vec![ + ("// @ts-ignore", "// @ts-expect-error", None), + ("// @ts-ignore: Suppress next line", "// @ts-expect-error: Suppress next line", None), + ("///@ts-ignore: Suppress next line", "///@ts-expect-error: Suppress next line", None), + ( + " +if (false) { +// @ts-ignore: Unreachable code error +console.log('hello'); +} + ", + " +if (false) { +// @ts-expect-error: Unreachable code error +console.log('hello'); +} + ", + None, + ), + ("/* @ts-ignore */", "/* @ts-expect-error */", None), + ( + " +/** +* Explaining comment +* +* @ts-ignore */ + ", + " +/** +* Explaining comment +* +* @ts-expect-error */ + ", + None, + ), + ("/* @ts-ignore in a single block */", "/* @ts-expect-error in a single block */", None), + ( + " +/* +// @ts-ignore in a block with single line comments */ + ", + " +/* +// @ts-expect-error in a block with single line comments */ + ", + None, + ), + ]; + + Tester::new(PreferTsExpectError::NAME, pass, fail).expect_fix(fix).test_and_snapshot(); +} diff --git a/crates/oxc_linter/src/snapshots/prefer_ts_expect_error.snap b/crates/oxc_linter/src/snapshots/prefer_ts_expect_error.snap new file mode 100644 index 0000000000000..72d5b9eb058c7 --- /dev/null +++ b/crates/oxc_linter/src/snapshots/prefer_ts_expect_error.snap @@ -0,0 +1,69 @@ +--- +source: crates/oxc_linter/src/tester.rs +expression: prefer_ts_expect_error +--- + + ⚠ typescript-eslint(prefer-ts-expect-error): Enforce using `@ts-expect-error` over `@ts-ignore` + ╭─[prefer_ts_expect_error.tsx:1:1] + 1 │ // @ts-ignore + · ───────────── + ╰──── + help: Use "@ts-expect-error" to ensure an error is actually being suppressed. + + ⚠ typescript-eslint(prefer-ts-expect-error): Enforce using `@ts-expect-error` over `@ts-ignore` + ╭─[prefer_ts_expect_error.tsx:1:1] + 1 │ // @ts-ignore: Suppress next line + · ───────────────────────────────── + ╰──── + help: Use "@ts-expect-error" to ensure an error is actually being suppressed. + + ⚠ typescript-eslint(prefer-ts-expect-error): Enforce using `@ts-expect-error` over `@ts-ignore` + ╭─[prefer_ts_expect_error.tsx:1:1] + 1 │ ///@ts-ignore: Suppress next line + · ───────────────────────────────── + ╰──── + help: Use "@ts-expect-error" to ensure an error is actually being suppressed. + + ⚠ typescript-eslint(prefer-ts-expect-error): Enforce using `@ts-expect-error` over `@ts-ignore` + ╭─[prefer_ts_expect_error.tsx:3:1] + 2 │ if (false) { + 3 │ // @ts-ignore: Unreachable code error + · ───────────────────────────────────── + 4 │ console.log('hello'); + ╰──── + help: Use "@ts-expect-error" to ensure an error is actually being suppressed. + + ⚠ typescript-eslint(prefer-ts-expect-error): Enforce using `@ts-expect-error` over `@ts-ignore` + ╭─[prefer_ts_expect_error.tsx:1:1] + 1 │ /* @ts-ignore */ + · ──────────────── + ╰──── + help: Use "@ts-expect-error" to ensure an error is actually being suppressed. + + ⚠ typescript-eslint(prefer-ts-expect-error): Enforce using `@ts-expect-error` over `@ts-ignore` + ╭─[prefer_ts_expect_error.tsx:2:1] + 1 │ + 2 │ ╭─▶ /** + 3 │ │ * Explaining comment + 4 │ │ * + 5 │ ╰─▶ * @ts-ignore */ + 6 │ + ╰──── + help: Use "@ts-expect-error" to ensure an error is actually being suppressed. + + ⚠ typescript-eslint(prefer-ts-expect-error): Enforce using `@ts-expect-error` over `@ts-ignore` + ╭─[prefer_ts_expect_error.tsx:1:1] + 1 │ /* @ts-ignore in a single block */ + · ────────────────────────────────── + ╰──── + help: Use "@ts-expect-error" to ensure an error is actually being suppressed. + + ⚠ typescript-eslint(prefer-ts-expect-error): Enforce using `@ts-expect-error` over `@ts-ignore` + ╭─[prefer_ts_expect_error.tsx:2:1] + 1 │ + 2 │ ╭─▶ /* + 3 │ ╰─▶ // @ts-ignore in a block with single line comments */ + 4 │ + ╰──── + help: Use "@ts-expect-error" to ensure an error is actually being suppressed. +