Skip to content

Commit 24962ba

Browse files
authored
feat: port rule valid-typeof (#520)
1 parent 2f2fee9 commit 24962ba

File tree

7 files changed

+1189
-0
lines changed

7 files changed

+1189
-0
lines changed

internal/config/config.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@ import (
147147
"github.com/web-infra-dev/rslint/internal/rules/prefer_const"
148148
"github.com/web-infra-dev/rslint/internal/rules/prefer_rest_params"
149149
"github.com/web-infra-dev/rslint/internal/rules/use_isnan"
150+
"github.com/web-infra-dev/rslint/internal/rules/valid_typeof"
150151
)
151152

152153
// RslintConfig represents the top-level configuration array
@@ -515,6 +516,7 @@ func registerAllCoreEslintRules() {
515516
GlobalRuleRegistry.Register("use-isnan", use_isnan.UseIsNaNRule)
516517
GlobalRuleRegistry.Register("eqeqeq", eqeqeq.EqeqeqRule)
517518
GlobalRuleRegistry.Register("no-fallthrough", no_fallthrough.NoFallthroughRule)
519+
GlobalRuleRegistry.Register("valid-typeof", valid_typeof.ValidTypeofRule)
518520
}
519521

520522
// isFileIgnored checks if a file is matched by ignore patterns, evaluated sequentially.
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
package valid_typeof
2+
3+
import (
4+
"github.com/microsoft/typescript-go/shim/ast"
5+
"github.com/web-infra-dev/rslint/internal/rule"
6+
"github.com/web-infra-dev/rslint/internal/utils"
7+
)
8+
9+
// validTypes is the set of strings that are valid results of the typeof operator.
10+
var validTypes = map[string]bool{
11+
"undefined": true,
12+
"object": true,
13+
"boolean": true,
14+
"number": true,
15+
"string": true,
16+
"function": true,
17+
"symbol": true,
18+
"bigint": true,
19+
}
20+
21+
func invalidValueMsg() rule.RuleMessage {
22+
return rule.RuleMessage{
23+
Id: "invalidValue",
24+
Description: "Invalid typeof comparison value.",
25+
}
26+
}
27+
28+
func notStringMsg() rule.RuleMessage {
29+
return rule.RuleMessage{
30+
Id: "notString",
31+
Description: "Typeof comparisons should be to string literals.",
32+
}
33+
}
34+
35+
func suggestStringMsg() rule.RuleMessage {
36+
return rule.RuleMessage{
37+
Id: "suggestString",
38+
Description: `Use "undefined" instead of undefined.`,
39+
}
40+
}
41+
42+
type validTypeofOptions struct {
43+
requireStringLiterals bool
44+
}
45+
46+
func parseOptions(opts any) validTypeofOptions {
47+
result := validTypeofOptions{
48+
requireStringLiterals: false,
49+
}
50+
51+
optsMap := utils.GetOptionsMap(opts)
52+
if optsMap != nil {
53+
if req, ok := optsMap["requireStringLiterals"].(bool); ok {
54+
result.requireStringLiterals = req
55+
}
56+
}
57+
58+
return result
59+
}
60+
61+
// isEqualityOperator checks if the operator kind is ==, ===, !=, or !==.
62+
func isEqualityOperator(kind ast.Kind) bool {
63+
return ast.GetBinaryOperatorPrecedence(kind) == ast.OperatorPrecedenceEquality
64+
}
65+
66+
// https://eslint.org/docs/latest/rules/valid-typeof
67+
var ValidTypeofRule = rule.Rule{
68+
Name: "valid-typeof",
69+
Run: func(ctx rule.RuleContext, options any) rule.RuleListeners {
70+
opts := parseOptions(options)
71+
72+
return rule.RuleListeners{
73+
ast.KindTypeOfExpression: func(node *ast.Node) {
74+
// Walk up through parenthesized expressions to find the enclosing
75+
// binary expression. The TS parser creates ParenthesizedExpression
76+
// nodes for parentheses, so we must skip them.
77+
parent := node.Parent
78+
for parent != nil && parent.Kind == ast.KindParenthesizedExpression {
79+
parent = parent.Parent
80+
}
81+
if parent == nil || parent.Kind != ast.KindBinaryExpression {
82+
return
83+
}
84+
85+
bin := parent.AsBinaryExpression()
86+
if bin == nil || bin.OperatorToken == nil {
87+
return
88+
}
89+
90+
if !isEqualityOperator(bin.OperatorToken.Kind) {
91+
return
92+
}
93+
94+
// Determine which operand is the sibling (not the typeof side).
95+
// Use SkipParentheses on both sides to match against the typeof node
96+
// since either side may be wrapped in parentheses.
97+
var sibling *ast.Node
98+
if ast.SkipParentheses(bin.Left) == node {
99+
sibling = ast.SkipParentheses(bin.Right)
100+
} else {
101+
sibling = ast.SkipParentheses(bin.Left)
102+
}
103+
104+
if sibling == nil {
105+
return
106+
}
107+
108+
switch {
109+
case ast.IsStringLiteralLike(sibling):
110+
// String literal or static template literal — check value.
111+
value := sibling.LiteralLikeData().Text
112+
if !validTypes[value] {
113+
ctx.ReportNode(sibling, invalidValueMsg())
114+
}
115+
116+
case sibling.Kind == ast.KindNumericLiteral,
117+
sibling.Kind == ast.KindBigIntLiteral,
118+
sibling.Kind == ast.KindRegularExpressionLiteral,
119+
ast.IsBooleanLiteral(sibling),
120+
utils.IsNullLiteral(sibling):
121+
// Non-string literals can never be valid typeof results.
122+
ctx.ReportNode(sibling, invalidValueMsg())
123+
124+
case sibling.Kind == ast.KindIdentifier:
125+
if sibling.Text() == "undefined" && !utils.IsShadowed(sibling, "undefined") {
126+
// Bare `undefined` referencing the global variable:
127+
// report with suggestion to use "undefined" string.
128+
msg := invalidValueMsg()
129+
if opts.requireStringLiterals {
130+
msg = notStringMsg()
131+
}
132+
ctx.ReportNodeWithSuggestions(sibling, msg, rule.RuleSuggestion{
133+
Message: suggestStringMsg(),
134+
FixesArr: []rule.RuleFix{
135+
rule.RuleFixReplace(ctx.SourceFile, sibling, `"undefined"`),
136+
},
137+
})
138+
} else if opts.requireStringLiterals {
139+
// Any other identifier (including shadowed `undefined`)
140+
// is not a string literal.
141+
ctx.ReportNode(sibling, notStringMsg())
142+
}
143+
144+
case sibling.Kind == ast.KindTypeOfExpression:
145+
// typeof === typeof is always valid
146+
147+
default:
148+
if opts.requireStringLiterals {
149+
ctx.ReportNode(sibling, notStringMsg())
150+
}
151+
}
152+
},
153+
}
154+
},
155+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<!-- cspell:ignore strnig undefned nunber fucntion -->
2+
3+
# valid-typeof
4+
5+
## Rule Details
6+
7+
Enforces comparing `typeof` expressions against valid string literals. The `typeof` operator can only return one of the following strings: `"undefined"`, `"object"`, `"boolean"`, `"number"`, `"string"`, `"function"`, `"symbol"`, `"bigint"`. Comparing a `typeof` expression against any other value is almost certainly a bug.
8+
9+
Examples of **incorrect** code for this rule:
10+
11+
```javascript
12+
typeof foo === 'strnig';
13+
typeof foo == 'undefned';
14+
typeof bar != 'nunber';
15+
typeof bar !== 'fucntion';
16+
typeof foo === undefined;
17+
```
18+
19+
Examples of **correct** code for this rule:
20+
21+
```javascript
22+
typeof foo === 'string';
23+
typeof bar == 'undefined';
24+
typeof baz === 'object';
25+
typeof qux !== 'function';
26+
typeof foo === typeof bar;
27+
```
28+
29+
## Options
30+
31+
### `requireStringLiterals`
32+
33+
When set to `true`, requires that `typeof` expressions are only compared to string literals or other `typeof` expressions, and disallows comparisons to any other value.
34+
35+
Examples of additional **incorrect** code with `{ "requireStringLiterals": true }`:
36+
37+
```javascript
38+
typeof foo === undefined;
39+
typeof foo === Object;
40+
typeof foo === someVariable;
41+
```
42+
43+
## Original Documentation
44+
45+
- [ESLint valid-typeof](https://eslint.org/docs/latest/rules/valid-typeof)

0 commit comments

Comments
 (0)