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 index e32b7895cde1e..692413b4364dc 100644 --- a/crates/oxc_linter/src/rules/typescript/prefer_ts_expect_error.rs +++ b/crates/oxc_linter/src/rules/typescript/prefer_ts_expect_error.rs @@ -1,15 +1,25 @@ +use lazy_static::lazy_static; +use regex::Regex; + +use oxc_ast::Comment; use oxc_diagnostics::{ miette::{self, Diagnostic}, - thiserror::{self, Error}, + thiserror::Error, }; use oxc_macros::declare_oxc_lint; use oxc_span::Span; -use crate::{context::LintContext, rule::Rule, AstNode}; +use crate::fixer::Fix; +use crate::{context::LintContext, rule::Rule}; #[derive(Debug, Error, Diagnostic)] -#[error("typescript-eslint(prefer-ts-expect-error):")] -#[diagnostic(severity(warning), help("Use " @ ts - expect - error" to ensure an error is actually being suppressed."))] +#[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)] @@ -43,7 +53,70 @@ declare_oxc_lint!( ); impl Rule for PreferTsExpectError { - fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {} + 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 { + lazy_static! { + static ref TS_IGNORE_SINGLE_LINE_REGEX: Regex = Regex::new(r"^\s*/?\s*@ts-ignore").unwrap(); + } + + TS_IGNORE_SINGLE_LINE_REGEX.is_match(line) +} + +fn test_multi_line_comment(line: &str) -> bool { + lazy_static! { + static ref TS_IGNORE_MULTI_LINE_REGEX: Regex = + Regex::new(r"^\s*(?:\/|\*)*\s*@ts-ignore").unwrap(); + } + + TS_IGNORE_MULTI_LINE_REGEX.is_match(line) } #[test] @@ -51,57 +124,106 @@ fn test() { use crate::tester::Tester; let pass = vec![ - r#"// @ts-nocheck"#, - r#"// @ts-check"#, - r#"// just a comment containing @ts-ignore somewhere"#, - r#" - { - /* - just a comment containing @ts-ignore somewhere in a block - */ - } - "#, - r#"// @ts-expect-error"#, - r#" - if (false) { - // @ts-expect-error: Unreachable code error - console.log('hello'); - } - "#, - r#" - /** - * Explaining comment - * - * @ts-expect-error - * - * Not last line - * */ - "#, + "// @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![ - r#"// @ts-ignore"#, - r#"// @ts-ignore: Suppress next line"#, - r#"///@ts-ignore: Suppress next line"#, - r#" - if (false) { - // @ts-ignore: Unreachable code error - console.log('hello'); - } - "#, - r#"/* @ts-ignore */"#, - r#" - /** - * Explaining comment - * - * @ts-ignore */ - "#, - r#"/* @ts-ignore in a single block */"#, - r#" - /* - // @ts-ignore in a block with single line comments */ - "#, + "// @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).test_and_snapshot(); + 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. +