Skip to content

Commit

Permalink
feat(eslint-plugin): [max-params] don't count this: void parameter (#…
Browse files Browse the repository at this point in the history
…7696)

* feat(eslint-plugin): [max-params] don't count `this: void` parameter

Closes #7538

* hard-code schema

* refactor

* lint

* snapshot

* wip

* Update packages/eslint-plugin/src/rules/max-params.ts

Co-authored-by: Josh Goldberg ✨ <git@joshuakgoldberg.com>

* Small PR nits, don't mind me, it's great either way

---------

Co-authored-by: Josh Goldberg ✨ <git@joshuakgoldberg.com>
  • Loading branch information
StyleShit and JoshuaKGoldberg committed Oct 17, 2023
1 parent 7e52f27 commit 6398d3f
Show file tree
Hide file tree
Showing 8 changed files with 267 additions and 0 deletions.
10 changes: 10 additions & 0 deletions 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.
2 changes: 2 additions & 0 deletions packages/eslint-plugin/src/configs/all.ts
Expand Up @@ -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',
Expand Down
2 changes: 2 additions & 0 deletions packages/eslint-plugin/src/rules/index.ts
Expand Up @@ -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';
Expand Down Expand Up @@ -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,
Expand Down
92 changes: 92 additions & 0 deletions 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<T extends FunctionLike> = (node: T) => void;

const baseRule = getESLintCoreRule('max-params');

export type Options = InferOptionsTypeFromRule<typeof baseRule>;
export type MessageIds = InferMessageIdsTypeFromRule<typeof baseRule>;

export default createRule<Options, MessageIds>({
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 = <T extends FunctionLike>(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 = <T extends FunctionLike>(
listener: FunctionRuleListener<T>,
): FunctionRuleListener<T> => {
return (node: T): void => {
listener(removeVoidThisParam(node));
};
};

return {
ArrowFunctionExpression: wrapListener(baseRules.ArrowFunctionExpression),
FunctionDeclaration: wrapListener(baseRules.FunctionDeclaration),
FunctionExpression: wrapListener(baseRules.FunctionExpression),
};
},
});
1 change: 1 addition & 0 deletions packages/eslint-plugin/src/util/getESLintCoreRule.ts
Expand Up @@ -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');
Expand Down
104 changes: 104 additions & 0 deletions 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' }],
},
],
});
38 changes: 38 additions & 0 deletions packages/eslint-plugin/tests/schema-snapshots/max-params.shot

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 18 additions & 0 deletions packages/eslint-plugin/typings/eslint-rules.d.ts
Expand Up @@ -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';

Expand Down

0 comments on commit 6398d3f

Please sign in to comment.