diff --git a/internal/config/config.go b/internal/config/config.go index bfde3da49..661c04881 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -14,6 +14,7 @@ import ( "github.com/web-infra-dev/rslint/internal/plugins/typescript/rules/array_type" "github.com/web-infra-dev/rslint/internal/plugins/typescript/rules/await_thenable" "github.com/web-infra-dev/rslint/internal/plugins/typescript/rules/ban_ts_comment" + "github.com/web-infra-dev/rslint/internal/plugins/typescript/rules/ban_tslint_comment" "github.com/web-infra-dev/rslint/internal/plugins/typescript/rules/ban_types" "github.com/web-infra-dev/rslint/internal/plugins/typescript/rules/class_literal_property_style" "github.com/web-infra-dev/rslint/internal/plugins/typescript/rules/consistent_generic_constructors" @@ -369,6 +370,7 @@ func registerAllTypeScriptEslintPluginRules() { GlobalRuleRegistry.Register("@typescript-eslint/array-type", array_type.ArrayTypeRule) GlobalRuleRegistry.Register("@typescript-eslint/await-thenable", await_thenable.AwaitThenableRule) GlobalRuleRegistry.Register("@typescript-eslint/ban-ts-comment", ban_ts_comment.BanTsCommentRule) + GlobalRuleRegistry.Register("@typescript-eslint/ban-tslint-comment", ban_tslint_comment.BanTslintCommentRule) GlobalRuleRegistry.Register("@typescript-eslint/ban-types", ban_types.BanTypesRule) GlobalRuleRegistry.Register("@typescript-eslint/class-literal-property-style", class_literal_property_style.ClassLiteralPropertyStyleRule) GlobalRuleRegistry.Register("@typescript-eslint/consistent-generic-constructors", consistent_generic_constructors.ConsistentGenericConstructorsRule) diff --git a/internal/plugins/typescript/rules/ban_tslint_comment/ban_tslint_comment.go b/internal/plugins/typescript/rules/ban_tslint_comment/ban_tslint_comment.go new file mode 100644 index 000000000..f0f4510cc --- /dev/null +++ b/internal/plugins/typescript/rules/ban_tslint_comment/ban_tslint_comment.go @@ -0,0 +1,108 @@ +package ban_tslint_comment + +import ( + "regexp" + "strings" + + "github.com/microsoft/typescript-go/shim/core" + "github.com/web-infra-dev/rslint/internal/rule" +) + +// Regular expression to match TSLint directive comments +// Matches patterns like: +// - tslint:disable +// - tslint:enable +// - tslint:disable-line +// - tslint:disable-next-line +// - tslint:enable-line +var tslintCommentRegex = regexp.MustCompile(`^\s*tslint:(enable|disable)(?:-(line|next-line))?(?::|s|$)`) + +// BanTslintCommentRule implements the ban-tslint-comment rule +// Bans // tslint: comments +var BanTslintCommentRule = rule.CreateRule(rule.Rule{ + Name: "ban-tslint-comment", + Run: run, +}) + +func run(ctx rule.RuleContext, options any) rule.RuleListeners { + // Get the full text of the source file + text := ctx.SourceFile.Text() + + // Process the text to find TSLint comments + processComments(ctx, text) + + return rule.RuleListeners{} +} + +// processComments scans the source text for comments and checks for TSLint directives +func processComments(ctx rule.RuleContext, text string) { + pos := 0 + length := len(text) + + for pos < length { + // Skip to next potential comment + if pos+1 < length { + if text[pos] == '/' && text[pos+1] == '/' { + // Single-line comment + commentStart := pos + pos += 2 + lineEnd := pos + for lineEnd < length && text[lineEnd] != '\n' && text[lineEnd] != '\r' { + lineEnd++ + } + commentText := text[commentStart:lineEnd] + checkComment(ctx, commentText, commentStart, false) + pos = lineEnd + } else if text[pos] == '/' && text[pos+1] == '*' { + // Multi-line comment + commentStart := pos + pos += 2 + commentEnd := pos + for commentEnd+1 < length { + if text[commentEnd] == '*' && text[commentEnd+1] == '/' { + commentEnd += 2 + break + } + commentEnd++ + } + commentText := text[commentStart:commentEnd] + checkComment(ctx, commentText, commentStart, true) + pos = commentEnd + } else { + pos++ + } + } else { + pos++ + } + } +} + +// checkComment checks a single comment for TSLint directives +func checkComment(ctx rule.RuleContext, commentText string, commentStart int, isMultiLine bool) { + var contentToCheck string + + if isMultiLine { + // For multi-line comments, remove /* and */ and check the content + contentToCheck = commentText + contentToCheck = strings.TrimPrefix(contentToCheck, "/*") + contentToCheck = strings.TrimSuffix(contentToCheck, "*/") + contentToCheck = strings.TrimSpace(contentToCheck) + } else { + // For single-line comments, remove // and check the content + contentToCheck = commentText + contentToCheck = strings.TrimPrefix(contentToCheck, "//") + contentToCheck = strings.TrimSpace(contentToCheck) + } + + // Check if the content matches TSLint directive pattern + if tslintCommentRegex.MatchString(contentToCheck) { + // Report the TSLint directive + ctx.ReportRange( + core.NewTextRange(commentStart, commentStart+len(commentText)), + rule.RuleMessage{ + Id: "commentDetected", + Description: "tslint comment detected: \"" + strings.TrimSpace(commentText) + "\"", + }, + ) + } +} diff --git a/internal/plugins/typescript/rules/ban_tslint_comment/ban_tslint_comment_test.go b/internal/plugins/typescript/rules/ban_tslint_comment/ban_tslint_comment_test.go new file mode 100644 index 000000000..1a5d9fb3c --- /dev/null +++ b/internal/plugins/typescript/rules/ban_tslint_comment/ban_tslint_comment_test.go @@ -0,0 +1,199 @@ +package ban_tslint_comment + +import ( + "testing" + + "github.com/web-infra-dev/rslint/internal/plugins/typescript/rules/fixtures" + "github.com/web-infra-dev/rslint/internal/rule_tester" +) + +func TestBanTslintCommentRule(t *testing.T) { + rule_tester.RunRuleTester( + fixtures.GetRootDir(), + "tsconfig.json", + t, + &BanTslintCommentRule, + // Valid cases - comments that should NOT be flagged + []rule_tester.ValidTestCase{ + // Valid TypeScript code + {Code: `let a: readonly any[] = [];`}, + {Code: `let a = new Array();`}, + + // Regular comments mentioning tslint (not directives) + {Code: `// some other comment`}, + {Code: `// TODO: this is a comment that mentions tslint`}, + {Code: `/* another comment that mentions tslint */`}, + {Code: `// This project used to use tslint`}, + {Code: `/* We migrated from tslint to eslint */`}, + + // Comments that don't match the directive pattern + {Code: `// tslint is deprecated`}, + {Code: `/* tslint was a linter */`}, + {Code: `// about tslint:disable`}, + {Code: `/* discussing tslint:enable */`}, + }, + // Invalid cases - TSLint directives that should be flagged + []rule_tester.InvalidTestCase{ + // Basic tslint:disable + { + Code: `/* tslint:disable */`, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "commentDetected", Line: 1, Column: 1}, + }, + }, + + // Basic tslint:enable + { + Code: `/* tslint:enable */`, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "commentDetected", Line: 1, Column: 1}, + }, + }, + + // tslint:disable with specific rules + { + Code: `/* tslint:disable:rule1 rule2 rule3... */`, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "commentDetected", Line: 1, Column: 1}, + }, + }, + + // tslint:enable with specific rules + { + Code: `/* tslint:enable:rule1 rule2 rule3... */`, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "commentDetected", Line: 1, Column: 1}, + }, + }, + + // Single-line comment: tslint:disable-next-line + { + Code: `// tslint:disable-next-line`, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "commentDetected", Line: 1, Column: 1}, + }, + }, + + // Inline tslint:disable-line + { + Code: `someCode(); // tslint:disable-line`, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "commentDetected", Line: 1, Column: 13}, + }, + }, + + // tslint:disable-next-line with specific rules + { + Code: `// tslint:disable-next-line:rule1 rule2 rule3...`, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "commentDetected", Line: 1, Column: 1}, + }, + }, + + // Multi-line code with tslint:disable-line + { + Code: `if (true) { + console.log("test"); +} +// tslint:disable-line +const x = 1;`, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "commentDetected", Line: 4, Column: 1}, + }, + }, + + // tslint:enable-line + { + Code: `// tslint:enable-line`, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "commentDetected", Line: 1, Column: 1}, + }, + }, + + // Multiple spaces before directive + { + Code: `// tslint:disable`, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "commentDetected", Line: 1, Column: 1}, + }, + }, + + // Block comment with spaces + { + Code: `/* tslint:disable */`, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "commentDetected", Line: 1, Column: 1}, + }, + }, + + // tslint:disable with colon separator + { + Code: `// tslint:disable:no-console`, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "commentDetected", Line: 1, Column: 1}, + }, + }, + + // tslint:enable with colon separator + { + Code: `// tslint:enable:no-console`, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "commentDetected", Line: 1, Column: 1}, + }, + }, + + // Tab character before directive + { + Code: "//\ttslint:disable", + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "commentDetected", Line: 1, Column: 1}, + }, + }, + + // Multiple tslint comments in one file + { + Code: `// tslint:disable +const x = 1; +// tslint:enable`, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "commentDetected", Line: 1, Column: 1}, + {MessageId: "commentDetected", Line: 3, Column: 1}, + }, + }, + + // tslint:disable-next-line before code + { + Code: `// tslint:disable-next-line +const value = "test";`, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "commentDetected", Line: 1, Column: 1}, + }, + }, + + // Block comment tslint:disable-next-line + { + Code: `/* tslint:disable-next-line */ +const value = "test";`, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "commentDetected", Line: 1, Column: 1}, + }, + }, + + // tslint directive with 's' suffix (alternative format) + { + Code: `// tslint:disables`, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "commentDetected", Line: 1, Column: 1}, + }, + }, + + // tslint directive with 's' suffix for enable + { + Code: `// tslint:enables`, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "commentDetected", Line: 1, Column: 1}, + }, + }, + }, + ) +} diff --git a/packages/rslint-test-tools/rstest.config.mts b/packages/rslint-test-tools/rstest.config.mts index d42c3e336..ca855bd13 100644 --- a/packages/rslint-test-tools/rstest.config.mts +++ b/packages/rslint-test-tools/rstest.config.mts @@ -33,7 +33,7 @@ export default defineConfig({ // Additional tests (commented out) // typescript-eslint - additional rules // './tests/typescript-eslint/rules/ban-ts-comment.test.ts', - // './tests/typescript-eslint/rules/ban-tslint-comment.test.ts', + './tests/typescript-eslint/rules/ban-tslint-comment.test.ts', // './tests/typescript-eslint/rules/class-methods-use-this/class-methods-use-this-core.test.ts', // './tests/typescript-eslint/rules/class-methods-use-this/class-methods-use-this.test.ts', // './tests/typescript-eslint/rules/consistent-generic-constructors.test.ts', diff --git a/packages/rslint-test-tools/tests/typescript-eslint/rules/ban-tslint-comment.test.ts b/packages/rslint-test-tools/tests/typescript-eslint/rules/ban-tslint-comment.test.ts index f6332203f..3657d20b0 100644 --- a/packages/rslint-test-tools/tests/typescript-eslint/rules/ban-tslint-comment.test.ts +++ b/packages/rslint-test-tools/tests/typescript-eslint/rules/ban-tslint-comment.test.ts @@ -1,8 +1,16 @@ import { RuleTester } from '@typescript-eslint/rule-tester'; +import { getFixturesRootDir } from '../RuleTester'; - -const ruleTester = new RuleTester(); +const rootDir = getFixturesRootDir(); +const ruleTester = new RuleTester({ + languageOptions: { + parserOptions: { + project: './tsconfig.json', + tsconfigRootDir: rootDir, + }, + }, +}); ruleTester.run('ban-tslint-comment', { valid: [ @@ -21,6 +29,24 @@ ruleTester.run('ban-tslint-comment', { { code: '/* another comment that mentions tslint */', }, + { + code: '// This project used to use tslint', + }, + { + code: '/* We migrated from tslint to eslint */', + }, + { + code: '// tslint is deprecated', + }, + { + code: '/* tslint was a linter */', + }, + { + code: '// about tslint:disable', + }, + { + code: '/* discussing tslint:enable */', + }, ], invalid: [ {