Skip to content

Commit

Permalink
feat(functional-parameters): add support for ignoring selector prefixes
Browse files Browse the repository at this point in the history
re #207 re #244
  • Loading branch information
RebeccaStevens committed Aug 11, 2021
1 parent 0e2c490 commit 32cd592
Show file tree
Hide file tree
Showing 7 changed files with 262 additions and 29 deletions.
32 changes: 30 additions & 2 deletions docs/rules/functional-parameters.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ This rule accepts an options object of the following type:
ignoreIIFE: boolean;
};
ignorePattern?: string | Array<string>;
ignorePrefixSelector?: string | Array<string>;
}
```

Expand All @@ -64,7 +65,8 @@ The default options:
enforceParameterCount: {
count: "atLeastOne",
ignoreIIFE: true
}
},
ignorePrefixSelector: undefined,
}
```

Expand All @@ -74,7 +76,8 @@ Note: the `lite` ruleset overrides the default options to:
{
allowRestParameter: false,
allowArgumentsKeyword: false,
enforceParameterCount: false
enforceParameterCount: false,
ignorePrefixSelector: undefined,
}
```

Expand Down Expand Up @@ -130,6 +133,31 @@ See [enforceParameterCount](#enforceparametercount).

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.

### `ignorePrefixSelector`

This allows for ignore functions where one of the given selectors matches the parent node in the AST of the function node.\
For more information see [ESLint Selectors](https://eslint.org/docs/developer-guide/selectors).

Example:

With the following config:

```json
{
"enforceParameterCount": "exactlyOne",
"ignorePrefixSelector": "CallExpression[callee.property.name='reduce']"
},
```

The following inline callback won't be flaged:

```js
const sum = [1, 2, 3].reduce(
(carry, current) => current,
0
);`,
```

### `ignorePattern`

Patterns will be matched against function names.
Expand Down
17 changes: 17 additions & 0 deletions src/common/ignore-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,23 @@ export const ignoreInterfaceOptionSchema: JSONSchema4 = {
additionalProperties: false,
};

export type IgnorePrefixSelectorOption = {
readonly ignorePrefixSelector?: ReadonlyArray<string> | string;
};

export const ignorePrefixSelectorOptionSchema: JSONSchema4 = {
type: "object",
properties: {
ignorePrefixSelector: {
type: ["string", "array"],
items: {
type: "string",
},
},
},
additionalProperties: false,
};

/**
* Get the identifier text of the given node.
*/
Expand Down
76 changes: 52 additions & 24 deletions src/rules/functional-parameters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,16 @@ import type { TSESTree } from "@typescript-eslint/experimental-utils";
import { all as deepMerge } from "deepmerge";
import type { JSONSchema4 } from "json-schema";

import type { IgnorePatternOption } from "~/common/ignore-options";
import { ignorePatternOptionSchema } from "~/common/ignore-options";
import type {
IgnorePatternOption,
IgnorePrefixSelectorOption,
} from "~/common/ignore-options";
import {
ignorePatternOptionSchema,
ignorePrefixSelectorOptionSchema,
} from "~/common/ignore-options";
import type { RuleContext, RuleMetaData, RuleResult } from "~/util/rule";
import { createRule } from "~/util/rule";
import { createRuleUsingFunction } from "~/util/rule";
import { isIIFE, isPropertyAccess, isPropertyName } from "~/util/tree";
import { isRestElement } from "~/util/typeguard";

Expand All @@ -15,22 +21,24 @@ export const name = "functional-parameters" as const;
type ParameterCountOptions = "atLeastOne" | "exactlyOne";

// The options this rule can take.
type Options = IgnorePatternOption & {
readonly allowRestParameter: boolean;
readonly allowArgumentsKeyword: boolean;
readonly enforceParameterCount:
| ParameterCountOptions
| false
| {
readonly count: ParameterCountOptions;
readonly ignoreIIFE: boolean;
};
};
type Options = IgnorePatternOption &
IgnorePrefixSelectorOption & {
readonly allowRestParameter: boolean;
readonly allowArgumentsKeyword: boolean;
readonly enforceParameterCount:
| ParameterCountOptions
| false
| {
readonly count: ParameterCountOptions;
readonly ignoreIIFE: boolean;
};
};

