Skip to content

Commit d399066

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

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
@@ -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: 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 "#/options";
1923
import { isExpected, ruleNameScope } from "#/utils/misc";
2024
import {
@@ -23,6 +27,7 @@ import {
2327
createRule,
2428
getTypeOfNode,
2529
} from "#/utils/rule";
30+
import { overridableOptionsSchema } from "#/utils/schemas";
2631
import {
2732
findRootIdentifier,
2833
isDefinedByMutableVariable,
@@ -50,62 +55,61 @@ export const name = "immutable-data";
5055
*/
5156
export const fullName = `${ruleNameScope}/${name}`;
5257

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

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

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

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

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

360406
if (
361407
!isMemberExpression(node.argument) ||
@@ -413,7 +459,7 @@ function checkUpdateExpression(
413459
*/
414460
function isInChainCallAndFollowsNew(
415461
node: TSESTree.Expression,
416-
context: Readonly<RuleContext<keyof typeof errorMessages, Options>>,
462+
context: Readonly<RuleContext<keyof typeof errorMessages, RawOptions>>,
417463
): boolean {
418464
if (isMemberExpression(node)) {
419465
return isInChainCallAndFollowsNew(node.object, context);
@@ -485,16 +531,30 @@ function isInChainCallAndFollowsNew(
485531
*/
486532
function checkCallExpression(
487533
node: TSESTree.CallExpression,
488-
context: Readonly<RuleContext<keyof typeof errorMessages, Options>>,
489-
options: Readonly<Options>,
490-
): RuleResult<keyof typeof errorMessages, Options> {
491-
const [optionsObject] = options;
534+
context: Readonly<RuleContext<keyof typeof errorMessages, RawOptions>>,
535+
rawOptions: Readonly<RawOptions>,
536+
): RuleResult<keyof typeof errorMessages, RawOptions> {
537+
const options = upgradeRawOverridableOptions(rawOptions[0]);
538+
const rootNode = findRootIdentifier(node.callee) ?? node.callee;
539+
const optionsToUse = getCoreOptions<CoreOptions, Options>(
540+
rootNode,
541+
context,
542+
options,
543+
);
544+
545+
if (optionsToUse === null) {
546+
return {
547+
context,
548+
descriptors: [],
549+
};
550+
}
551+
492552
const {
493553
ignoreIdentifierPattern,
494554
ignoreAccessorPattern,
495555
ignoreNonConstDeclarations,
496556
ignoreClasses,
497-
} = optionsObject;
557+
} = optionsToUse;
498558

499559
// Not potential object mutation?
500560
if (
@@ -514,7 +574,7 @@ function checkCallExpression(
514574
};
515575
}
516576

517-
const { ignoreImmediateMutation } = optionsObject;
577+
const { ignoreImmediateMutation } = optionsToUse;
518578

519579
// Array mutation?
520580
if (
@@ -605,7 +665,7 @@ function checkCallExpression(
605665
}
606666

607667
// Create the rule.
608-
export const rule = createRule<keyof typeof errorMessages, Options>(
668+
export const rule = createRule<keyof typeof errorMessages, RawOptions>(
609669
name,
610670
meta,
611671
defaultOptions,

0 commit comments

Comments
 (0)