@blumintinc/eslint-plugin-blumint 1.20.10 → 1.20.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/index.js +1 -1
- package/lib/rules/prefer-clone-deep.js +101 -63
- package/package.json +1 -1
- package/release-manifest.json +14 -0
package/lib/index.js
CHANGED
|
@@ -43,63 +43,6 @@ exports.preferCloneDeep = (0, createRule_1.createRule)({
|
|
|
43
43
|
create(context) {
|
|
44
44
|
// Track processed nodes to avoid duplicate reports
|
|
45
45
|
const processedNodes = new Set();
|
|
46
|
-
function hasNestedSpread(node) {
|
|
47
|
-
let hasFunction = false;
|
|
48
|
-
let hasSymbol = false;
|
|
49
|
-
let hasSpread = false;
|
|
50
|
-
let hasNestedSpread = false;
|
|
51
|
-
let hasNestedObject = false;
|
|
52
|
-
function visit(node, depth = 0) {
|
|
53
|
-
if (node.type === utils_1.AST_NODE_TYPES.SpreadElement) {
|
|
54
|
-
hasSpread = true;
|
|
55
|
-
if (depth > 0) {
|
|
56
|
-
hasNestedSpread = true;
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
else if (node.type === utils_1.AST_NODE_TYPES.FunctionExpression ||
|
|
60
|
-
node.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression) {
|
|
61
|
-
hasFunction = true;
|
|
62
|
-
}
|
|
63
|
-
else if (
|
|
64
|
-
// Check for Symbol usage in computed properties
|
|
65
|
-
(node.type === utils_1.AST_NODE_TYPES.Property &&
|
|
66
|
-
node.computed &&
|
|
67
|
-
node.key.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
68
|
-
node.key.name === 'Symbol') ||
|
|
69
|
-
// Check for direct Symbol constructor calls
|
|
70
|
-
(node.type === utils_1.AST_NODE_TYPES.Property &&
|
|
71
|
-
node.computed &&
|
|
72
|
-
node.key.type === utils_1.AST_NODE_TYPES.CallExpression &&
|
|
73
|
-
node.key.callee.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
74
|
-
node.key.callee.name === 'Symbol')) {
|
|
75
|
-
hasSymbol = true;
|
|
76
|
-
}
|
|
77
|
-
// Visit child nodes without traversing parent references. Depth tracks
|
|
78
|
-
// object-nesting level: an object's OWN direct properties stay at the
|
|
79
|
-
// object's depth, and only descending into a property's value (a
|
|
80
|
-
// genuinely nested child) increments it. This keeps the top-level
|
|
81
|
-
// object's own `...spread` at depth 0 so it is never miscounted as a
|
|
82
|
-
// nested spread.
|
|
83
|
-
if (node.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
|
|
84
|
-
if (depth > 0) {
|
|
85
|
-
hasNestedObject = true;
|
|
86
|
-
}
|
|
87
|
-
node.properties.forEach((prop) => visit(prop, depth));
|
|
88
|
-
}
|
|
89
|
-
else if (node.type === utils_1.AST_NODE_TYPES.Property) {
|
|
90
|
-
visit(node.value, depth + 1);
|
|
91
|
-
}
|
|
92
|
-
else if (node.type === utils_1.AST_NODE_TYPES.SpreadElement) {
|
|
93
|
-
visit(node.argument, depth);
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
visit(node);
|
|
97
|
-
return (hasSpread &&
|
|
98
|
-
hasNestedSpread &&
|
|
99
|
-
hasNestedObject &&
|
|
100
|
-
!hasFunction &&
|
|
101
|
-
!hasSymbol);
|
|
102
|
-
}
|
|
103
46
|
const sourceCode = context.sourceCode;
|
|
104
47
|
function normalizedTextOf(node) {
|
|
105
48
|
return sourceCode.getText(node).replace(/\s+/g, '');
|
|
@@ -142,6 +85,100 @@ exports.preferCloneDeep = (0, createRule_1.createRule)({
|
|
|
142
85
|
}
|
|
143
86
|
return normalizedTextOf(unwrapped);
|
|
144
87
|
}
|
|
88
|
+
const partialDeepCopyCache = new WeakMap();
|
|
89
|
+
/**
|
|
90
|
+
* The hazard this rule describes is a hand-written PARTIAL deep copy: a
|
|
91
|
+
* literal that spreads a base AND separately spreads one of that same
|
|
92
|
+
* base's sub-paths, as in `{ ...base, a: { ...base.a, x: 1 } }`. Only the
|
|
93
|
+
* sub-objects spelled out that way get their own copy; every OTHER
|
|
94
|
+
* sub-object of `base` keeps aliasing the original, so a later mutation
|
|
95
|
+
* leaks back.
|
|
96
|
+
*
|
|
97
|
+
* Merging unrelated sources copies nothing twice and is safe, which is why
|
|
98
|
+
* shapes such as `{ ...a, nested: { ...b } }`, MUI `sx` style maps and
|
|
99
|
+
* static config maps must not be flagged (#1371). A spread of the exact
|
|
100
|
+
* same path (`{ ...a, x: { ...a } }`) is deliberately excluded as well: it
|
|
101
|
+
* is a redundant copy rather than a partial one, and this repo prefers
|
|
102
|
+
* false negatives over false positives.
|
|
103
|
+
*/
|
|
104
|
+
function isPartialDeepCopy(node) {
|
|
105
|
+
const cached = partialDeepCopyCache.get(node);
|
|
106
|
+
if (cached !== undefined) {
|
|
107
|
+
return cached;
|
|
108
|
+
}
|
|
109
|
+
let hasFunction = false;
|
|
110
|
+
let hasSymbol = false;
|
|
111
|
+
const basePaths = new Set();
|
|
112
|
+
const nestedPaths = [];
|
|
113
|
+
function visit(current, depth = 0) {
|
|
114
|
+
if (current.type === utils_1.AST_NODE_TYPES.SpreadElement) {
|
|
115
|
+
const path = accessPathOf(current.argument);
|
|
116
|
+
if (depth === 0) {
|
|
117
|
+
basePaths.add(path);
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
nestedPaths.push(path);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
else if (current.type === utils_1.AST_NODE_TYPES.FunctionExpression ||
|
|
124
|
+
current.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression) {
|
|
125
|
+
hasFunction = true;
|
|
126
|
+
}
|
|
127
|
+
else if (
|
|
128
|
+
// Check for Symbol usage in computed properties
|
|
129
|
+
(current.type === utils_1.AST_NODE_TYPES.Property &&
|
|
130
|
+
current.computed &&
|
|
131
|
+
current.key.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
132
|
+
current.key.name === 'Symbol') ||
|
|
133
|
+
// Check for direct Symbol constructor calls
|
|
134
|
+
(current.type === utils_1.AST_NODE_TYPES.Property &&
|
|
135
|
+
current.computed &&
|
|
136
|
+
current.key.type === utils_1.AST_NODE_TYPES.CallExpression &&
|
|
137
|
+
current.key.callee.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
138
|
+
current.key.callee.name === 'Symbol')) {
|
|
139
|
+
hasSymbol = true;
|
|
140
|
+
}
|
|
141
|
+
// Visit child nodes without traversing parent references. Depth tracks
|
|
142
|
+
// object-nesting level: an object's OWN direct properties stay at the
|
|
143
|
+
// object's depth, and only descending into a property's value (a
|
|
144
|
+
// genuinely nested child) increments it. This keeps the literal's own
|
|
145
|
+
// `...spread` at depth 0, where it names a base rather than a
|
|
146
|
+
// hand-copied sub-path.
|
|
147
|
+
if (current.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
|
|
148
|
+
current.properties.forEach((prop) => visit(prop, depth));
|
|
149
|
+
}
|
|
150
|
+
else if (current.type === utils_1.AST_NODE_TYPES.Property) {
|
|
151
|
+
visit(current.value, depth + 1);
|
|
152
|
+
}
|
|
153
|
+
else if (current.type === utils_1.AST_NODE_TYPES.SpreadElement) {
|
|
154
|
+
visit(current.argument, depth);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
visit(node);
|
|
158
|
+
// cloneDeep cannot faithfully reproduce functions or symbol keys, so
|
|
159
|
+
// their presence suppresses the report regardless of the copy shape.
|
|
160
|
+
const result = !hasFunction &&
|
|
161
|
+
!hasSymbol &&
|
|
162
|
+
nestedPaths.some((nested) =>
|
|
163
|
+
// The separators guard against a sibling whose name merely starts
|
|
164
|
+
// with a base's name (`abc.x` is not a sub-path of `ab`).
|
|
165
|
+
[...basePaths].some((base) => nested.startsWith(`${base}.`) || nested.startsWith(`${base}[`)));
|
|
166
|
+
partialDeepCopyCache.set(node, result);
|
|
167
|
+
return result;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* A literal that merely wraps a partial deep copy (the #365 membership
|
|
171
|
+
* shape) is still the site the report belongs on, so the predicate reaches
|
|
172
|
+
* through property values into descendant literals.
|
|
173
|
+
*/
|
|
174
|
+
function containsPartialDeepCopy(node) {
|
|
175
|
+
if (isPartialDeepCopy(node)) {
|
|
176
|
+
return true;
|
|
177
|
+
}
|
|
178
|
+
return node.properties.some((prop) => prop.type === utils_1.AST_NODE_TYPES.Property &&
|
|
179
|
+
prop.value.type === utils_1.AST_NODE_TYPES.ObjectExpression &&
|
|
180
|
+
containsPartialDeepCopy(prop.value));
|
|
181
|
+
}
|
|
145
182
|
function accessorForKey(prop) {
|
|
146
183
|
if (prop.computed) {
|
|
147
184
|
return `[${normalizedTextOf(prop.key)}]`;
|
|
@@ -270,7 +307,7 @@ exports.preferCloneDeep = (0, createRule_1.createRule)({
|
|
|
270
307
|
const visit = (current) => {
|
|
271
308
|
if (current !== node &&
|
|
272
309
|
startsWithSpread(current) &&
|
|
273
|
-
|
|
310
|
+
isPartialDeepCopy(current)) {
|
|
274
311
|
targets.push(current);
|
|
275
312
|
return;
|
|
276
313
|
}
|
|
@@ -331,7 +368,7 @@ exports.preferCloneDeep = (0, createRule_1.createRule)({
|
|
|
331
368
|
return [fixer.insertTextBeforeRange([0, 0], importText)];
|
|
332
369
|
}
|
|
333
370
|
// Find the outermost object expression that needs cloneDeep
|
|
334
|
-
function
|
|
371
|
+
function findOutermostPartialDeepCopy(node) {
|
|
335
372
|
let current = node.parent;
|
|
336
373
|
let result = node;
|
|
337
374
|
// Walk up the tree to find the outermost object expression
|
|
@@ -339,8 +376,9 @@ exports.preferCloneDeep = (0, createRule_1.createRule)({
|
|
|
339
376
|
if (current.type === utils_1.AST_NODE_TYPES.Property &&
|
|
340
377
|
current.parent &&
|
|
341
378
|
current.parent.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
|
|
342
|
-
//
|
|
343
|
-
|
|
379
|
+
// Reporting on the enclosing literal keeps sibling copies under a
|
|
380
|
+
// single error, so one fix pass rewrites them all together.
|
|
381
|
+
if (containsPartialDeepCopy(current.parent)) {
|
|
344
382
|
result = current.parent;
|
|
345
383
|
}
|
|
346
384
|
}
|
|
@@ -354,9 +392,9 @@ exports.preferCloneDeep = (0, createRule_1.createRule)({
|
|
|
354
392
|
if (processedNodes.has(node)) {
|
|
355
393
|
return;
|
|
356
394
|
}
|
|
357
|
-
if (
|
|
395
|
+
if (containsPartialDeepCopy(node)) {
|
|
358
396
|
// Find the outermost object that should use cloneDeep
|
|
359
|
-
const outermostNode =
|
|
397
|
+
const outermostNode = findOutermostPartialDeepCopy(node);
|
|
360
398
|
// Mark all nested object expressions as processed
|
|
361
399
|
const markProcessed = (n) => {
|
|
362
400
|
if (n.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
|
package/package.json
CHANGED
package/release-manifest.json
CHANGED
|
@@ -1,4 +1,18 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "1.20.11",
|
|
4
|
+
"date": "2026-07-28T20:40:15.882Z",
|
|
5
|
+
"rules": [
|
|
6
|
+
{
|
|
7
|
+
"name": "prefer-clone-deep",
|
|
8
|
+
"changeType": "fix",
|
|
9
|
+
"issues": [
|
|
10
|
+
1371
|
|
11
|
+
],
|
|
12
|
+
"summary": "flag only partial deep copies of the same base (closes #1371)"
|
|
13
|
+
}
|
|
14
|
+
]
|
|
15
|
+
},
|
|
2
16
|
{
|
|
3
17
|
"version": "1.20.10",
|
|
4
18
|
"date": "2026-07-28T19:39:55.260Z",
|