Skip to content

Commit 5ca4972

Browse files
Merge pull request #257 from eslint-functional/issue/244
2 parents 9a50dda + af3cbcc commit 5ca4972

File tree

7 files changed

+301
-47
lines changed

7 files changed

+301
-47
lines changed

docs/rules/functional-parameters.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ type Options = {
5656
ignoreIIFE: boolean;
5757
};
5858
ignorePattern?: string[] | string;
59+
ignorePrefixSelector?: string[] | string;
5960
}
6061
```
6162
@@ -136,6 +137,31 @@ See [enforceParameterCount](#enforceparametercount).
136137

137138
If true, this option allows for the use of [IIFEs](https://developer.mozilla.org/en-US/docs/Glossary/IIFE) that do not have any parameters.
138139

140+
### `ignorePrefixSelector`
141+
142+
This allows for ignore functions where one of the given selectors matches the parent node in the AST of the function node.\
143+
For more information see [ESLint Selectors](https://eslint.org/docs/developer-guide/selectors).
144+
145+
Example:
146+
147+
With the following config:
148+
149+
```json
150+
{
151+
"enforceParameterCount": "exactlyOne",
152+
"ignorePrefixSelector": "CallExpression[callee.property.name='reduce']"
153+
},
154+
```
155+
156+
The following inline callback won't be flagged:
157+
158+
```js
159+
const sum = [1, 2, 3].reduce(
160+
(carry, current) => current,
161+
0
162+
);
163+
```
164+
139165
### `ignorePattern`
140166

141167
Patterns will be matched against function names.

src/common/ignore-options.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,25 @@ export const ignoreInterfaceOptionSchema: JSONSchema4["properties"] = {
108108
},
109109
};
110110

111+
/**
112+
* The option to ignore prefix selector.
113+
*/
114+
export type IgnorePrefixSelectorOption = {
115+
readonly ignorePrefixSelector?: ReadonlyArray<string> | string;
116+
};
117+
118+
/**
119+
* The schema for the option to ignore prefix selector.
120+
*/
121+
export const ignorePrefixSelectorOptionSchema: JSONSchema4["properties"] = {
122+
ignorePrefixSelector: {
123+
type: ["string", "array"],
124+
items: {
125+
type: "string",
126+
},
127+
},
128+
};
129+
111130
/**
112131
* Should the given text be allowed?
113132
*

src/rules/functional-parameters.ts

Lines changed: 74 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,18 @@ import { deepmerge } from "deepmerge-ts";
33
import type { JSONSchema4 } from "json-schema";
44
import type { ReadonlyDeep } from "type-fest";
55

6-
import type { IgnorePatternOption } from "~/common/ignore-options";
6+
import type {
7+
IgnorePatternOption,
8+
IgnorePrefixSelectorOption,
9+
} from "~/common/ignore-options";
710
import {
811
shouldIgnorePattern,
912
ignorePatternOptionSchema,
13+
ignorePrefixSelectorOptionSchema,
1014
} from "~/common/ignore-options";
1115
import type { ESFunction } from "~/src/util/node-types";
1216
import type { RuleResult } from "~/util/rule";
13-
import { createRule } from "~/util/rule";
17+
import { createRuleUsingFunction } from "~/util/rule";
1418
import { isIIFE, isPropertyAccess, isPropertyName } from "~/util/tree";
1519
import { isRestElement } from "~/util/typeguard";
1620

@@ -29,6 +33,7 @@ type ParameterCountOptions = "atLeastOne" | "exactlyOne";
2933
*/
3034
type Options = readonly [
3135
IgnorePatternOption &
36+
IgnorePrefixSelectorOption &
3237
Readonly<{
3338
allowRestParameter: boolean;
3439
allowArgumentsKeyword: boolean;
@@ -48,39 +53,43 @@ type Options = readonly [
4853
const schema: JSONSchema4 = [
4954
{
5055
type: "object",
51-
properties: deepmerge(ignorePatternOptionSchema, {
52-
allowRestParameter: {
53-
type: "boolean",
54-
},
55-
allowArgumentsKeyword: {
56-
type: "boolean",
57-
},
58-
enforceParameterCount: {
59-
oneOf: [
60-
{
61-
type: "boolean",
62-
enum: [false],
63-
},
64-
{
65-
type: "string",
66-
enum: ["atLeastOne", "exactlyOne"],
67-
},
68-
{
69-
type: "object",
70-
properties: {
71-
count: {
72-
type: "string",
73-
enum: ["atLeastOne", "exactlyOne"],
74-
},
75-
ignoreIIFE: {
76-
type: "boolean",
56+
properties: deepmerge(
57+
ignorePatternOptionSchema,
58+
ignorePrefixSelectorOptionSchema,
59+
{
60+
allowRestParameter: {
61+
type: "boolean",
62+
},
63+
allowArgumentsKeyword: {
64+
type: "boolean",
65+
},
66+
enforceParameterCount: {
67+
oneOf: [
68+
{
69+
type: "boolean",
70+
enum: [false],
71+
},
72+
{
73+
type: "string",
74+
enum: ["atLeastOne", "exactlyOne"],
75+
},
76+
{
77+
type: "object",
78+
properties: {
79+
count: {
80+
type: "string",
81+
enum: ["atLeastOne", "exactlyOne"],
82+
},
83+
ignoreIIFE: {
84+
type: "boolean",
85+
},
7786
},
87+
additionalProperties: false,
7888
},
79-
additionalProperties: false,
80-
},
81-
],
82-
},
83-
}),
89+
],
90+
},
91+
}
92+
),
8493
additionalProperties: false,
8594
},
8695
];
@@ -255,14 +264,36 @@ function checkIdentifier(
255264
}
256265

257266
// Create the rule.
258-
export const rule = createRule<keyof typeof errorMessages, Options>(
259-
name,
260-
meta,
261-
defaultOptions,
262-
{
263-
FunctionDeclaration: checkFunction,
264-
FunctionExpression: checkFunction,
265-
ArrowFunctionExpression: checkFunction,
267+
export const rule = createRuleUsingFunction<
268+
keyof typeof errorMessages,
269+
Options
270+
>(name, meta, defaultOptions, (context, options) => {
271+
const [optionsObject] = options;
272+
const { ignorePrefixSelector } = optionsObject;
273+
274+
const baseFunctionSelectors = [
275+
"ArrowFunctionExpression",
276+
"FunctionDeclaration",
277+
"FunctionExpression",
278+
];
279+
280+
const ignoreSelectors: ReadonlyArray<string> | undefined =
281+
ignorePrefixSelector === undefined
282+
? undefined
283+
: Array.isArray(ignorePrefixSelector)
284+
? ignorePrefixSelector
285+
: [ignorePrefixSelector];
286+
287+
const fullFunctionSelectors = baseFunctionSelectors.flatMap((baseSelector) =>
288+
ignoreSelectors === undefined
289+
? [baseSelector]
290+
: `:not(:matches(${ignoreSelectors.join(",")})) > ${baseSelector}`
291+
);
292+
293+
return {
294+
...Object.fromEntries(
295+
fullFunctionSelectors.map((selector) => [selector, checkFunction])
296+
),
266297
Identifier: checkIdentifier,
267-
}
268-
);
298+
};
299+
});

src/util/rule.ts

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -89,16 +89,45 @@ export function createRule<
8989
meta: ESLintUtils.NamedCreateRuleMeta<MessageIds>,
9090
defaultOptions: Options,
9191
ruleFunctionsMap: RuleFunctionsMap<any, MessageIds, Options>
92-
) {
92+
): Rule.RuleModule {
93+
return createRuleUsingFunction(
94+
name,
95+
meta,
96+
defaultOptions,
97+
() => ruleFunctionsMap
98+
);
99+
}
100+
101+
/**
102+
* Create a rule.
103+
*/
104+
export function createRuleUsingFunction<
105+
MessageIds extends string,
106+
Options extends BaseOptions
107+
>(
108+
name: string,
109+
meta: ESLintUtils.NamedCreateRuleMeta<MessageIds>,
110+
defaultOptions: Options,
111+
createFunction: (
112+
context: ReadonlyDeep<TSESLint.RuleContext<MessageIds, Options>>,
113+
options: Options
114+
) => RuleFunctionsMap<any, MessageIds, Options>
115+
): Rule.RuleModule {
93116
return ESLintUtils.RuleCreator(
94117
(ruleName) =>
95118
`https://github.com/eslint-functional/eslint-plugin-functional/blob/v${__VERSION__}/docs/rules/${ruleName}.md`
96119
)({
97120
name,
98121
meta,
99122
defaultOptions,
100-
create: (context, options) =>
101-
Object.fromEntries(
123+
create: (context, options) => {
124+
const ruleFunctionsMap = createFunction(
125+
context as unknown as ReadonlyDeep<
126+
TSESLint.RuleContext<MessageIds, Options>
127+
>,
128+
options as unknown as Options
129+
);
130+
return Object.fromEntries(
102131
Object.entries(ruleFunctionsMap).map(([nodeSelector, ruleFunction]) => [
103132
nodeSelector,
104133
checkNode(
@@ -109,7 +138,8 @@ export function createRule<
109138
options as unknown as Options
110139
),
111140
])
112-
),
141+
);
142+
},
113143
}) as unknown as Rule.RuleModule;
114144
}
115145

