Skip to content

Commit 67497ee

Browse files
committed
feat: port rule prefer-rest-params
1 parent 1e5a9c6 commit 67497ee

File tree

7 files changed

+401
-0
lines changed

7 files changed

+401
-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/prefer_rest_params"
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("prefer-rest-params", prefer_rest_params.PreferRestParamsRule)
425427
}
426428

427429
// getAllTypeScriptEslintPluginRules returns all rules from the global registry.
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
package prefer_rest_params
2+
3+
import (
4+
"github.com/microsoft/typescript-go/shim/ast"
5+
"github.com/web-infra-dev/rslint/internal/rule"
6+
)
7+
8+
// https://eslint.org/docs/latest/rules/prefer-rest-params
9+
var PreferRestParamsRule = rule.Rule{
10+
Name: "prefer-rest-params",
11+
Run: func(ctx rule.RuleContext, options any) rule.RuleListeners {
12+
return rule.RuleListeners{
13+
ast.KindIdentifier: func(node *ast.Node) {
14+
if node.Text() != "arguments" {
15+
return
16+
}
17+
18+
// Skip if this is a property name (e.g., obj.arguments)
19+
if isPropertyName(node) {
20+
return
21+
}
22+
23+
// Skip if this is a declaration name (var arguments, function arguments(params))
24+
if isDeclarationName(node) {
25+
return
26+
}
27+
28+
// Find the enclosing function (not arrow)
29+
enclosingFunc := findEnclosingFunction(node)
30+
if enclosingFunc == nil {
31+
// arguments at module/program level - not in a function
32+
return
33+
}
34+
35+
// If enclosing function is arrow function, skip (arrows don't have their own arguments)
36+
if enclosingFunc.Kind == ast.KindArrowFunction {
37+
return
38+
}
39+
40+
// Check if `arguments` is shadowed by a parameter or local var
41+
if isShadowedInFunction(enclosingFunc) {
42+
return
43+
}
44+
45+
// Check if this is a non-computed member access (arguments.length is OK)
46+
if isNonComputedMemberAccess(node) {
47+
return
48+
}
49+
50+
ctx.ReportNode(node, rule.RuleMessage{
51+
Id: "preferRestParams",
52+
Description: "Use the rest parameters instead of 'arguments'.",
53+
})
54+
},
55+
}
56+
},
57+
}
58+
59+
// isPropertyName checks if the node is the property name part of a property access (e.g., obj.arguments).
60+
func isPropertyName(node *ast.Node) bool {
61+
parent := node.Parent
62+
if parent == nil {
63+
return false
64+
}
65+
if parent.Kind == ast.KindPropertyAccessExpression {
66+
propAccess := parent.AsPropertyAccessExpression()
67+
if propAccess != nil && propAccess.Name() == node {
68+
return true
69+
}
70+
}
71+
return false
72+
}
73+
74+
// isDeclarationName checks if the node is the name of a declaration (parameter, variable, function).
75+
func isDeclarationName(node *ast.Node) bool {
76+
parent := node.Parent
77+
if parent == nil {
78+
return false
79+
}
80+
switch parent.Kind {
81+
case ast.KindParameter:
82+
param := parent.AsParameterDeclaration()
83+
if param != nil && param.Name() == node {
84+
return true
85+
}
86+
case ast.KindVariableDeclaration:
87+
varDecl := parent.AsVariableDeclaration()
88+
if varDecl != nil && varDecl.Name() == node {
89+
return true
90+
}
91+
case ast.KindFunctionDeclaration:
92+
funcDecl := parent.AsFunctionDeclaration()
93+
if funcDecl != nil && funcDecl.Name() == node {
94+
return true
95+
}
96+
}
97+
return false
98+
}
99+
100+
// findEnclosingFunction walks up the AST to find the nearest enclosing function-like node.
101+
// Returns nil if none is found.
102+
func findEnclosingFunction(node *ast.Node) *ast.Node {
103+
current := node.Parent
104+
for current != nil {
105+
switch current.Kind {
106+
case ast.KindFunctionDeclaration, ast.KindFunctionExpression, ast.KindArrowFunction,
107+
ast.KindMethodDeclaration, ast.KindConstructor, ast.KindGetAccessor, ast.KindSetAccessor:
108+
return current
109+
}
110+
current = current.Parent
111+
}
112+
return nil
113+
}
114+
115+
// isShadowedInFunction checks if `arguments` is shadowed by a parameter or a var declaration
116+
// anywhere inside the given function node. var declarations are hoisted to function scope,
117+
// so `var arguments` in any nested block still shadows the built-in arguments object.
118+
func isShadowedInFunction(funcNode *ast.Node) bool {
119+
// Check parameters
120+
paramList := funcNode.ParameterList()
121+
if paramList != nil {
122+
for _, param := range paramList.Nodes {
123+
name := param.Name()
124+
if name != nil && name.Kind == ast.KindIdentifier && name.Text() == "arguments" {
125+
return true
126+
}
127+
}
128+
}
129+
130+
// Recursively check for var declarations named "arguments" in the function body.
131+
body := funcNode.Body()
132+
if body == nil {
133+
return false
134+
}
135+
136+
found := false
137+
var walk func(*ast.Node)
138+
walk = func(n *ast.Node) {
139+
if n == nil || found {
140+
return
141+
}
142+
143+
// Don't recurse into nested functions (they have their own arguments)
144+
if n != funcNode {
145+
switch n.Kind {
146+
case ast.KindFunctionDeclaration, ast.KindFunctionExpression,
147+
ast.KindArrowFunction, ast.KindMethodDeclaration,
148+
ast.KindConstructor, ast.KindGetAccessor, ast.KindSetAccessor:
149+
return
150+
}
151+
}
152+
153+
if n.Kind == ast.KindVariableDeclaration {
154+
varDecl := n.AsVariableDeclaration()
155+
if varDecl != nil && varDecl.Name() != nil &&
156+
varDecl.Name().Kind == ast.KindIdentifier &&
157+
varDecl.Name().Text() == "arguments" {
158+
found = true
159+
return
160+
}
161+
}
162+
163+
n.ForEachChild(func(child *ast.Node) bool {
164+
walk(child)
165+
return found
166+
})
167+
}
168+
walk(body)
169+
return found
170+
}
171+
172+
// isNonComputedMemberAccess checks if the node is the object of a non-computed property access
173+
// (e.g., arguments.length). In that case, the usage is OK and should not be flagged.
174+
func isNonComputedMemberAccess(node *ast.Node) bool {
175+
parent := node.Parent
176+
if parent == nil {
177+
return false
178+
}
179+
if parent.Kind == ast.KindPropertyAccessExpression {
180+
propAccess := parent.AsPropertyAccessExpression()
181+
if propAccess != nil && propAccess.Expression == node {
182+
return true
183+
}
184+
}
185+
return false
186+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# prefer-rest-params
2+
3+
## Rule Details
4+
5+
Requires rest parameters instead of `arguments`.
6+
7+
There are rest parameters in ES2015. We can use that feature for variadic functions instead of the `arguments` variable. `arguments` does not have methods of `Array.prototype`, so it's a bit inconvenient.
8+
9+
Examples of **incorrect** code for this rule:
10+
11+
```javascript
12+
function foo() {
13+
console.log(arguments);
14+
}
15+
16+
function foo(action) {
17+
var args = Array.prototype.slice.call(arguments, 1);
18+
action.apply(null, args);
19+
}
20+
21+
function foo(action) {
22+
var args = [].slice.call(arguments, 1);
23+
action.apply(null, args);
24+
}
25+
```
26+
27+
Examples of **correct** code for this rule:
28+
29+
```javascript
30+
function foo(...args) {
31+
console.log(args);
32+
}
33+
34+
function foo(action, ...args) {
35+
action.apply(null, args);
36+
}
37+
38+
// This is not a use of `arguments` itself
39+
function foo() {
40+
arguments.length;
41+
arguments.callee;
42+
}
43+
```
44+
45+
## Original Documentation
46+
47+
- [ESLint prefer-rest-params](https://eslint.org/docs/latest/rules/prefer-rest-params)
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package prefer_rest_params
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 TestPreferRestParamsRule(t *testing.T) {
11+
rule_tester.RunRuleTester(
12+
fixtures.GetRootDir(),
13+
"tsconfig.json",
14+
t,
15+
&PreferRestParamsRule,
16+
// Valid cases - ported from ESLint
17+
[]rule_tester.ValidTestCase{
18+
{Code: `arguments;`},
19+
{Code: `function foo(arguments: any) { arguments; }`},
20+
{Code: `function foo() { var arguments: any; arguments; }`},
21+
{Code: `var foo = () => arguments;`},
22+
{Code: `function foo(...args: any[]) { args; }`},
23+
{Code: `function foo() { arguments.length; }`},
24+
{Code: `function foo() { arguments.callee; }`},
25+
// var arguments in nested block — hoisted to function scope, shadows built-in
26+
{Code: `function foo() { if (true) { var arguments: any = []; } arguments; }`},
27+
},
28+
// Invalid cases - ported from ESLint
29+
[]rule_tester.InvalidTestCase{
30+
{
31+
Code: `function foo() { arguments; }`,
32+
Errors: []rule_tester.InvalidTestCaseError{
33+
{MessageId: "preferRestParams", Line: 1, Column: 18},
34+
},
35+
},
36+
{
37+
Code: `function foo() { arguments[0]; }`,
38+
Errors: []rule_tester.InvalidTestCaseError{
39+
{MessageId: "preferRestParams", Line: 1, Column: 18},
40+
},
41+
},
42+
{
43+
Code: `function foo() { arguments[1]; }`,
44+
Errors: []rule_tester.InvalidTestCaseError{
45+
{MessageId: "preferRestParams", Line: 1, Column: 18},
46+
},
47+
},
48+
// Storing arguments in a variable
49+
{
50+
Code: `function foo() { var x = arguments; }`,
51+
Errors: []rule_tester.InvalidTestCaseError{
52+
{MessageId: "preferRestParams", Line: 1, Column: 26},
53+
},
54+
},
55+
},
56+
)
57+
}

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

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

24+
'./tests/eslint/rules/prefer-rest-params.test.ts',
2425
// eslint-plugin-import
2526
'./tests/eslint-plugin-import/rules/no-self-import.test.ts',
2627
'./tests/eslint-plugin-import/rules/no-webpack-loader-syntax.test.ts',
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
// Rstest Snapshot v1
2+
3+
exports[`prefer-rest-params > invalid 1`] = `
4+
{
5+
"code": "function foo() { arguments; }",
6+
"diagnostics": [
7+
{
8+
"message": "Use the rest parameters instead of 'arguments'.",
9+
"messageId": "preferRestParams",
10+
"range": {
11+
"end": {
12+
"column": 27,
13+
"line": 1,
14+
},
15+
"start": {
16+
"column": 18,
17+
"line": 1,
18+
},
19+
},
20+
"ruleName": "prefer-rest-params",
21+
},
22+
],
23+
"errorCount": 1,
24+
"fileCount": 1,
25+
"ruleCount": 1,
26+
}
27+
`;
28+
29+
exports[`prefer-rest-params > invalid 2`] = `
30+
{
31+
"code": "function foo() { arguments[0]; }",
32+
"diagnostics": [
33+
{
34+
"message": "Use the rest parameters instead of 'arguments'.",
35+
"messageId": "preferRestParams",
36+
"range": {
37+
"end": {
38+
"column": 27,
39+
"line": 1,
40+
},
41+
"start": {
42+
"column": 18,
43+
"line": 1,
44+
},
45+
},
46+
"ruleName": "prefer-rest-params",
47+
},
48+
],
49+
"errorCount": 1,
50+
"fileCount": 1,
51+
"ruleCount": 1,
52+
}
53+
`;
54+
55+
exports[`prefer-rest-params > invalid 3`] = `
56+
{
57+
"code": "function foo() { arguments[1]; }",
58+
"diagnostics": [
59+
{
60+
"message": "Use the rest parameters instead of 'arguments'.",
61+
"messageId": "preferRestParams",
62+
"range": {
63+
"end": {
64+
"column": 27,
65+
"line": 1,
66+
},
67+
"start": {
68+
"column": 18,
69+
"line": 1,
70+
},
71+
},
72+
"ruleName": "prefer-rest-params",
73+
},
74+
],
75+
"errorCount": 1,
76+
"fileCount": 1,
77+
"ruleCount": 1,
78+
}
79+
`;

0 commit comments

Comments
 (0)