Skip to content

Commit 059591a

Browse files
feat(immutable-data): add option for ignoreNonConstDeclarations to treatParametersAsConst (#794)
fix #724
1 parent f147c2e commit 059591a

File tree

5 files changed

+398
-36
lines changed

5 files changed

+398
-36
lines changed

docs/rules/immutable-data.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,11 @@ This rule accepts an options object of the following type:
6363
type Options = {
6464
ignoreClasses: boolean | "fieldsOnly";
6565
ignoreImmediateMutation: boolean;
66-
ignoreNonConstDeclarations: boolean;
66+
ignoreNonConstDeclarations:
67+
| boolean
68+
| {
69+
treatParametersAsConst: boolean;
70+
};
6771
ignoreIdentifierPattern?: string[] | string;
6872
ignoreAccessorPattern?: string[] | string;
6973
};
@@ -110,6 +114,10 @@ Note: If a value is referenced by both a `let` and a `const` variable, the `let`
110114
reference can be modified while the `const` one can't. The may lead to value of
111115
the `const` variable unexpectedly changing when the `let` one is modified elsewhere.
112116

117+
#### `treatParametersAsConst`
118+
119+
If true, parameters won't be ignored, while other non-const variables will be.
120+
113121
### `ignoreClasses`
114122

115123
Ignore mutations inside classes.

src/rules/immutable-data.ts

Lines changed: 107 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,11 @@ type Options = [
6161
IgnoreClassesOption &
6262
IgnoreIdentifierPatternOption & {
6363
ignoreImmediateMutation: boolean;
64-
ignoreNonConstDeclarations: boolean;
64+
ignoreNonConstDeclarations:
65+
| boolean
66+
| {
67+
treatParametersAsConst: boolean;
68+
};
6569
},
6670
];
6771

@@ -80,7 +84,20 @@ const schema: JSONSchema4[] = [
8084
type: "boolean",
8185
},
8286
ignoreNonConstDeclarations: {
83-
type: "boolean",
87+
oneOf: [
88+
{
89+
type: "boolean",
90+
},
91+
{
92+
type: "object",
93+
properties: {
94+
treatParametersAsConst: {
95+
type: "boolean",
96+
},
97+
},
98+
additionalProperties: false,
99+
},
100+
],
84101
},
85102
} satisfies JSONSchema4ObjectSchema["properties"],
86103
),
@@ -230,11 +247,23 @@ function checkAssignmentExpression(
230247
};
231248
}
232249

