Skip to content

Commit 414f358

Browse files
feat(prefer-immutable-types): allow overriding options based on where the type is declared
fix #800
1 parent 71364aa commit 414f358

File tree

4 files changed

+230
-84
lines changed

4 files changed

+230
-84
lines changed

docs/rules/prefer-immutable-types.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,24 @@ type Options = {
244244
ReadonlyDeep?: Array<Array<{ pattern: string; replace: string }>>;
245245
Immutable?: Array<Array<{ pattern: string; replace: string }>>;
246246
};
247+
248+
overrides?: Array<{
249+
match:
250+
| {
251+
from: "file";
252+
path?: string;
253+
}
254+
| {
255+
from: "lib";
256+
}
257+
| {
258+
from: "package";
259+
package?: string;
260+
}
261+
| TypeDeclarationSpecifier[];
262+
options: Omit<Options, "overrides">;
263+
disable: boolean;
264+
}>;
247265
};
248266
```
249267

@@ -475,3 +493,22 @@ It allows for the ability to ignore violations based on the identifier (name) of
475493

476494
This option takes a `RegExp` string or an array of `RegExp` strings.
477495
It allows for the ability to ignore violations based on the type (as written, with whitespace removed) of the node in question.
496+
497+
### `overrides`
498+
499+
Allows for applying overrides to the options based on where the type is defined.
500+
This can be used to override the settings for types coming from 3rd party libraries.
501+
502+
Note: Only the first matching override will be used.
503+
504+
#### `overrides[n].specifiers`
505+
506+
A specifier, or an array of specifiers to match the function type against.
507+
508+
#### `overrides[n].options`
509+
510+
The options to use when a specifiers matches.
511+
512+
#### `overrides[n].disable`
513+
514+
If true, when a specifier matches, this rule will not be applied to the matching node.

src/rules/prefer-immutable-types.ts

Lines changed: 126 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,14 @@ import { Immutability } from "is-immutable-type";
1212

1313
import {
1414
type IgnoreClassesOption,
15+
type OverridableOptions,
16+
getCoreOptions,
1517
ignoreClassesOptionSchema,
1618
shouldIgnoreClasses,
1719
shouldIgnoreInFunction,
1820
shouldIgnorePattern,
1921
} from "#eslint-plugin-functional/options";
22+
import { typeSpecifiersSchema } from "#eslint-plugin-functional/utils/common-schemas";
2023
import { ruleNameScope } from "#eslint-plugin-functional/utils/misc";
2124
import { type ESFunctionType } from "#eslint-plugin-functional/utils/node-types";
2225
import {
@@ -55,7 +58,8 @@ export const fullName = `${ruleNameScope}/${name}`;
5558
type RawEnforcement =
5659
| Exclude<Immutability | keyof typeof Immutability, "Unknown" | "Mutable">
5760
| "None"
58-
| false;
61+
| false
62+
| undefined;
5963

6064
type Option = IgnoreClassesOption & {
6165
enforcement: RawEnforcement;
@@ -64,6 +68,20 @@ type Option = IgnoreClassesOption & {
6468
ignoreTypePattern?: string[] | string;
6569
};
6670

71+
type CoreOptions = Option & {
72+
parameters?: Partial<Option> | RawEnforcement;
73+
returnTypes?: Partial<Option> | RawEnforcement;
74+
variables?:
75+
| Partial<
76+
Option & {
77+
ignoreInFunctions?: boolean;
78+
}
79+
>
80+
| RawEnforcement;
81+
fixer?: FixerConfigRawMap;
82+
suggestions?: SuggestionConfigRawMap;
83+
};
84+
6785
type FixerConfigRaw = {
6886
pattern: string;
6987
replace: string;
@@ -93,21 +111,7 @@ type SuggestionsConfig = FixerConfig[];
93111
/**
94112
* The options this rule can take.
95113
*/
96-
type Options = [
97-
Option & {
98-
parameters?: Partial<Option> | RawEnforcement;
99-
returnTypes?: Partial<Option> | RawEnforcement;
100-
variables?:
101-
| Partial<
102-
Option & {
103-
ignoreInFunctions?: boolean;
104-
}
105-
>
106-
| RawEnforcement;
107-
fixer?: FixerConfigRawMap;
108-
suggestions?: SuggestionConfigRawMap;
109-
},
110-
];
114+
type Options = [OverridableOptions<CoreOptions>];
111115

112116
/**
113117
* The enum options for the level of enforcement.
@@ -211,51 +215,75 @@ const suggestionsSchema: JSONSchema4 = {
211215
},
212216
};
213217

218+
const coreOptionsPropertiesSchema: JSONSchema4ObjectSchema["properties"] = {
219+
parameters: optionSchema,
220+
returnTypes: optionSchema,
221+
variables: {
222+
oneOf: [
223+
{
224+
type: "object",
225+
properties: deepmerge(optionExpandedSchema, {
226+
ignoreInFunctions: {
227+
type: "boolean",
228+
},
229+
}),
230+
additionalProperties: false,
231+
},
232+
{
233+
type: ["string", "number", "boolean"],
234+
enum: enforcementEnumOptions,
235+
},
236+
],
237+
},
238+
fixer: {
239+
type: "object",
240+
properties: {
241+
ReadonlyShallow: fixerSchema,
242+
ReadonlyDeep: fixerSchema,
243+
Immutable: fixerSchema,
244+
},
245+
additionalProperties: false,
246+
},
247+
suggestions: {
248+
type: "object",
249+
properties: {
250+
ReadonlyShallow: suggestionsSchema,
251+
ReadonlyDeep: suggestionsSchema,
252+
Immutable: suggestionsSchema,
253+
},
254+
additionalProperties: false,
255+
},
256+
};
257+
214258
/**
215259
* The schema for the rule options.
216260
*/
217261
const schema: JSONSchema4[] = [
218262
{
219263
type: "object",
220-
properties: deepmerge(optionExpandedSchema, {
221-
parameters: optionSchema,
222-
returnTypes: optionSchema,
223-
variables: {
224-
oneOf: [
225-
{
226-
type: "object",
227-
properties: deepmerge(optionExpandedSchema, {
228-
ignoreInFunctions: {
229-
type: "boolean",
230-
},
231-
}),
232-
additionalProperties: false,
233-
},
234-
{
235-
type: ["string", "number", "boolean"],
236-
enum: enforcementEnumOptions,
264+
properties: deepmerge(optionExpandedSchema, coreOptionsPropertiesSchema, {
265+
overrides: {
266+
type: "array",
267+
items: {
268+
type: "object",
269+
properties: {
270+
specifiers: typeSpecifiersSchema,
271+
options: {
272+
type: "object",
273+
properties: deepmerge(
274+
optionExpandedSchema,
275+
coreOptionsPropertiesSchema,
276+
),
277+
additionalProperties: false,
278+
},
279+
disable: {
280+
type: "boolean",
281+
},
237282
},
238-
],
239-
},
240-
fixer: {
241-
type: "object",
242-
properties: {
243-
ReadonlyShallow: fixerSchema,
244-
ReadonlyDeep: fixerSchema,
245-
Immutable: fixerSchema,
246-
},
247-
additionalProperties: false,
248-
},
249-
suggestions: {
250-
type: "object",
251-
properties: {
252-
ReadonlyShallow: suggestionsSchema,
253-
ReadonlyDeep: suggestionsSchema,
254-
Immutable: suggestionsSchema,
283+
additionalProperties: false,
255284
},
256-
additionalProperties: false,
257285
},
258-
}),
286+
} satisfies JSONSchema4ObjectSchema["properties"]),
259287
additionalProperties: false,
260288
},
261289
];
@@ -398,7 +426,7 @@ function getConfiguredSuggestionFixers(
398426
* Get the level of enforcement from the raw value given.
399427
*/
400428
function parseEnforcement(rawEnforcement: RawEnforcement) {
401-
return rawEnforcement === "None"
429+
return rawEnforcement === "None" || rawEnforcement === undefined
402430
? false
403431
: typeof rawEnforcement === "string"
404432
? Immutability[rawEnforcement]
@@ -454,35 +482,32 @@ function parseSuggestionsConfigs(
454482
function getParameterTypeViolations(
455483
node: ESFunctionType,
456484
context: Readonly<RuleContext<keyof typeof errorMessages, Options>>,
457-
options: Readonly<Options>,
485+
options: Readonly<CoreOptions>,
458486
): Descriptor[] {
459-
const [optionsObject] = options;
460487
const {
461488
parameters: rawOption,
462489
fixer: rawFixerConfig,
463490
suggestions: rawSuggestionsConfigs,
464-
} = optionsObject;
491+
} = options;
465492
const {
466493
enforcement: rawEnforcement,
467494
ignoreInferredTypes,
468495
ignoreClasses,
469496
ignoreNamePattern,
470497
ignoreTypePattern,
471498
} = {
472-
ignoreInferredTypes: optionsObject.ignoreInferredTypes,
473-
ignoreClasses: optionsObject.ignoreClasses,
474-
ignoreNamePattern: optionsObject.ignoreNamePattern,
475-
ignoreTypePattern: optionsObject.ignoreTypePattern,
499+
ignoreInferredTypes: options.ignoreInferredTypes,
500+
ignoreClasses: options.ignoreClasses,
501+
ignoreNamePattern: options.ignoreNamePattern,
502+
ignoreTypePattern: options.ignoreTypePattern,
476503
...(typeof rawOption === "object"
477504
? rawOption
478505
: {
479506
enforcement: rawOption,
480507
}),
481508
};
482509

483-
const enforcement = parseEnforcement(
484-
rawEnforcement ?? optionsObject.enforcement,
485-
);
510+
const enforcement = parseEnforcement(rawEnforcement ?? options.enforcement);
486511
if (
487512
enforcement === false ||
488513
shouldIgnoreClasses(node, context, ignoreClasses)
@@ -592,31 +617,28 @@ function getParameterTypeViolations(
592617
function getReturnTypeViolations(
593618
node: ESFunctionType,
594619
context: Readonly<RuleContext<keyof typeof errorMessages, Options>>,
595-
options: Readonly<Options>,
620+
options: Readonly<CoreOptions>,
596621
): Descriptor[] {
597-
const [optionsObject] = options;
598622
const {
599623
returnTypes: rawOption,
600624
fixer: rawFixerConfig,
601625
suggestions: rawSuggestionsConfigs,
602-
} = optionsObject;
626+
} = options;
603627
const {
604628
enforcement: rawEnforcement,
605629
ignoreInferredTypes,
606630
ignoreClasses,
607631
ignoreNamePattern,
608632
ignoreTypePattern,
609633
} = {
610-
ignoreInferredTypes: optionsObject.ignoreInferredTypes,
611-
ignoreClasses: optionsObject.ignoreClasses,
612-
ignoreNamePattern: optionsObject.ignoreNamePattern,
613-
ignoreTypePattern: optionsObject.ignoreTypePattern,
634+
ignoreInferredTypes: options.ignoreInferredTypes,
635+
ignoreClasses: options.ignoreClasses,
636+
ignoreNamePattern: options.ignoreNamePattern,
637+
ignoreTypePattern: options.ignoreTypePattern,
614638
...(typeof rawOption === "object" ? rawOption : { enforcement: rawOption }),
615639
};
616640

617-
const enforcement = parseEnforcement(
618-
rawEnforcement ?? optionsObject.enforcement,
619-
);
641+
const enforcement = parseEnforcement(rawEnforcement ?? options.enforcement);
620642

621643
if (
622644
enforcement === false ||
@@ -743,10 +765,19 @@ function checkFunction(
743765
context: Readonly<RuleContext<keyof typeof errorMessages, Options>>,
744766
options: Readonly<Options>,
745767
): RuleResult<keyof typeof errorMessages, Options> {
746-
const descriptors = [
747-
...getParameterTypeViolations(node, context, options),
748-
...getReturnTypeViolations(node, context, options),
749-
];
768+
const optionsToUse = getCoreOptions<CoreOptions, Options>(
769+
node,
770+
context,
771+
options,
772+
);
773+
774+
const descriptors =
775+
optionsToUse === null
776+
? []
777+
: [
778+
...getParameterTypeViolations(node, context, optionsToUse),
779+
...getReturnTypeViolations(node, context, optionsToUse),
780+
];
750781

751782
return {
752783
context,
@@ -762,13 +793,24 @@ function checkVariable(
762793
context: Readonly<RuleContext<keyof typeof errorMessages, Options>>,
763794
options: Readonly<Options>,
764795
): RuleResult<keyof typeof errorMessages, Options> {
765-
const [optionsObject] = options;
796+
const optionsToUse = getCoreOptions<CoreOptions, Options>(
797+
node,
798+
context,
799+
options,
800+
);
801+
802+
if (optionsToUse === null) {
803+
return {
804+
context,
805+
descriptors: [],
806+
};
807+
}
766808

767809
const {
768810
variables: rawOption,
769811
fixer: rawFixerConfig,
770812
suggestions: rawSuggestionsConfigs,
771-
} = optionsObject;
813+
} = optionsToUse;
772814
const {
773815
enforcement: rawEnforcement,
774816
ignoreInferredTypes,
@@ -777,16 +819,16 @@ function checkVariable(
777819
ignoreTypePattern,
778820
ignoreInFunctions,
779821
} = {
780-
ignoreInferredTypes: optionsObject.ignoreInferredTypes,
781-
ignoreClasses: optionsObject.ignoreClasses,
782-
ignoreNamePattern: optionsObject.ignoreNamePattern,
783-
ignoreTypePattern: optionsObject.ignoreTypePattern,
822+
ignoreInferredTypes: optionsToUse.ignoreInferredTypes,
823+
ignoreClasses: optionsToUse.ignoreClasses,
824+
ignoreNamePattern: optionsToUse.ignoreNamePattern,
825+
ignoreTypePattern: optionsToUse.ignoreTypePattern,
784826
ignoreInFunctions: false,
785827
...(typeof rawOption === "object" ? rawOption : { enforcement: rawOption }),
786828
};
787829

788830
const enforcement = parseEnforcement(
789-
rawEnforcement ?? optionsObject.enforcement,
831+
rawEnforcement ?? optionsToUse.enforcement,
790832
);
791833

792834
if (

0 commit comments

Comments
 (0)