Skip to content
Closed
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
5117057
feat: port rule @typescript-eslint/max-params
ScriptedAlchemy Feb 20, 2026
703e7a6
Merge branch 'main' into codex/ts-eslint-max-params-ac37
ScriptedAlchemy Feb 25, 2026
94ffd14
fix(typescript/max-params): align signature node parity
ScriptedAlchemy Feb 25, 2026
536b1db
fix: support max-params shorthand options
ScriptedAlchemy Feb 26, 2026
e45bebe
Merge branch 'main' into codex/ts-eslint-max-params-ac37
ScriptedAlchemy Mar 3, 2026
e3207fc
Merge branch 'main' into codex/ts-eslint-max-params-ac37
ScriptedAlchemy Mar 4, 2026
81aeb77
Merge branch 'main' into codex/ts-eslint-max-params-ac37
ScriptedAlchemy Mar 5, 2026
5ce5760
Merge remote-tracking branch 'origin/main' into codex/ts-eslint-max-p…
ScriptedAlchemy Mar 11, 2026
a30a61d
Merge branch 'main' into codex/ts-eslint-max-params-ac37
ScriptedAlchemy Mar 11, 2026
5fb9856
Merge branch 'main' into codex/ts-eslint-max-params-ac37
ScriptedAlchemy Mar 12, 2026
4c41a9a
Merge remote-tracking branch 'origin/main' into codex/ts-eslint-max-p…
ScriptedAlchemy Mar 12, 2026
06ebc85
fix: cover ts signature nodes in max-params
ScriptedAlchemy Mar 12, 2026
bc7530d
Merge remote-tracking branch 'origin/main' into codex/ts-eslint-max-p…
ScriptedAlchemy Mar 12, 2026
566c0ab
Merge remote-tracking branch 'origin/main' into codex/ts-eslint-max-p…
ScriptedAlchemy Mar 12, 2026
999cf30
Merge branch 'main' into codex/ts-eslint-max-params-ac37
ScriptedAlchemy Mar 12, 2026
c2c1093
Merge branch 'main' into codex/ts-eslint-max-params-ac37
ScriptedAlchemy Mar 12, 2026
d4d2968
Merge remote-tracking branch 'origin/codex/ts-eslint-max-params-ac37'…
ScriptedAlchemy Mar 13, 2026
066aabd
fix: align max-params diagnostics with upstream
ScriptedAlchemy Mar 13, 2026
d7473aa
Merge remote-tracking branch 'origin/main' into codex/ts-eslint-max-p…
ScriptedAlchemy Mar 16, 2026
0826bbf
Merge branch 'main' into codex/ts-eslint-max-params-ac37
ScriptedAlchemy Mar 17, 2026
98a7de3
Merge remote-tracking branch 'origin/main' into codex/ts-eslint-max-p…
ScriptedAlchemy Mar 19, 2026
a63a0ec
Merge branch 'main' into codex/ts-eslint-max-params-ac37
ScriptedAlchemy Mar 19, 2026
9f5b293
Merge main into codex/ts-eslint-max-params-ac37
ScriptedAlchemy May 25, 2026
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 @@ -25,6 +25,7 @@ import (
"github.com/web-infra-dev/rslint/internal/plugins/typescript/rules/consistent_type_imports"
"github.com/web-infra-dev/rslint/internal/plugins/typescript/rules/default_param_last"
"github.com/web-infra-dev/rslint/internal/plugins/typescript/rules/dot_notation"
"github.com/web-infra-dev/rslint/internal/plugins/typescript/rules/max_params"
"github.com/web-infra-dev/rslint/internal/plugins/typescript/rules/no_array_constructor"
"github.com/web-infra-dev/rslint/internal/plugins/typescript/rules/no_array_delete"
"github.com/web-infra-dev/rslint/internal/plugins/typescript/rules/no_base_to_string"
Expand Down Expand Up @@ -391,6 +392,7 @@ func registerAllTypeScriptEslintPluginRules() {
GlobalRuleRegistry.Register("@typescript-eslint/consistent-type-imports", consistent_type_imports.ConsistentTypeImportsRule)
GlobalRuleRegistry.Register("@typescript-eslint/default-param-last", default_param_last.DefaultParamLastRule)
GlobalRuleRegistry.Register("@typescript-eslint/dot-notation", dot_notation.DotNotationRule)
GlobalRuleRegistry.Register("@typescript-eslint/max-params", max_params.MaxParamsRule)
GlobalRuleRegistry.Register("@typescript-eslint/no-array-constructor", no_array_constructor.NoArrayConstructorRule)
GlobalRuleRegistry.Register("@typescript-eslint/no-array-delete", no_array_delete.NoArrayDeleteRule)
GlobalRuleRegistry.Register("@typescript-eslint/no-base-to-string", no_base_to_string.NoBaseToStringRule)
Expand Down
139 changes: 139 additions & 0 deletions internal/plugins/typescript/rules/max_params/max_params.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
package max_params

import (
"fmt"

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

type MaxParamsOptions struct {
Max int `json:"max"`
CountVoidThis bool `json:"countVoidThis"`
}

func parseNumericOption(value interface{}) (int, bool) {
switch v := value.(type) {
case int:
return v, true
case int32:
return int(v), true
case int64:
return int(v), true
case float32:
return int(v), true
case float64:
return int(v), true
default:
return 0, false
}
}

func parseOptions(options any) MaxParamsOptions {
opts := MaxParamsOptions{
Max: 3,
CountVoidThis: false,
}

if options == nil {
return opts
}

var optsMap map[string]interface{}
if arr, ok := options.([]interface{}); ok && len(arr) > 0 {
if m, ok := arr[0].(map[string]interface{}); ok {
optsMap = m
Comment thread
ScriptedAlchemy marked this conversation as resolved.
Outdated
}
} else if m, ok := options.(map[string]interface{}); ok {
optsMap = m
}

if optsMap == nil {
return opts
}

hasMax := false
if value, ok := optsMap["max"]; ok {
if maxValue, ok := parseNumericOption(value); ok {
opts.Max = maxValue
hasMax = true
}
}
if !hasMax {
if value, ok := optsMap["maximum"]; ok {
if maxValue, ok := parseNumericOption(value); ok {
opts.Max = maxValue
}
}
}
if value, ok := optsMap["countVoidThis"]; ok {
if flag, ok := value.(bool); ok {
opts.CountVoidThis = flag
}
}

return opts
}

func isVoidThisParameter(param *ast.Node) bool {
if param == nil || !ast.IsParameter(param) {
return false
}

decl := param.AsParameterDeclaration()
if decl == nil {
return false
}

name := decl.Name()
if name == nil || name.Kind != ast.KindIdentifier {
return false
}

if name.AsIdentifier().Text != "this" {
return false
}

return decl.Type != nil && decl.Type.Kind == ast.KindVoidKeyword
}

func buildExceedMessage(count int, maxCount int) rule.RuleMessage {
return rule.RuleMessage{
Id: "exceed",
Description: fmt.Sprintf("Function has too many parameters (%d). Maximum allowed is %d.", count, maxCount),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The original ESLint core max-params rule uses a dynamic name in the message — e.g. "Function 'foo'...", "Arrow function...", "Method 'bar'..." depending on the function type. Currently this is hardcoded as "Function" for all cases.

It might be worth generating the appropriate name based on the node kind to stay consistent with the upstream behavior. Something like:

  • KindFunctionDeclarationFunction 'name'
  • KindArrowFunctionArrow function
  • KindMethodDeclarationMethod 'name'
  • KindConstructorConstructor
  • KindGetAccessorGetter 'name'
  • KindSetAccessorSetter 'name'
  • KindFunctionTypeFunction type

Not a blocker, but would be nice to align!

}
}

var MaxParamsRule = rule.CreateRule(rule.Rule{
Name: "max-params",
Run: func(ctx rule.RuleContext, options any) rule.RuleListeners {
opts := parseOptions(options)

checkParameters := func(node *ast.Node) {
params := node.Parameters()
if params == nil {
return
}

count := len(params)
if !opts.CountVoidThis && count > 0 && isVoidThisParameter(params[0]) {
count--
}

if count > opts.Max {
ctx.ReportNode(node, buildExceedMessage(count, opts.Max))
}
}

return rule.RuleListeners{
ast.KindFunctionDeclaration: checkParameters,
ast.KindFunctionExpression: checkParameters,
ast.KindArrowFunction: checkParameters,
ast.KindMethodDeclaration: checkParameters,
ast.KindConstructor: checkParameters,
ast.KindGetAccessor: checkParameters,
ast.KindSetAccessor: checkParameters,
ast.KindFunctionType: checkParameters,
Comment thread
ScriptedAlchemy marked this conversation as resolved.
Outdated
}
},
})
29 changes: 29 additions & 0 deletions internal/plugins/typescript/rules/max_params/max_params.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# max-params

## Rule Details

Enforce a maximum number of parameters in function-like declarations.

Comment thread
ScriptedAlchemy marked this conversation as resolved.
Examples of **incorrect** code for this rule:

```typescript
function foo(a, b, c, d) {}

class Foo {
method(this: Foo, a, b, c) {}
}
```

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

```typescript
function foo(a, b, c) {}

class Foo {
method(this: void, a, b, c) {}
}
```

## Original Documentation

https://typescript-eslint.io/rules/max-params
176 changes: 176 additions & 0 deletions internal/plugins/typescript/rules/max_params/max_params_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
package max_params

import (
"testing"

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

func TestMaxParamsRule(t *testing.T) {
rule_tester.RunRuleTester(fixtures.GetRootDir(), "tsconfig.json", t, &MaxParamsRule, []rule_tester.ValidTestCase{
{Code: `function foo() {}`},
{Code: `const foo = function () {};`},
{Code: `const foo = () => {};`},
{Code: `function foo(a) {}`},
{
Code: `
class Foo {
constructor(a) {}
}
`,
},
{
Code: `
class Foo {
method(this: void, a, b, c) {}
}
`,
},
{
Code: `
class Foo {
method(this: Foo, a, b) {}
}
`,
},
{
Code: `function foo(a, b, c, d) {}`,
Options: []interface{}{map[string]interface{}{"max": 4}},
},
{
Code: `function foo(a, b, c, d) {}`,
Options: []interface{}{map[string]interface{}{"maximum": 4}},
},
{
Code: `
class Foo {
method(this: void) {}
}
`,
Options: []interface{}{map[string]interface{}{"max": 0}},
},
{
Code: `
class Foo {
method(this: void, a) {}
}
`,
Options: []interface{}{map[string]interface{}{"max": 1}},
},
{
Code: `
class Foo {
method(this: void, a) {}
}
`,
Options: []interface{}{map[string]interface{}{"countVoidThis": true, "max": 2}},
},
{
Code: `
declare function makeDate(m: number, d: number, y: number): Date;
`,
Options: []interface{}{map[string]interface{}{"max": 3}},
},
{
Code: `
type sum = (a: number, b: number) => number;
`,
Options: []interface{}{map[string]interface{}{"max": 2}},
},
{
Code: `
interface Foo {
method(a: number, b: number, c: number, d: number): void;
}
`,
},
{
Code: `
type CallSig = {
(a: number, b: number, c: number, d: number): void;
};
`,
},
{
Code: `
type Ctor = new (a: number, b: number, c: number, d: number) => Foo;
`,
},
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These “valid” cases include interface/type signatures with 4 parameters while the default max is 3. Given the current listener list, they won’t be checked at all, so this test may be accidentally encoding a coverage gap. Either add coverage + listeners for the relevant TS signature node kinds (and move these to invalid), or remove them and clarify that the rule only targets runtime functions.

Suggested change
{
Code: `
interface Foo {
method(a: number, b: number, c: number, d: number): void;
}
`,
},
{
Code: `
type CallSig = {
(a: number, b: number, c: number, d: number): void;
};
`,
},
{
Code: `
type Ctor = new (a: number, b: number, c: number, d: number) => Foo;
`,
},

Copilot uses AI. Check for mistakes.
}, []rule_tester.InvalidTestCase{
{
Code: `function foo(a, b, c, d) {}`,
Errors: []rule_tester.InvalidTestCaseError{
{MessageId: "exceed", Line: 1, Column: 1, EndLine: 1, EndColumn: 28},
},
},
{
Code: `const foo = function (a, b, c, d) {};`,
Errors: []rule_tester.InvalidTestCaseError{
{MessageId: "exceed", Line: 1, Column: 13, EndLine: 1, EndColumn: 37},
},
},
{
Code: `const foo = (a, b, c, d) => {};`,
Errors: []rule_tester.InvalidTestCaseError{
{MessageId: "exceed", Line: 1, Column: 13, EndLine: 1, EndColumn: 31},
},
},
{
Code: `const foo = a => {};`,
Options: []interface{}{map[string]interface{}{"max": 0}},
Errors: []rule_tester.InvalidTestCaseError{
{MessageId: "exceed", Line: 1, Column: 13, EndLine: 1, EndColumn: 20},
},
},
{
Code: `
class Foo {
method(this: void, a, b, c, d) {}
}
`,
Errors: []rule_tester.InvalidTestCaseError{
{MessageId: "exceed", Line: 3, Column: 3, EndLine: 3, EndColumn: 36},
},
},
{
Code: `
class Foo {
method(this: void, a) {}
}
`,
Options: []interface{}{map[string]interface{}{"countVoidThis": true, "max": 1}},
Errors: []rule_tester.InvalidTestCaseError{
{MessageId: "exceed", Line: 3, Column: 3, EndLine: 3, EndColumn: 27},
},
},
{
Code: `
class Foo {
method(this: Foo, a, b, c) {}
}
`,
Errors: []rule_tester.InvalidTestCaseError{
{MessageId: "exceed", Line: 3, Column: 3, EndLine: 3, EndColumn: 32},
},
},
{
Code: `
declare function makeDate(m: number, d: number, y: number): Date;
`,
Options: []interface{}{map[string]interface{}{"max": 1}},
Errors: []rule_tester.InvalidTestCaseError{
{MessageId: "exceed", Line: 2, Column: 1, EndLine: 2, EndColumn: 66},
},
},
{
Code: `
type sum = (a: number, b: number) => number;
`,
Options: []interface{}{map[string]interface{}{"max": 1}},
Errors: []rule_tester.InvalidTestCaseError{
{MessageId: "exceed", Line: 2, Column: 12, EndLine: 2, EndColumn: 44},
},
},
})
}
2 changes: 1 addition & 1 deletion packages/rslint-test-tools/rstest.config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export default defineConfig({
// './tests/typescript-eslint/rules/explicit-member-accessibility.test.ts',
// './tests/typescript-eslint/rules/explicit-module-boundary-types.test.ts',
// './tests/typescript-eslint/rules/init-declarations.test.ts',
// './tests/typescript-eslint/rules/max-params.test.ts',
'./tests/typescript-eslint/rules/max-params.test.ts',
// './tests/typescript-eslint/rules/member-ordering.test.ts',
// './tests/typescript-eslint/rules/member-ordering/member-ordering-alphabetically-case-insensitive-order.test.ts',
// './tests/typescript-eslint/rules/member-ordering/member-ordering-alphabetically-order.test.ts',
Expand Down
Loading
Loading