Skip to content

Commit 491af38

Browse files
feat(immutable-data): allows for applying overrides to the options based on the root object's type
1 parent 36aef11 commit 491af38

File tree

2 files changed

+178
-63
lines changed

2 files changed

+178
-63
lines changed

docs/rules/immutable-data.md

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,36 @@ type Options = {
7070
};
7171
ignoreIdentifierPattern?: string[] | string;
7272
ignoreAccessorPattern?: string[] | string;
73+
overrides?: Array<{
74+
match: Array<
75+
| {
76+
from: "file";
77+
path?: string;
78+
name?: string | string[];
79+
pattern?: RegExp | RegExp[];
80+
ignoreName?: string | string[];
81+
ignorePattern?: RegExp | RegExp[];
82+
}
83+
| {
84+
from: "lib";
85+
name?: string | string[];
86+
pattern?: RegExp | RegExp[];
87+
ignoreName?: string | string[];
88+
ignorePattern?: RegExp | RegExp[];
89+
}
90+
| {
91+
from: "package";
92+
package?: string;
93+
name?: string | string[];
94+
pattern?: RegExp | RegExp[];
95+
ignoreName?: string | string[];
96+
ignorePattern?: RegExp | RegExp[];
97+
}
98+
>;
99+
options: Omit<Options, "overrides">;
100+
inherit?: boolean;
101+
disable: boolean;
102+
}>;
73103
};
74104
```
75105

@@ -172,3 +202,28 @@ The following wildcards can be used when specifying a pattern:
172202

173203
`**` - Match any depth (including zero). Can only be used as a full accessor.\
174204
`*` - When used as a full accessor, match the next accessor (there must be one). When used as part of an accessor, match any characters.
205+
206+
### `overrides`
207+
208+
Allows for applying overrides to the options based on the root object's type.
209+
210+
Note: Only the first matching override will be used.
211+
212+
#### `overrides[n].specifiers`
213+
214+
A specifier, or an array of specifiers to match the function type against.
215+
216+
In the case of reference types, both the type and its generics will be recursively checked.
217+
If any of them match, the specifier will be considered a match.
218+
219+
#### `overrides[n].options`
220+
221+
The options to use when a specifiers matches.
222+
223+
#### `overrides[n].inherit`
224+
225+
Inherit the root options? Default is `true`.
226+
227+
#### `overrides[n].disable`
228+
229+
If true, when a specifier matches, this rule will not be applied to the matching node.

src/rules/immutable-data.ts

Lines changed: 123 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,15 @@ import {
1010
type IgnoreAccessorPatternOption,
1111
type IgnoreClassesOption,
1212
type IgnoreIdentifierPatternOption,
13+
type OverridableOptions,
14+
type RawOverridableOptions,
15+
getCoreOptions,
1316
ignoreAccessorPatternOptionSchema,
1417
ignoreClassesOptionSchema,
1518
ignoreIdentifierPatternOptionSchema,
1619
shouldIgnoreClasses,
1720
shouldIgnorePattern,
21+
upgradeRawOverridableOptions,
1822
} from "#eslint-plugin-functional/options";
1923
import {
2024
isExpected,
@@ -26,6 +30,7 @@ import {
2630
createRule,
2731
getTypeOfNode,
2832
} from "#eslint-plugin-functional/utils/rule";
33+
import { overridableOptionsSchema } from "#eslint-plugin-functional/utils/schemas";
2934
import {
3035
findRootIdentifier,
3136
isDefinedByMutableVariable,
@@ -53,62 +58,61 @@ export const name = "immutable-data";
5358
*/
5459
export const fullName = `${ruleNameScope}/${name}`;
5560

61+
type CoreOptions = IgnoreAccessorPatternOption &
62+
IgnoreClassesOption &
63+
IgnoreIdentifierPatternOption & {
64+
ignoreImmediateMutation: boolean;
65+
ignoreNonConstDeclarations:
66+
| boolean
67+
| {
68+
treatParametersAsConst: boolean;
69+
};
70+
};
71+
5672
/**
5773
* The options this rule can take.
5874
*/
59-
type Options = [
60-
IgnoreAccessorPatternOption &
61-
IgnoreClassesOption &
62-
IgnoreIdentifierPatternOption & {
63-
ignoreImmediateMutation: boolean;
64-
ignoreNonConstDeclarations:
65-
| boolean
66-
| {
67-
treatParametersAsConst: boolean;
68-
};
69-
},
70-
];
75+
type RawOptions = [RawOverridableOptions<CoreOptions>];
76+
type Options = OverridableOptions<CoreOptions>;
7177

72-
/**
73-
* The schema for the rule options.
74-
*/
75-
const schema: JSONSchema4[] = [
78+
const coreOptionsPropertiesSchema = deepmerge(
79+
ignoreIdentifierPatternOptionSchema,
80+
ignoreAccessorPatternOptionSchema,
81+
ignoreClassesOptionSchema,
7682
{
77-
type: "object",
78-
properties: deepmerge(
79-
ignoreIdentifierPatternOptionSchema,
80-
ignoreAccessorPatternOptionSchema,
81-
ignoreClassesOptionSchema,
82-
{
83-
ignoreImmediateMutation: {
83+
ignoreImmediateMutation: {
84+
type: "boolean",
85+
},
86+
ignoreNonConstDeclarations: {
87+
oneOf: [
88+
{
8489
type: "boolean",
8590
},
86-
ignoreNonConstDeclarations: {
87-
oneOf: [
88-
{
91+
{
92+
type: "object",
93+
properties: {
94+
treatParametersAsConst: {
8995
type: "boolean",
9096
},
91-
{
92-
type: "object",
93-
properties: {
94-
treatParametersAsConst: {
95-
type: "boolean",
96-
},
97-
},
98-
additionalProperties: false,
99-
},
100-
],
97+
},
98+
additionalProperties: false,
10199
},
102-
} satisfies JSONSchema4ObjectSchema["properties"],
103-
),
104-
additionalProperties: false,
100+
],
101+
},
105102
},
103+
) as NonNullable<JSONSchema4ObjectSchema["properties"]>;
104+
105+
/**
106+
* The schema for the rule options.
107+
*/
108+
const schema: JSONSchema4[] = [
109+
overridableOptionsSchema(coreOptionsPropertiesSchema),
106110
];
107111

108112
/**
109113
* The default options for the rule.
110114
*/
111-
const defaultOptions: Options = [
115+
const defaultOptions: RawOptions = [
112116
{
113117
ignoreClasses: false,
114118
ignoreImmediateMutation: true,
@@ -220,16 +224,30 @@ const stringConstructorNewObjectReturningMethods = ["split"];
220224
*/
221225
function checkAssignmentExpression(
222226
node: TSESTree.AssignmentExpression,
223-
context: Readonly<RuleContext<keyof typeof errorMessages, Options>>,
224-
options: Readonly<Options>,
225-
): RuleResult<keyof typeof errorMessages, Options> {
226-
const [optionsObject] = options;
227+
context: Readonly<RuleContext<keyof typeof errorMessages, RawOptions>>,
228+
rawOptions: Readonly<RawOptions>,
229+
): RuleResult<keyof typeof errorMessages, RawOptions> {
230+
const options = upgradeRawOverridableOptions(rawOptions[0]);
231+
const rootNode = findRootIdentifier(node.left) ?? node.left;
232+
const optionsToUse = getCoreOptions<CoreOptions, Options>(
233+
rootNode,
234+
context,
235+
options,
236+
);
237+
238+
if (optionsToUse === null) {
239+
return {
240+
context,
241+
descriptors: [],
242+
};
243+
}
244+
227245
const {
228246
ignoreIdentifierPattern,
229247
ignoreAccessorPattern,
230248
ignoreNonConstDeclarations,
231249
ignoreClasses,
232-
} = optionsObject;
250+
} = optionsToUse;
233251

234252
if (
235253
!isMemberExpression(node.left) ||
@@ -285,16 +303,30 @@ function checkAssignmentExpression(
285303
*/
286304
function checkUnaryExpression(
287305
node: TSESTree.UnaryExpression,
288-
context: Readonly<RuleContext<keyof typeof errorMessages, Options>>,
289-
options: Readonly<Options>,
290-
): RuleResult<keyof typeof errorMessages, Options> {
291-
const [optionsObject] = options;
306+
context: Readonly<RuleContext<keyof typeof errorMessages, RawOptions>>,
307+
rawOptions: Readonly<RawOptions>,
308+
): RuleResult<keyof typeof errorMessages, RawOptions> {
309+
const options = upgradeRawOverridableOptions(rawOptions[0]);
310+
const rootNode = findRootIdentifier(node.argument) ?? node.argument;
311+
const optionsToUse = getCoreOptions<CoreOptions, Options>(
312+
rootNode,
313+
context,
314+
options,
315+
);
316+
317+
if (optionsToUse === null) {
318+
return {
319+
context,
320+
descriptors: [],
321+
};
322+
}
323+
292324
const {
293325
ignoreIdentifierPattern,
294326
ignoreAccessorPattern,
295327
ignoreNonConstDeclarations,
296328
ignoreClasses,
297-
} = optionsObject;
329+
} = optionsToUse;
298330

299331
if (
300332
!isMemberExpression(node.argument) ||
@@ -349,16 +381,30 @@ function checkUnaryExpression(
349381
*/
350382
function checkUpdateExpression(
351383
node: TSESTree.UpdateExpression,
352-
context: Readonly<RuleContext<keyof typeof errorMessages, Options>>,
353-
options: Readonly<Options>,
354-
): RuleResult<keyof typeof errorMessages, Options> {
355-
const [optionsObject] = options;
384+
context: Readonly<RuleContext<keyof typeof errorMessages, RawOptions>>,
385+
rawOptions: Readonly<RawOptions>,
386+
): RuleResult<keyof typeof errorMessages, RawOptions> {
387+
const options = upgradeRawOverridableOptions(rawOptions[0]);
388+
const rootNode = findRootIdentifier(node.argument) ?? node.argument;
389+
const optionsToUse = getCoreOptions<CoreOptions, Options>(
390+
rootNode,
391+
context,
392+
options,
393+
);
394+
395+
if (optionsToUse === null) {
396+
return {
397+
context,
398+
descriptors: [],
399+
};
400+
}
401+
356402
const {
357403
ignoreIdentifierPattern,
358404
ignoreAccessorPattern,
359405
ignoreNonConstDeclarations,
360406
ignoreClasses,
361-
} = optionsObject;
407+
} = optionsToUse;
362408

363409
if (
364410
!isMemberExpression(node.argument) ||
@@ -416,7 +462,7 @@ function checkUpdateExpression(
416462
*/
417463
function isInChainCallAndFollowsNew(
418464
node: TSESTree.Expression,
419-
context: Readonly<RuleContext<keyof typeof errorMessages, Options>>,
465+
context: Readonly<RuleContext<keyof typeof errorMessages, RawOptions>>,
420466
): boolean {
421467
if (isMemberExpression(node)) {
422468
return isInChainCallAndFollowsNew(node.object, context);
@@ -488,16 +534,30 @@ function isInChainCallAndFollowsNew(
488534
*/
489535
function checkCallExpression(
490536
node: TSESTree.CallExpression,
491-
context: Readonly<RuleContext<keyof typeof errorMessages, Options>>,
492-
options: Readonly<Options>,
493-
): RuleResult<keyof typeof errorMessages, Options> {
494-
const [optionsObject] = options;
537+
context: Readonly<RuleContext<keyof typeof errorMessages, RawOptions>>,
538+
rawOptions: Readonly<RawOptions>,
539+
): RuleResult<keyof typeof errorMessages, RawOptions> {
540+
const options = upgradeRawOverridableOptions(rawOptions[0]);
541+
const rootNode = findRootIdentifier(node.callee) ?? node.callee;
542+
const optionsToUse = getCoreOptions<CoreOptions, Options>(
543+
rootNode,
544+
context,
545+
options,
546+
);
547+
548+
if (optionsToUse === null) {
549+
return {
550+
context,
551+
descriptors: [],
552+
};
553+
}
554+
495555
const {
496556
ignoreIdentifierPattern,
497557
ignoreAccessorPattern,
498558
ignoreNonConstDeclarations,
499559
ignoreClasses,
500-
} = optionsObject;
560+
} = optionsToUse;
501561

502562
// Not potential object mutation?
503563
if (
@@ -517,7 +577,7 @@ function checkCallExpression(
517577
};
518578
}
519579

520-
const { ignoreImmediateMutation } = optionsObject;
580+
const { ignoreImmediateMutation } = optionsToUse;
521581

522582
// Array mutation?
523583
if (
@@ -608,7 +668,7 @@ function checkCallExpression(
608668
}
609669

610670
// Create the rule.
611-
export const rule = createRule<keyof typeof errorMessages, Options>(
671+
export const rule = createRule<keyof typeof errorMessages, RawOptions>(
612672
name,
613673
meta,
614674
defaultOptions,

0 commit comments

Comments
 (0)