@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 CHANGED
@@ -223,7 +223,7 @@ function noFrontendImportsFromFunctionsPatterns(pattern) {
223
223
  module.exports = {
224
224
  meta: {
225
225
  name: '@blumintinc/eslint-plugin-blumint',
226
- version: '1.20.10',
226
+ version: '1.20.11',
227
227
  },
228
228
  parseOptions: {
229
229
  ecmaVersion: 2020,
@@ -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
- hasNestedSpread(current)) {
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 findOutermostObjectWithNestedSpread(node) {
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
- // If this is a property of an object expression, check if that object has nested spreads
343
- if (hasNestedSpread(current.parent)) {
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 (hasNestedSpread(node)) {
395
+ if (containsPartialDeepCopy(node)) {
358
396
  // Find the outermost object that should use cloneDeep
359
- const outermostNode = findOutermostObjectWithNestedSpread(node);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blumintinc/eslint-plugin-blumint",
3
- "version": "1.20.10",
3
+ "version": "1.20.11",
4
4
  "description": "Custom eslint rules for use within BluMint",
5
5
  "author": {
6
6
  "name": "Brodie McGuire",
@@ -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",