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 Sep 20, 2022
1 parent 9a50dda commit af3cbcc
Show file tree
Hide file tree
Showing 7 changed files with 301 additions and 47 deletions.
26 changes: 26 additions & 0 deletions docs/rules/functional-parameters.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ type Options = {
ignoreIIFE: boolean;
};
ignorePattern?: string[] | string;
ignorePrefixSelector?: string[] | string;
}
```
Expand Down Expand Up @@ -136,6 +137,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 flagged:

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

### `ignorePattern`

Patterns will be matched against function names.
Expand Down
19 changes: 19 additions & 0 deletions src/common/ignore-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,25 @@ export const ignoreInterfaceOptionSchema: JSONSchema4["properties"] = {
},
};

/**
* The option to ignore prefix selector.
*/
export type IgnorePrefixSelectorOption = {
readonly ignorePrefixSelector?: ReadonlyArray<string> | string;
};

/**
* The schema for the option to ignore prefix selector.
*/
export const ignorePrefixSelectorOptionSchema: JSONSchema4["properties"] = {
ignorePrefixSelector: {
type: ["string", "array"],
items: {
type: "string",
},
},
};

/**
* Should the given text be allowed?
*
Expand Down
117 changes: 74 additions & 43 deletions src/rules/functional-parameters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,18 @@ import { deepmerge } from "deepmerge-ts";
import type { JSONSchema4 } from "json-schema";
import type { ReadonlyDeep } from "type-fest";

import type { IgnorePatternOption } from "~/common/ignore-options";
import type {
IgnorePatternOption,
IgnorePrefixSelectorOption,
} from "~/common/ignore-options";
import {
shouldIgnorePattern,
ignorePatternOptionSchema,
ignorePrefixSelectorOptionSchema,
} from "~/common/ignore-options";
import type { ESFunction } from "~/src/util/node-types";
import type { 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 @@ -29,6 +33,7 @@ type ParameterCountOptions = "atLeastOne" | "exactlyOne";
*/
type Options = readonly [
IgnorePatternOption &
IgnorePrefixSelectorOption &
Readonly<{
allowRestParameter: boolean;
allowArgumentsKeyword: boolean;
Expand All @@ -48,39 +53,43 @@ type Options = readonly [
const schema: JSONSchema4 = [
{
type: "object",
properties: deepmerge(ignorePatternOptionSchema, {
allowRestParameter: {
type: "boolean",
},
allowArgumentsKeyword: {
type: "boolean",
},
enforceParameterCount: {
oneOf: [
{
type: "boolean",
enum: [false],
},
{
type: "string",
enum: ["atLeastOne", "exactlyOne"],
},
{
type: "object",
properties: {
count: {
type: "string",
enum: ["atLeastOne", "exactlyOne"],
},
ignoreIIFE: {
type: "boolean",
properties: deepmerge(
ignorePatternOptionSchema,
ignorePrefixSelectorOptionSchema,
{
allowRestParameter: {
type: "boolean",
},
allowArgumentsKeyword: {
type: "boolean",
},
enforceParameterCount: {
oneOf: [
{
type: "boolean",
enum: [false],
},
{
type: "string",
enum: ["atLeastOne", "exactlyOne"],
},
{
type: "object",
properties: {
count: {
type: "string",
enum: ["atLeastOne", "exactlyOne"],
},
ignoreIIFE: {
type: "boolean",
},
},
additionalProperties: false,
},
additionalProperties: false,
},
],
},
}),
],
},
}
),
additionalProperties: false,
},
];
Expand Down Expand Up @@ -255,14 +264,36 @@ 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 [optionsObject] = options;
const { ignorePrefixSelector } = optionsObject;

const baseFunctionSelectors = [
"ArrowFunctionExpression",
"FunctionDeclaration",
"FunctionExpression",
];

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

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

return {
...Object.fromEntries(
fullFunctionSelectors.map((selector) => [selector, checkFunction])
),
Identifier: checkIdentifier,
}
);
};
});
38 changes: 34 additions & 4 deletions src/util/rule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,16 +89,45 @@ export function createRule<
meta: ESLintUtils.NamedCreateRuleMeta<MessageIds>,
defaultOptions: Options,
ruleFunctionsMap: RuleFunctionsMap<any, 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: ESLintUtils.NamedCreateRuleMeta<MessageIds>,
defaultOptions: Options,
createFunction: (
context: ReadonlyDeep<TSESLint.RuleContext<MessageIds, Options>>,
options: Options
) => RuleFunctionsMap<any, MessageIds, Options>
): Rule.RuleModule {
return ESLintUtils.RuleCreator(
(ruleName) =>
`https://github.com/eslint-functional/eslint-plugin-functional/blob/v${__VERSION__}/docs/rules/${ruleName}.md`
)({
name,
meta,
defaultOptions,
create: (context, options) =>
Object.fromEntries(
create: (context, options) => {
const ruleFunctionsMap = createFunction(
context as unknown as ReadonlyDeep<
TSESLint.RuleContext<MessageIds, Options>
>,
options as unknown as Options
);
return Object.fromEntries(
Object.entries(ruleFunctionsMap).map(([nodeSelector, ruleFunction]) => [
nodeSelector,
checkNode(
Expand All @@ -109,7 +138,8 @@ export function createRule<
options as unknown as Options
),
])
),
);
},
}) as unknown as Rule.RuleModule;
}

Expand Down
31 changes: 31 additions & 0 deletions tests/rules/functional-parameters/es3/invalid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,37 @@ 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: "FunctionExpression",
line: 3,
column: 5,
},
{
messageId: "paramCountExactlyOne",
type: "FunctionExpression",
line: 8,
column: 5,
},
],
},
];

export default tests;
63 changes: 63 additions & 0 deletions tests/rules/functional-parameters/es3/valid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,69 @@ 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;
Loading

0 comments on commit af3cbcc

Please sign in to comment.