Skip to content

Commit 63f36f3

Browse files
feat(prefer-immutable-types): allow overriding options based on where the type is declared
1 parent f31eb30 commit 63f36f3

File tree

7 files changed

+290
-140
lines changed

7 files changed

+290
-140
lines changed

docs/rules/prefer-immutable-types.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -475,3 +475,22 @@ It allows for the ability to ignore violations based on the identifier (name) of
475475

476476
This option takes a `RegExp` string or an array of `RegExp` strings.
477477
It allows for the ability to ignore violations based on the type (as written, with whitespace removed) of the node in question.
478+
479+
### `overrides`
480+
481+
Allows for applying overrides to the options based on where the type is defined.
482+
This can be used to override the settings for types coming from 3rd party libraries.
483+
484+
Note: Only the first matching override will be used.
485+
486+
#### `overrides[n].specifiers`
487+
488+
A specifier, or an array of specifiers to match the function type against.
489+
490+
#### `overrides[n].options`
491+
492+
The options to use when a specifiers matches.
493+
494+
#### `overrides[n].disable`
495+
496+
If true, when a specifier matches, this rule will not be applied to the matching node.

src/options/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
export * from "./ignore";
2+
export * from "./overrides";

src/options/overrides.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { type TSESTree } from "@typescript-eslint/utils";
2+
import { type RuleContext } from "@typescript-eslint/utils/ts-eslint";
3+
import typeMatchesSpecifier, {
4+
type TypeDeclarationSpecifier,
5+
} from "ts-declaration-location";
6+
7+
import { getTypeOfNode } from "../utils/rule";
8+
9+
/**
10+
* Options that can be overridden.
11+
*/
12+
export type OverridableOptions<CoreOptions> = CoreOptions & {
13+
overrides?: Array<
14+
{
15+
specifiers: TypeDeclarationSpecifier | TypeDeclarationSpecifier[];
16+
} & (
17+
| {
18+
options: CoreOptions;
19+
disable?: false;
20+
}
21+
| {
22+
disable: true;
23+
}
24+
)
25+
>;
26+
};
27+
28+
/**
29+
* Get the core options to use, taking into account overrides.
30+
*
31+
* @throws when there is a configuration error.
32+
*/
33+
export function getCoreOptions<
34+
CoreOptions extends object,
35+
Options extends readonly [Readonly<OverridableOptions<CoreOptions>>],
36+
>(
37+
node: TSESTree.Node,
38+
context: Readonly<RuleContext<string, Options>>,
39+
options: Readonly<Options>,
40+
): CoreOptions | null {
41+
const [optionsObject] = options;
42+
43+
const program = context.sourceCode.parserServices?.program ?? undefined;
44+
if (program === undefined) {
45+
return optionsObject;
46+
}
47+
48+
const type = getTypeOfNode(node, context);
49+
const found = optionsObject.overrides?.find((override) =>
50+
(Array.isArray(override.specifiers)
51+
? override.specifiers
52+
: [override.specifiers]
53+
).some((specifier) => typeMatchesSpecifier(program, specifier, type)),
54+
);
55+
56+
if (found !== undefined) {
57+
if (found.disable === true) {
58+
return null;
59+
}
60+
if (found.options === undefined) {
61+
// eslint-disable-next-line functional/no-throw-statements
62+
throw new Error("Configuration error: No options found for override.");
63+
}
64+
return found.options;
65+
}
66+
67+
return optionsObject;
68+
}

src/rules/functional-parameters.ts

Lines changed: 9 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -5,23 +5,21 @@ import {
55
} from "@typescript-eslint/utils/json-schema";
66
import { type RuleContext } from "@typescript-eslint/utils/ts-eslint";
77
import { deepmerge } from "deepmerge-ts";
8-
import typeMatchesSpecifier, {
9-
type TypeDeclarationSpecifier,
10-
} from "ts-declaration-location";
118

129
import {
10+
getCoreOptions,
1311
ignoreIdentifierPatternOptionSchema,
1412
ignorePrefixSelectorOptionSchema,
1513
shouldIgnorePattern,
1614
type IgnoreIdentifierPatternOption,
1715
type IgnorePrefixSelectorOption,
16+
type OverridableOptions,
1817
} from "#eslint-plugin-functional/options";
1918
import { typeSpecifiersSchema } from "#eslint-plugin-functional/utils/common-schemas";
2019
import { ruleNameScope } from "#eslint-plugin-functional/utils/misc";
2120
import { type ESFunction } from "#eslint-plugin-functional/utils/node-types";
2221
import {
2322
createRuleUsingFunction,
24-
getTypeOfNode,
2523
type NamedCreateRuleCustomMeta,
2624
type RuleResult,
2725
} from "#eslint-plugin-functional/utils/rule";
@@ -69,23 +67,7 @@ type CoreOptions = IgnoreIdentifierPatternOption &
6967
/**
7068
* The options this rule can take.
7169
*/
72-
type Options = [
73-
CoreOptions & {
74-
overrides?: Array<
75-
{
76-
specifiers: TypeDeclarationSpecifier | TypeDeclarationSpecifier[];
77-
} & (
78-
| {
79-
options: CoreOptions;
80-
disable?: false;
81-
}
82-
| {
83-
disable: true;
84-
}
85-
)
86-
>;
87-
},
88-
];
70+
type Options = [OverridableOptions<CoreOptions>];
8971

9072
const coreOptionsPropertiesSchema: JSONSchema4ObjectSchema["properties"] = {
9173
allowRestParameter: {
@@ -206,39 +188,6 @@ const meta: NamedCreateRuleCustomMeta<keyof typeof errorMessages, Options> = {
206188
schema,
207189
};
208190

209-
/**
210-
* Get the core options to use, taking into account overrides.
211-
*/
212-
function getCoreOptions(
213-
node: TSESTree.Node,
214-
context: Readonly<RuleContext<keyof typeof errorMessages, Options>>,
215-
options: Readonly<Options>,
216-
): CoreOptions | null {
217-
const [optionsObject] = options;
218-
219-
const program = context.sourceCode.parserServices?.program ?? undefined;
220-
if (program === undefined) {
221-
return optionsObject;
222-
}
223-
224-
const type = getTypeOfNode(node, context);
225-
const found = optionsObject.overrides?.find((override) =>
226-
(Array.isArray(override.specifiers)
227-
? override.specifiers
228-
: [override.specifiers]
229-
).some((specifier) => typeMatchesSpecifier(program, specifier, type)),
230-
);
231-
232-
if (found !== undefined) {
233-
if (found.disable === true) {
234-
return null;
235-
}
236-
return found.options;
237-
}
238-
239-
return optionsObject;
240-
}
241-
242191
/**
243192
* Get the rest parameter violations.
244193
*/
@@ -313,7 +262,11 @@ function checkFunction(
313262
context: Readonly<RuleContext<keyof typeof errorMessages, Options>>,
314263
options: Readonly<Options>,
315264
): RuleResult<keyof typeof errorMessages, Options> {
316-
const optionsToUse = getCoreOptions(node, context, options);
265+
const optionsToUse = getCoreOptions<CoreOptions, Options>(
266+
node,
267+
context,
268+
options,
269+
);
317270

318271
if (optionsToUse === null) {
319272
return {
@@ -359,7 +312,7 @@ function checkIdentifier(
359312
const optionsToUse =
360313
functionNode === null
361314
? options[0]
362-
: getCoreOptions(functionNode, context, options);
315+
: getCoreOptions<CoreOptions, Options>(functionNode, context, options);
363316

364317
if (optionsToUse === null) {
365318
return {

0 commit comments

Comments
 (0)