diff --git a/crates/oxc_linter/src/rules.rs b/crates/oxc_linter/src/rules.rs index ec19595e5b184..37d9a59a0312b 100644 --- a/crates/oxc_linter/src/rules.rs +++ b/crates/oxc_linter/src/rules.rs @@ -102,6 +102,7 @@ mod typescript { pub mod adjacent_overload_signatures; pub mod array_type; pub mod ban_ts_comment; + pub mod ban_tslint_comment; pub mod ban_types; pub mod no_duplicate_enum_values; pub mod no_empty_interface; @@ -392,6 +393,7 @@ oxc_macros::declare_all_lint_rules! { typescript::adjacent_overload_signatures, typescript::array_type, typescript::ban_ts_comment, + typescript::ban_tslint_comment, typescript::ban_types, typescript::no_duplicate_enum_values, typescript::no_empty_interface, diff --git a/crates/oxc_linter/src/rules/typescript/ban_tslint_comment.rs b/crates/oxc_linter/src/rules/typescript/ban_tslint_comment.rs new file mode 100644 index 0000000000000..34957f052376b --- /dev/null +++ b/crates/oxc_linter/src/rules/typescript/ban_tslint_comment.rs @@ -0,0 +1,131 @@ +use lazy_static::lazy_static; +use oxc_diagnostics::{ + miette::{self, Diagnostic}, + thiserror::{self, Error}, +}; +use oxc_macros::declare_oxc_lint; +use oxc_span::Span; +use regex::Regex; + +use crate::{context::LintContext, fixer::Fix, rule::Rule}; + +#[derive(Debug, Error, Diagnostic)] +#[error("typescript-eslint(ban-tslint-comment): tslint comment detected: \"{0}\"")] +#[diagnostic(severity(warning))] +struct BanTslintCommentDiagnostic(String, #[label] pub Span); + +#[derive(Debug, Default, Clone)] +pub struct BanTslintComment; + +declare_oxc_lint!( + /// ### What it does + /// This rule disallows `tslint:` comments + /// + /// ### Why is this bad? + /// Useful when migrating from TSLint to ESLint. Once TSLint has been + /// removed, this rule helps locate TSLint annotations + /// + /// ### Example + /// ```javascript + /// // tslint:disable-next-line + /// someCode(); + /// ``` + BanTslintComment, + style +); + +impl Rule for BanTslintComment { + fn run_once(&self, ctx: &LintContext) { + let comments = ctx.semantic().trivias().comments(); + let source_text_len = ctx.semantic().source_text().len(); + + for (start, comment) in comments { + let raw = &ctx.semantic().source_text()[*start as usize..comment.end() as usize]; + + if is_tslint_comment_directive(raw) { + let comment_span = get_full_comment( + source_text_len, + *start, + comment.end(), + comment.is_multi_line(), + ); + + ctx.diagnostic_with_fix( + BanTslintCommentDiagnostic(raw.trim().to_string(), comment_span), + || Fix::delete(comment_span), + ); + } + } + } +} + +fn is_tslint_comment_directive(raw: &str) -> bool { + lazy_static! { + static ref ENABLE_DISABLE_REGEX: Regex = + Regex::new(r"^\s*tslint:(enable|disable)(?:-(line|next-line))?(:|\s|$)").unwrap(); + } + + ENABLE_DISABLE_REGEX.is_match(raw) +} + +fn get_full_comment(source_text_len: usize, start: u32, end: u32, is_multi_line: bool) -> Span { + let comment_start = start - 2; + let mut comment_end = if is_multi_line { end + 2 } else { end }; + + // Take into account new line at the end of the comment + if source_text_len > comment_end as usize { + comment_end += 1; + } + + Span { start: comment_start, end: comment_end } +} + +#[test] +fn test() { + use crate::tester::Tester; + + let pass = vec![ + r"let a: readonly any[] = [];", + r"let a = new Array();", + r"// some other comment", + r"// TODO: this is a comment that mentions tslint", + r"/* another comment that mentions tslint */", + r"someCode(); // This is a comment that just happens to mention tslint", + ]; + + let fail = vec![ + r"/* tslint:disable */", + r"/* tslint:enable */", + r"/* tslint:disable:rule1 rule2 rule3... */", + r"/* tslint:enable:rule1 rule2 rule3... */", + r"// tslint:disable-next-line", + r"someCode(); // tslint:disable-line", + r"// tslint:disable-next-line:rule1 rule2 rule3...", + r"const woah = doSomeStuff(); + // tslint:disable-line + console.log(woah); + ", + ]; + + let fix = vec![ + ( + r"const woah = doSomeStuff(); + // tslint:disable-line + console.log(woah);", + r"const woah = doSomeStuff(); + console.log(woah);", + None, + ), + ( + r"const woah = doSomeStuff(); + /* tslint:disable-line */ + console.log(woah);", + r"const woah = doSomeStuff(); + console.log(woah);", + None, + ), + (r"/* tslint:disable-line */", r"", None), + ]; + + Tester::new(BanTslintComment::NAME, pass, fail).expect_fix(fix).test_and_snapshot(); +} diff --git a/crates/oxc_linter/src/snapshots/ban_tslint_comment.snap b/crates/oxc_linter/src/snapshots/ban_tslint_comment.snap new file mode 100644 index 0000000000000..23aefcf1ea5b7 --- /dev/null +++ b/crates/oxc_linter/src/snapshots/ban_tslint_comment.snap @@ -0,0 +1,56 @@ +--- +source: crates/oxc_linter/src/tester.rs +expression: ban_tslint_comment +--- + + ⚠ typescript-eslint(ban-tslint-comment): tslint comment detected: "tslint:disable" + ╭─[ban_tslint_comment.tsx:1:1] + 1 │ /* tslint:disable */ + · ──────────────────── + ╰──── + + ⚠ typescript-eslint(ban-tslint-comment): tslint comment detected: "tslint:enable" + ╭─[ban_tslint_comment.tsx:1:1] + 1 │ /* tslint:enable */ + · ─────────────────── + ╰──── + + ⚠ typescript-eslint(ban-tslint-comment): tslint comment detected: "tslint:disable:rule1 rule2 rule3..." + ╭─[ban_tslint_comment.tsx:1:1] + 1 │ /* tslint:disable:rule1 rule2 rule3... */ + · ───────────────────────────────────────── + ╰──── + + ⚠ typescript-eslint(ban-tslint-comment): tslint comment detected: "tslint:enable:rule1 rule2 rule3..." + ╭─[ban_tslint_comment.tsx:1:1] + 1 │ /* tslint:enable:rule1 rule2 rule3... */ + · ──────────────────────────────────────── + ╰──── + + ⚠ typescript-eslint(ban-tslint-comment): tslint comment detected: "tslint:disable-next-line" + ╭─[ban_tslint_comment.tsx:1:1] + 1 │ // tslint:disable-next-line + · ─────────────────────────── + ╰──── + + ⚠ typescript-eslint(ban-tslint-comment): tslint comment detected: "tslint:disable-line" + ╭─[ban_tslint_comment.tsx:1:13] + 1 │ someCode(); // tslint:disable-line + · ────────────────────── + ╰──── + + ⚠ typescript-eslint(ban-tslint-comment): tslint comment detected: "tslint:disable-next-line:rule1 rule2 rule3..." + ╭─[ban_tslint_comment.tsx:1:1] + 1 │ // tslint:disable-next-line:rule1 rule2 rule3... + · ──────────────────────────────────────────────── + ╰──── + + ⚠ typescript-eslint(ban-tslint-comment): tslint comment detected: "tslint:disable-line" + ╭─[ban_tslint_comment.tsx:2:9] + 1 │ const woah = doSomeStuff(); + 2 │ // tslint:disable-line + · ─────────────────────── + 3 │ console.log(woah); + 4 │ + ╰──── +