Skip to content

Commit c04e425

Browse files
feat(immutable-data): allows for applying overrides to the options based on the root object's type (#826)
1 parent defd713 commit c04e425

File tree

2 files changed

+179
-64
lines changed

2 files changed

+179
-64
lines changed

docs/rules/immutable-data.md

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,36 @@ type Options = {
7575
};
7676
ignoreIdentifierPattern?: string[] | string;
7777
ignoreAccessorPattern?: string[] | string;
78+
overrides?: Array<{
79+
match: Array<
80+
| {
81+
from: "file";
82+
path?: string;
83+
name?: string | string[];
84+
pattern?: RegExp | RegExp[];
85+
ignoreName?: string | string[];
86+
ignorePattern?: RegExp | RegExp[];
87+
}
88+
| {
89+
from: "lib";
90+
name?: string | string[];
91+
pattern?: RegExp | RegExp[];
92+
ignoreName?: string | string[];
93+
ignorePattern?: RegExp | RegExp[];
94+
}
95+
| {
96+
from: "package";
97+
package?: string;
98+
name?: string | string[];
99+
pattern?: RegExp | RegExp[];
100+
ignoreName?: string | string[];
101+
ignorePattern?: RegExp | RegExp[];
102+
}
103+
>;
104+
options: Omit<Options, "overrides">;
105+
inherit?: boolean;
106+
disable: boolean;
107+
}>;
78108
};
79109
```
80110

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

src/rules/immutable-data.ts

Lines changed: 124 additions & 64 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 "#/options";
1923
import { isExpected, ruleNameScope } from "#/utils/misc";
2024
import {
@@ -24,6 +28,7 @@ import {
2428
createRule,
2529
getTypeOfNode,
2630
} from "#/utils/rule";
31+
import { overridableOptionsSchema } from "#/utils/schemas";
2732
import {
2833
findRootIdentifier,
2934
isDefinedByMutableVariable,
@@ -51,62 +56,61 @@ export const name = "immutable-data";
5156
*/
5257
export const fullName: `${typeof ruleNameScope}/${typeof name}` = `${ruleNameScope}/${name}`;
5358

59+
type CoreOptions = IgnoreAccessorPatternOption &
60+
IgnoreClassesOption &
61+
IgnoreIdentifierPatternOption & {
62+
ignoreImmediateMutation: boolean;
63+
ignoreNonConstDeclarations:
64+
| boolean
65+
| {
66+
treatParametersAsConst: boolean;
67+
};
68+
};
69+
5470
/**
5571
* The options this rule can take.
5672
*/
57-
type Options = [
58-
IgnoreAccessorPatternOption &
59-
IgnoreClassesOption &
60-
IgnoreIdentifierPatternOption & {
61-
ignoreImmediateMutation: boolean;
62-
ignoreNonConstDeclarations:
63-
| boolean
64-
| {
65-
treatParametersAsConst: boolean;
66-
};
67-
},
68-
];
73+
type RawOptions = [RawOverridableOptions<CoreOptions>];
74+
type Options = OverridableOptions<CoreOptions>;
6975

70-
/**
71-
* The schema for the rule options.
72-
*/
73-
const schema: JSONSchema4[] = [
76+
const coreOptionsPropertiesSchema = deepmerge(
77+
ignoreIdentifierPatternOptionSchema,
78+
ignoreAccessorPatternOptionSchema,
79+
ignoreClassesOptionSchema,
7480
{
75-
type: "object",
76-
properties: deepmerge(
77-
ignoreIdentifierPatternOptionSchema,
78-
ignoreAccessorPatternOptionSchema,
79-
ignoreClassesOptionSchema,
80-
{
81-
ignoreImmediateMutation: {
81+
ignoreImmediateMutation: {
82+
type: "boolean",
83+
},
84+
ignoreNonConstDeclarations: {
85+
oneOf: [
86+
{
8287
type: "boolean",
8388
},
84-
ignoreNonConstDeclarations: {
85-
oneOf: [
86-
{
89+
{
90+
type: "object",
91+
properties: {
92+
treatParametersAsConst: {
8793
type: "boolean",
8894
},
89-
{
90-
type: "object",
91-
properties: {
92-
treatParametersAsConst: {
93-
type: "boolean",
94-
},
95-
},
96-
additionalProperties: false,
97-
},
98-
],
95+
},
96+
additionalProperties: false,
9997
},
100-
} satisfies JSONSchema4ObjectSchema["properties"],
101-
),
102-
additionalProperties: false,
98+
],
99+
},
103100
},
101+
) as NonNullable<JSONSchema4ObjectSchema["properties"]>;
102+
103+
/**
104+
* The schema for the rule options.
105+
*/
106+
const schema: JSONSchema4[] = [
107+
overridableOptionsSchema(coreOptionsPropertiesSchema),
104108
];
105109

106110
/**
107111
* The default options for the rule.
108112
*/
109-
const defaultOptions: Options = [
113+
const defaultOptions: RawOptions = [
110114
{
111115
ignoreClasses: false,
112116
ignoreImmediateMutation: true,
@@ -218,16 +222,30 @@ const stringConstructorNewObjectReturningMethods = ["split"];
218222
*/
219223
function checkAssignmentExpression(
220224
node: TSESTree.AssignmentExpression,
221-
context: Readonly<RuleContext<keyof typeof errorMessages, Options>>,
222-
options: Readonly<Options>,
223-
): RuleResult<keyof typeof errorMessages, Options> {
224-
const [optionsObject] = options;
225+
context: Readonly<RuleContext<keyof typeof errorMessages, RawOptions>>,
226+
rawOptions: Readonly<RawOptions>,
227+
): RuleResult<keyof typeof errorMessages, RawOptions> {
228+
const options = upgradeRawOverridableOptions(rawOptions[0]);
229+
const rootNode = findRootIdentifier(node.left) ?? node.left;
230+
const optionsToUse = getCoreOptions<CoreOptions, Options>(
231+
rootNode,
232+
context,
233+
options,
234+
);
235+
236+
if (optionsToUse === null) {
237+
return {
238+
context,
239+
descriptors: [],
240+
};
241+
}
242+
225243
const {
226244
ignoreIdentifierPattern,
227245
ignoreAccessorPattern,
228246
ignoreNonConstDeclarations,
229247
ignoreClasses,
230-
} = optionsObject;
248+
} = optionsToUse;
231249

232250
if (
233251
!isMemberExpression(node.left) ||
@@ -283,16 +301,30 @@ function checkAssignmentExpression(
283301
*/
284302
function checkUnaryExpression(
285303
node: TSESTree.UnaryExpression,
286-
context: Readonly<RuleContext<keyof typeof errorMessages, Options>>,
287-
options: Readonly<Options>,
288-
): RuleResult<keyof typeof errorMessages, Options> {
289-
const [optionsObject] = options;
304+
context: Readonly<RuleContext<keyof typeof errorMessages, RawOptions>>,
305+
rawOptions: Readonly<RawOptions>,
306+
): RuleResult<keyof typeof errorMessages, RawOptions> {
307+
const options = upgradeRawOverridableOptions(rawOptions[0]);
308+
const rootNode = findRootIdentifier(node.argument) ?? node.argument;
309+
const optionsToUse = getCoreOptions<CoreOptions, Options>(
310+
rootNode,
311+
context,
312+
options,
313+
);
314+
315+
if (optionsToUse === null) {
316+
return {
317+
context,
318+
descriptors: [],
319+
};
320+
}
321+
290322
const {
291323
ignoreIdentifierPattern,
292324
ignoreAccessorPattern,
293325
ignoreNonConstDeclarations,
294326
ignoreClasses,
295-
} = optionsObject;
327+
} = optionsToUse;
296328

297329
if (
298330
!isMemberExpression(node.argument) ||
@@ -347,16 +379,30 @@ function checkUnaryExpression(
347379
*/
348380
function checkUpdateExpression(
349381
node: TSESTree.UpdateExpression,
350-
context: Readonly<RuleContext<keyof typeof errorMessages, Options>>,
351-
options: Readonly<Options>,
352-
): RuleResult<keyof typeof errorMessages, Options> {
353-
const [optionsObject] = options;
382+
context: Readonly<RuleContext<keyof typeof errorMessages, RawOptions>>,
383+
rawOptions: Readonly<RawOptions>,
384+
): RuleResult<keyof typeof errorMessages, RawOptions> {
385+
const options = upgradeRawOverridableOptions(rawOptions[0]);
386+
const rootNode = findRootIdentifier(node.argument) ?? node.argument;
387+
const optionsToUse = getCoreOptions<CoreOptions, Options>(
388+
rootNode,
389+
context,
390+
options,
391+
);
392+
393+
if (optionsToUse === null) {
394+
return {
395+
context,
396+
descriptors: [],
397+
};
398+
}
399+
354400
const {
355401
ignoreIdentifierPattern,
356402
ignoreAccessorPattern,
357403
ignoreNonConstDeclarations,
358404
ignoreClasses,
359-
} = optionsObject;
405+
} = optionsToUse;
360406

361407
if (
362408
!isMemberExpression(node.argument) ||
@@ -414,7 +460,7 @@ function checkUpdateExpression(
414460
*/
415461
function isInChainCallAndFollowsNew(
416462
node: TSESTree.Expression,
417-
context: Readonly<RuleContext<keyof typeof errorMessages, Options>>,
463+
context: Readonly<RuleContext<keyof typeof errorMessages, RawOptions>>,
418464
): boolean {
419465
if (isMemberExpression(node)) {
420466
return isInChainCallAndFollowsNew(node.object, context);
@@ -486,16 +532,30 @@ function isInChainCallAndFollowsNew(
486532
*/
487533
function checkCallExpression(
488534
node: TSESTree.CallExpression,
489-
context: Readonly<RuleContext<keyof typeof errorMessages, Options>>,
490-
options: Readonly<Options>,
491-
): RuleResult<keyof typeof errorMessages, Options> {
492-
const [optionsObject] = options;
535+
context: Readonly<RuleContext<keyof typeof errorMessages, RawOptions>>,
536+
rawOptions: Readonly<RawOptions>,
537+
): RuleResult<keyof typeof errorMessages, RawOptions> {
538+
const options = upgradeRawOverridableOptions(rawOptions[0]);
539+
const rootNode = findRootIdentifier(node.callee) ?? node.callee;
540+
const optionsToUse = getCoreOptions<CoreOptions, Options>(
541+
rootNode,
542+
context,
543+
options,
544+
);
545+
546+
if (optionsToUse === null) {
547+
return {
548+
context,
549+
descriptors: [],
550+
};
551+
}
552+
493553
const {
494554
ignoreIdentifierPattern,
495555
ignoreAccessorPattern,
496556
ignoreNonConstDeclarations,
497557
ignoreClasses,
498-
} = optionsObject;
558+
} = optionsToUse;
499559

500560
// Not potential object mutation?
501561
if (
@@ -515,7 +575,7 @@ function checkCallExpression(
515575
};
516576
}
517577

518-
const { ignoreImmediateMutation } = optionsObject;
578+
const { ignoreImmediateMutation } = optionsToUse;
519579

520580
// Array mutation?
521581
if (
@@ -606,9 +666,9 @@ function checkCallExpression(
606666
}
607667

608668
// Create the rule.
609-
export const rule: Rule<keyof typeof errorMessages, Options> = createRule<
669+
export const rule: Rule<keyof typeof errorMessages, RawOptions> = createRule<
610670
keyof typeof errorMessages,
611-
Options
671+
RawOptions
612672
>(name, meta, defaultOptions, {
613673
AssignmentExpression: checkAssignmentExpression,
614674
UnaryExpression: checkUnaryExpression,

0 commit comments

Comments
 (0)