Skip to content

Commit

Permalink
feat(linter): implement @typescript-eslint/prefer-ts-expect-error (#2435
Browse files Browse the repository at this point in the history
  • Loading branch information
alexttyip and Boshen authored Feb 24, 2024
1 parent 7a796c4 commit 696818a
Show file tree
Hide file tree
Showing 3 changed files with 289 additions and 0 deletions.
2 changes: 2 additions & 0 deletions crates/oxc_linter/src/rules.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down Expand Up @@ -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,
Expand Down
218 changes: 218 additions & 0 deletions crates/oxc_linter/src/rules/typescript/prefer_ts_expect_error.rs
Original file line number Diff line number Diff line change
@@ -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();
}
69 changes: 69 additions & 0 deletions crates/oxc_linter/src/snapshots/prefer_ts_expect_error.snap
Original file line number Diff line number Diff line change
@@ -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]
2if (false) {
3// @ts-ignore: Unreachable code error
· ─────────────────────────────────────
4console.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.

0 comments on commit 696818a

Please sign in to comment.