233-
if (ignoreNonConstDeclarations) {
250+
if (ignoreNonConstDeclarations !== false) {
234251
const rootIdentifier = findRootIdentifier(node.left.object);
235252
if (
236253
rootIdentifier !== undefined &&
237-
isDefinedByMutableVariable(rootIdentifier, context)
254+
isDefinedByMutableVariable(
255+
rootIdentifier,
256+
context,
257+
(variableNode) =>
258+
ignoreNonConstDeclarations === true ||
259+
!ignoreNonConstDeclarations.treatParametersAsConst ||
260+
shouldIgnorePattern(
261+
variableNode,
262+
context,
263+
ignoreIdentifierPattern,
264+
ignoreAccessorPattern,
265+
),
266+
)
238267
) {
239268
return {
240269
context,
@@ -283,11 +312,23 @@ function checkUnaryExpression(
283312
};
284313
}
285314

286-
if (ignoreNonConstDeclarations) {
315+
if (ignoreNonConstDeclarations !== false) {
287316
const rootIdentifier = findRootIdentifier(node.argument.object);
288317
if (
289318
rootIdentifier !== undefined &&
290-
isDefinedByMutableVariable(rootIdentifier, context)
319+
isDefinedByMutableVariable(
320+
rootIdentifier,
321+
context,
322+
(variableNode) =>
323+
ignoreNonConstDeclarations === true ||
324+
!ignoreNonConstDeclarations.treatParametersAsConst ||
325+
shouldIgnorePattern(
326+
variableNode,
327+
context,
328+
ignoreIdentifierPattern,
329+
ignoreAccessorPattern,
330+
),
331+
)
291332
) {
292333
return {
293334
context,
@@ -335,11 +376,23 @@ function checkUpdateExpression(
335376
};
336377
}
337378

338-
if (ignoreNonConstDeclarations) {
379+
if (ignoreNonConstDeclarations !== false) {
339380
const rootIdentifier = findRootIdentifier(node.argument.object);
340381
if (
341382
rootIdentifier !== undefined &&
342-
isDefinedByMutableVariable(rootIdentifier, context)
383+
isDefinedByMutableVariable(
384+
rootIdentifier,
385+
context,
386+
(variableNode) =>
387+
ignoreNonConstDeclarations === true ||
388+
!ignoreNonConstDeclarations.treatParametersAsConst ||
389+
shouldIgnorePattern(
390+
variableNode,
391+
context,
392+
ignoreIdentifierPattern,
393+
ignoreAccessorPattern,
394+
),
395+
)
343396
) {
344397
return {
345398
context,
@@ -473,18 +526,29 @@ function checkCallExpression(
473526
!isInChainCallAndFollowsNew(node.callee, context)) &&
474527
isArrayType(getTypeOfNode(node.callee.object, context))
475528
) {
476-
if (ignoreNonConstDeclarations) {
477-
const rootIdentifier = findRootIdentifier(node.callee.object);
478-
if (
479-
rootIdentifier === undefined ||
480-
!isDefinedByMutableVariable(rootIdentifier, context)
481-
) {
482-
return {
483-
context,
484-
descriptors: [{ node, messageId: "array" }],
485-
};
486-
}
487-
} else {
529+
if (ignoreNonConstDeclarations === false) {
530+
return {
531+
context,
532+
descriptors: [{ node, messageId: "array" }],
533+
};
534+
}
535+
const rootIdentifier = findRootIdentifier(node.callee.object);
536+
if (
537+
rootIdentifier === undefined ||
538+
!isDefinedByMutableVariable(
539+
rootIdentifier,
540+
context,
541+
(variableNode) =>
542+
ignoreNonConstDeclarations === true ||
543+
!ignoreNonConstDeclarations.treatParametersAsConst ||
544+
shouldIgnorePattern(
545+
variableNode,
546+
context,
547+
ignoreIdentifierPattern,
548+
ignoreAccessorPattern,
549+
),
550+
)
551+
) {
488552
return {
489553
context,
490554
descriptors: [{ node, messageId: "array" }],
@@ -507,18 +571,29 @@ function checkCallExpression(
507571
) &&
508572
isObjectConstructorType(getTypeOfNode(node.callee.object, context))
509573
) {
510-
if (ignoreNonConstDeclarations) {
511-
const rootIdentifier = findRootIdentifier(node.callee.object);
512-
if (
513-
rootIdentifier === undefined ||
514-
!isDefinedByMutableVariable(rootIdentifier, context)
515-
) {
516-
return {
517-
context,
518-
descriptors: [{ node, messageId: "object" }],
519-
};
520-
}
521-
} else {
574+
if (ignoreNonConstDeclarations === false) {
575+
return {
576+
context,
577+
descriptors: [{ node, messageId: "object" }],
578+
};
579+
}
580+
const rootIdentifier = findRootIdentifier(node.callee.object);
581+
if (
582+
rootIdentifier === undefined ||
583+
!isDefinedByMutableVariable(
584+
rootIdentifier,
585+
context,
586+
(variableNode) =>
587+
ignoreNonConstDeclarations === true ||
588+
!ignoreNonConstDeclarations.treatParametersAsConst ||
589+
shouldIgnorePattern(
590+
variableNode,
591+
context,
592+
ignoreIdentifierPattern,
593+
ignoreAccessorPattern,
594+
),
595+
)
596+
) {
522597
return {
523598
context,
524599
descriptors: [{ node, messageId: "object" }],

src/utils/tree.ts

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,18 @@ export function isArgument(node: TSESTree.Node): boolean {
213213
);
214214
}
215215

216+
/**
217+
* Is the given node a parameter?
218+
*/
219+
export function isParameter(node: TSESTree.Node): boolean {
220+
return (
221+
node.parent !== undefined &&
222+
isFunctionLike(node.parent) &&
223+
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
224+
node.parent.params.includes(node as any)
225+
);
226+
}
227+
216228
/**
217229
* Is the given node a getter function?
218230
*/
@@ -273,14 +285,27 @@ export function getKeyOfValueInObjectExpression(
273285
*/
274286
export function isDefinedByMutableVariable<
275287
Context extends RuleContext<string, BaseOptions>,
276-
>(node: TSESTree.Identifier, context: Context) {
288+
>(
289+
node: TSESTree.Identifier,
290+
context: Context,
291+
treatParametersAsMutable: (node: TSESTree.Node) => boolean,
292+
): boolean {
277293
const services = getParserServices(context);
278294
const symbol = services.getSymbolAtLocation(node);
279295
const variableDeclaration = symbol?.valueDeclaration;
296+
297+
if (variableDeclaration === undefined) {
298+
return true;
299+
}
300+
const variableDeclarationNode =
301+
services.tsNodeToESTreeNodeMap.get(variableDeclaration);
280302
if (
281-
variableDeclaration === undefined ||
282-
!typescript!.isVariableDeclaration(variableDeclaration)
303+
variableDeclarationNode !== undefined &&
304+
isParameter(variableDeclarationNode)
283305
) {
306+
return treatParametersAsMutable(variableDeclarationNode);
307+
}
308+
if (!typescript!.isVariableDeclaration(variableDeclaration)) {
284309
return true;
285310
}
286311

0 commit comments

Comments
 (0)