Skip to content

Commit 66aac2b

Browse files
feat: no-explicit-any rule (#302)
1 parent 2795b0a commit 66aac2b

File tree

7 files changed

+2054
-1
lines changed

7 files changed

+2054
-1
lines changed

internal/config/config.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"github.com/web-infra-dev/rslint/internal/plugins/typescript/rules/no_duplicate_type_constituents"
2121
"github.com/web-infra-dev/rslint/internal/plugins/typescript/rules/no_empty_function"
2222
"github.com/web-infra-dev/rslint/internal/plugins/typescript/rules/no_empty_interface"
23+
"github.com/web-infra-dev/rslint/internal/plugins/typescript/rules/no_explicit_any"
2324
"github.com/web-infra-dev/rslint/internal/plugins/typescript/rules/no_floating_promises"
2425
"github.com/web-infra-dev/rslint/internal/plugins/typescript/rules/no_for_in_array"
2526
"github.com/web-infra-dev/rslint/internal/plugins/typescript/rules/no_implied_eval"
@@ -110,7 +111,6 @@ type ParserOptions struct {
110111
Project ProjectPaths `json:"project,omitempty"`
111112
}
112113

113-
114114
// Rules represents the rules configuration
115115
// This can be extended to include specific rule configurations
116116
type Rules map[string]interface{}
@@ -339,6 +339,7 @@ func registerAllTypeScriptEslintPluginRules() {
339339
GlobalRuleRegistry.Register("@typescript-eslint/no-base-to-string", no_base_to_string.NoBaseToStringRule)
340340
GlobalRuleRegistry.Register("@typescript-eslint/no-confusing-void-expression", no_confusing_void_expression.NoConfusingVoidExpressionRule)
341341
GlobalRuleRegistry.Register("@typescript-eslint/no-duplicate-type-constituents", no_duplicate_type_constituents.NoDuplicateTypeConstituentsRule)
342+
GlobalRuleRegistry.Register("@typescript-eslint/no-explicit-any", no_explicit_any.NoExplicitAnyRule)
342343
GlobalRuleRegistry.Register("@typescript-eslint/no-empty-function", no_empty_function.NoEmptyFunctionRule)
343344
GlobalRuleRegistry.Register("@typescript-eslint/no-empty-interface", no_empty_interface.NoEmptyInterfaceRule)
344345
GlobalRuleRegistry.Register("@typescript-eslint/no-floating-promises", no_floating_promises.NoFloatingPromisesRule)
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
package no_explicit_any
2+
3+
import (
4+
"github.com/microsoft/typescript-go/shim/ast"
5+
"github.com/web-infra-dev/rslint/internal/rule"
6+
)
7+
8+
type NoExplicitAnyOptions struct {
9+
FixToUnknown bool `json:"fixToUnknown"`
10+
IgnoreRestArgs bool `json:"ignoreRestArgs"`
11+
}
12+
13+
func buildUnexpectedAnyMessage() rule.RuleMessage {
14+
return rule.RuleMessage{
15+
Id: "unexpectedAny",
16+
Description: "Unexpected any. Specify a different type.",
17+
}
18+
}
19+
20+
func buildSuggestUnknownMessage() rule.RuleMessage {
21+
return rule.RuleMessage{
22+
Id: "suggestUnknown",
23+
Description: "Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct.",
24+
}
25+
}
26+
27+
func buildSuggestNeverMessage() rule.RuleMessage {
28+
return rule.RuleMessage{
29+
Id: "suggestNever",
30+
Description: "Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of.",
31+
}
32+
}
33+
34+
func buildSuggestPropertyKeyMessage() rule.RuleMessage {
35+
return rule.RuleMessage{
36+
Id: "suggestPropertyKey",
37+
Description: "Use `PropertyKey` instead, this is more explicit than `keyof any`.",
38+
}
39+
}
40+
41+
func parseOptions(options any) NoExplicitAnyOptions {
42+
opts := NoExplicitAnyOptions{}
43+
if options == nil {
44+
return opts
45+
}
46+
// Handle array format: [{ option: value }]
47+
if arr, ok := options.([]interface{}); ok {
48+
if len(arr) > 0 {
49+
if m, ok := arr[0].(map[string]interface{}); ok {
50+
if v, ok := m["fixToUnknown"].(bool); ok {
51+
opts.FixToUnknown = v
52+
}
53+
if v, ok := m["ignoreRestArgs"].(bool); ok {
54+
opts.IgnoreRestArgs = v
55+
}
56+
}
57+
}
58+
return opts
59+
}
60+
// Handle direct object format
61+
if m, ok := options.(map[string]interface{}); ok {
62+
if v, ok := m["fixToUnknown"].(bool); ok {
63+
opts.FixToUnknown = v
64+
}
65+
if v, ok := m["ignoreRestArgs"].(bool); ok {
66+
opts.IgnoreRestArgs = v
67+
}
68+
}
69+
return opts
70+
}
71+
72+
func isAnyInRestParameter(node *ast.Node) bool {
73+
// Check if the any keyword is inside a rest parameter with array type
74+
// We need to check if the any is part of an array type in a rest parameter
75+
// Valid patterns to ignore: ...args: any[], ...args: readonly any[], ...args: Array<any>, ...args: ReadonlyArray<any>
76+
77+
// First check if we're inside an ArrayType
78+
inArrayType := false
79+
for p := node.Parent; p != nil; p = p.Parent {
80+
if p.Kind == ast.KindArrayType {
81+
inArrayType = true
82+
break
83+
}
84+
if p.Kind == ast.KindTypeReference {
85+
typeRef := p.AsTypeReference()
86+
if typeRef != nil && ast.IsIdentifier(typeRef.TypeName) {
87+
identifier := typeRef.TypeName.AsIdentifier()
88+
if identifier != nil && (identifier.Text == "Array" || identifier.Text == "ReadonlyArray") {
89+
inArrayType = true
90+
break
91+
}
92+
}
93+
}
94+
}
95+
96+
if !inArrayType {
97+
return false
98+
}
99+
100+
// Then check if we're in a rest parameter
101+
for p := node.Parent; p != nil; p = p.Parent {
102+
if p.Kind == ast.KindParameter {
103+
param := p.AsParameterDeclaration()
104+
return param.DotDotDotToken != nil
105+
}
106+
}
107+
return false
108+
}
109+
110+
func isWithinKeyofAny(node *ast.Node) bool {
111+
if node.Parent == nil || node.Parent.Kind != ast.KindTypeOperator {
112+
return false
113+
}
114+
typeOp := node.Parent.AsTypeOperatorNode()
115+
return typeOp != nil && typeOp.Operator == ast.KindKeyOfKeyword
116+
}
117+
118+
var NoExplicitAnyRule = rule.CreateRule(rule.Rule{
119+
Name: "no-explicit-any",
120+
Run: func(ctx rule.RuleContext, options any) rule.RuleListeners {
121+
opts := parseOptions(options)
122+
123+
return rule.RuleListeners{
124+
ast.KindAnyKeyword: func(node *ast.Node) {
125+
if opts.IgnoreRestArgs && isAnyInRestParameter(node) {
126+
return
127+
}
128+
if isWithinKeyofAny(node) {
129+
if opts.FixToUnknown {
130+
ctx.ReportNodeWithFixes(node, buildUnexpectedAnyMessage(), rule.RuleFixReplace(ctx.SourceFile, node.Parent, "PropertyKey"))
131+
} else {
132+
ctx.ReportNodeWithSuggestions(node, buildUnexpectedAnyMessage(), rule.RuleSuggestion{
133+
Message: buildSuggestPropertyKeyMessage(),
134+
FixesArr: []rule.RuleFix{rule.RuleFixReplace(ctx.SourceFile, node.Parent, "PropertyKey")},
135+
})
136+
}
137+
return
138+
}
139+
140+
if opts.FixToUnknown {
141+
ctx.ReportNodeWithFixes(node, buildUnexpectedAnyMessage(), rule.RuleFixReplace(ctx.SourceFile, node, "unknown"))
142+
} else {
143+
ctx.ReportNodeWithSuggestions(node, buildUnexpectedAnyMessage(),
144+
rule.RuleSuggestion{
145+
Message: buildSuggestUnknownMessage(),
146+
FixesArr: []rule.RuleFix{rule.RuleFixReplace(ctx.SourceFile, node, "unknown")},
147+
},
148+
rule.RuleSuggestion{
149+
Message: buildSuggestNeverMessage(),
150+
FixesArr: []rule.RuleFix{rule.RuleFixReplace(ctx.SourceFile, node, "never")},
151+
},
152+
)
153+
}
154+
},
155+
}
156+
},
157+
})
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
package no_explicit_any
2+
3+
import (
4+
"testing"
5+
6+
"github.com/web-infra-dev/rslint/internal/plugins/typescript/rules/fixtures"
7+
"github.com/web-infra-dev/rslint/internal/rule_tester"
8+
)
9+
10+
func TestNoExplicitAnyRule(t *testing.T) {
11+
rule_tester.RunRuleTester(fixtures.GetRootDir(), "tsconfig.json", t, &NoExplicitAnyRule, []rule_tester.ValidTestCase{
12+
{Code: `const number: number = 1;`},
13+
{
14+
Code: `function foo(...args: any[]) {}`,
15+
Options: []interface{}{map[string]interface{}{"ignoreRestArgs": true}},
16+
},
17+
}, []rule_tester.InvalidTestCase{
18+
{
19+
Code: `const number: any = 1;`,
20+
Errors: []rule_tester.InvalidTestCaseError{
21+
{
22+
MessageId: "unexpectedAny",
23+
Line: 1,
24+
Column: 15,
25+
EndLine: 1,
26+
EndColumn: 18,
27+
Suggestions: []rule_tester.InvalidTestCaseSuggestion{
28+
{
29+
MessageId: "suggestUnknown",
30+
Output: `const number: unknown = 1;`,
31+
},
32+
{
33+
MessageId: "suggestNever",
34+
Output: `const number: never = 1;`,
35+
},
36+
},
37+
},
38+
},
39+
},
40+
{
41+
Code: `type T = keyof any;`,
42+
Errors: []rule_tester.InvalidTestCaseError{
43+
{
44+
MessageId: "unexpectedAny",
45+
Line: 1,
46+
Column: 16,
47+
EndLine: 1,
48+
EndColumn: 19,
49+
Suggestions: []rule_tester.InvalidTestCaseSuggestion{
50+
{
51+
MessageId: "suggestPropertyKey",
52+
Output: `type T = PropertyKey;`,
53+
},
54+
},
55+
},
56+
},
57+
},
58+
{
59+
Code: `function foo(...args: any[]) {}`,
60+
Errors: []rule_tester.InvalidTestCaseError{
61+
{
62+
MessageId: "unexpectedAny",
63+
Line: 1,
64+
Column: 23,
65+
EndLine: 1,
66+
EndColumn: 26,
67+
Suggestions: []rule_tester.InvalidTestCaseSuggestion{
68+
{
69+
MessageId: "suggestUnknown",
70+
Output: `function foo(...args: unknown[]) {}`,
71+
},
72+
{
73+
MessageId: "suggestNever",
74+
Output: `function foo(...args: never[]) {}`,
75+
},
76+
},
77+
},
78+
},
79+
},
80+
{
81+
Code: `const number: any = 1;`,
82+
Options: []interface{}{map[string]interface{}{"fixToUnknown": true}},
83+
Output: []string{`const number: unknown = 1;`},
84+
Errors: []rule_tester.InvalidTestCaseError{
85+
{
86+
MessageId: "unexpectedAny",
87+
Line: 1,
88+
Column: 15,
89+
EndLine: 1,
90+
EndColumn: 18,
91+
},
92+
},
93+
},
94+
})
95+
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export default defineConfig({
1818
// './tests/typescript-eslint/rules/no-confusing-void-expression.test.ts',
1919
'./tests/typescript-eslint/rules/no-empty-function.test.ts',
2020
'./tests/typescript-eslint/rules/no-empty-interface.test.ts',
21+
'./tests/typescript-eslint/rules/no-explicit-any.test.ts',
2122
'./tests/typescript-eslint/rules/no-require-imports.test.ts',
2223
// too many autofix errors
2324
'./tests/typescript-eslint/rules/no-duplicate-type-constituents.test.ts',

packages/rslint-test-tools/rule-manifest.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,12 @@
6969
"status": "full",
7070
"failing_case": []
7171
},
72+
{
73+
"name": "no-explicit-any",
74+
"group": "@typescript-eslint",
75+
"status": "full",
76+
"failing_case": []
77+
},
7278
{
7379
"name": "no-floating-promises",
7480
"group": "@typescript-eslint",

0 commit comments

Comments
 (0)