Skip to content

Commit 21396d5

Browse files
committedAug 5, 2024··
feat(functional-parameters): allow overriding options based on where the function type is declared (#803)
Fix #575
1 parent 4eca1bb commit 21396d5

File tree

6 files changed

+326
-91
lines changed

6 files changed

+326
-91
lines changed
 

‎README.md

+3-3
Original file line numberDiff line numberDiff line change
@@ -111,9 +111,9 @@ The [below section](#rules) gives details on which rules are enabled by each rul
111111

112112
### Currying
113113

114-
| Name | Description | 💼 | ⚠️ | 🚫 | 🔧 | 💡 | 💭 ||
115-
| :----------------------------------------------------------- | :----------------------------- | :--------------------------- | :-- | :-- | :-- | :-- | :-- | :-- |
116-
| [functional-parameters](docs/rules/functional-parameters.md) | Enforce functional parameters. | ☑️ ✅ 🔒 ![badge-currying][] | | | | | | |
114+
| Name | Description | 💼 | ⚠️ | 🚫 | 🔧 | 💡 | 💭 ||
115+
| :----------------------------------------------------------- | :----------------------------- | :--------------------------- | :-- | :---------------------------- | :-- | :-- | :-- | :-- |
116+
| [functional-parameters](docs/rules/functional-parameters.md) | Enforce functional parameters. | ☑️ ✅ 🔒 ![badge-currying][] | | ![badge-disableTypeChecked][] | | | 💭 | |
117117

118118
### No Exceptions
119119

‎docs/rules/functional-parameters.md

+63-1
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,18 @@
33

44
# Enforce functional parameters (`functional/functional-parameters`)
55

6-
💼 This rule is enabled in the following configs: `currying`, ☑️ `lite`, ✅ `recommended`, 🔒 `strict`.
6+
💼🚫 This rule is enabled in the following configs: `currying`, ☑️ `lite`, ✅ `recommended`, 🔒 `strict`. This rule is _disabled_ in the `disableTypeChecked` config.
7+
8+
💭 This rule requires [type information](https://typescript-eslint.io/linting/typed-linting).
79

810
<!-- end auto-generated rule header -->
911
<!-- markdownlint-restore -->
1012
<!-- markdownlint-restore -->
1113

1214
Disallow use of rest parameters, the `arguments` keyword and enforces that functions take at least 1 parameter.
1315

16+
Note: type information is only required when using the [overrides](#overrides) option.
17+
1418
## Rule Details
1519

1620
In functions, `arguments` is a special variable that is implicitly available.
@@ -74,6 +78,36 @@ type Options = {
7478
};
7579
ignoreIdentifierPattern?: string[] | string;
7680
ignorePrefixSelector?: string[] | string;
81+
overrides?: Array<{
82+
match: Array<
83+
| {
84+
from: "file";
85+
path?: string;
86+
name?: string | string[];
87+
pattern?: RegExp | RegExp[];
88+
ignoreName?: string | string[];
89+
ignorePattern?: RegExp | RegExp[];
90+
}
91+
| {
92+
from: "lib";
93+
name?: string | string[];
94+
pattern?: RegExp | RegExp[];
95+
ignoreName?: string | string[];
96+
ignorePattern?: RegExp | RegExp[];
97+
}
98+
| {
99+
from: "package";
100+
package?: string;
101+
name?: string | string[];
102+
pattern?: RegExp | RegExp[];
103+
ignoreName?: string | string[];
104+
ignorePattern?: RegExp | RegExp[];
105+
}
106+
>;
107+
options: Omit<Options, "overrides">;
108+
inherit?: boolean;
109+
disable: boolean;
110+
}>;
77111
};
78112
```
79113

@@ -208,3 +242,31 @@ const sum = [1, 2, 3].reduce((carry, current) => current, 0);
208242

209243
This option takes a RegExp string or an array of RegExp strings.
210244
It allows for the ability to ignore violations based on a function's name.
245+
246+
### `overrides`
247+
248+
_Using this option requires type infomation._
249+
250+
Allows for applying overrides to the options based on the function's type.
251+
This can be used to override the settings for types coming from 3rd party libraries.
252+
253+
Note: Only the first matching override will be used.
254+
255+
#### `overrides[n].specifiers`
256+
257+
A specifier, or an array of specifiers to match the function type against.
258+
259+
In the case of reference types, both the type and its generics will be recursively checked.
260+
If any of them match, the specifier will be considered a match.
261+
262+
#### `overrides[n].options`
263+
264+
The options to use when a specifiers matches.
265+
266+
#### `overrides[n].inherit`
267+
268+
Inherit the root options? Default is `true`.
269+
270+
#### `overrides[n].disable`
271+
272+
If true, when a specifier matches, this rule will not be applied to the matching node.

‎knip.jsonc

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"$schema": "node_modules/knip/schema-jsonc.json",
33
"entry": ["src/index.ts!", "tests/**/*.test.ts"],
44
"project": ["src/**/*.ts!", "tests/**/*.{js,ts}"],
5-
"ignore": ["lib/**", "tests/fixture/file.ts", "src/utils/schemas.ts"],
5+
"ignore": ["lib/**", "tests/fixture/file.ts"],
66
"ignoreDependencies": [
77
// Unknown reason for issue.
88
"@vitest/coverage-v8",

‎src/rules/functional-parameters.ts

+117-82
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,13 @@ import { deepmerge } from "deepmerge-ts";
99
import {
1010
type IgnoreIdentifierPatternOption,
1111
type IgnorePrefixSelectorOption,
12+
type OverridableOptions,
13+
type RawOverridableOptions,
14+
getCoreOptions,
1215
ignoreIdentifierPatternOptionSchema,
1316
ignorePrefixSelectorOptionSchema,
1417
shouldIgnorePattern,
18+
upgradeRawOverridableOptions,
1519
} from "#/options";
1620
import { ruleNameScope } from "#/utils/misc";
1721
import type { ESFunction } from "#/utils/node-types";
@@ -21,7 +25,9 @@ import {
2125
type RuleResult,
2226
createRuleUsingFunction,
2327
} from "#/utils/rule";
28+
import { overridableOptionsSchema } from "#/utils/schemas";
2429
import {
30+
getEnclosingFunction,
2531
isArgument,
2632
isGetter,
2733
isIIFE,
@@ -46,83 +52,82 @@ export const fullName: `${typeof ruleNameScope}/${typeof name}` = `${ruleNameSco
4652
*/
4753
type ParameterCountOptions = "atLeastOne" | "exactlyOne";
4854

55+
type CoreOptions = IgnoreIdentifierPatternOption &
56+
IgnorePrefixSelectorOption & {
57+
allowRestParameter: boolean;
58+
allowArgumentsKeyword: boolean;
59+
enforceParameterCount:
60+
| ParameterCountOptions
61+
| false
62+
| {
63+
count: ParameterCountOptions;
64+
ignoreLambdaExpression: boolean;
65+
ignoreIIFE: boolean;
66+
ignoreGettersAndSetters: boolean;
67+
};
68+
};
69+
4970
/**
5071
* The options this rule can take.
5172
*/
52-
type Options = [
53-
IgnoreIdentifierPatternOption &
54-
IgnorePrefixSelectorOption & {
55-
allowRestParameter: boolean;
56-
allowArgumentsKeyword: boolean;
57-
enforceParameterCount:
58-
| ParameterCountOptions
59-
| false
60-
| {
61-
count: ParameterCountOptions;
62-
ignoreLambdaExpression: boolean;
63-
ignoreIIFE: boolean;
64-
ignoreGettersAndSetters: boolean;
65-
};
66-
},
67-
];
73+
type RawOptions = [RawOverridableOptions<CoreOptions>];
74+
type Options = OverridableOptions<CoreOptions>;
6875

69-
/**
70-
* The schema for the rule options.
71-
*/
72-
const schema: JSONSchema4[] = [
76+
const coreOptionsPropertiesSchema = deepmerge(
77+
ignoreIdentifierPatternOptionSchema,
78+
ignorePrefixSelectorOptionSchema,
7379
{
74-
type: "object",
75-
properties: deepmerge(
76-
ignoreIdentifierPatternOptionSchema,
77-
ignorePrefixSelectorOptionSchema,
78-
{
79-
allowRestParameter: {
80+
allowRestParameter: {
81+
type: "boolean",
82+
},
83+
allowArgumentsKeyword: {
84+
type: "boolean",
85+
},
86+
enforceParameterCount: {
87+
oneOf: [
88+
{
8089
type: "boolean",
90+
enum: [false],
8191
},
82-
allowArgumentsKeyword: {
83-
type: "boolean",
92+
{
93+
type: "string",
94+
enum: ["atLeastOne", "exactlyOne"],
8495
},
85-
enforceParameterCount: {
86-
oneOf: [
87-
{
88-
type: "boolean",
89-
enum: [false],
90-
},
91-
{
96+
{
97+
type: "object",
98+
properties: {
99+
count: {
92100
type: "string",
93101
enum: ["atLeastOne", "exactlyOne"],
94102
},
95-
{
96-
type: "object",
97-
properties: {
98-
count: {
99-
type: "string",
100-
enum: ["atLeastOne", "exactlyOne"],
101-
},
102-
ignoreGettersAndSetters: {
103-
type: "boolean",
104-
},
105-
ignoreLambdaExpression: {
106-
type: "boolean",
107-
},
108-
ignoreIIFE: {
109-
type: "boolean",
110-
},
111-
},
112-
additionalProperties: false,
103+
ignoreGettersAndSetters: {
104+
type: "boolean",
105+
},
106+
ignoreLambdaExpression: {
107+
type: "boolean",
108+
},
109+
ignoreIIFE: {
110+
type: "boolean",
113111
},
114-
],
112+
},
113+
additionalProperties: false,
115114
},
116-
} satisfies JSONSchema4ObjectSchema["properties"],
117-
),
118-
additionalProperties: false,
115+
],
116+
},
119117
},
118+
) as NonNullable<JSONSchema4ObjectSchema["properties"]>;
119+
120+
/**
121+
* The schema for the rule options.
122+
*/
123+
const schema: JSONSchema4[] = [
124+
overridableOptionsSchema(coreOptionsPropertiesSchema),
120125
];
121126

122127
/**
123128
* The default options for the rule.
124129
*/
125-
const defaultOptions: Options = [
130+
const defaultOptions: RawOptions = [
126131
{
127132
allowRestParameter: false,
128133
allowArgumentsKeyword: false,
@@ -157,7 +162,7 @@ const meta: NamedCreateRuleCustomMeta<keyof typeof errorMessages> = {
157162
description: "Enforce functional parameters.",
158163
recommended: "recommended",
159164
recommendedSeverity: "error",
160-
requiresTypeChecking: false,
165+
requiresTypeChecking: true,
161166
},
162167
messages: errorMessages,
163168
schema,
@@ -167,9 +172,9 @@ const meta: NamedCreateRuleCustomMeta<keyof typeof errorMessages> = {
167172
* Get the rest parameter violations.
168173
*/
169174
function getRestParamViolations(
170-
[{ allowRestParameter }]: Readonly<Options>,
175+
{ allowRestParameter }: Readonly<CoreOptions>,
171176
node: ESFunction,
172-
): RuleResult<keyof typeof errorMessages, Options>["descriptors"] {
177+
): RuleResult<keyof typeof errorMessages, RawOptions>["descriptors"] {
173178
return !allowRestParameter &&
174179
node.params.length > 0 &&
175180
isRestElement(node.params.at(-1))
@@ -186,9 +191,9 @@ function getRestParamViolations(
186191
* Get the parameter count violations.
187192
*/
188193
function getParamCountViolations(
189-
[{ enforceParameterCount }]: Readonly<Options>,
194+
{ enforceParameterCount }: Readonly<CoreOptions>,
190195
node: ESFunction,
191-
): RuleResult<keyof typeof errorMessages, Options>["descriptors"] {
196+
): RuleResult<keyof typeof errorMessages, RawOptions>["descriptors"] {
192197
if (
193198
enforceParameterCount === false ||
194199
(node.params.length === 0 &&
@@ -234,11 +239,24 @@ function getParamCountViolations(
234239
*/
235240
function checkFunction(
236241
node: ESFunction,
237-
context: Readonly<RuleContext<keyof typeof errorMessages, Options>>,
238-
options: Readonly<Options>,
239-
): RuleResult<keyof typeof errorMessages, Options> {
240-
const [optionsObject] = options;
241-
const { ignoreIdentifierPattern } = optionsObject;
242+
context: Readonly<RuleContext<keyof typeof errorMessages, RawOptions>>,
243+
rawOptions: Readonly<RawOptions>,
244+
): RuleResult<keyof typeof errorMessages, RawOptions> {
245+
const options = upgradeRawOverridableOptions(rawOptions[0]);
246+
const optionsToUse = getCoreOptions<CoreOptions, Options>(
247+
node,
248+
context,
249+
options,
250+
);
251+
252+
if (optionsToUse === null) {
253+
return {
254+
context,
255+
descriptors: [],
256+
};
257+
}
258+
259+
const { ignoreIdentifierPattern } = optionsToUse;
242260

243261
if (shouldIgnorePattern(node, context, ignoreIdentifierPattern)) {
244262
return {
@@ -250,8 +268,8 @@ function checkFunction(
250268
return {
251269
context,
252270
descriptors: [
253-
...getRestParamViolations(options, node),
254-
...getParamCountViolations(options, node),
271+
...getRestParamViolations(optionsToUse, node),
272+
...getParamCountViolations(optionsToUse, node),
255273
],
256274
};
257275
}
@@ -261,11 +279,31 @@ function checkFunction(
261279
*/
262280
function checkIdentifier(
263281
node: TSESTree.Identifier,
264-
context: Readonly<RuleContext<keyof typeof errorMessages, Options>>,
265-
options: Readonly<Options>,
266-
): RuleResult<keyof typeof errorMessages, Options> {
267-
const [optionsObject] = options;
268-
const { ignoreIdentifierPattern } = optionsObject;
282+
context: Readonly<RuleContext<keyof typeof errorMessages, RawOptions>>,
283+
rawOptions: Readonly<RawOptions>,
284+
): RuleResult<keyof typeof errorMessages, RawOptions> {
285+
if (node.name !== "arguments") {
286+
return {
287+
context,
288+
descriptors: [],
289+
};
290+
}
291+
292+
const functionNode = getEnclosingFunction(node);
293+
const options = upgradeRawOverridableOptions(rawOptions[0]);
294+
const optionsToUse =
295+
functionNode === null
296+
? options
297+
: getCoreOptions<CoreOptions, Options>(functionNode, context, options);
298+
299+
if (optionsToUse === null) {
300+
return {
301+
context,
302+
descriptors: [],
303+
};
304+
}
305+
306+
const { ignoreIdentifierPattern } = optionsToUse;
269307

270308
if (shouldIgnorePattern(node, context, ignoreIdentifierPattern)) {
271309
return {
@@ -274,15 +312,12 @@ function checkIdentifier(
274312
};
275313
}
276314

277-
const { allowArgumentsKeyword } = optionsObject;
315+
const { allowArgumentsKeyword } = optionsToUse;
278316

279317
return {
280318
context,
281319
descriptors:
282-
!allowArgumentsKeyword &&
283-
node.name === "arguments" &&
284-
!isPropertyName(node) &&
285-
!isPropertyAccess(node)
320+
!allowArgumentsKeyword && !isPropertyName(node) && !isPropertyAccess(node)
286321
? [
287322
{
288323
node,
@@ -294,8 +329,8 @@ function checkIdentifier(
294329
}
295330

296331
// Create the rule.
297-
export const rule: Rule<keyof typeof errorMessages, Options> =
298-
createRuleUsingFunction<keyof typeof errorMessages, Options>(
332+
export const rule: Rule<keyof typeof errorMessages, RawOptions> =
333+
createRuleUsingFunction<keyof typeof errorMessages, RawOptions>(
299334
name,
300335
meta,
301336
defaultOptions,

‎src/utils/schemas.ts

+35-3
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
JSONSchema4,
33
JSONSchema4ObjectSchema,
44
} from "@typescript-eslint/utils/json-schema";
5+
import { deepmerge } from "deepmerge-ts";
56

67
const typeSpecifierPatternSchemaProperties: JSONSchema4ObjectSchema["properties"] =
78
{
@@ -63,10 +64,7 @@
6364
],
6465
};
6566

66-
export const typeSpecifiersSchema: JSONSchema4 =
67-
schemaInstanceOrInstanceArray(typeSpecifierSchema);
68-
6967
export function schemaInstanceOrInstanceArray(
7068
items: JSONSchema4,
7169
): NonNullable<JSONSchema4ObjectSchema["properties"]>[string] {
7270
return {
@@ -79,3 +77,37 @@
7977
],
8078
};
8179
}
80+
81+
export function overridableOptionsSchema(
82+
coreOptionsPropertiesSchema: NonNullable<
83+
JSONSchema4ObjectSchema["properties"]
84+
>,
85+
): JSONSchema4 {
86+
return {
87+
type: "object",
88+
properties: deepmerge(coreOptionsPropertiesSchema, {
89+
overrides: {
90+
type: "array",
91+
items: {
92+
type: "object",
93+
properties: {
94+
specifiers: schemaInstanceOrInstanceArray(typeSpecifierSchema),
95+
options: {
96+
type: "object",
97+
properties: coreOptionsPropertiesSchema,
98+
additionalProperties: false,
99+
},
100+
inherit: {
101+
type: "boolean",
102+
},
103+
disable: {
104+
type: "boolean",
105+
},
106+
},
107+
additionalProperties: false,
108+
},
109+
},
110+
} satisfies JSONSchema4ObjectSchema["properties"]),
111+
additionalProperties: false,
112+
};
113+
}

‎tests/rules/functional-parameters.test.ts

+107-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { describe, expect, it } from "vitest";
44

55
import { name, rule } from "#/rules/functional-parameters";
66

7-
import { esLatestConfig } from "../utils/configs";
7+
import { esLatestConfig, typescriptConfig } from "../utils/configs";
88

99
describe(name, () => {
1010
describe("javascript - es latest", () => {
@@ -435,4 +435,110 @@ describe(name, () => {
435435
});
436436
});
437437
});
438+
439+
describe("typescript", () => {
440+
const { valid, invalid } = createRuleTester({
441+
name,
442+
rule,
443+
configs: typescriptConfig,
444+
});
445+
446+
describe("overrides", () => {
447+
it('override value works - "allowRestParameter"', () => {
448+
const code = dedent`
449+
function foo(...bar: string[]) {
450+
console.log(bar);
451+
}
452+
`;
453+
454+
valid({
455+
code,
456+
options: {
457+
allowRestParameter: false,
458+
overrides: [
459+
{
460+
specifiers: {
461+
from: "file",
462+
},
463+
options: {
464+
allowRestParameter: true,
465+
},
466+
},
467+
],
468+
},
469+
});
470+
});
471+
472+
it('override value works - "allowArgumentsKeyword"', () => {
473+
const code = dedent`
474+
function foo(bar: string[]) {
475+
console.log(arguments);
476+
}
477+
`;
478+
479+
valid({
480+
code,
481+
options: {
482+
allowArgumentsKeyword: false,
483+
overrides: [
484+
{
485+
specifiers: {
486+
from: "file",
487+
},
488+
options: {
489+
allowArgumentsKeyword: true,
490+
},
491+
},
492+
],
493+
},
494+
});
495+
});
496+
497+
it('disbale override works - "allowRestParameter"', () => {
498+
const code = dedent`
499+
function foo(...bar: string[]) {
500+
console.log(bar);
501+
}
502+
`;
503+
504+
valid({
505+
code,
506+
options: {
507+
allowRestParameter: false,
508+
overrides: [
509+
{
510+
specifiers: {
511+
from: "file",
512+
},
513+
disable: true,
514+
},
515+
],
516+
},
517+
});
518+
});
519+
520+
it('disbale override works - "allowArgumentsKeyword"', () => {
521+
const code = dedent`
522+
function foo(bar: string[]) {
523+
console.log(arguments);
524+
}
525+
`;
526+
527+
valid({
528+
code,
529+
options: {
530+
allowArgumentsKeyword: false,
531+
overrides: [
532+
{
533+
specifiers: {
534+
from: "file",
535+
},
536+
disable: true,
537+
},
538+
],
539+
},
540+
});
541+
});
542+
});
543+
});
438544
});

0 commit comments

Comments
 (0)
Please sign in to comment.