Skip to content

Commit 9644994

Browse files
fix(immutable-data): rework logic, it should work now
fix #692
1 parent 147d82f commit 9644994

File tree

7 files changed

+508
-129
lines changed

7 files changed

+508
-129
lines changed

docs/rules/immutable-data.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,10 @@ const sorted = [...original].sort((a, b) => a.localeCompare(b)); // This is OK w
104104
If true, this rule will ignore any mutations that happen on non-const variables.
105105
This allow for more easily using mutable data by simply using the `let` keyword instead of `const`.
106106

107+
Note: If a value is referenced by both a `let` and a `const` variable, the `let`
108+
reference can be modified while the `const` one can't. The may lead to value of
109+
the `const` variable unexpectedly changing when the `let` one is modified elsewhere.
110+
107111
### `ignoreClasses`
108112

109113
Ignore mutations inside classes.

src/rules/immutable-data.ts

Lines changed: 74 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,7 @@ import {
55
} from "@typescript-eslint/utils/json-schema";
66
import { type RuleContext } from "@typescript-eslint/utils/ts-eslint";
77
import { deepmerge } from "deepmerge-ts";
8-
import { isNodeFlagSet } from "ts-api-utils";
9-
import type ts from "typescript";
108

11-
import typescript from "#eslint-plugin-functional/conditional-imports/typescript";
129
import {
1310
type IgnoreAccessorPatternOption,
1411
type IgnoreIdentifierPatternOption,
@@ -30,7 +27,11 @@ import {
3027
type RuleResult,
3128
type NamedCreateRuleMetaWithCategory,
3229
} from "#eslint-plugin-functional/utils/rule";
33-
import { isInConstructor } from "#eslint-plugin-functional/utils/tree";
30+
import {
31+
findRootIdentifier,
32+
isDefinedByMutableVaraible,
33+
isInConstructor,
34+
} from "#eslint-plugin-functional/utils/tree";
3435
import {
3536
isArrayConstructorType,
3637
isArrayExpression,
@@ -166,39 +167,6 @@ const objectConstructorMutatorFunctions = new Set([
166167
"setPrototypeOf",
167168
]);
168169

169-
/**
170-
* Is the given identifier defined by a mutable variable (let or var)?
171-
*/
172-
function isDefinedByMutableVaraible(
173-
node: TSESTree.Identifier,
174-
context: Readonly<RuleContext<keyof typeof errorMessages, Options>>,
175-
) {
176-
if (typescript === undefined) {
177-
return true;
178-
}
179-
180-
const tsNode = context.parserServices?.esTreeNodeToTSNodeMap.get(node);
181-
const variableDeclaration =
182-
tsNode !== undefined &&
183-
"flowNode" in tsNode &&
184-
typeof tsNode.flowNode === "object" &&
185-
tsNode.flowNode !== null &&
186-
"node" in tsNode.flowNode &&
187-
typeof tsNode.flowNode.node === "object" &&
188-
tsNode.flowNode.node !== null &&
189-
typescript.isVariableDeclaration(tsNode.flowNode.node as ts.Node)
190-
? (tsNode.flowNode.node as ts.VariableDeclaration)
191-
: undefined;
192-
193-
const variableDeclarationList = variableDeclaration?.parent;
194-
195-
return (
196-
variableDeclarationList === undefined ||
197-
!typescript.isVariableDeclarationList(variableDeclarationList) ||
198-
!isNodeFlagSet(variableDeclarationList, typescript.NodeFlags.Const)
199-
);
200-
}
201-
202170
/**
203171
* Check if the given assignment expression violates this rule.
204172
*/
@@ -231,15 +199,17 @@ function checkAssignmentExpression(
231199
};
232200
}
233201

234-
if (
235-
ignoreNonConstDeclarations &&
236-
isIdentifier(node.left.object) &&
237-
isDefinedByMutableVaraible(node.left.object, context)
238-
) {
239-
return {
240-
context,
241-
descriptors: [],
242-
};
202+
if (ignoreNonConstDeclarations) {
203+
const rootIdentifier = findRootIdentifier(node.left.object);
204+
if (
205+
rootIdentifier !== undefined &&
206+
isDefinedByMutableVaraible(rootIdentifier, context)
207+
) {
208+
return {
209+
context,
210+
descriptors: [],
211+
};
212+
}
243213
}
244214

245215
return {
@@ -282,15 +252,17 @@ function checkUnaryExpression(
282252
};
283253
}
284254

285-
if (
286-
ignoreNonConstDeclarations &&
287-
isIdentifier(node.argument.object) &&
288-
isDefinedByMutableVaraible(node.argument.object, context)
289-
) {
290-
return {
291-
context,
292-
descriptors: [],
293-
};
255+
if (ignoreNonConstDeclarations) {
256+
const rootIdentifier = findRootIdentifier(node.argument.object);
257+
if (
258+
rootIdentifier !== undefined &&
259+
isDefinedByMutableVaraible(rootIdentifier, context)
260+
) {
261+
return {
262+
context,
263+
descriptors: [],
264+
};
265+
}
294266
}
295267

296268
return {
@@ -332,15 +304,17 @@ function checkUpdateExpression(
332304
};
333305
}
334306

335-
if (
336-
ignoreNonConstDeclarations &&
337-
isIdentifier(node.argument.object) &&
338-
isDefinedByMutableVaraible(node.argument.object, context)
339-
) {
340-
return {
341-
context,
342-
descriptors: [],
343-
};
307+
if (ignoreNonConstDeclarations) {
308+
const rootIdentifier = findRootIdentifier(node.argument.object);
309+
if (
310+
rootIdentifier !== undefined &&
311+
isDefinedByMutableVaraible(rootIdentifier, context)
312+
) {
313+
return {
314+
context,
315+
descriptors: [],
316+
};
317+
}
344318
}
345319

346320
return {
@@ -424,15 +398,25 @@ function checkCallExpression(
424398
arrayMutatorMethods.has(node.callee.property.name) &&
425399
(!ignoreImmediateMutation ||
426400
!isInChainCallAndFollowsNew(node.callee, context)) &&
427-
isArrayType(getTypeOfNode(node.callee.object, context)) &&
428-
(!ignoreNonConstDeclarations ||
429-
!isIdentifier(node.callee.object) ||
430-
!isDefinedByMutableVaraible(node.callee.object, context))
401+
isArrayType(getTypeOfNode(node.callee.object, context))
431402
) {
432-
return {
433-
context,
434-
descriptors: [{ node, messageId: "array" }],
435-
};
403+
if (ignoreNonConstDeclarations) {
404+
const rootIdentifier = findRootIdentifier(node.callee.object);
405+
if (
406+
rootIdentifier === undefined ||
407+
!isDefinedByMutableVaraible(rootIdentifier, context)
408+
) {
409+
return {
410+
context,
411+
descriptors: [{ node, messageId: "array" }],
412+
};
413+
}
414+
} else {
415+
return {
416+
context,
417+
descriptors: [{ node, messageId: "array" }],
418+
};
419+
}
436420
}
437421

438422
// Non-array object mutation (ex. Object.assign on identifier)?
@@ -448,15 +432,25 @@ function checkCallExpression(
448432
ignoreIdentifierPattern,
449433
ignoreAccessorPattern,
450434
) &&
451-
isObjectConstructorType(getTypeOfNode(node.callee.object, context)) &&
452-
(!ignoreNonConstDeclarations ||
453-
!isIdentifier(node.callee.object) ||
454-
!isDefinedByMutableVaraible(node.callee.object, context))
435+
isObjectConstructorType(getTypeOfNode(node.callee.object, context))
455436
) {
456-
return {
457-
context,
458-
descriptors: [{ node, messageId: "object" }],
459-
};
437+
if (ignoreNonConstDeclarations) {
438+
const rootIdentifier = findRootIdentifier(node.callee.object);
439+
if (
440+
rootIdentifier === undefined ||
441+
!isDefinedByMutableVaraible(rootIdentifier, context)
442+
) {
443+
return {
444+
context,
445+
descriptors: [{ node, messageId: "object" }],
446+
};
447+
}
448+
} else {
449+
return {
450+
context,
451+
descriptors: [{ node, messageId: "object" }],
452+
};
453+
}
460454
}
461455

462456
return {

src/utils/tree.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
import { type TSESTree } from "@typescript-eslint/utils";
2+
import { getParserServices } from "@typescript-eslint/utils/eslint-utils";
3+
import { type RuleContext } from "@typescript-eslint/utils/ts-eslint";
24

5+
import typescript from "#eslint-plugin-functional/conditional-imports/typescript";
6+
7+
import { type BaseOptions } from "./rule";
38
import {
49
isBlockStatement,
510
isCallExpression,
@@ -19,6 +24,7 @@ import {
1924
isTSTypeAnnotation,
2025
isTSTypeLiteral,
2126
isTSTypeReference,
27+
isVariableDeclaration,
2228
} from "./type-guards";
2329

2430
/**
@@ -261,3 +267,44 @@ export function getKeyOfValueInObjectExpression(
261267

262268
return objectExpressionProp.key.name;
263269
}
270+
271+
/**
272+
* Is the given identifier defined by a mutable variable (let or var)?
273+
*/
274+
export function isDefinedByMutableVaraible<
275+
Context extends RuleContext<string, BaseOptions>,
276+
>(node: TSESTree.Identifier, context: Context) {
277+
const services = getParserServices(context);
278+
const symbol = services.getSymbolAtLocation(node);
279+
const variableDeclaration = symbol?.valueDeclaration;
280+
if (
281+
variableDeclaration === undefined ||
282+
!typescript!.isVariableDeclaration(variableDeclaration)
283+
) {
284+
return true;
285+
}
286+
287+
const variableDeclarator =
288+
context.parserServices?.tsNodeToESTreeNodeMap.get(variableDeclaration);
289+
if (
290+
variableDeclarator?.parent === undefined ||
291+
!isVariableDeclaration(variableDeclarator.parent)
292+
) {
293+
return true;
294+
}
295+
296+
return variableDeclarator.parent.kind !== "const";
297+
}
298+
299+
/**
300+
* Get the root identifier of an expression.
301+
*/
302+
export function findRootIdentifier(node: TSESTree.Expression) {
303+
if (isIdentifier(node)) {
304+
return node;
305+
}
306+
if (isMemberExpression(node)) {
307+
return findRootIdentifier(node.object);
308+
}
309+
return undefined;
310+
}

0 commit comments

Comments
 (0)