Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ import (
"github.com/web-infra-dev/rslint/internal/rules/no_loss_of_precision"
"github.com/web-infra-dev/rslint/internal/rules/no_sparse_arrays"
"github.com/web-infra-dev/rslint/internal/rules/no_template_curly_in_string"
"github.com/web-infra-dev/rslint/internal/rules/valid_typeof"
)

// RslintConfig represents the top-level configuration array
Expand Down Expand Up @@ -425,6 +426,7 @@ func registerAllCoreEslintRules() {
GlobalRuleRegistry.Register("no-loss-of-precision", no_loss_of_precision.NoLossOfPrecisionRule)
GlobalRuleRegistry.Register("no-template-curly-in-string", no_template_curly_in_string.NoTemplateCurlyInString)
GlobalRuleRegistry.Register("no-sparse-arrays", no_sparse_arrays.NoSparseArraysRule)
GlobalRuleRegistry.Register("valid-typeof", valid_typeof.ValidTypeofRule)
}

// getAllTypeScriptEslintPluginRules returns all rules from the global registry.
Expand Down
134 changes: 134 additions & 0 deletions internal/rules/valid_typeof/valid_typeof.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
package valid_typeof

import (
"github.com/microsoft/typescript-go/shim/ast"
"github.com/web-infra-dev/rslint/internal/rule"
"github.com/web-infra-dev/rslint/internal/utils"
)

// validTypes is the set of strings that are valid results of the typeof operator.
var validTypes = map[string]bool{
"undefined": true,
"object": true,
"boolean": true,
"number": true,
"string": true,
"function": true,
"symbol": true,
"bigint": true,
}

func invalidValueMsg() rule.RuleMessage {
return rule.RuleMessage{
Id: "invalidValue",
Description: "Invalid typeof comparison value.",
}
}

func notStringMsg() rule.RuleMessage {
return rule.RuleMessage{
Id: "notString",
Description: "Typeof comparisons should be to string literals.",
}
}

type validTypeofOptions struct {
requireStringLiterals bool
}

func parseOptions(opts any) validTypeofOptions {
result := validTypeofOptions{
requireStringLiterals: false,
}

optsMap := utils.GetOptionsMap(opts)
if optsMap != nil {
if req, ok := optsMap["requireStringLiterals"].(bool); ok {
result.requireStringLiterals = req
}
}

return result
}

// isEqualityOperator checks if the operator kind is ==, ===, !=, or !==.
func isEqualityOperator(kind ast.Kind) bool {
return kind == ast.KindEqualsEqualsToken ||
kind == ast.KindEqualsEqualsEqualsToken ||
kind == ast.KindExclamationEqualsToken ||
kind == ast.KindExclamationEqualsEqualsToken
}

// https://eslint.org/docs/latest/rules/valid-typeof
var ValidTypeofRule = rule.Rule{
Name: "valid-typeof",
Run: func(ctx rule.RuleContext, options any) rule.RuleListeners {
opts := parseOptions(options)

return rule.RuleListeners{
ast.KindTypeOfExpression: func(node *ast.Node) {
parent := node.Parent
if parent == nil || parent.Kind != ast.KindBinaryExpression {
return
}

bin := parent.AsBinaryExpression()
if bin == nil || bin.OperatorToken == nil {
return
}

// Only check equality/inequality operators
if !isEqualityOperator(bin.OperatorToken.Kind) {
return
}

// Get the sibling operand (the one that is not the typeof expression)
var sibling *ast.Node
if bin.Left == node {
sibling = bin.Right
} else {
sibling = bin.Left
}

if sibling == nil {
return
}

switch sibling.Kind {
case ast.KindStringLiteral:
// Check if the string value is a valid typeof result
// Use AsStringLiteral().Text to get unquoted value (not .Text() which includes quotes)
value := sibling.AsStringLiteral().Text
if !validTypes[value] {
ctx.ReportNode(sibling, invalidValueMsg())
}

case ast.KindIdentifier:
// Bare `undefined` identifier is always invalid in typeof comparisons.
// With requireStringLiterals, report as "notString";
// without, report as "invalidValue" (since typeof never actually
// returns the undefined *value*, only the string "undefined").
if sibling.Text() == "undefined" {
if opts.requireStringLiterals {
ctx.ReportNode(sibling, notStringMsg())
} else {
ctx.ReportNode(sibling, invalidValueMsg())
}
} else if opts.requireStringLiterals {
// Any non-undefined identifier is not a string literal
ctx.ReportNode(sibling, notStringMsg())
}

case ast.KindTypeOfExpression:
// typeof === typeof is always valid

default:
// For any other expression (template literals, variables, etc.)
if opts.requireStringLiterals {
ctx.ReportNode(sibling, notStringMsg())
}
}
},
}
},
}
45 changes: 45 additions & 0 deletions internal/rules/valid_typeof/valid_typeof.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<!-- cspell:ignore strnig undefned nunber fucntion -->

