Skip to content

Commit e5ffbf6

Browse files
delino[bot]github-actions[bot]claude
authored andcommitted
test: Enable test for ban-tslint-comment rule (#151)
## Summary - Uncommented the test file for the `ban-tslint-comment` rule in `rstest.config.mts` to enable testing - Implemented the ban-tslint-comment rule in Go - Registered the rule in the configuration ## Context This PR is a follow-up to #149 which enabled the `ban-ts-comment` rule. This PR enables the `ban-tslint-comment` rule which helps enforce migration from deprecated TSLint to ESLint. ## Changes 1. **Test Configuration** (rstest.config.mts:36) - Uncommented the line enabling the ban-tslint-comment test file 2. **Implementation** (internal/plugins/typescript/rules/ban_tslint_comment/ban_tslint_comment.go) - Created new Go implementation for the ban-tslint-comment rule - Detects `tslint:disable` and `tslint:enable` comments in both single-line (`//`) and multi-line (`/* */`) formats - Provides automatic fixes that remove tslint comments - Handles various comment positions (standalone lines and inline comments) - Reports errors with messageId 'commentDetected' and appropriate description 3. **Registration** (internal/config/config.go) - Added import for the ban_tslint_comment package - Registered the rule as `@typescript-eslint/ban-tslint-comment` ## Implementation Details The rule implementation: - Scans source text for tslint comment patterns - Matches comments starting with `tslint:disable` or `tslint:enable` - Creates fixes that intelligently remove comments: - For standalone comment lines: removes the entire line including newline - For inline comments (e.g., `someCode(); // tslint:disable-line`): removes just the comment - Follows the pattern established by the ban-ts-comment rule ## Test Plan - [x] Run `npm test` to verify all tests pass - [ ] Check CI pipeline for any failures - [ ] Fix any implementation issues if tests fail in CI ## TypeScript-ESLint Reference The implementation follows the behavior of the TypeScript-ESLint ban-tslint-comment rule: https://typescript-eslint.io/rules/ban-tslint-comment/ TSLint was deprecated in favor of ESLint, and this rule helps enforce the migration by detecting and removing TSLint directive comments. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> Co-authored-by: Claude <noreply@anthropic.com>
1 parent b286c88 commit e5ffbf6

File tree

5 files changed

+223
-1
lines changed

5 files changed

+223
-1
lines changed

internal/config/config.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"github.com/web-infra-dev/rslint/internal/plugins/typescript/rules/array_type"
1515
"github.com/web-infra-dev/rslint/internal/plugins/typescript/rules/await_thenable"
1616
"github.com/web-infra-dev/rslint/internal/plugins/typescript/rules/ban_ts_comment"
17+
"github.com/web-infra-dev/rslint/internal/plugins/typescript/rules/ban_tslint_comment"
1718
"github.com/web-infra-dev/rslint/internal/plugins/typescript/rules/ban_types"
1819
"github.com/web-infra-dev/rslint/internal/plugins/typescript/rules/class_literal_property_style"
1920
"github.com/web-infra-dev/rslint/internal/plugins/typescript/rules/consistent_generic_constructors"
@@ -365,6 +366,7 @@ func registerAllTypeScriptEslintPluginRules() {
365366
GlobalRuleRegistry.Register("@typescript-eslint/array-type", array_type.ArrayTypeRule)
366367
GlobalRuleRegistry.Register("@typescript-eslint/await-thenable", await_thenable.AwaitThenableRule)
367368
GlobalRuleRegistry.Register("@typescript-eslint/ban-ts-comment", ban_ts_comment.BanTsCommentRule)
369+
GlobalRuleRegistry.Register("@typescript-eslint/ban-tslint-comment", ban_tslint_comment.BanTslintCommentRule)
368370
GlobalRuleRegistry.Register("@typescript-eslint/ban-types", ban_types.BanTypesRule)
369371
GlobalRuleRegistry.Register("@typescript-eslint/class-literal-property-style", class_literal_property_style.ClassLiteralPropertyStyleRule)
370372
GlobalRuleRegistry.Register("@typescript-eslint/consistent-generic-constructors", consistent_generic_constructors.ConsistentGenericConstructorsRule)

internal/linter/linter.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,20 @@ func RunLinterInProgram(program *compiler.Program, allowFiles []string, skipFile
9999
Severity: r.Severity,
100100
})
101101
},
102+
ReportRangeWithFixes: func(textRange core.TextRange, msg rule.RuleMessage, fixes ...rule.RuleFix) {
103+
// Check if rule is disabled at this position
104+
if disableManager.IsRuleDisabled(r.Name, textRange.Pos()) {
105+
return
106+
}
107+
onDiagnostic(rule.RuleDiagnostic{
108+
RuleName: r.Name,
109+
Range: textRange,
110+
Message: msg,
111+
FixesPtr: &fixes,
112+
SourceFile: file,
113+
Severity: r.Severity,
114+
})
115+
},
102116
ReportNode: func(node *ast.Node, msg rule.RuleMessage) {
103117
// Check if rule is disabled at this position
104118
if disableManager.IsRuleDisabled(r.Name, node.Pos()) {
Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
package ban_tslint_comment
2+
3+
import (
4+
"regexp"
5+
"strings"
6+
7+
"github.com/microsoft/typescript-go/shim/core"
8+
"github.com/web-infra-dev/rslint/internal/rule"
9+
)
10+
11+
// Regular expressions for matching TSLint directives
12+
var (
13+
// Matches single-line comments: // tslint:disable or // tslint:enable
14+
singleLineTslintRegex = regexp.MustCompile(`^//\s*tslint:(disable|enable)`)
15+
16+
// Matches multi-line comments: /* tslint:disable */ or /* tslint:enable */
17+
multiLineTslintRegex = regexp.MustCompile(`^/\*\s*tslint:(disable|enable)`)
18+
)
19+
20+
// BanTslintCommentRule implements the ban-tslint-comment rule
21+
// Bans // tslint:<rule-flag> comments
22+
var BanTslintCommentRule = rule.CreateRule(rule.Rule{
23+
Name: "ban-tslint-comment",
24+
Run: run,
25+
})
26+
27+
func run(ctx rule.RuleContext, options any) rule.RuleListeners {
28+
// Get the full text of the source file
29+
text := ctx.SourceFile.Text()
30+
31+
// Process the text to find tslint comments
32+
processComments(ctx, text)
33+
34+
return rule.RuleListeners{}
35+
}
36+
37+
// processComments scans the source text for tslint comments
38+
func processComments(ctx rule.RuleContext, text string) {
39+
pos := 0
40+
length := len(text)
41+
lineStarts := calculateLineStarts(text)
42+
43+
for pos < length {
44+
// Skip to next potential comment
45+
if pos+1 < length {
46+
if text[pos] == '/' && text[pos+1] == '/' {
47+
// Single-line comment
48+
commentStart := pos
49+
pos += 2
50+
lineEnd := pos
51+
for lineEnd < length && text[lineEnd] != '\n' && text[lineEnd] != '\r' {
52+
lineEnd++
53+
}
54+
commentText := text[commentStart:lineEnd]
55+
56+
// Check if this is a tslint comment
57+
if singleLineTslintRegex.MatchString(commentText) {
58+
reportTslintComment(ctx, commentText, commentStart, lineEnd, lineStarts, text)
59+
}
60+
61+
pos = lineEnd
62+
} else if text[pos] == '/' && text[pos+1] == '*' {
63+
// Multi-line comment
64+
commentStart := pos
65+
pos += 2
66+
commentEnd := pos
67+
for commentEnd+1 < length {
68+
if text[commentEnd] == '*' && text[commentEnd+1] == '/' {
69+
commentEnd += 2
70+
break
71+
}
72+
commentEnd++
73+
}
74+
commentText := text[commentStart:commentEnd]
75+
76+
// Check if this is a tslint comment
77+
if multiLineTslintRegex.MatchString(commentText) {
78+
reportTslintComment(ctx, commentText, commentStart, commentEnd, lineStarts, text)
79+
}
80+
81+
pos = commentEnd
82+
} else {
83+
pos++
84+
}
85+
} else {
86+
pos++
87+
}
88+
}
89+
}
90+
91+
// calculateLineStarts returns the starting positions of each line
92+
func calculateLineStarts(text string) []int {
93+
lineStarts := []int{0}
94+
for i := 0; i < len(text); i++ {
95+
if text[i] == '\n' {
96+
lineStarts = append(lineStarts, i+1)
97+
}
98+
}
99+
return lineStarts
100+
}
101+
102+
// getLineAndColumn returns the line and column numbers for a given position
103+
func getLineAndColumn(pos int, lineStarts []int) (line, column int) {
104+
for i := len(lineStarts) - 1; i >= 0; i-- {
105+
if pos >= lineStarts[i] {
106+
line = i + 1
107+
column = pos - lineStarts[i] + 1
108+
return
109+
}
110+
}
111+
return 1, 1
112+
}
113+
114+
// reportTslintComment reports a tslint comment with autofix
115+
func reportTslintComment(ctx rule.RuleContext, commentText string, start, end int, lineStarts []int, fullText string) {
116+
line, column := getLineAndColumn(start, lineStarts)
117+
118+
// Create the fix
119+
fix := createFix(start, end, fullText)
120+
121+
ctx.ReportRangeWithFixes(
122+
core.NewTextRange(start, end),
123+
rule.RuleMessage{
124+
Id: "commentDetected",
125+
Description: "tslint is deprecated and you should stop using it",
126+
},
127+
*fix,
128+
)
129+
130+
_ = line
131+
_ = column
132+
}
133+
134+
// createFix creates a fix that removes the tslint comment
135+
func createFix(start, end int, fullText string) *rule.RuleFix {
136+
// Check if we need to remove the entire line or just the comment
137+
138+
// Look backwards to see if there's any non-whitespace before the comment
139+
hasContentBefore := false
140+
lineStart := start
141+
for lineStart > 0 && fullText[lineStart-1] != '\n' && fullText[lineStart-1] != '\r' {
142+
lineStart--
143+
if !isWhitespace(fullText[lineStart]) {
144+
hasContentBefore = true
145+
}
146+
}
147+
148+
// Look forwards to see if there's any non-whitespace after the comment (on the same line)
149+
hasContentAfter := false
150+
lineEnd := end
151+
for lineEnd < len(fullText) && fullText[lineEnd] != '\n' && fullText[lineEnd] != '\r' {
152+
if !isWhitespace(fullText[lineEnd]) {
153+
hasContentAfter = true
154+
break
155+
}
156+
lineEnd++
157+
}
158+
159+
// Skip the newline characters if removing the entire line
160+
if !hasContentBefore && !hasContentAfter {
161+
// Include the newline in the removal
162+
if lineEnd < len(fullText) && fullText[lineEnd] == '\r' {
163+
lineEnd++
164+
}
165+
if lineEnd < len(fullText) && fullText[lineEnd] == '\n' {
166+
lineEnd++
167+
}
168+
169+
return &rule.RuleFix{
170+
Range: core.NewTextRange(lineStart, lineEnd),
171+
Text: "",
172+
}
173+
}
174+
175+
// If there's content before the comment (e.g., "someCode(); // tslint:disable-line")
176+
if hasContentBefore {
177+
// Remove just the comment, preserving whitespace before it but removing the comment
178+
// Find where the actual code ends
179+
codeEnd := start
180+
for codeEnd > lineStart && isWhitespace(fullText[codeEnd-1]) {
181+
codeEnd--
182+
}
183+
184+
return &rule.RuleFix{
185+
Range: core.NewTextRange(codeEnd, end),
186+
Text: "",
187+
}
188+
}
189+
190+
// Otherwise, just remove the comment
191+
return &rule.RuleFix{
192+
Range: core.NewTextRange(start, end),
193+
Text: "",
194+
}
195+
}
196+
197+
// isWhitespace checks if a character is whitespace
198+
func isWhitespace(ch byte) bool {
199+
return ch == ' ' || ch == '\t' || ch == '\r' || ch == '\n'
200+
}
201+
202+
// trimTrailingWhitespace removes trailing whitespace and newlines
203+
func trimTrailingWhitespace(s string) string {
204+
return strings.TrimRight(s, " \t\r\n")
205+
}

internal/rule/rule.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@ type RuleContext struct {
168168
DisableManager *DisableManager
169169
ReportRange func(textRange core.TextRange, msg RuleMessage)
170170
ReportRangeWithSuggestions func(textRange core.TextRange, msg RuleMessage, suggestions ...RuleSuggestion)
171+
ReportRangeWithFixes func(textRange core.TextRange, msg RuleMessage, fixes ...RuleFix)
171172
ReportNode func(node *ast.Node, msg RuleMessage)
172173
ReportNodeWithFixes func(node *ast.Node, msg RuleMessage, fixes ...RuleFix)
173174
ReportNodeWithSuggestions func(node *ast.Node, msg RuleMessage, suggestions ...RuleSuggestion)

packages/rslint-test-tools/rstest.config.mts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ export default defineConfig({
3333
// Additional tests (commented out)
3434
// typescript-eslint - additional rules
3535
'./tests/typescript-eslint/rules/ban-ts-comment.test.ts',
36-
// './tests/typescript-eslint/rules/ban-tslint-comment.test.ts',
36+
'./tests/typescript-eslint/rules/ban-tslint-comment.test.ts',
3737
// './tests/typescript-eslint/rules/class-methods-use-this/class-methods-use-this-core.test.ts',
3838
// './tests/typescript-eslint/rules/class-methods-use-this/class-methods-use-this.test.ts',
3939
// './tests/typescript-eslint/rules/consistent-generic-constructors.test.ts',

0 commit comments

Comments
 (0)