tests/rules/functional-parameters/es3/invalid.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,37 @@ const tests: ReadonlyArray<InvalidTestCase> = [
9999
},
100100
],
101101
},
102+
{
103+
code: dedent`
104+
[1, 2, 3]
105+
.map(
106+
function(element, index) {
107+
return element + index;
108+
}
109+
)
110+
.reduce(
111+
function(carry, current) {
112+
return carry + current;
113+
},
114+
0
115+
);
116+
`,
117+
optionsSet: [[{ enforceParameterCount: "exactlyOne" }]],
118+
errors: [
119+
{
120+
messageId: "paramCountExactlyOne",
121+
type: "FunctionExpression",
122+
line: 3,
123+
column: 5,
124+
},
125+
{
126+
messageId: "paramCountExactlyOne",
127+
type: "FunctionExpression",
128+
line: 8,
129+
column: 5,
130+
},
131+
],
132+
},
102133
];
103134

104135
export default tests;

tests/rules/functional-parameters/es3/valid.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,69 @@ const tests: ReadonlyArray<ValidTestCase> = [
4242
[{ ignorePattern: "^foo", enforceParameterCount: "exactlyOne" }],
4343
],
4444
},
45+
{
46+
code: dedent`
47+
[1, 2, 3].reduce(
48+
function(carry, current) {
49+
return carry + current;
50+
},
51+
0
52+
);
53+
`,
54+
optionsSet: [
55+
[
56+
{
57+
ignorePrefixSelector: "CallExpression[callee.property.name='reduce']",
58+
enforceParameterCount: "exactlyOne",
59+
},
60+
],
61+
],
62+
},
63+
{
64+
code: dedent`
65+
[1, 2, 3].map(
66+
function(element, index) {
67+
return element + index;
68+
},
69+
0
70+
);
71+
`,
72+
optionsSet: [
73+
[
74+
{
75+
enforceParameterCount: "exactlyOne",
76+
ignorePrefixSelector: "CallExpression[callee.property.name='map']",
77+
},
78+
],
79+
],
80+
},
81+
{
82+
code: dedent`
83+
[1, 2, 3]
84+
.map(
85+
function(element, index) {
86+
return element + index;
87+
}
88+
)
89+
.reduce(
90+
function(carry, current) {
91+
return carry + current;
92+
},
93+
0
94+
);
95+
`,
96+
optionsSet: [
97+
[
98+
{
99+
enforceParameterCount: "exactlyOne",
100+
ignorePrefixSelector: [
101+
"CallExpression[callee.property.name='reduce']",
102+
"CallExpression[callee.property.name='map']",
103+
],
104+
},
105+
],
106+
],
107+
},
45108
];
46109

47110
export default tests;

0 commit comments

Comments
 (0)