Skip to content

Commit b730f80

Browse files
delino[bot]github-actions[bot]claude
authored
feat: Port TypeScript-ESLint rule default-param-last (#85)
## Summary This PR ports the TypeScript-ESLint rule `default-param-last` to rslint, which enforces default parameters to be last in function parameter lists. Follow-up task of PR #84. ## Implementation Details - ✅ Created new rule in `internal/plugins/typescript/rules/default_param_last/` - ✅ Enforces that parameters with defaults appear after those without defaults - ✅ Supports optional parameters (marked with `?`) - ✅ Handles rest parameters correctly - ✅ Supports parameter properties in constructors - ✅ Registered in TypeScript plugin registry ## Rule Behavior The rule ensures that function parameters with default values or optional modifiers appear after required parameters. ### Invalid Patterns ```typescript // Default before required function f(a = 0, b: number) {} // Optional before required function f(a?: number, b: number) {} // Default in the middle function f(a: number, b = 0, c: number) {} ``` ### Valid Patterns ```typescript // Default parameters at the end function f(a: number, b = 0) {} // Optional parameters at the end function f(a: number, b?: number) {} // Rest parameters can come after defaults function f(a: number, b = 0, ...c: number[]) {} ``` ## Test Coverage - ✅ Comprehensive test suite ported from TypeScript-ESLint repository - ✅ 27 valid test cases covering various scenarios - ✅ 16 invalid test cases with expected error detection - ✅ All tests passing - Tests include: - Function declarations, expressions, and arrow functions - Methods and constructors - Parameter properties - Destructuring patterns - Rest parameters - Mixed optional and default parameters ## Test Plan - [x] All unit tests pass - [ ] CI tests pass - [ ] Manual testing with example code ## Notes This implementation follows the TypeScript-ESLint rule specification exactly: - Detects default parameters (`= value`) and optional parameters (`?`) that precede required parameters - Rest parameters (`...args`) are correctly handled and can appear after defaults - Parameter properties in constructors are properly supported ## References - Rule documentation: https://typescript-eslint.io/rules/default-param-last/ - TypeScript-ESLint source: https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/src/rules/default-param-last.ts - TypeScript-ESLint tests: https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/tests/rules/default-param-last.test.ts - Related PR #84: #84 ## Files Changed - `internal/config/config.go` - Added import and rule registration (2 lines) - `internal/plugins/typescript/rules/default_param_last/default_param_last.go` - Complete rule implementation (~177 lines) - `internal/plugins/typescript/rules/default_param_last/default_param_last_test.go` - Comprehensive test suite (~192 lines) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> Co-authored-by: Claude <noreply@anthropic.com>
1 parent 1fe602c commit b730f80

File tree

3 files changed

+377
-0
lines changed

3 files changed

+377
-0
lines changed

internal/config/config.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323
"github.com/web-infra-dev/rslint/internal/plugins/typescript/rules/consistent_type_definitions"
2424
"github.com/web-infra-dev/rslint/internal/plugins/typescript/rules/consistent_type_exports"
2525
"github.com/web-infra-dev/rslint/internal/plugins/typescript/rules/consistent_type_imports"
26+
"github.com/web-infra-dev/rslint/internal/plugins/typescript/rules/default_param_last"
2627
"github.com/web-infra-dev/rslint/internal/plugins/typescript/rules/no_array_delete"
2728
"github.com/web-infra-dev/rslint/internal/plugins/typescript/rules/no_base_to_string"
2829
"github.com/web-infra-dev/rslint/internal/plugins/typescript/rules/no_confusing_void_expression"
@@ -373,6 +374,7 @@ func registerAllTypeScriptEslintPluginRules() {
373374
GlobalRuleRegistry.Register("@typescript-eslint/consistent-type-definitions", consistent_type_definitions.ConsistentTypeDefinitionsRule)
374375
GlobalRuleRegistry.Register("@typescript-eslint/consistent-type-exports", consistent_type_exports.ConsistentTypeExportsRule)
375376
GlobalRuleRegistry.Register("@typescript-eslint/consistent-type-imports", consistent_type_imports.ConsistentTypeImportsRule)
377+
GlobalRuleRegistry.Register("@typescript-eslint/default-param-last", default_param_last.DefaultParamLastRule)
376378
GlobalRuleRegistry.Register("@typescript-eslint/dot-notation", dot_notation.DotNotationRule)
377379
GlobalRuleRegistry.Register("@typescript-eslint/no-array-delete", no_array_delete.NoArrayDeleteRule)
378380
GlobalRuleRegistry.Register("@typescript-eslint/no-base-to-string", no_base_to_string.NoBaseToStringRule)
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
package default_param_last
2+
3+
import (
4+
"github.com/microsoft/typescript-go/shim/ast"
5+
"github.com/web-infra-dev/rslint/internal/rule"
6+
)
7+
8+
// DefaultParamLastRule enforces default parameters to be last
9+
var DefaultParamLastRule = rule.CreateRule(rule.Rule{
10+
Name: "default-param-last",
11+
Run: run,
12+
})
13+
14+
func run(ctx rule.RuleContext, options any) rule.RuleListeners {
15+
// Helper function to check if a parameter is optional
16+
isOptionalParam := func(node *ast.Node) bool {
17+
if node == nil {
18+
return false
19+
}
20+
21+
// Check if parameter has optional modifier
22+
if node.Kind == ast.KindParameter {
23+
param := node.AsParameterDeclaration()
24+
if param != nil && param.QuestionToken != nil {
25+
return true
26+
}
27+
}
28+
29+
return false
30+
}
31+
32+
// Helper function to check if a parameter is a rest parameter
33+
isRestParam := func(node *ast.Node) bool {
34+
if node == nil {
35+
return false
36+
}
37+
38+
// Check for rest parameter (...)
39+
if node.Kind == ast.KindParameter {
40+
param := node.AsParameterDeclaration()
41+
if param != nil && param.DotDotDotToken != nil {
42+
return true
43+
}
44+
}
45+
46+
return false
47+
}
48+
49+
// Helper function to check if a parameter is plain (no default, not rest, not optional)
50+
isPlainParam := func(node *ast.Node) bool {
51+
if node == nil {
52+
return false
53+
}
54+
55+
// Rest parameter is not plain
56+
if isRestParam(node) {
57+
return false
58+
}
59+
60+
// Parameter with default value is not plain
61+
if node.Kind == ast.KindParameter {
62+
param := node.AsParameterDeclaration()
63+
if param != nil {
64+
// Has initializer (default value)
65+
if param.Initializer != nil {
66+
return false
67+
}
68+
// Is optional
69+
if param.QuestionToken != nil {
70+
return false
71+
}
72+
}
73+
}
74+
75+
return true
76+
}
77+
78+
// Check function for default parameter positioning
79+
checkDefaultParamLast := func(node *ast.Node) {
80+
var params []*ast.Node
81+
82+
// Get parameters based on node type
83+
switch node.Kind {
84+
case ast.KindFunctionDeclaration:
85+
funcDecl := node.AsFunctionDeclaration()
86+
if funcDecl != nil && funcDecl.Parameters != nil {
87+
params = funcDecl.Parameters.Nodes
88+
}
89+
case ast.KindFunctionExpression:
90+
funcExpr := node.AsFunctionExpression()
91+
if funcExpr != nil && funcExpr.Parameters != nil {
92+
params = funcExpr.Parameters.Nodes
93+
}
94+
case ast.KindArrowFunction:
95+
arrowFunc := node.AsArrowFunction()
96+
if arrowFunc != nil && arrowFunc.Parameters != nil {
97+
params = arrowFunc.Parameters.Nodes
98+
}
99+
case ast.KindMethodDeclaration:
100+
methodDecl := node.AsMethodDeclaration()
101+
if methodDecl != nil && methodDecl.Parameters != nil {
102+
params = methodDecl.Parameters.Nodes
103+
}
104+
case ast.KindConstructor:
105+
constructor := node.AsConstructorDeclaration()
106+
if constructor != nil && constructor.Parameters != nil {
107+
params = constructor.Parameters.Nodes
108+
}
109+
default:
110+
return
111+
}
112+
113+
if len(params) == 0 {
114+
return
115+
}
116+
117+
// Iterate backward through parameters
118+
hasSeenPlainParam := false
119+
for i := len(params) - 1; i >= 0; i-- {
120+
current := params[i]
121+
if current == nil {
122+
continue
123+
}
124+
125+
// Get the actual parameter (unwrap if it's a parameter property)
126+
param := current
127+
if current.Kind == ast.KindParameter {
128+
p := current.AsParameterDeclaration()
129+
if p != nil && p.Name() != nil {
130+
// Check if parameter has modifiers (public/private/protected/readonly)
131+
// which would make it a parameter property
132+
hasModifiers := ast.GetCombinedModifierFlags(current)&(ast.ModifierFlagsPublic|ast.ModifierFlagsPrivate|ast.ModifierFlagsProtected|ast.ModifierFlagsReadonly) != 0
133+
if hasModifiers {
134+
// For parameter properties, check the parameter itself
135+
param = current
136+
}
137+
}
138+
}
139+
140+
// Skip rest parameters - they can come after defaults
141+
if isRestParam(param) {
142+
continue
143+
}
144+
145+
if isPlainParam(param) {
146+
hasSeenPlainParam = true
147+
continue
148+
}
149+
150+
// Check if this is a default or optional parameter that comes before a plain parameter
151+
if hasSeenPlainParam {
152+
if param.Kind == ast.KindParameter {
153+
paramDecl := param.AsParameterDeclaration()
154+
isDefaultParam := paramDecl != nil && paramDecl.Initializer != nil
155+
isOptional := isOptionalParam(param)
156+
157+
if isDefaultParam || isOptional {
158+
ctx.ReportNode(current, rule.RuleMessage{
159+
Id: "shouldBeLast",
160+
Description: "Default parameters should be last.",
161+
})
162+
}
163+
}
164+
}
165+
}
166+
}
167+
168+
return rule.RuleListeners{
169+
ast.KindFunctionDeclaration: checkDefaultParamLast,
170+
ast.KindFunctionExpression: checkDefaultParamLast,
171+
ast.KindArrowFunction: checkDefaultParamLast,
172+
ast.KindMethodDeclaration: checkDefaultParamLast,
173+
ast.KindConstructor: checkDefaultParamLast,
174+
}
175+
}
Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
package default_param_last
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 TestDefaultParamLastRule(t *testing.T) {
11+
rule_tester.RunRuleTester(fixtures.GetRootDir(), "tsconfig.json", t, &DefaultParamLastRule, []rule_tester.ValidTestCase{
12+
// Valid: no parameters
13+
{Code: `function f() {}`},
14+
15+
// Valid: only required parameters
16+
{Code: `function f(a: number) {}`},
17+
{Code: `function f(a: number, b: number) {}`},
18+
19+
// Valid: default parameters at the end
20+
{Code: `function f(a = 0) {}`},
21+
{Code: `function f(a: number, b = 0) {}`},
22+
{Code: `function f(a: number, b: number, c = 0) {}`},
23+
24+
// Valid: optional parameters at the end
25+
{Code: `function f(a: number, b?: number) {}`},
26+
{Code: `function f(a: number, b?: number, c?: number) {}`},
27+
28+
// Valid: both optional and default at the end
29+
{Code: `function f(a: number, b?: number, c = 0) {}`},
30+
{Code: `function f(a: number, b = 0, c?: number) {}`},
31+
32+
// Valid: rest parameter after defaults
33+
{Code: `function f(a: number, b = 0, ...c: number[]) {}`},
34+
35+
// Valid: arrow functions
36+
{Code: `const f = (a: number, b = 0) => {}`},
37+
{Code: `const f = (a: number, b?: number) => {}`},
38+
39+
// Valid: function expressions
40+
{Code: `const f = function(a: number, b = 0) {}`},
41+
{Code: `const f = function(a: number, b?: number) {}`},
42+
43+
// Valid: methods
44+
{Code: `class A { method(a: number, b = 0) {} }`},
45+
{Code: `class A { method(a: number, b?: number) {} }`},
46+
47+
// Valid: constructors
48+
{Code: `class A { constructor(a: number, b = 0) {} }`},
49+
{Code: `class A { constructor(a: number, b?: number) {} }`},
50+
51+
// Valid: parameter properties
52+
{Code: `class A { constructor(public a: number, public b = 0) {} }`},
53+
{Code: `class A { constructor(private a: number, public b?: number) {} }`},
54+
55+
// Valid: destructuring with defaults at the end
56+
{Code: `function f(a: number, { b } = { b: 0 }) {}`},
57+
{Code: `function f(a: number, [b] = [0]) {}`},
58+
59+
// Valid: multiple defaults at the end
60+
{Code: `function f(a: number, b = 0, c = 1) {}`},
61+
{Code: `function f(a: number, b = 0, c = 1, d = 2) {}`},
62+
63+
// Valid: all parameters have defaults
64+
{Code: `function f(a = 0, b = 1, c = 2) {}`},
65+
66+
// Valid: all parameters are optional
67+
{Code: `function f(a?: number, b?: number, c?: number) {}`},
68+
}, []rule_tester.InvalidTestCase{
69+
// Invalid: default before required
70+
{
71+
Code: `function f(a = 0, b: number) {}`,
72+
Errors: []rule_tester.InvalidTestCaseError{
73+
{MessageId: "shouldBeLast"},
74+
},
75+
},
76+
77+
// Invalid: optional before required
78+
{
79+
Code: `function f(a?: number, b: number) {}`,
80+
Errors: []rule_tester.InvalidTestCaseError{
81+
{MessageId: "shouldBeLast"},
82+
},
83+
},
84+
85+
// Invalid: default in the middle
86+
{
87+
Code: `function f(a: number, b = 0, c: number) {}`,
88+
Errors: []rule_tester.InvalidTestCaseError{
89+
{MessageId: "shouldBeLast"},
90+
},
91+
},
92+
93+
// Invalid: optional in the middle
94+
{
95+
Code: `function f(a: number, b?: number, c: number) {}`,
96+
Errors: []rule_tester.InvalidTestCaseError{
97+
{MessageId: "shouldBeLast"},
98+
},
99+
},
100+
101+
// Invalid: multiple violations
102+
{
103+
Code: `function f(a = 0, b: number, c = 1, d: number) {}`,
104+
Errors: []rule_tester.InvalidTestCaseError{
105+
{MessageId: "shouldBeLast"},
106+
{MessageId: "shouldBeLast"},
107+
},
108+
},
109+
110+
// Invalid: arrow function
111+
{
112+
Code: `const f = (a = 0, b: number) => {}`,
113+
Errors: []rule_tester.InvalidTestCaseError{
114+
{MessageId: "shouldBeLast"},
115+
},
116+
},
117+
118+
// Invalid: function expression
119+
{
120+
Code: `const f = function(a = 0, b: number) {}`,
121+
Errors: []rule_tester.InvalidTestCaseError{
122+
{MessageId: "shouldBeLast"},
123+
},
124+
},
125+
126+
// Invalid: method
127+
{
128+
Code: `class A { method(a = 0, b: number) {} }`,
129+
Errors: []rule_tester.InvalidTestCaseError{
130+
{MessageId: "shouldBeLast"},
131+
},
132+
},
133+
134+
// Invalid: constructor
135+
{
136+
Code: `class A { constructor(a = 0, b: number) {} }`,
137+
Errors: []rule_tester.InvalidTestCaseError{
138+
{MessageId: "shouldBeLast"},
139+
},
140+
},
141+
142+
// Invalid: parameter properties
143+
{
144+
Code: `class A { constructor(public a = 0, private b: number) {} }`,
145+
Errors: []rule_tester.InvalidTestCaseError{
146+
{MessageId: "shouldBeLast"},
147+
},
148+
},
149+
150+
// Invalid: optional parameter property before required
151+
{
152+
Code: `class A { constructor(public a?: number, private b: number) {} }`,
153+
Errors: []rule_tester.InvalidTestCaseError{
154+
{MessageId: "shouldBeLast"},
155+
},
156+
},
157+
158+
// Invalid: destructuring with default before required
159+
{
160+
Code: `function f({ a } = { a: 0 }, b: number) {}`,
161+
Errors: []rule_tester.InvalidTestCaseError{
162+
{MessageId: "shouldBeLast"},
163+
},
164+
},
165+
166+
// Invalid: array destructuring with default before required
167+
{
168+
Code: `function f([a] = [0], b: number) {}`,
169+
Errors: []rule_tester.InvalidTestCaseError{
170+
{MessageId: "shouldBeLast"},
171+
},
172+
},
173+
174+
// Invalid: mixed defaults and optionals before required
175+
{
176+
Code: `function f(a = 0, b?: number, c: number) {}`,
177+
Errors: []rule_tester.InvalidTestCaseError{
178+
{MessageId: "shouldBeLast"},
179+
{MessageId: "shouldBeLast"},
180+
},
181+
},
182+
183+
// Invalid: optional before required in arrow function
184+
{
185+
Code: `const f = (a?: number, b: number) => {}`,
186+
Errors: []rule_tester.InvalidTestCaseError{
187+
{MessageId: "shouldBeLast"},
188+
},
189+
},
190+
191+
// Invalid: default after optional before required
192+
{
193+
Code: `function f(a?: number, b = 0, c: number) {}`,
194+
Errors: []rule_tester.InvalidTestCaseError{
195+
{MessageId: "shouldBeLast"},
196+
{MessageId: "shouldBeLast"},
197+
},
198+
},
199+
})
200+
}

0 commit comments

Comments
 (0)