diff --git a/internal/config/config.go b/internal/config/config.go index e0dc14aa1..fbd63a45d 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -28,6 +28,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" @@ -350,6 +351,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) diff --git a/internal/plugins/typescript/rules/max_params/max_params.go b/internal/plugins/typescript/rules/max_params/max_params.go new file mode 100644 index 000000000..a4fda5499 --- /dev/null +++ b/internal/plugins/typescript/rules/max_params/max_params.go @@ -0,0 +1,207 @@ +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 { + first := arr[0] + if maxValue, ok := parseNumericOption(first); ok { + opts.Max = maxValue + return opts + } + if m, ok := first.(map[string]interface{}); ok { + optsMap = m + } + } else if maxValue, ok := parseNumericOption(options); ok { + opts.Max = maxValue + return opts + } 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 getNamedFunctionLabel(prefix string, nameNode *ast.Node) string { + if nameNode != nil && nameNode.Kind == ast.KindIdentifier { + return fmt.Sprintf("%s '%s'", prefix, nameNode.AsIdentifier().Text) + } + return prefix +} + +func getFunctionLabel(node *ast.Node) string { + switch node.Kind { + case ast.KindFunctionDeclaration: + if decl := node.AsFunctionDeclaration(); decl != nil { + return getNamedFunctionLabel("Function", decl.Name()) + } + return "Function" + case ast.KindFunctionExpression: + if expr := node.AsFunctionExpression(); expr != nil { + return getNamedFunctionLabel("Function", expr.Name()) + } + return "Function" + case ast.KindArrowFunction: + return "Arrow function" + case ast.KindMethodDeclaration: + if decl := node.AsMethodDeclaration(); decl != nil { + return getNamedFunctionLabel("Method", decl.Name()) + } + return "Method" + case ast.KindMethodSignature: + if sig := node.AsMethodSignatureDeclaration(); sig != nil { + return getNamedFunctionLabel("Method", sig.Name()) + } + return "Method" + case ast.KindConstructor: + return "Constructor" + case ast.KindGetAccessor: + if accessor := node.AsGetAccessorDeclaration(); accessor != nil { + return getNamedFunctionLabel("Getter", accessor.Name()) + } + return "Getter" + case ast.KindSetAccessor: + if accessor := node.AsSetAccessorDeclaration(); accessor != nil { + return getNamedFunctionLabel("Setter", accessor.Name()) + } + return "Setter" + case ast.KindFunctionType: + return "Function type" + case ast.KindCallSignature: + return "Call signature" + case ast.KindConstructSignature: + return "Constructor signature" + case ast.KindConstructorType: + return "Constructor type" + default: + return "Function" + } +} + +func buildExceedMessage(node *ast.Node, count int, maxCount int) rule.RuleMessage { + return rule.RuleMessage{ + Id: "exceed", + Description: fmt.Sprintf("%s has too many parameters (%d). Maximum allowed is %d.", getFunctionLabel(node), count, maxCount), + } +} + +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(node, count, opts.Max)) + } + } + + return rule.RuleListeners{ + ast.KindFunctionDeclaration: checkParameters, + ast.KindFunctionExpression: checkParameters, + ast.KindArrowFunction: checkParameters, + ast.KindMethodDeclaration: checkParameters, + ast.KindMethodSignature: checkParameters, + ast.KindCallSignature: checkParameters, + ast.KindConstructSignature: checkParameters, + ast.KindConstructorType: checkParameters, + ast.KindConstructor: checkParameters, + ast.KindGetAccessor: checkParameters, + ast.KindSetAccessor: checkParameters, + ast.KindFunctionType: checkParameters, + } + }, +}) diff --git a/internal/plugins/typescript/rules/max_params/max_params.md b/internal/plugins/typescript/rules/max_params/max_params.md new file mode 100644 index 000000000..a1bf4c007 --- /dev/null +++ b/internal/plugins/typescript/rules/max_params/max_params.md @@ -0,0 +1,42 @@ +# max-params + +## Rule Details + +Enforce a maximum number of parameters in function-like declarations. + +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) {} +} +``` + +## Options + +- `max` (`number`, default: `3`): Maximum number of parameters. +- `maximum` (`number`, deprecated): Alias of `max`. +- `countVoidThis` (`boolean`, default: `false`): When `false`, a leading `this: void` parameter is not counted. + +The numeric shorthand is also supported: + +```typescript +// equivalent to { max: 4 } +['@typescript-eslint/max-params', 2, 4]; +``` + +## Original Documentation + +https://typescript-eslint.io/rules/max-params diff --git a/internal/plugins/typescript/rules/max_params/max_params_test.go b/internal/plugins/typescript/rules/max_params/max_params_test.go new file mode 100644 index 000000000..28ace2122 --- /dev/null +++ b/internal/plugins/typescript/rules/max_params/max_params_test.go @@ -0,0 +1,196 @@ +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: `function foo(a, b, c, d) {}`, + Options: []interface{}{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}}, + }, + }, []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}, + }, + }, + { + Code: `function foo(a, b) {}`, + Options: []interface{}{1}, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "exceed", Line: 1, Column: 1, EndLine: 1, EndColumn: 22}, + }, + }, + { + Code: ` +interface Foo { + method(a: number, b: number, c: number, d: number): void; +} + `, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "exceed", Line: 3, Column: 3, EndLine: 3, EndColumn: 60}, + }, + }, + { + Code: ` +type CallSig = { + (a: number, b: number, c: number, d: number): void; +}; + `, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "exceed", Line: 3, Column: 3, EndLine: 3, EndColumn: 54}, + }, + }, + { + Code: ` +type Ctor = new (a: number, b: number, c: number, d: number) => Foo; + `, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "exceed", Line: 2, Column: 13, EndLine: 2, EndColumn: 68}, + }, + }, + }) +} diff --git a/packages/rslint-test-tools/rstest.config.mts b/packages/rslint-test-tools/rstest.config.mts index 62116adae..5ef9db235 100644 --- a/packages/rslint-test-tools/rstest.config.mts +++ b/packages/rslint-test-tools/rstest.config.mts @@ -65,7 +65,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', diff --git a/packages/rslint-test-tools/tests/typescript-eslint/rules/__snapshots__/max-params.test.ts.snap b/packages/rslint-test-tools/tests/typescript-eslint/rules/__snapshots__/max-params.test.ts.snap new file mode 100644 index 000000000..0b003dcba --- /dev/null +++ b/packages/rslint-test-tools/tests/typescript-eslint/rules/__snapshots__/max-params.test.ts.snap @@ -0,0 +1,339 @@ +// Rstest Snapshot v1 + +exports[`max-params > invalid 1`] = ` +{ + "code": "function foo(a, b, c, d) {}", + "diagnostics": [ + { + "message": "Function 'foo' has too many parameters (4). Maximum allowed is 3.", + "messageId": "exceed", + "range": { + "end": { + "column": 28, + "line": 1, + }, + "start": { + "column": 1, + "line": 1, + }, + }, + "ruleName": "@typescript-eslint/max-params", + }, + ], + "errorCount": 1, + "fileCount": 1, + "ruleCount": 1, +} +`; + +exports[`max-params > invalid 2`] = ` +{ + "code": "const foo = function (a, b, c, d) {};", + "diagnostics": [ + { + "message": "Function has too many parameters (4). Maximum allowed is 3.", + "messageId": "exceed", + "range": { + "end": { + "column": 37, + "line": 1, + }, + "start": { + "column": 13, + "line": 1, + }, + }, + "ruleName": "@typescript-eslint/max-params", + }, + ], + "errorCount": 1, + "fileCount": 1, + "ruleCount": 1, +} +`; + +exports[`max-params > invalid 3`] = ` +{ + "code": "const foo = (a, b, c, d) => {};", + "diagnostics": [ + { + "message": "Arrow function has too many parameters (4). Maximum allowed is 3.", + "messageId": "exceed", + "range": { + "end": { + "column": 31, + "line": 1, + }, + "start": { + "column": 13, + "line": 1, + }, + }, + "ruleName": "@typescript-eslint/max-params", + }, + ], + "errorCount": 1, + "fileCount": 1, + "ruleCount": 1, +} +`; + +exports[`max-params > invalid 4`] = ` +{ + "code": "const foo = a => {};", + "diagnostics": [ + { + "message": "Arrow function has too many parameters (1). Maximum allowed is 0.", + "messageId": "exceed", + "range": { + "end": { + "column": 20, + "line": 1, + }, + "start": { + "column": 13, + "line": 1, + }, + }, + "ruleName": "@typescript-eslint/max-params", + }, + ], + "errorCount": 1, + "fileCount": 1, + "ruleCount": 1, +} +`; + +exports[`max-params > invalid 5`] = ` +{ + "code": " +class Foo { + method(this: void, a, b, c, d) {} +} + ", + "diagnostics": [ + { + "message": "Method 'method' has too many parameters (4). Maximum allowed is 3.", + "messageId": "exceed", + "range": { + "end": { + "column": 36, + "line": 3, + }, + "start": { + "column": 3, + "line": 3, + }, + }, + "ruleName": "@typescript-eslint/max-params", + }, + ], + "errorCount": 1, + "fileCount": 1, + "ruleCount": 1, +} +`; + +exports[`max-params > invalid 6`] = ` +{ + "code": " +class Foo { + method(this: void, a) {} +} + ", + "diagnostics": [ + { + "message": "Method 'method' has too many parameters (2). Maximum allowed is 1.", + "messageId": "exceed", + "range": { + "end": { + "column": 27, + "line": 3, + }, + "start": { + "column": 3, + "line": 3, + }, + }, + "ruleName": "@typescript-eslint/max-params", + }, + ], + "errorCount": 1, + "fileCount": 1, + "ruleCount": 1, +} +`; + +exports[`max-params > invalid 7`] = ` +{ + "code": " +class Foo { + method(this: Foo, a, b, c) {} +} + ", + "diagnostics": [ + { + "message": "Method 'method' has too many parameters (4). Maximum allowed is 3.", + "messageId": "exceed", + "range": { + "end": { + "column": 32, + "line": 3, + }, + "start": { + "column": 3, + "line": 3, + }, + }, + "ruleName": "@typescript-eslint/max-params", + }, + ], + "errorCount": 1, + "fileCount": 1, + "ruleCount": 1, +} +`; + +exports[`max-params > invalid 8`] = ` +{ + "code": " +declare function makeDate(m: number, d: number, y: number): Date; + ", + "diagnostics": [ + { + "message": "Function 'makeDate' has too many parameters (3). Maximum allowed is 1.", + "messageId": "exceed", + "range": { + "end": { + "column": 66, + "line": 2, + }, + "start": { + "column": 1, + "line": 2, + }, + }, + "ruleName": "@typescript-eslint/max-params", + }, + ], + "errorCount": 1, + "fileCount": 1, + "ruleCount": 1, +} +`; + +exports[`max-params > invalid 9`] = ` +{ + "code": " +type sum = (a: number, b: number) => number; + ", + "diagnostics": [ + { + "message": "Function type has too many parameters (2). Maximum allowed is 1.", + "messageId": "exceed", + "range": { + "end": { + "column": 44, + "line": 2, + }, + "start": { + "column": 12, + "line": 2, + }, + }, + "ruleName": "@typescript-eslint/max-params", + }, + ], + "errorCount": 1, + "fileCount": 1, + "ruleCount": 1, +} +`; + +exports[`max-params > invalid 10`] = ` +{ + "code": " +interface Foo { + method(a: number, b: number, c: number, d: number): void; +} + ", + "diagnostics": [ + { + "message": "Method 'method' has too many parameters (4). Maximum allowed is 3.", + "messageId": "exceed", + "range": { + "end": { + "column": 60, + "line": 3, + }, + "start": { + "column": 3, + "line": 3, + }, + }, + "ruleName": "@typescript-eslint/max-params", + }, + ], + "errorCount": 1, + "fileCount": 1, + "ruleCount": 1, +} +`; + +exports[`max-params > invalid 11`] = ` +{ + "code": " +type CallSig = { + (a: number, b: number, c: number, d: number): void; +}; + ", + "diagnostics": [ + { + "message": "Call signature has too many parameters (4). Maximum allowed is 3.", + "messageId": "exceed", + "range": { + "end": { + "column": 54, + "line": 3, + }, + "start": { + "column": 3, + "line": 3, + }, + }, + "ruleName": "@typescript-eslint/max-params", + }, + ], + "errorCount": 1, + "fileCount": 1, + "ruleCount": 1, +} +`; + +exports[`max-params > invalid 12`] = ` +{ + "code": " +type Ctor = new (a: number, b: number, c: number, d: number) => Foo; + ", + "diagnostics": [ + { + "message": "Constructor type has too many parameters (4). Maximum allowed is 3.", + "messageId": "exceed", + "range": { + "end": { + "column": 68, + "line": 2, + }, + "start": { + "column": 13, + "line": 2, + }, + }, + "ruleName": "@typescript-eslint/max-params", + }, + ], + "errorCount": 1, + "fileCount": 1, + "ruleCount": 1, +} +`; diff --git a/packages/rslint-test-tools/tests/typescript-eslint/rules/max-params.test.ts b/packages/rslint-test-tools/tests/typescript-eslint/rules/max-params.test.ts index ffa8231cc..fff51e4a7 100644 --- a/packages/rslint-test-tools/tests/typescript-eslint/rules/max-params.test.ts +++ b/packages/rslint-test-tools/tests/typescript-eslint/rules/max-params.test.ts @@ -124,5 +124,27 @@ type sum = (a: number, b: number) => number; errors: [{ messageId: 'exceed' }], options: [{ max: 1 }], }, + { + code: ` +interface Foo { + method(a: number, b: number, c: number, d: number): void; +} + `, + errors: [{ messageId: 'exceed' }], + }, + { + code: ` +type CallSig = { + (a: number, b: number, c: number, d: number): void; +}; + `, + errors: [{ messageId: 'exceed' }], + }, + { + code: ` +type Ctor = new (a: number, b: number, c: number, d: number) => Foo; + `, + errors: [{ messageId: 'exceed' }], + }, ], }); diff --git a/rslint.json b/rslint.json index e00a7dc3d..89ecae71d 100644 --- a/rslint.json +++ b/rslint.json @@ -54,6 +54,7 @@ "@typescript-eslint/require-await": "warn", "@typescript-eslint/prefer-readonly": "warn", "@typescript-eslint/no-non-null-assertion": "warn", + "@typescript-eslint/max-params": "off", "@typescript-eslint/no-dynamic-delete": "off", "@typescript-eslint/prefer-includes": "off", "@typescript-eslint/prefer-regexp-exec": "off",