diff --git a/internal/config/config.go b/internal/config/config.go index 2fb18ab35..453720876 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -79,6 +79,7 @@ import ( "github.com/web-infra-dev/rslint/internal/plugins/typescript/rules/prefer_reduce_type_parameter" "github.com/web-infra-dev/rslint/internal/plugins/typescript/rules/prefer_return_this_type" "github.com/web-infra-dev/rslint/internal/plugins/typescript/rules/prefer_string_starts_ends_with" + "github.com/web-infra-dev/rslint/internal/plugins/typescript/rules/prefer_ts_expect_error" "github.com/web-infra-dev/rslint/internal/plugins/typescript/rules/promise_function_async" "github.com/web-infra-dev/rslint/internal/plugins/typescript/rules/related_getter_setter_pairs" "github.com/web-infra-dev/rslint/internal/plugins/typescript/rules/require_array_sort_compare" @@ -448,6 +449,7 @@ func registerAllTypeScriptEslintPluginRules() { GlobalRuleRegistry.Register("@typescript-eslint/prefer-reduce-type-parameter", prefer_reduce_type_parameter.PreferReduceTypeParameterRule) GlobalRuleRegistry.Register("@typescript-eslint/prefer-return-this-type", prefer_return_this_type.PreferReturnThisTypeRule) GlobalRuleRegistry.Register("@typescript-eslint/prefer-string-starts-ends-with", prefer_string_starts_ends_with.PreferStringStartsEndsWithRule) + GlobalRuleRegistry.Register("@typescript-eslint/prefer-ts-expect-error", prefer_ts_expect_error.PreferTsExpectErrorRule) GlobalRuleRegistry.Register("@typescript-eslint/promise-function-async", promise_function_async.PromiseFunctionAsyncRule) GlobalRuleRegistry.Register("@typescript-eslint/related-getter-setter-pairs", related_getter_setter_pairs.RelatedGetterSetterPairsRule) GlobalRuleRegistry.Register("@typescript-eslint/require-array-sort-compare", require_array_sort_compare.RequireArraySortCompareRule) diff --git a/internal/plugins/typescript/rules/prefer_ts_expect_error/prefer_ts_expect_error.go b/internal/plugins/typescript/rules/prefer_ts_expect_error/prefer_ts_expect_error.go new file mode 100644 index 000000000..f7c4e9216 --- /dev/null +++ b/internal/plugins/typescript/rules/prefer_ts_expect_error/prefer_ts_expect_error.go @@ -0,0 +1,131 @@ +package prefer_ts_expect_error + +import ( + "strings" + + "github.com/microsoft/typescript-go/shim/ast" + "github.com/microsoft/typescript-go/shim/core" + "github.com/web-infra-dev/rslint/internal/rule" + "github.com/web-infra-dev/rslint/internal/utils" +) + +const tsIgnoreDirective = "@ts-ignore" +const tsExpectErrorDirective = "@ts-expect-error" + +func buildPreferExpectErrorMessage() rule.RuleMessage { + return rule.RuleMessage{ + Id: "preferExpectErrorComment", + Description: "Use @ts-expect-error instead of @ts-ignore.", + } +} + +func findDirectiveInLineComment(commentText string) (int, int, bool) { + if len(commentText) < 2 || commentText[0] != '/' || commentText[1] != '/' { + return 0, 0, false + } + idx := 2 + for idx < len(commentText) && (commentText[idx] == ' ' || commentText[idx] == '\t') { + idx++ + } + if idx < len(commentText) && commentText[idx] == '/' { + idx++ + } + for idx < len(commentText) && (commentText[idx] == ' ' || commentText[idx] == '\t') { + idx++ + } + if hasTsIgnoreDirectiveAt(commentText, idx) { + return idx, idx + len(tsIgnoreDirective), true + } + return 0, 0, false +} + +func isDirectiveBoundaryChar(ch byte) bool { + return (ch < 'a' || ch > 'z') && + (ch < 'A' || ch > 'Z') && + (ch < '0' || ch > '9') && + ch != '_' && + ch != '$' +} + +func hasTsIgnoreDirectiveAt(text string, idx int) bool { + if idx < 0 || idx >= len(text) || !strings.HasPrefix(text[idx:], tsIgnoreDirective) { + return false + } + end := idx + len(tsIgnoreDirective) + if end >= len(text) { + return true + } + return isDirectiveBoundaryChar(text[end]) +} + +func findDirectiveInBlockComment(commentText string) (int, int, bool) { + if len(commentText) < 4 { + return 0, 0, false + } + + contentStart := 2 + contentEnd := len(commentText) - 2 + if contentEnd <= contentStart { + return 0, 0, false + } + content := commentText[contentStart:contentEnd] + + lastLineStart := strings.LastIndexByte(content, '\n') + if lastLineStart == -1 { + lastLineStart = 0 + } else { + lastLineStart++ + } + + line := content[lastLineStart:] + idx := 0 + for idx < len(line) && (line[idx] == ' ' || line[idx] == '\t' || line[idx] == '\r') { + idx++ + } + for idx < len(line) && (line[idx] == '/' || line[idx] == '*') { + idx++ + } + for idx < len(line) && (line[idx] == ' ' || line[idx] == '\t') { + idx++ + } + if idx < len(line) && hasTsIgnoreDirectiveAt(line, idx) { + start := contentStart + lastLineStart + idx + return start, start + len(tsIgnoreDirective), true + } + + return 0, 0, false +} + +func findTsIgnoreDirective(commentText string, kind ast.Kind) (int, int, bool) { + switch kind { + case ast.KindSingleLineCommentTrivia: + return findDirectiveInLineComment(commentText) + case ast.KindMultiLineCommentTrivia: + return findDirectiveInBlockComment(commentText) + } + return 0, 0, false +} + +var PreferTsExpectErrorRule = rule.CreateRule(rule.Rule{ + Name: "prefer-ts-expect-error", + Run: func(ctx rule.RuleContext, options any) rule.RuleListeners { + fullText := ctx.SourceFile.Text() + utils.ForEachComment(ctx.SourceFile.AsNode(), func(comment *ast.CommentRange) { + if comment == nil { + return + } + commentText := fullText[comment.Pos():comment.End()] + start, end, ok := findTsIgnoreDirective(commentText, comment.Kind) + if !ok { + return + } + + fixRange := core.NewTextRange(comment.Pos()+start, comment.Pos()+end) + fix := rule.RuleFixReplaceRange(fixRange, tsExpectErrorDirective) + commentRange := core.NewTextRange(comment.Pos(), comment.End()) + ctx.ReportRangeWithFixes(commentRange, buildPreferExpectErrorMessage(), fix) + }, ctx.SourceFile) + + return rule.RuleListeners{} + }, +}) diff --git a/internal/plugins/typescript/rules/prefer_ts_expect_error/prefer_ts_expect_error.md b/internal/plugins/typescript/rules/prefer_ts_expect_error/prefer_ts_expect_error.md new file mode 100644 index 000000000..229d5a098 --- /dev/null +++ b/internal/plugins/typescript/rules/prefer_ts_expect_error/prefer_ts_expect_error.md @@ -0,0 +1,21 @@ +# prefer-ts-expect-error + +## Rule Details + +Prefer `@ts-expect-error` over `@ts-ignore` in TypeScript directive comments. + +Examples of **incorrect** code for this rule: + +```typescript +// @ts-ignore +``` + +Examples of **correct** code for this rule: + +```typescript +// @ts-expect-error +``` + +## Original Documentation + +https://typescript-eslint.io/rules/prefer-ts-expect-error diff --git a/internal/plugins/typescript/rules/prefer_ts_expect_error/prefer_ts_expect_error_test.go b/internal/plugins/typescript/rules/prefer_ts_expect_error/prefer_ts_expect_error_test.go new file mode 100644 index 000000000..e09eef53b --- /dev/null +++ b/internal/plugins/typescript/rules/prefer_ts_expect_error/prefer_ts_expect_error_test.go @@ -0,0 +1,157 @@ +package prefer_ts_expect_error + +import ( + "testing" + + "github.com/web-infra-dev/rslint/internal/plugins/typescript/rules/fixtures" + "github.com/web-infra-dev/rslint/internal/rule_tester" +) + +// cspell:ignore ignorefoo + +func TestPreferTsExpectErrorRule(t *testing.T) { + rule_tester.RunRuleTester(fixtures.GetRootDir(), "tsconfig.json", t, &PreferTsExpectErrorRule, []rule_tester.ValidTestCase{ + {Code: `// @ts-nocheck`}, + {Code: `// @ts-check`}, + {Code: `// just a comment containing @ts-ignore somewhere`}, + {Code: `// @ts-ignorefoo`}, + {Code: `/* @ts-ignorefoo */`}, + { + Code: ` +{ + /* + just a comment containing @ts-ignore somewhere in a block + */ +} + `, + }, + {Code: `// @ts-expect-error`}, + { + Code: ` +if (false) { + // @ts-expect-error: Unreachable code error + console.log('hello'); +} + `, + }, + { + Code: ` +/** + * Explaining comment + * + * @ts-expect-error + * + * Not last line + * */ + `, + }, + }, []rule_tester.InvalidTestCase{ + { + Code: `// @ts-ignore`, + Output: []string{ + `// @ts-expect-error`, + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "preferExpectErrorComment", Line: 1, Column: 1, EndLine: 1, EndColumn: 14}, + }, + }, + { + Code: `// @ts-ignore: Suppress next line`, + Output: []string{ + `// @ts-expect-error: Suppress next line`, + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "preferExpectErrorComment", Line: 1, Column: 1, EndLine: 1, EndColumn: 34}, + }, + }, + { + Code: `///@ts-ignore: Suppress next line`, + Output: []string{ + `///@ts-expect-error: Suppress next line`, + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "preferExpectErrorComment", Line: 1, Column: 1, EndLine: 1, EndColumn: 34}, + }, + }, + { + Code: `/// @ts-ignore: Suppress next line`, + Output: []string{ + `/// @ts-expect-error: Suppress next line`, + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "preferExpectErrorComment", Line: 1, Column: 1, EndLine: 1, EndColumn: 35}, + }, + }, + { + Code: ` +if (false) { + // @ts-ignore: Unreachable code error + console.log('hello'); +} + `, + Output: []string{ + ` +if (false) { + // @ts-expect-error: Unreachable code error + console.log('hello'); +} + `, + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "preferExpectErrorComment", Line: 3, Column: 3, EndLine: 3, EndColumn: 40}, + }, + }, + { + Code: `/* @ts-ignore */`, + Output: []string{ + `/* @ts-expect-error */`, + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "preferExpectErrorComment", Line: 1, Column: 1, EndLine: 1, EndColumn: 17}, + }, + }, + { + Code: ` +/** + * Explaining comment + * + * @ts-ignore */ + `, + Output: []string{ + ` +/** + * Explaining comment + * + * @ts-expect-error */ + `, + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "preferExpectErrorComment", Line: 2, Column: 1, EndLine: 5, EndColumn: 17}, + }, + }, + { + Code: `/* @ts-ignore in a single block */`, + Output: []string{ + `/* @ts-expect-error in a single block */`, + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "preferExpectErrorComment", Line: 1, Column: 1, EndLine: 1, EndColumn: 35}, + }, + }, + { + Code: ` +/* +// @ts-ignore in a block with single line comments */ + `, + Output: []string{ + ` +/* +// @ts-expect-error in a block with single line comments */ + `, + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "preferExpectErrorComment", Line: 2, Column: 1, EndLine: 3, EndColumn: 54}, + }, + }, + }) +} diff --git a/packages/rslint-test-tools/rstest.config.mts b/packages/rslint-test-tools/rstest.config.mts index c1b8a03d5..68a81dd5a 100644 --- a/packages/rslint-test-tools/rstest.config.mts +++ b/packages/rslint-test-tools/rstest.config.mts @@ -155,7 +155,7 @@ export default defineConfig({ // './tests/typescript-eslint/rules/prefer-regexp-exec.test.ts', './tests/typescript-eslint/rules/prefer-return-this-type.test.ts', './tests/typescript-eslint/rules/prefer-string-starts-ends-with.test.ts', - // './tests/typescript-eslint/rules/prefer-ts-expect-error.test.ts', + './tests/typescript-eslint/rules/prefer-ts-expect-error.test.ts', // './tests/typescript-eslint/rules/promise-function-async.test.ts', './tests/typescript-eslint/rules/related-getter-setter-pairs.test.ts', // './tests/typescript-eslint/rules/require-array-sort-compare.test.ts', diff --git a/packages/rslint-test-tools/tests/typescript-eslint/rules/__snapshots__/prefer-ts-expect-error.test.ts.snap b/packages/rslint-test-tools/tests/typescript-eslint/rules/__snapshots__/prefer-ts-expect-error.test.ts.snap new file mode 100644 index 000000000..96d27b223 --- /dev/null +++ b/packages/rslint-test-tools/tests/typescript-eslint/rules/__snapshots__/prefer-ts-expect-error.test.ts.snap @@ -0,0 +1,243 @@ +// Rstest Snapshot v1 + +exports[`prefer-ts-expect-error > invalid 1`] = ` +{ + "code": "// @ts-ignore", + "diagnostics": [ + { + "message": "Use @ts-expect-error instead of @ts-ignore.", + "messageId": "preferExpectErrorComment", + "range": { + "end": { + "column": 14, + "line": 1, + }, + "start": { + "column": 1, + "line": 1, + }, + }, + "ruleName": "@typescript-eslint/prefer-ts-expect-error", + }, + ], + "errorCount": 1, + "fileCount": 1, + "output": "// @ts-expect-error", + "ruleCount": 1, +} +`; + +exports[`prefer-ts-expect-error > invalid 2`] = ` +{ + "code": "// @ts-ignore: Suppress next line", + "diagnostics": [ + { + "message": "Use @ts-expect-error instead of @ts-ignore.", + "messageId": "preferExpectErrorComment", + "range": { + "end": { + "column": 34, + "line": 1, + }, + "start": { + "column": 1, + "line": 1, + }, + }, + "ruleName": "@typescript-eslint/prefer-ts-expect-error", + }, + ], + "errorCount": 1, + "fileCount": 1, + "output": "// @ts-expect-error: Suppress next line", + "ruleCount": 1, +} +`; + +exports[`prefer-ts-expect-error > invalid 3`] = ` +{ + "code": "///@ts-ignore: Suppress next line", + "diagnostics": [ + { + "message": "Use @ts-expect-error instead of @ts-ignore.", + "messageId": "preferExpectErrorComment", + "range": { + "end": { + "column": 34, + "line": 1, + }, + "start": { + "column": 1, + "line": 1, + }, + }, + "ruleName": "@typescript-eslint/prefer-ts-expect-error", + }, + ], + "errorCount": 1, + "fileCount": 1, + "output": "///@ts-expect-error: Suppress next line", + "ruleCount": 1, +} +`; + +exports[`prefer-ts-expect-error > invalid 4`] = ` +{ + "code": " +if (false) { + // @ts-ignore: Unreachable code error + console.log('hello'); +} + ", + "diagnostics": [ + { + "message": "Use @ts-expect-error instead of @ts-ignore.", + "messageId": "preferExpectErrorComment", + "range": { + "end": { + "column": 40, + "line": 3, + }, + "start": { + "column": 3, + "line": 3, + }, + }, + "ruleName": "@typescript-eslint/prefer-ts-expect-error", + }, + ], + "errorCount": 1, + "fileCount": 1, + "output": " +if (false) { + // @ts-expect-error: Unreachable code error + console.log('hello'); +} + ", + "ruleCount": 1, +} +`; + +exports[`prefer-ts-expect-error > invalid 5`] = ` +{ + "code": "/* @ts-ignore */", + "diagnostics": [ + { + "message": "Use @ts-expect-error instead of @ts-ignore.", + "messageId": "preferExpectErrorComment", + "range": { + "end": { + "column": 17, + "line": 1, + }, + "start": { + "column": 1, + "line": 1, + }, + }, + "ruleName": "@typescript-eslint/prefer-ts-expect-error", + }, + ], + "errorCount": 1, + "fileCount": 1, + "output": "/* @ts-expect-error */", + "ruleCount": 1, +} +`; + +exports[`prefer-ts-expect-error > invalid 6`] = ` +{ + "code": " +/** + * Explaining comment + * + * @ts-ignore */ + ", + "diagnostics": [ + { + "message": "Use @ts-expect-error instead of @ts-ignore.", + "messageId": "preferExpectErrorComment", + "range": { + "end": { + "column": 17, + "line": 5, + }, + "start": { + "column": 1, + "line": 2, + }, + }, + "ruleName": "@typescript-eslint/prefer-ts-expect-error", + }, + ], + "errorCount": 1, + "fileCount": 1, + "output": " +/** + * Explaining comment + * + * @ts-expect-error */ + ", + "ruleCount": 1, +} +`; + +exports[`prefer-ts-expect-error > invalid 7`] = ` +{ + "code": "/* @ts-ignore in a single block */", + "diagnostics": [ + { + "message": "Use @ts-expect-error instead of @ts-ignore.", + "messageId": "preferExpectErrorComment", + "range": { + "end": { + "column": 35, + "line": 1, + }, + "start": { + "column": 1, + "line": 1, + }, + }, + "ruleName": "@typescript-eslint/prefer-ts-expect-error", + }, + ], + "errorCount": 1, + "fileCount": 1, + "output": "/* @ts-expect-error in a single block */", + "ruleCount": 1, +} +`; + +exports[`prefer-ts-expect-error > invalid 8`] = ` +{ + "code": " +/* +// @ts-ignore in a block with single line comments */ + ", + "diagnostics": [ + { + "message": "Use @ts-expect-error instead of @ts-ignore.", + "messageId": "preferExpectErrorComment", + "range": { + "end": { + "column": 54, + "line": 3, + }, + "start": { + "column": 1, + "line": 2, + }, + }, + "ruleName": "@typescript-eslint/prefer-ts-expect-error", + }, + ], + "errorCount": 1, + "fileCount": 1, + "output": " +/* +// @ts-expect-error in a block with single line comments */ + ", + "ruleCount": 1, +} +`; diff --git a/rslint.json b/rslint.json index 46461fc17..d5be861f9 100644 --- a/rslint.json +++ b/rslint.json @@ -55,7 +55,8 @@ "@typescript-eslint/no-unused-vars": "warn", "@typescript-eslint/require-await": "warn", "@typescript-eslint/prefer-readonly": "warn", - "@typescript-eslint/no-non-null-assertion": "warn" + "@typescript-eslint/no-non-null-assertion": "warn", + "@typescript-eslint/prefer-ts-expect-error": "off" }, "plugins": ["@typescript-eslint"] }