Skip to content

Commit 245886f

Browse files
fix(immutable-data): treat Object.entries({}).sort() as immediate mutation
fix #773
1 parent cfd6f8f commit 245886f

File tree

3 files changed

+93
-27
lines changed

3 files changed

+93
-27
lines changed

src/rules/immutable-data.ts

Lines changed: 69 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -7,21 +7,21 @@ import { type RuleContext } from "@typescript-eslint/utils/ts-eslint";
77
import { deepmerge } from "deepmerge-ts";
88

99
import {
10-
type IgnoreAccessorPatternOption,
11-
type IgnoreIdentifierPatternOption,
12-
type IgnoreClassesOption,
13-
shouldIgnorePattern,
14-
shouldIgnoreClasses,
1510
ignoreAccessorPatternOptionSchema,
1611
ignoreClassesOptionSchema,
1712
ignoreIdentifierPatternOptionSchema,
13+
shouldIgnoreClasses,
14+
shouldIgnorePattern,
15+
type IgnoreAccessorPatternOption,
16+
type IgnoreClassesOption,
17+
type IgnoreIdentifierPatternOption,
1818
} from "#eslint-plugin-functional/options";
1919
import { isExpected } from "#eslint-plugin-functional/utils/misc";
2020
import {
2121
createRule,
2222
getTypeOfNode,
23-
type RuleResult,
2423
type NamedCreateRuleMetaWithCategory,
24+
type RuleResult,
2525
} from "#eslint-plugin-functional/utils/rule";
2626
import {
2727
findRootIdentifier,
@@ -163,6 +163,22 @@ const objectConstructorMutatorFunctions = new Set([
163163
"setPrototypeOf",
164164
]);
165165

166+
/**
167+
* Object constructor functions that return a new array.
168+
*/
169+
const objectConstructorNewObjectReturningMethods = [
170+
"create",
171+
"entries",
172+
"fromEntries",
173+
"getOwnPropertyDescriptor",
174+
"getOwnPropertyDescriptors",
175+
"getOwnPropertyNames",
176+
"getOwnPropertySymbols",
177+
"groupBy",
178+
"keys",
179+
"values",
180+
];
181+
166182
/**
167183
* Check if the given assignment expression violates this rule.
168184
*/
@@ -330,27 +346,55 @@ function isInChainCallAndFollowsNew(
330346
node: TSESTree.MemberExpression,
331347
context: Readonly<RuleContext<keyof typeof errorMessages, Options>>,
332348
): boolean {
333-
return (
334-
// Check for: [0, 1, 2]
335-
isArrayExpression(node.object) ||
336-
// Check for: new Array()
337-
(isNewExpression(node.object) &&
338-
isArrayConstructorType(getTypeOfNode(node.object.callee, context))) ||
339-
(isCallExpression(node.object) &&
340-
isMemberExpression(node.object.callee) &&
341-
isIdentifier(node.object.callee.property) &&
342-
// Check for: Array.from(iterable)
343-
((arrayConstructorFunctions.some(
349+
// Check for: [0, 1, 2]
350+
if (isArrayExpression(node.object)) {
351+
return true;
352+
}
353+
354+
// Check for: new Array()
355+
if (
356+
isNewExpression(node.object) &&
357+
isArrayConstructorType(getTypeOfNode(node.object.callee, context))
358+
) {
359+
return true;
360+
}
361+
362+
if (
363+
isCallExpression(node.object) &&
364+
isMemberExpression(node.object.callee) &&
365+
isIdentifier(node.object.callee.property)
366+
) {
367+
// Check for: Array.from(iterable)
368+
if (
369+
arrayConstructorFunctions.some(
370+
isExpected(node.object.callee.property.name),
371+
) &&
372+
isArrayConstructorType(getTypeOfNode(node.object.callee.object, context))
373+
) {
374+
return true;
375+
}
376+
377+
// Check for: array.slice(0)
378+
if (
379+
arrayNewObjectReturningMethods.some(
380+
isExpected(node.object.callee.property.name),
381+
)
382+
) {
383+
return true;
384+
}
385+
386+
// Check for: Object.entries(object)
387+
if (
388+
objectConstructorNewObjectReturningMethods.some(
344389
isExpected(node.object.callee.property.name),
345390
) &&
346-
isArrayConstructorType(
347-
getTypeOfNode(node.object.callee.object, context),
348-
)) ||
349-
// Check for: array.slice(0)
350-
arrayNewObjectReturningMethods.some(
351-
isExpected(node.object.callee.property.name),
352-
)))
353-
);
391+
isObjectConstructorType(getTypeOfNode(node.object.callee.object, context))
392+
) {
393+
return true;
394+
}
395+
}
396+
397+
return false;
354398
}
355399

356400
/**

tests/rules/immutable-data/ts/array/invalid.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -412,7 +412,7 @@ const tests: Array<
412412
[0, 1, 2].shift();
413413
[0, 1, 2].sort();
414414
[0, 1, 2].splice(0, 1, 9);
415-
[0, 1, 2].unshift(6)
415+
[0, 1, 2].unshift(6);
416416
`,
417417
optionsSet: [[{ ignoreImmediateMutation: false }]],
418418
errors: [

tests/rules/immutable-data/ts/array/valid.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ import dedent from "dedent";
22

33
import { type rule } from "#eslint-plugin-functional/rules/immutable-data";
44
import {
5-
type ValidTestCaseSet,
65
type OptionsOf,
6+
type ValidTestCaseSet,
77
} from "#eslint-plugin-functional/tests/helpers/util";
88

99
const tests: Array<ValidTestCaseSet<OptionsOf<typeof rule>>> = [
@@ -308,6 +308,28 @@ const tests: Array<ValidTestCaseSet<OptionsOf<typeof rule>>> = [
308308
`,
309309
optionsSet: [[{ ignoreNonConstDeclarations: true }]],
310310
},
311+
{
312+
code: dedent`
313+
[0, 1, 2].copyWithin(0, 1, 2);
314+
[0, 1, 2].fill(3);
315+
[0, 1, 2].pop();
316+
[0, 1, 2].push(3);
317+
[0, 1, 2].reverse();
318+
[0, 1, 2].shift();
319+
[0, 1, 2].sort();
320+
[0, 1, 2].splice(0, 1, 9);
321+
[0, 1, 2].unshift(6);
322+
`,
323+
optionsSet: [[{ ignoreImmediateMutation: true }]],
324+
},
325+
{
326+
code: dedent`
327+
Object.entries({}).sort();
328+
Object.keys({}).sort();
329+
Object.values({}).sort();
330+
`,
331+
optionsSet: [[{ ignoreImmediateMutation: true }]],
332+
},
311333
];
312334

313335
export default tests;

0 commit comments

Comments
 (0)