Skip to content

Commit e7f4f05

Browse files
committed
feat: port rule no-obj-calls
1 parent 1e5a9c6 commit e7f4f05

File tree

7 files changed

+481
-0
lines changed

7 files changed

+481
-0
lines changed

internal/config/config.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ import (
123123
"github.com/web-infra-dev/rslint/internal/rules/no_loss_of_precision"
124124
"github.com/web-infra-dev/rslint/internal/rules/no_sparse_arrays"
125125
"github.com/web-infra-dev/rslint/internal/rules/no_template_curly_in_string"
126+
"github.com/web-infra-dev/rslint/internal/rules/no_obj_calls"
126127
)
127128

128129
// RslintConfig represents the top-level configuration array
@@ -422,6 +423,7 @@ func registerAllCoreEslintRules() {
422423
GlobalRuleRegistry.Register("no-loss-of-precision", no_loss_of_precision.NoLossOfPrecisionRule)
423424
GlobalRuleRegistry.Register("no-template-curly-in-string", no_template_curly_in_string.NoTemplateCurlyInString)
424425
GlobalRuleRegistry.Register("no-sparse-arrays", no_sparse_arrays.NoSparseArraysRule)
426+
GlobalRuleRegistry.Register("no-obj-calls", no_obj_calls.NoObjCallsRule)
425427
}
426428