// The schema for the rule options.
const schema: JSONSchema4 = [
deepMerge([
ignorePatternOptionSchema,
ignorePrefixSelectorOptionSchema,
{
type: "object",
properties: {
Expand Down Expand Up @@ -79,6 +87,7 @@ const defaultOptions: Options = {
count: "atLeastOne",
ignoreIIFE: true,
},
ignorePrefixSelector: undefined,
};

// The possible error messages.
Expand Down Expand Up @@ -219,14 +228,33 @@ function checkIdentifier(
}

// Create the rule.
export const rule = createRule<keyof typeof errorMessages, Options>(
name,
meta,
defaultOptions,
{
FunctionDeclaration: checkFunction,
FunctionExpression: checkFunction,
ArrowFunctionExpression: checkFunction,
export const rule = createRuleUsingFunction<
keyof typeof errorMessages,
Options
>(name, meta, defaultOptions, (context, options) => {
const baseFunctionSelectors = [
"FunctionDeclaration",
"FunctionExpression",
"ArrowFunctionExpression",
];

const ignoreSelectors: ReadonlyArray<string> | undefined =
options.ignorePrefixSelector === undefined
? undefined
: Array.isArray(options.ignorePrefixSelector)
? options.ignorePrefixSelector
: [options.ignorePrefixSelector];

const fullFunctionSelectors = baseFunctionSelectors.flatMap((baseSelector) =>
ignoreSelectors === undefined
? [baseSelector]
: `:not(:matches(${ignoreSelectors.join(",")})) > ${baseSelector}`
);

return {
...Object.fromEntries(
fullFunctionSelectors.map((selector) => [selector, checkFunction])
),
Identifier: checkIdentifier,
}
);
};
});
31 changes: 28 additions & 3 deletions src/util/rule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,29 @@ export function createRule<
meta: RuleMetaData<MessageIds>,
defaultOptions: Options,
ruleFunctionsMap: RuleFunctionsMap<MessageIds, Options>
): Rule.RuleModule {
return createRuleUsingFunction(
name,
meta,
defaultOptions,
() => ruleFunctionsMap
);
}

/**
* Create a rule.
*/
export function createRuleUsingFunction<
MessageIds extends string,
Options extends BaseOptions
>(
name: string,
meta: RuleMetaData<MessageIds>,
defaultOptions: Options,
createFunction: (
context: TSESLint.RuleContext<MessageIds, readonly [Options]>,
options: Options
) => RuleFunctionsMap<MessageIds, Options>
): Rule.RuleModule {
return ESLintUtils.RuleCreator(
(name) =>
Expand All @@ -103,13 +126,15 @@ export function createRule<
create: (
context: TSESLint.RuleContext<MessageIds, readonly [Options]>,
[options]: readonly [Options]
) =>
Object.fromEntries(
) => {
const ruleFunctionsMap = createFunction(context, options);
return Object.fromEntries(
Object.entries(ruleFunctionsMap).map(([nodeSelector, ruleFunction]) => [
nodeSelector,
checkNode(ruleFunction, context, options),
])
),
);
},
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
} as any) as any;
}
Expand Down
24 changes: 24 additions & 0 deletions tests/rules/functional-parameters/es3/invalid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,30 @@ const tests: ReadonlyArray<InvalidTestCase> = [
},
],
},
{
code: dedent`
[1, 2, 3]
.map(
function(element, index) {
return element + index;
}
)
.reduce(
function(carry, current) {
return carry + current;
},
0
);`,
optionsSet: [[{ enforceParameterCount: "exactlyOne" }]],
errors: [
{
messageId: "paramCountExactlyOne",
type: "FunctionDeclaration",
line: 1,
column: 1,
},
],
},
];

export default tests;
60 changes: 60 additions & 0 deletions tests/rules/functional-parameters/es3/valid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,66 @@ const tests: ReadonlyArray<ValidTestCase> = [
[{ ignorePattern: "^foo", enforceParameterCount: "exactlyOne" }],
],
},
{
code: dedent`
[1, 2, 3].reduce(
function(carry, current) {
return carry + current;
},
0
);`,
optionsSet: [
[
{
ignorePrefixSelector: "CallExpression[callee.property.name='reduce']",
enforceParameterCount: "exactlyOne",
},
],
],
},
{
code: dedent`
[1, 2, 3].map(
function(element, index) {
return element + index;
},
0
);`,
optionsSet: [
[
{
enforceParameterCount: "exactlyOne",
ignorePrefixSelector: "CallExpression[callee.property.name='map']",
},
],
],
},
{
code: dedent`
[1, 2, 3]
.map(
function(element, index) {
return element + index;
}
)
.reduce(
function(carry, current) {
return carry + current;
},
0
);`,
optionsSet: [
[
{
enforceParameterCount: "exactlyOne",
ignorePrefixSelector: [
"CallExpression[callee.property.name='reduce']",
"CallExpression[callee.property.name='map']",
],
},
],
],
},
];

export default tests;
51 changes: 51 additions & 0 deletions tests/rules/functional-parameters/es6/valid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,57 @@ const tests: ReadonlyArray<ValidTestCase> = [
}`,
optionsSet: [[{ ignorePattern: "^foo" }]],
},
{
code: dedent`
[1, 2, 3].reduce(
(carry, current) => carry + current,
0
);`,
optionsSet: [
[
{
enforceParameterCount: "exactlyOne",
ignorePrefixSelector: "CallExpression[callee.property.name='reduce']",
},
],
],
},
{
code: dedent`
[1, 2, 3].map(
(element, index) => element + index,
0
);`,
optionsSet: [
[
{
enforceParameterCount: "exactlyOne",
ignorePrefixSelector: "CallExpression[callee.property.name='map']",
},
],
],
},
{
code: dedent`
[1, 2, 3]
.map(
(element, index) => element + index
)
.reduce(
(carry, current) => carry + current, 0
);`,
optionsSet: [
[
{
enforceParameterCount: "exactlyOne",
ignorePrefixSelector: [
"CallExpression[callee.property.name='reduce']",
"CallExpression[callee.property.name='map']",
],
},
],
],
},
];

export default tests;

0 comments on commit 32cd592

Please sign in to comment.