diff --git a/packages/eslint-plugin/docs/rules/max-params.md b/packages/eslint-plugin/docs/rules/max-params.md new file mode 100644 index 000000000000..03854473cf36 --- /dev/null +++ b/packages/eslint-plugin/docs/rules/max-params.md @@ -0,0 +1,10 @@ +--- +description: 'Enforce a maximum number of parameters in function definitions.' +--- + +> 🛑 This file is source code, not the primary documentation location! 🛑 +> +> See **https://typescript-eslint.io/rules/max-params** for documentation. + +This rule extends the base [`eslint/max-params`](https://eslint.org/docs/rules/max-params) rule. +This version adds support for TypeScript `this` parameters so they won't be counted as a parameter. diff --git a/packages/eslint-plugin/src/configs/all.ts b/packages/eslint-plugin/src/configs/all.ts index bb3873bc3449..f7a3cc3dbcb0 100644 --- a/packages/eslint-plugin/src/configs/all.ts +++ b/packages/eslint-plugin/src/configs/all.ts @@ -52,6 +52,8 @@ export = { '@typescript-eslint/lines-around-comment': 'error', 'lines-between-class-members': 'off', '@typescript-eslint/lines-between-class-members': 'error', + 'max-params': 'off', + '@typescript-eslint/max-params': 'error', '@typescript-eslint/member-delimiter-style': 'error', '@typescript-eslint/member-ordering': 'error', '@typescript-eslint/method-signature-style': 'error', diff --git a/packages/eslint-plugin/src/rules/index.ts b/packages/eslint-plugin/src/rules/index.ts index 0cc9c880759a..9d87b8cac412 100644 --- a/packages/eslint-plugin/src/rules/index.ts +++ b/packages/eslint-plugin/src/rules/index.ts @@ -28,6 +28,7 @@ import keySpacing from './key-spacing'; import keywordSpacing from './keyword-spacing'; import linesAroundComment from './lines-around-comment'; import linesBetweenClassMembers from './lines-between-class-members'; +import maxParams from './max-params'; import memberDelimiterStyle from './member-delimiter-style'; import memberOrdering from './member-ordering'; import methodSignatureStyle from './method-signature-style'; @@ -164,6 +165,7 @@ export default { 'keyword-spacing': keywordSpacing, 'lines-around-comment': linesAroundComment, 'lines-between-class-members': linesBetweenClassMembers, + 'max-params': maxParams, 'member-delimiter-style': memberDelimiterStyle, 'member-ordering': memberOrdering, 'method-signature-style': methodSignatureStyle, diff --git a/packages/eslint-plugin/src/rules/max-params.ts b/packages/eslint-plugin/src/rules/max-params.ts new file mode 100644 index 000000000000..29a18358946f --- /dev/null +++ b/packages/eslint-plugin/src/rules/max-params.ts @@ -0,0 +1,92 @@ +import { AST_NODE_TYPES, type TSESTree } from '@typescript-eslint/utils'; + +import type { + InferMessageIdsTypeFromRule, + InferOptionsTypeFromRule, +} from '../util'; +import { createRule } from '../util'; +import { getESLintCoreRule } from '../util/getESLintCoreRule'; + +type FunctionLike = + | TSESTree.FunctionDeclaration + | TSESTree.FunctionExpression + | TSESTree.ArrowFunctionExpression; + +type FunctionRuleListener = (node: T) => void; + +const baseRule = getESLintCoreRule('max-params'); + +export type Options = InferOptionsTypeFromRule; +export type MessageIds = InferMessageIdsTypeFromRule; + +export default createRule({ + name: 'max-params', + meta: { + type: 'suggestion', + docs: { + description: + 'Enforce a maximum number of parameters in function definitions', + extendsBaseRule: true, + }, + schema: [ + { + type: 'object', + properties: { + maximum: { + type: 'integer', + minimum: 0, + }, + max: { + type: 'integer', + minimum: 0, + }, + countVoidThis: { + type: 'boolean', + }, + }, + additionalProperties: false, + }, + ], + messages: baseRule.meta.messages, + }, + defaultOptions: [{ max: 3, countVoidThis: false }], + + create(context, [{ countVoidThis }]) { + const baseRules = baseRule.create(context); + + if (countVoidThis === true) { + return baseRules; + } + + const removeVoidThisParam = (node: T): T => { + if ( + node.params.length === 0 || + node.params[0].type !== AST_NODE_TYPES.Identifier || + node.params[0].name !== 'this' || + node.params[0].typeAnnotation?.typeAnnotation.type !== + AST_NODE_TYPES.TSVoidKeyword + ) { + return node; + } + + return { + ...node, + params: node.params.slice(1), + }; + }; + + const wrapListener = ( + listener: FunctionRuleListener, + ): FunctionRuleListener => { + return (node: T): void => { + listener(removeVoidThisParam(node)); + }; + }; + + return { + ArrowFunctionExpression: wrapListener(baseRules.ArrowFunctionExpression), + FunctionDeclaration: wrapListener(baseRules.FunctionDeclaration), + FunctionExpression: wrapListener(baseRules.FunctionExpression), + }; + }, +}); diff --git a/packages/eslint-plugin/src/util/getESLintCoreRule.ts b/packages/eslint-plugin/src/util/getESLintCoreRule.ts index 6cfa0db393e9..ef9f4c12503a 100644 --- a/packages/eslint-plugin/src/util/getESLintCoreRule.ts +++ b/packages/eslint-plugin/src/util/getESLintCoreRule.ts @@ -17,6 +17,7 @@ interface RuleMap { 'keyword-spacing': typeof import('eslint/lib/rules/keyword-spacing'); 'lines-around-comment': typeof import('eslint/lib/rules/lines-around-comment'); 'lines-between-class-members': typeof import('eslint/lib/rules/lines-between-class-members'); + 'max-params': typeof import('eslint/lib/rules/max-params'); 'no-dupe-args': typeof import('eslint/lib/rules/no-dupe-args'); 'no-dupe-class-members': typeof import('eslint/lib/rules/no-dupe-class-members'); 'no-empty-function': typeof import('eslint/lib/rules/no-empty-function'); diff --git a/packages/eslint-plugin/tests/rules/max-params.test.ts b/packages/eslint-plugin/tests/rules/max-params.test.ts new file mode 100644 index 000000000000..054c57363a39 --- /dev/null +++ b/packages/eslint-plugin/tests/rules/max-params.test.ts @@ -0,0 +1,104 @@ +import { RuleTester } from '@typescript-eslint/rule-tester'; + +import rule from '../../src/rules/max-params'; + +const ruleTester = new RuleTester({ + parser: '@typescript-eslint/parser', +}); + +ruleTester.run('max-params', rule, { + valid: [ + 'function foo() {}', + 'const foo = function () {};', + 'const foo = () => {};', + 'function foo(a) {}', + ` +class Foo { + constructor(a) {} +} + `, + ` +class Foo { + method(this: void, a, b, c) {} +} + `, + ` +class Foo { + method(this: Foo, a, b) {} +} + `, + { + code: 'function foo(a, b, c, d) {}', + options: [{ max: 4 }], + }, + { + code: 'function foo(a, b, c, d) {}', + options: [{ maximum: 4 }], + }, + { + code: ` +class Foo { + method(this: void) {} +} + `, + options: [{ max: 0 }], + }, + { + code: ` +class Foo { + method(this: void, a) {} +} + `, + options: [{ max: 1 }], + }, + { + code: ` +class Foo { + method(this: void, a) {} +} + `, + options: [{ max: 2, countVoidThis: true }], + }, + ], + invalid: [ + { code: 'function foo(a, b, c, d) {}', errors: [{ messageId: 'exceed' }] }, + { + code: 'const foo = function (a, b, c, d) {};', + errors: [{ messageId: 'exceed' }], + }, + { + code: 'const foo = (a, b, c, d) => {};', + errors: [{ messageId: 'exceed' }], + }, + { + code: 'const foo = a => {};', + options: [{ max: 0 }], + errors: [{ messageId: 'exceed' }], + }, + { + code: ` +class Foo { + method(this: void, a, b, c, d) {} +} + `, + errors: [{ messageId: 'exceed' }], + }, + { + code: ` +class Foo { + method(this: void, a) {} +} + `, + options: [{ max: 1, countVoidThis: true }], + errors: [{ messageId: 'exceed' }], + }, + { + code: ` +class Foo { + method(this: Foo, a, b, c) {} +} + `, + errors: [{ messageId: 'exceed' }], + }, + ], +}); diff --git a/packages/eslint-plugin/tests/schema-snapshots/max-params.shot b/packages/eslint-plugin/tests/schema-snapshots/max-params.shot new file mode 100644 index 000000000000..646c1287dfae --- /dev/null +++ b/packages/eslint-plugin/tests/schema-snapshots/max-params.shot @@ -0,0 +1,38 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Rule schemas should be convertible to TS types for documentation purposes max-params 1`] = ` +" +# SCHEMA: + +[ + { + "additionalProperties": false, + "properties": { + "countVoidThis": { + "type": "boolean" + }, + "max": { + "minimum": 0, + "type": "integer" + }, + "maximum": { + "minimum": 0, + "type": "integer" + } + }, + "type": "object" + } +] + + +# TYPES: + +type Options = [ + { + countVoidThis?: boolean; + max?: number; + maximum?: number; + }, +]; +" +`; diff --git a/packages/eslint-plugin/typings/eslint-rules.d.ts b/packages/eslint-plugin/typings/eslint-rules.d.ts index dea1fcd69972..5182111d3455 100644 --- a/packages/eslint-plugin/typings/eslint-rules.d.ts +++ b/packages/eslint-plugin/typings/eslint-rules.d.ts @@ -272,6 +272,24 @@ declare module 'eslint/lib/rules/keyword-spacing' { export = rule; } +declare module 'eslint/lib/rules/max-params' { + import type { TSESLint, TSESTree } from '@typescript-eslint/utils'; + + const rule: TSESLint.RuleModule< + 'exceed', + ( + | { max: number; countVoidThis?: boolean } + | { maximum: number; countVoidThis?: boolean } + )[], + { + FunctionDeclaration(node: TSESTree.FunctionDeclaration): void; + FunctionExpression(node: TSESTree.FunctionExpression): void; + ArrowFunctionExpression(node: TSESTree.ArrowFunctionExpression): void; + } + >; + export = rule; +} + declare module 'eslint/lib/rules/no-dupe-class-members' { import type { TSESLint, TSESTree } from '@typescript-eslint/utils';