427429
// getAllTypeScriptEslintPluginRules returns all rules from the global registry.
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
package no_obj_calls
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/microsoft/typescript-go/shim/ast"
7+
"github.com/web-infra-dev/rslint/internal/rule"
8+
)
9+
10+
var nonCallableGlobals = map[string]bool{
11+
"Math": true, "JSON": true, "Reflect": true, "Atomics": true, "Intl": true,
12+
}
13+
14+
// https://eslint.org/docs/latest/rules/no-obj-calls
15+
var NoObjCallsRule = rule.Rule{
16+
Name: "no-obj-calls",
17+
Run: func(ctx rule.RuleContext, options any) rule.RuleListeners {
18+
checkCallee := func(node *ast.Node, calleeNode *ast.Node) {
19+
if calleeNode.Kind == ast.KindIdentifier {
20+
name := calleeNode.AsIdentifier().Text
21+
if nonCallableGlobals[name] && !isShadowed(calleeNode, name) {
22+
ctx.ReportNode(node, rule.RuleMessage{
23+
Id: "unexpectedCall",
24+
Description: fmt.Sprintf("'%s' is not a function.", name),
25+
})
26+
}
27+
}
28+
}
29+
30+
return rule.RuleListeners{
31+
ast.KindCallExpression: func(node *ast.Node) {
32+
callExpr := node.AsCallExpression()
33+
checkCallee(node, callExpr.Expression)
34+
},
35+
ast.KindNewExpression: func(node *ast.Node) {
36+
newExpr := node.AsNewExpression()
37+
checkCallee(node, newExpr.Expression)
38+
},
39+
}
40+
},
41+
}
42+
43+
// isShadowed checks if an identifier is shadowed by a local variable, parameter,
44+
// or function declaration in an enclosing scope (not global).
45+
func isShadowed(node *ast.Node, name string) bool {
46+
current := node.Parent
47+
for current != nil {
48+
switch current.Kind {
49+
case ast.KindBlock, ast.KindCaseClause, ast.KindDefaultClause:
50+
if blockDeclaresName(current, name) {
51+
return true
52+
}
53+
case ast.KindFunctionDeclaration, ast.KindFunctionExpression,
54+
ast.KindArrowFunction, ast.KindMethodDeclaration:
55+
if functionParamDeclaresName(current, name) {
56+
return true
57+
}
58+
case ast.KindVariableStatement:
59+
if varStatementDeclaresName(current, name) {
60+
return true
61+
}
62+
case ast.KindSourceFile:
63+
// Reached the top-level scope — not shadowed
64+
return false
65+
}
66+
current = current.Parent
67+
}
68+
return false
69+
}
70+
71+
// blockDeclaresName checks if a block contains a variable/function declaration with the given name.
72+
func blockDeclaresName(block *ast.Node, name string) bool {
73+
var statements *ast.NodeList
74+
switch block.Kind {
75+
case ast.KindBlock:
76+
b := block.AsBlock()
77+
if b != nil {
78+
statements = b.Statements
79+
}
80+
case ast.KindCaseClause, ast.KindDefaultClause:
81+
c := block.AsCaseOrDefaultClause()
82+
if c != nil {
83+
statements = c.Statements
84+
}
85+
}
86+
if statements == nil {
87+
return false
88+
}
89+
for _, stmt := range statements.Nodes {
90+
switch stmt.Kind {
91+
case ast.KindVariableStatement:
92+
if varStatementDeclaresName(stmt, name) {
93+
return true
94+
}
95+
case ast.KindFunctionDeclaration:
96+
fd := stmt.AsFunctionDeclaration()
97+
if fd != nil && fd.Name() != nil && fd.Name().Text() == name {
98+
return true
99+
}
100+
}
101+
}
102+
return false
103+
}
104+
105+
// varStatementDeclaresName checks if a variable statement declares the given name.
106+
func varStatementDeclaresName(node *ast.Node, name string) bool {
107+
vs := node.AsVariableStatement()
108+
if vs == nil || vs.DeclarationList == nil {
109+
return false
110+
}
111+
dl := vs.DeclarationList.AsVariableDeclarationList()
112+
if dl == nil || dl.Declarations == nil {
113+
return false
114+
}
115+
for _, decl := range dl.Declarations.Nodes {
116+
vd := decl.AsVariableDeclaration()
117+
if vd != nil && vd.Name() != nil && vd.Name().Kind == ast.KindIdentifier && vd.Name().Text() == name {
118+
return true
119+
}
120+
}
121+
return false
122+
}
123+
124+
// functionParamDeclaresName checks if a function's parameters include the given name.
125+
func functionParamDeclaresName(node *ast.Node, name string) bool {
126+
var params *ast.NodeList
127+
switch node.Kind {
128+
case ast.KindFunctionDeclaration:
129+
fd := node.AsFunctionDeclaration()
130+
if fd != nil {
131+
params = fd.Parameters
132+
// Also check function name itself
133+
if fd.Name() != nil && fd.Name().Text() == name {
134+
return true
135+
}
136+
}
137+
case ast.KindFunctionExpression:
138+
fe := node.AsFunctionExpression()
139+
if fe != nil {
140+
params = fe.Parameters
141+
}
142+
case ast.KindArrowFunction:
143+
af := node.AsArrowFunction()
144+
if af != nil {
145+
params = af.Parameters
146+
}
147+
case ast.KindMethodDeclaration:
148+
md := node.AsMethodDeclaration()
149+
if md != nil {
150+
params = md.Parameters
151+
}
152+
}
153+
if params == nil {
154+
return false
155+
}
156+
for _, p := range params.Nodes {
157+
pd := p.AsParameterDeclaration()
158+
if pd != nil && pd.Name() != nil && pd.Name().Kind == ast.KindIdentifier && pd.Name().Text() == name {
159+
return true
160+
}
161+
}
162+
return false
163+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# no-obj-calls
2+
3+
## Rule Details
4+
5+
Disallows calling global objects (`Math`, `JSON`, `Reflect`, `Atomics`, `Intl`) as functions or constructors. These are namespace objects that provide properties and methods but are not themselves callable. Attempting to call them will throw a `TypeError` at runtime.
6+
7+
Examples of **incorrect** code for this rule:
8+
9+
```javascript
10+
var x = Math();
11+
var y = JSON();
12+
var z = Reflect();
13+
var a = new Math();
14+
var b = new JSON();
15+
```
16+
17+
Examples of **correct** code for this rule:
18+
19+
```javascript
20+
var x = Math.random();
21+
var y = JSON.parse('{}');
22+
var z = Reflect.get(obj, 'key');
23+
var a = new Intl.Segmenter();
24+
var b = Math.PI;
25+
```
26+
27+
## Original Documentation
28+
29+
- [ESLint no-obj-calls](https://eslint.org/docs/latest/rules/no-obj-calls)
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
package no_obj_calls
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 TestNoObjCallsRule(t *testing.T) {
11+
rule_tester.RunRuleTester(
12+
fixtures.GetRootDir(),
13+
"tsconfig.json",
14+
t,
15+
&NoObjCallsRule,
16+
// Valid cases
17+
[]rule_tester.ValidTestCase{
18+
{Code: `var x = Math.random();`},
19+
{Code: `var x = JSON.parse(foo);`},
20+
{Code: `Reflect.get(foo, 'x');`},
21+
{Code: `new Intl.Segmenter();`},
22+
{Code: `var x = Math;`},
23+
// Shadowed variable — should not be flagged
24+
{Code: `function f() { var Math = 1; Math(); }`},
25+
{Code: `function f(JSON: any) { JSON(); }`},
26+
},
27+
// Invalid cases
28+
[]rule_tester.InvalidTestCase{
29+
{
30+
Code: `Math();`,
31+
Errors: []rule_tester.InvalidTestCaseError{
32+
{MessageId: "unexpectedCall", Line: 1, Column: 1},
33+
},
34+
},
35+
{
36+
Code: `var x = JSON();`,
37+
Errors: []rule_tester.InvalidTestCaseError{
38+
{MessageId: "unexpectedCall", Line: 1, Column: 9},
39+
},
40+
},
41+
{
42+
Code: `var x = Reflect();`,
43+
Errors: []rule_tester.InvalidTestCaseError{
44+
{MessageId: "unexpectedCall", Line: 1, Column: 9},
45+
},
46+
},
47+
{
48+
Code: `Atomics();`,
49+
Errors: []rule_tester.InvalidTestCaseError{
50+
{MessageId: "unexpectedCall", Line: 1, Column: 1},
51+
},
52+
},
53+
{
54+
Code: `Intl();`,
55+
Errors: []rule_tester.InvalidTestCaseError{
56+
{MessageId: "unexpectedCall", Line: 1, Column: 1},
57+
},
58+
},
59+
{
60+
Code: `new Math();`,
61+
Errors: []rule_tester.InvalidTestCaseError{
62+
{MessageId: "unexpectedCall", Line: 1, Column: 1},
63+
},
64+
},
65+
{
66+
Code: `new JSON();`,
67+
Errors: []rule_tester.InvalidTestCaseError{
68+
{MessageId: "unexpectedCall", Line: 1, Column: 1},
69+
},
70+
},
71+
{
72+
Code: `new Reflect();`,
73+
Errors: []rule_tester.InvalidTestCaseError{
74+
{MessageId: "unexpectedCall", Line: 1, Column: 1},
75+
},
76+
},
77+
{
78+
Code: `new Atomics();`,
79+
Errors: []rule_tester.InvalidTestCaseError{
80+
{MessageId: "unexpectedCall", Line: 1, Column: 1},
81+
},
82+
},
83+
{
84+
Code: `new Intl();`,
85+
Errors: []rule_tester.InvalidTestCaseError{
86+
{MessageId: "unexpectedCall", Line: 1, Column: 1},
87+
},
88+
},
89+
},
90+
)
91+
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export default defineConfig({
2020
'./tests/eslint/rules/no-empty-pattern.test.ts',
2121
'./tests/eslint/rules/getter-return.test.ts',
2222
'./tests/eslint/rules/no-loss-of-precision.test.ts',
23+
'./tests/eslint/rules/no-obj-calls.test.ts',
2324

2425
// eslint-plugin-import
2526
'./tests/eslint-plugin-import/rules/no-self-import.test.ts',

0 commit comments

Comments
 (0)