diff --git a/packages/eslint-plugin/src/rules/ban-ts-comment.ts b/packages/eslint-plugin/src/rules/ban-ts-comment.ts index 4f64e85f5b9..4413af5bf1a 100644 --- a/packages/eslint-plugin/src/rules/ban-ts-comment.ts +++ b/packages/eslint-plugin/src/rules/ban-ts-comment.ts @@ -1,4 +1,4 @@ -import { AST_TOKEN_TYPES } from '@typescript-eslint/utils'; +import { AST_TOKEN_TYPES, type TSESLint } from '@typescript-eslint/utils'; import { createRule, getStringLength } from '../util'; @@ -19,8 +19,10 @@ export const defaultMinimumDescriptionLength = 3; type MessageIds = | 'tsDirectiveComment' + | 'tsIgnoreInsteadOfExpectError' | 'tsDirectiveCommentDescriptionNotMatchPattern' - | 'tsDirectiveCommentRequiresDescription'; + | 'tsDirectiveCommentRequiresDescription' + | 'replaceTsIgnoreWithTsExpectError'; export default createRule<[Options], MessageIds>({ name: 'ban-ts-comment', @@ -34,11 +36,16 @@ export default createRule<[Options], MessageIds>({ messages: { tsDirectiveComment: 'Do not use "@ts-{{directive}}" because it alters compilation errors.', + tsIgnoreInsteadOfExpectError: + 'Use "@ts-expect-error" instead of "@ts-ignore", as "@ts-ignore" will do nothing if the following line is error-free.', tsDirectiveCommentRequiresDescription: 'Include a description after the "@ts-{{directive}}" directive to explain why the @ts-{{directive}} is necessary. The description must be {{minimumDescriptionLength}} characters or longer.', tsDirectiveCommentDescriptionNotMatchPattern: 'The description for the "@ts-{{directive}}" directive must match the {{format}} format.', + replaceTsIgnoreWithTsExpectError: + 'Replace "@ts-ignore" with "@ts-expect-error".', }, + hasSuggestions: true, schema: [ { $defs: { @@ -130,11 +137,36 @@ export default createRule<[Options], MessageIds>({ const option = options[fullDirective]; if (option === true) { - context.report({ - data: { directive }, - node: comment, - messageId: 'tsDirectiveComment', - }); + if (directive === 'ignore') { + // Special case to suggest @ts-expect-error instead of @ts-ignore + context.report({ + node: comment, + messageId: 'tsIgnoreInsteadOfExpectError', + suggest: [ + { + messageId: 'replaceTsIgnoreWithTsExpectError', + fix(fixer): TSESLint.RuleFix { + const commentText = comment.value.replace( + /@ts-ignore/, + '@ts-expect-error', + ); + return fixer.replaceText( + comment, + comment.type === AST_TOKEN_TYPES.Line + ? `//${commentText}` + : `/*${commentText}*/`, + ); + }, + }, + ], + }); + } else { + context.report({ + data: { directive }, + node: comment, + messageId: 'tsDirectiveComment', + }); + } } if ( diff --git a/packages/eslint-plugin/tests/rules/ban-ts-comment.test.ts b/packages/eslint-plugin/tests/rules/ban-ts-comment.test.ts index 271b2d27a03..640f618200f 100644 --- a/packages/eslint-plugin/tests/rules/ban-ts-comment.test.ts +++ b/packages/eslint-plugin/tests/rules/ban-ts-comment.test.ts @@ -303,24 +303,53 @@ ruleTester.run('ts-ignore', rule, { invalid: [ { code: '// @ts-ignore', - options: [{ 'ts-ignore': true }], + options: [{ 'ts-ignore': true, 'ts-expect-error': true }], errors: [ { - data: { directive: 'ignore' }, - messageId: 'tsDirectiveComment', + messageId: 'tsIgnoreInsteadOfExpectError', line: 1, column: 1, + suggestions: [ + { + messageId: 'replaceTsIgnoreWithTsExpectError', + output: '// @ts-expect-error', + }, + ], }, ], }, { code: '// @ts-ignore', + options: [ + { 'ts-ignore': true, 'ts-expect-error': 'allow-with-description' }, + ], errors: [ { - data: { directive: 'ignore' }, - messageId: 'tsDirectiveComment', + messageId: 'tsIgnoreInsteadOfExpectError', line: 1, column: 1, + suggestions: [ + { + messageId: 'replaceTsIgnoreWithTsExpectError', + output: '// @ts-expect-error', + }, + ], + }, + ], + }, + { + code: '// @ts-ignore', + errors: [ + { + messageId: 'tsIgnoreInsteadOfExpectError', + line: 1, + column: 1, + suggestions: [ + { + messageId: 'replaceTsIgnoreWithTsExpectError', + output: '// @ts-expect-error', + }, + ], }, ], }, @@ -329,10 +358,15 @@ ruleTester.run('ts-ignore', rule, { options: [{ 'ts-ignore': true }], errors: [ { - data: { directive: 'ignore' }, - messageId: 'tsDirectiveComment', + messageId: 'tsIgnoreInsteadOfExpectError', line: 1, column: 1, + suggestions: [ + { + messageId: 'replaceTsIgnoreWithTsExpectError', + output: '/* @ts-expect-error */', + }, + ], }, ], }, @@ -345,22 +379,36 @@ ruleTester.run('ts-ignore', rule, { options: [{ 'ts-ignore': true }], errors: [ { - data: { directive: 'ignore' }, - messageId: 'tsDirectiveComment', + messageId: 'tsIgnoreInsteadOfExpectError', line: 2, column: 1, + suggestions: [ + { + messageId: 'replaceTsIgnoreWithTsExpectError', + output: ` +/* + @ts-expect-error +*/ + `, + }, + ], }, ], }, { code: '/** @ts-ignore */', - options: [{ 'ts-ignore': true }], + options: [{ 'ts-ignore': true, 'ts-expect-error': false }], errors: [ { - data: { directive: 'ignore' }, - messageId: 'tsDirectiveComment', + messageId: 'tsIgnoreInsteadOfExpectError', line: 1, column: 1, + suggestions: [ + { + messageId: 'replaceTsIgnoreWithTsExpectError', + output: '/** @ts-expect-error */', + }, + ], }, ], }, @@ -368,10 +416,15 @@ ruleTester.run('ts-ignore', rule, { code: '// @ts-ignore: Suppress next line', errors: [ { - data: { directive: 'ignore' }, - messageId: 'tsDirectiveComment', + messageId: 'tsIgnoreInsteadOfExpectError', line: 1, column: 1, + suggestions: [ + { + messageId: 'replaceTsIgnoreWithTsExpectError', + output: '// @ts-expect-error: Suppress next line', + }, + ], }, ], }, @@ -379,10 +432,15 @@ ruleTester.run('ts-ignore', rule, { code: '/////@ts-ignore: Suppress next line', errors: [ { - data: { directive: 'ignore' }, - messageId: 'tsDirectiveComment', + messageId: 'tsIgnoreInsteadOfExpectError', line: 1, column: 1, + suggestions: [ + { + messageId: 'replaceTsIgnoreWithTsExpectError', + output: '/////@ts-expect-error: Suppress next line', + }, + ], }, ], }, @@ -395,10 +453,20 @@ if (false) { `, errors: [ { - data: { directive: 'ignore' }, - messageId: 'tsDirectiveComment', + messageId: 'tsIgnoreInsteadOfExpectError', line: 3, column: 3, + suggestions: [ + { + messageId: 'replaceTsIgnoreWithTsExpectError', + output: ` +if (false) { + // @ts-expect-error: Unreachable code error + console.log('hello'); +} + `, + }, + ], }, ], },