Skip to content

Commit 20bfc24

Browse files
feat(immutable-data): allows for applying overrides to the options based on the root object's type
1 parent ff1550f commit 20bfc24

File tree

2 files changed

+191
-75
lines changed

2 files changed

+191
-75
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: 136 additions & 75 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,
@@ -128,18 +132,19 @@ const errorMessages = {
128132
/**
129133
* The meta data for this rule.
130134
*/
131-
const meta: NamedCreateRuleCustomMeta<keyof typeof errorMessages, Options> = {
132-
type: "suggestion",
133-
docs: {
134-
category: "No Mutations",
135-
description: "Enforce treating data as immutable.",
136-
recommended: "recommended",
137-
recommendedSeverity: "error",
138-
requiresTypeChecking: true,
139-
},
140-
messages: errorMessages,
141-
schema,
142-
};
135+
const meta: NamedCreateRuleCustomMeta<keyof typeof errorMessages, RawOptions> =
136+
{
137+
type: "suggestion",
138+
docs: {
139+
category: "No Mutations",
140+
description: "Enforce treating data as immutable.",
141+
recommended: "recommended",
142+
recommendedSeverity: "error",
143+
requiresTypeChecking: true,
144+
},
145+
messages: errorMessages,
146+
schema,
147+
};
143148

144149
/**
145150
* Array methods that mutate an array.
@@ -220,16 +225,30 @@ const stringConstructorNewObjectReturningMethods = ["split"];
220225
*/
221226
function checkAssignmentExpression(
222227
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;
228+
context: Readonly<RuleContext<keyof typeof errorMessages, RawOptions>>,
229+
rawOptions: Readonly<RawOptions>,
230+
): RuleResult<keyof typeof errorMessages, RawOptions> {
231+
const options = upgradeRawOverridableOptions(rawOptions[0]);
232+
const rootNode = findRootIdentifier(node.left) ?? node.left;
233+
const optionsToUse = getCoreOptions<CoreOptions, Options>(
234+
rootNode,
235+
context,
236+
options,
237+
);
238+
239+
if (optionsToUse === null) {
240+
return {
241+
context,
242+
descriptors: [],
243+
};
244+
}
245+
227246
const {
228247
ignoreIdentifierPattern,
229248
ignoreAccessorPattern,
230249
ignoreNonConstDeclarations,
231250
ignoreClasses,
232-
} = optionsObject;
251+
} = optionsToUse;
233252

234253
if (
235254
!isMemberExpression(node.left) ||
@@ -285,16 +304,30 @@ function checkAssignmentExpression(
285304
*/
286305
function checkUnaryExpression(
287306
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;
307+
context: Readonly<RuleContext<keyof typeof errorMessages, RawOptions>>,
308+
rawOptions: Readonly<RawOptions>,
309+
): RuleResult<keyof typeof errorMessages, RawOptions> {
310+
const options = upgradeRawOverridableOptions(rawOptions[0]);
311+
const rootNode = findRootIdentifier(node.argument) ?? node.argument;
312+
const optionsToUse = getCoreOptions<CoreOptions, Options>(
313+
rootNode,
314+
context,
315+
options,
316+
);
317+
318+
if (optionsToUse === null) {
319+
return {
320+
context,
321+
descriptors: [],
322+
};
323+
}
324+
292325
const {
293326
ignoreIdentifierPattern,
294327
ignoreAccessorPattern,
295328
ignoreNonConstDeclarations,
296329
ignoreClasses,
297-
} = optionsObject;
330+
} = optionsToUse;
298331

299332
if (
300333
!isMemberExpression(node.argument) ||
@@ -349,16 +382,30 @@ function checkUnaryExpression(
349382
*/
350383
function checkUpdateExpression(
351384
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;
385+
context: Readonly<RuleContext<keyof typeof errorMessages, RawOptions>>,
386+
rawOptions: Readonly<RawOptions>,
387+
): RuleResult<keyof typeof errorMessages, RawOptions> {
388+
const options = upgradeRawOverridableOptions(rawOptions[0]);
389+
const rootNode = findRootIdentifier(node.argument) ?? node.argument;
390+
const optionsToUse = getCoreOptions<CoreOptions, Options>(
391+
rootNode,
392+
context,
393+
options,
394+
);
395+
396+
if (optionsToUse === null) {
397+
return {
398+
context,
399+
descriptors: [],
400+
};
401+
}
402+
356403
const {
357404
ignoreIdentifierPattern,
358405
ignoreAccessorPattern,
359406
ignoreNonConstDeclarations,
360407
ignoreClasses,
361-
} = optionsObject;
408+
} = optionsToUse;
362409

363410
if (
364411
!isMemberExpression(node.argument) ||
@@ -416,7 +463,7 @@ function checkUpdateExpression(
416463
*/
417464
function isInChainCallAndFollowsNew(
418465
node: TSESTree.Expression,
419-
context: Readonly<RuleContext<keyof typeof errorMessages, Options>>,
466+
context: Readonly<RuleContext<keyof typeof errorMessages, RawOptions>>,
420467
): boolean {
421468
if (isMemberExpression(node)) {
422469
return isInChainCallAndFollowsNew(node.object, context);
@@ -488,16 +535,30 @@ function isInChainCallAndFollowsNew(
488535
*/
489536
function checkCallExpression(
490537
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;
538+
context: Readonly<RuleContext<keyof typeof errorMessages, RawOptions>>,
539+
rawOptions: Readonly<RawOptions>,
540+
): RuleResult<keyof typeof errorMessages, RawOptions> {
541+
const options = upgradeRawOverridableOptions(rawOptions[0]);
542+
const rootNode = findRootIdentifier(node.callee) ?? node.callee;
543+
const optionsToUse = getCoreOptions<CoreOptions, Options>(
544+
rootNode,
545+
context,
546+
options,
547+
);
548+
549+
if (optionsToUse === null) {
550+
return {
551+
context,
552+
descriptors: [],
553+
};
554+
}
555+
495556
const {
496557
ignoreIdentifierPattern,
497558
ignoreAccessorPattern,
498559
ignoreNonConstDeclarations,
499560
ignoreClasses,
500-
} = optionsObject;
561+
} = optionsToUse;
501562

502563
// Not potential object mutation?
503564
if (
@@ -517,7 +578,7 @@ function checkCallExpression(
517578
};
518579
}
519580

520-
const { ignoreImmediateMutation } = optionsObject;
581+
const { ignoreImmediateMutation } = optionsToUse;
521582

522583
// Array mutation?
523584
if (
@@ -608,7 +669,7 @@ function checkCallExpression(
608669
}
609670

610671
// Create the rule.
611-
export const rule = createRule<keyof typeof errorMessages, Options>(
672+
export const rule = createRule<keyof typeof errorMessages, RawOptions>(
612673
name,
613674
meta,
614675
defaultOptions,

0 commit comments

Comments
 (0)