# valid-typeof

## Rule Details

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.

Examples of **incorrect** code for this rule:

```javascript
typeof foo === 'strnig';
typeof foo == 'undefned';
typeof bar != 'nunber';
typeof bar !== 'fucntion';
typeof foo === undefined;
```

Examples of **correct** code for this rule:

```javascript
typeof foo === 'string';
typeof bar == 'undefined';
typeof baz === 'object';
typeof qux !== 'function';
typeof foo === typeof bar;
```

## Options

### `requireStringLiterals`

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.

Examples of additional **incorrect** code with `{ "requireStringLiterals": true }`:

```javascript
typeof foo === undefined;
typeof foo === Object;
typeof foo === someVariable;
```

## Original Documentation

- [ESLint valid-typeof](https://eslint.org/docs/latest/rules/valid-typeof)
133 changes: 133 additions & 0 deletions internal/rules/valid_typeof/valid_typeof_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
//nolint:misspell // cspell:ignore strnig undefned nunber fucntion
package valid_typeof

import (
"testing"

"github.com/web-infra-dev/rslint/internal/plugins/typescript/rules/fixtures"
"github.com/web-infra-dev/rslint/internal/rule_tester"
)

func TestValidTypeofRule(t *testing.T) {
rule_tester.RunRuleTester(
fixtures.GetRootDir(),
"tsconfig.json",
t,
&ValidTypeofRule,
// Valid cases
[]rule_tester.ValidTestCase{
// All valid typeof comparison values
{Code: `typeof foo === "string"`},
{Code: `typeof foo === "object"`},
{Code: `typeof foo === "function"`},
{Code: `typeof foo === "undefined"`},
{Code: `typeof foo === "boolean"`},
{Code: `typeof foo === "number"`},
{Code: `typeof foo === "bigint"`},
{Code: `typeof foo === "symbol"`},

// Reversed operands
{Code: `"string" === typeof foo`},
{Code: `"object" === typeof foo`},

// typeof compared to typeof (always valid)
{Code: `typeof foo === typeof bar`},

// Non-equality operators are not checked
{Code: `typeof foo > "string"`},

// Without requireStringLiterals, non-string comparisons are OK (except bare undefined)
{Code: `typeof foo === baz`},
{Code: `typeof foo === Object`},

// Not a comparison (typeof in other contexts)
{Code: `var x = typeof foo`},
{Code: `typeof foo`},

// With requireStringLiterals: valid cases still valid
{Code: `typeof foo === "string"`, Options: map[string]interface{}{"requireStringLiterals": true}},
{Code: `typeof foo === typeof bar`, Options: map[string]interface{}{"requireStringLiterals": true}},
{Code: `"undefined" === typeof foo`, Options: map[string]interface{}{"requireStringLiterals": true}},

// != and !== with valid strings
{Code: `typeof foo !== "string"`},
{Code: `typeof foo != "function"`},
{Code: `typeof foo == "number"`},
},
// Invalid cases
[]rule_tester.InvalidTestCase{
// Invalid typeof comparison value (misspelled)
{
Code: `typeof foo === "strnig"`,
Errors: []rule_tester.InvalidTestCaseError{
{MessageId: "invalidValue", Line: 1, Column: 16},
},
},
// Reversed operands with invalid value
{
Code: `"strnig" === typeof foo`,
Errors: []rule_tester.InvalidTestCaseError{
{MessageId: "invalidValue", Line: 1, Column: 1},
},
},
// !== with invalid value
{
Code: `typeof foo !== "strnig"`,
Errors: []rule_tester.InvalidTestCaseError{
{MessageId: "invalidValue", Line: 1, Column: 16},
},
},
// == with invalid value
{
Code: `typeof foo == "strnig"`,
Errors: []rule_tester.InvalidTestCaseError{
{MessageId: "invalidValue", Line: 1, Column: 15},
},
},
// != with invalid value
{
Code: `typeof foo != "strnig"`,
Errors: []rule_tester.InvalidTestCaseError{
{MessageId: "invalidValue", Line: 1, Column: 15},
},
},
// Bare undefined identifier without requireStringLiterals → invalidValue
{
Code: `typeof foo === undefined`,
Errors: []rule_tester.InvalidTestCaseError{
{MessageId: "invalidValue", Line: 1, Column: 16},
},
},
// Bare undefined identifier with requireStringLiterals → notString
{
Code: `typeof foo === undefined`,
Options: map[string]interface{}{"requireStringLiterals": true},
Errors: []rule_tester.InvalidTestCaseError{
{MessageId: "notString", Line: 1, Column: 16},
},
},
// Non-string identifier with requireStringLiterals → notString
{
Code: `typeof foo === Object`,
Options: map[string]interface{}{"requireStringLiterals": true},
Errors: []rule_tester.InvalidTestCaseError{
{MessageId: "notString", Line: 1, Column: 16},
},
},
// Completely invalid string
{
Code: `typeof foo === "foobar"`,
Errors: []rule_tester.InvalidTestCaseError{
{MessageId: "invalidValue", Line: 1, Column: 16},
},
},
// Empty string
{
Code: `typeof foo === ""`,
Errors: []rule_tester.InvalidTestCaseError{
{MessageId: "invalidValue", Line: 1, Column: 16},
},
},
},
)
}
1 change: 1 addition & 0 deletions packages/rslint-test-tools/rstest.config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -196,5 +196,6 @@ export default defineConfig({
// './tests/typescript-eslint/rules/unbound-method.test.ts',
// './tests/typescript-eslint/rules/unified-signatures.test.ts',
// './tests/typescript-eslint/rules/use-unknown-in-catch-callback-variable.test.ts',
'./tests/eslint/rules/valid-typeof.test.ts',
],
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// Rstest Snapshot v1

exports[`valid-typeof > invalid 1`] = `
{
"code": "typeof foo === 'strnig'",
"diagnostics": [
{
"message": "Invalid typeof comparison value.",
"messageId": "invalidValue",
"range": {
"end": {
"column": 24,
"line": 1,
},
"start": {
"column": 16,
"line": 1,
},
},
"ruleName": "valid-typeof",
},
],
"errorCount": 1,
"fileCount": 1,
"ruleCount": 1,
}
`;

exports[`valid-typeof > invalid 2`] = `
{
"code": "typeof foo === 'nubmer'",
"diagnostics": [
{
"message": "Invalid typeof comparison value.",
"messageId": "invalidValue",
"range": {
"end": {
"column": 24,
"line": 1,
},
"start": {
"column": 16,
"line": 1,
},
},
"ruleName": "valid-typeof",
},
],
"errorCount": 1,
"fileCount": 1,
"ruleCount": 1,
}
`;
Loading
Loading