Skip to content

Commit aeb981c

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

File tree

5 files changed

+322
-36
lines changed

5 files changed

+322
-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: 72 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,16 @@ 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+
ignoreNonConstDeclarations === true ||
258+
!ignoreNonConstDeclarations.treatParametersAsConst,
259+
)
238260
) {
239261
return {
240262
context,
@@ -283,11 +305,16 @@ function checkUnaryExpression(
283305
};
284306
}
285307

286-
if (ignoreNonConstDeclarations) {
308+
if (ignoreNonConstDeclarations !== false) {
287309
const rootIdentifier = findRootIdentifier(node.argument.object);
288310
if (
289311
rootIdentifier !== undefined &&
290-
isDefinedByMutableVariable(rootIdentifier, context)
312+
isDefinedByMutableVariable(
313+
rootIdentifier,
314+
context,
315+
ignoreNonConstDeclarations === true ||
316+
!ignoreNonConstDeclarations.treatParametersAsConst,
317+
)
291318
) {
292319
return {
293320
context,
@@ -335,11 +362,16 @@ function checkUpdateExpression(
335362
};
336363
}
337364

338-
if (ignoreNonConstDeclarations) {
365+
if (ignoreNonConstDeclarations !== false) {
339366
const rootIdentifier = findRootIdentifier(node.argument.object);
340367
if (
341368
rootIdentifier !== undefined &&
342-
isDefinedByMutableVariable(rootIdentifier, context)
369+
isDefinedByMutableVariable(
370+
rootIdentifier,
371+
context,
372+
ignoreNonConstDeclarations === true ||
373+
!ignoreNonConstDeclarations.treatParametersAsConst,
374+
)
343375
) {
344376
return {
345377
context,
@@ -473,18 +505,22 @@ function checkCallExpression(
473505
!isInChainCallAndFollowsNew(node.callee, context)) &&
474506
isArrayType(getTypeOfNode(node.callee.object, context))
475507
) {
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 {
508+
if (ignoreNonConstDeclarations === false) {
509+
return {
510+
context,
511+
descriptors: [{ node, messageId: "array" }],
512+
};
513+
}
514+
const rootIdentifier = findRootIdentifier(node.callee.object);
515+
if (
516+
rootIdentifier === undefined ||
517+
!isDefinedByMutableVariable(
518+
rootIdentifier,
519+
context,
520+
ignoreNonConstDeclarations === true ||
521+
!ignoreNonConstDeclarations.treatParametersAsConst,
522+
)
523+
) {
488524
return {
489525
context,
490526
descriptors: [{ node, messageId: "array" }],
@@ -507,18 +543,22 @@ function checkCallExpression(
507543
) &&
508544
isObjectConstructorType(getTypeOfNode(node.callee.object, context))
509545
) {
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 {
546+
if (ignoreNonConstDeclarations === false) {
547+
return {
548+
context,
549+
descriptors: [{ node, messageId: "object" }],
550+
};
551+
}
552+
const rootIdentifier = findRootIdentifier(node.callee.object);
553+
if (
554+
rootIdentifier === undefined ||
555+
!isDefinedByMutableVariable(
556+
rootIdentifier,
557+
context,
558+
ignoreNonConstDeclarations === true ||
559+
!ignoreNonConstDeclarations.treatParametersAsConst,
560+
)
561+
) {
522562
return {
523563
context,
524564
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: 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;
307+
}
308+
if (!typescript!.isVariableDeclaration(variableDeclaration)) {
284309
return true;
285310
}
286311

0 commit comments

Comments
 (0)