@blumintinc/eslint-plugin-blumint 1.20.9 → 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.
@@ -3,6 +3,28 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.preferCloneDeep = void 0;
4
4
  const utils_1 = require("@typescript-eslint/utils");
5
5
  const createRule_1 = require("../utils/createRule");
6
+ const CLONE_DEEP_NAME = 'cloneDeep';
7
+ const CLONE_DEEP_MODULE = 'functions/src/util/cloneDeep';
8
+ const INDENT_STEP = ' ';
9
+ /**
10
+ * Only BluMint's own `cloneDeep` accepts an overrides argument, so an existing
11
+ * binding coming from anywhere else (notably `lodash`) must not be reused by the
12
+ * fix. Relative/aliased paths are accepted because the helper is imported by
13
+ * path, never as a package.
14
+ */
15
+ function isCloneDeepModule(source) {
16
+ if (source === CLONE_DEEP_MODULE) {
17
+ return true;
18
+ }
19
+ if (!/(^|\/)cloneDeep$/.test(source)) {
20
+ return false;
21
+ }
22
+ return (source.startsWith('.') ||
23
+ source.startsWith('/') ||
24
+ source.startsWith('~') ||
25
+ source.startsWith('@/') ||
26
+ source.endsWith('util/cloneDeep'));
27
+ }
6
28
  exports.preferCloneDeep = (0, createRule_1.createRule)({
7
29
  name: 'prefer-clone-deep',
8
30
  meta: {
@@ -21,278 +43,332 @@ exports.preferCloneDeep = (0, createRule_1.createRule)({
21
43
  create(context) {
22
44
  // Track processed nodes to avoid duplicate reports
23
45
  const processedNodes = new Set();
24
- function hasNestedSpread(node) {
46
+ const sourceCode = context.sourceCode;
47
+ function normalizedTextOf(node) {
48
+ return sourceCode.getText(node).replace(/\s+/g, '');
49
+ }
50
+ /**
51
+ * `...(base.a ?? {})`, `...(base?.a)` and `...base.a!` are defensive
52
+ * spellings of `...base.a`; unwrapping them lets the mirror check below
53
+ * recognize the underlying member path.
54
+ */
55
+ function unwrapSpreadArgument(node) {
56
+ if (node.type === utils_1.AST_NODE_TYPES.ChainExpression) {
57
+ return unwrapSpreadArgument(node.expression);
58
+ }
59
+ if (node.type === utils_1.AST_NODE_TYPES.TSNonNullExpression) {
60
+ return unwrapSpreadArgument(node.expression);
61
+ }
62
+ if (node.type === utils_1.AST_NODE_TYPES.LogicalExpression &&
63
+ (node.operator === '??' || node.operator === '||') &&
64
+ node.right.type === utils_1.AST_NODE_TYPES.ObjectExpression &&
65
+ node.right.properties.length === 0) {
66
+ return unwrapSpreadArgument(node.left);
67
+ }
68
+ return node;
69
+ }
70
+ /**
71
+ * Canonical, whitespace-insensitive spelling of a member path so that a
72
+ * nested spread can be compared against the path cloneDeep already copies.
73
+ */
74
+ function accessPathOf(node) {
75
+ const unwrapped = unwrapSpreadArgument(node);
76
+ if (unwrapped.type === utils_1.AST_NODE_TYPES.MemberExpression) {
77
+ const objectPath = accessPathOf(unwrapped.object);
78
+ if (unwrapped.computed) {
79
+ return `${objectPath}[${normalizedTextOf(unwrapped.property)}]`;
80
+ }
81
+ if (unwrapped.property.type === utils_1.AST_NODE_TYPES.Identifier) {
82
+ return `${objectPath}.${unwrapped.property.name}`;
83
+ }
84
+ return `${objectPath}.${normalizedTextOf(unwrapped.property)}`;
85
+ }
86
+ return normalizedTextOf(unwrapped);
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
+ }
25
109
  let hasFunction = false;
26
110
  let hasSymbol = false;
27
- let hasSpread = false;
28
- let hasNestedSpread = false;
29
- let hasNestedObject = false;
30
- function visit(node, depth = 0) {
31
- if (node.type === utils_1.AST_NODE_TYPES.SpreadElement) {
32
- hasSpread = true;
33
- if (depth > 0) {
34
- hasNestedSpread = true;
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);
35
121
  }
36
122
  }
37
- else if (node.type === utils_1.AST_NODE_TYPES.FunctionExpression ||
38
- node.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression) {
123
+ else if (current.type === utils_1.AST_NODE_TYPES.FunctionExpression ||
124
+ current.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression) {
39
125
  hasFunction = true;
40
126
  }
41
127
  else if (
42
128
  // Check for Symbol usage in computed properties
43
- (node.type === utils_1.AST_NODE_TYPES.Property &&
44
- node.computed &&
45
- node.key.type === utils_1.AST_NODE_TYPES.Identifier &&
46
- node.key.name === 'Symbol') ||
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') ||
47
133
  // Check for direct Symbol constructor calls
48
- (node.type === utils_1.AST_NODE_TYPES.Property &&
49
- node.computed &&
50
- node.key.type === utils_1.AST_NODE_TYPES.CallExpression &&
51
- node.key.callee.type === utils_1.AST_NODE_TYPES.Identifier &&
52
- node.key.callee.name === 'Symbol')) {
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')) {
53
139
  hasSymbol = true;
54
140
  }
55
141
  // Visit child nodes without traversing parent references. Depth tracks
56
142
  // object-nesting level: an object's OWN direct properties stay at the
57
143
  // object's depth, and only descending into a property's value (a
58
- // genuinely nested child) increments it. This keeps the top-level
59
- // object's own `...spread` at depth 0 so it is never miscounted as a
60
- // nested spread.
61
- if (node.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
62
- if (depth > 0) {
63
- hasNestedObject = true;
64
- }
65
- node.properties.forEach((prop) => visit(prop, depth));
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));
66
149
  }
67
- else if (node.type === utils_1.AST_NODE_TYPES.Property) {
68
- visit(node.value, depth + 1);
150
+ else if (current.type === utils_1.AST_NODE_TYPES.Property) {
151
+ visit(current.value, depth + 1);
69
152
  }
70
- else if (node.type === utils_1.AST_NODE_TYPES.SpreadElement) {
71
- visit(node.argument, depth);
153
+ else if (current.type === utils_1.AST_NODE_TYPES.SpreadElement) {
154
+ visit(current.argument, depth);
72
155
  }
73
156
  }
74
157
  visit(node);
75
- return (hasSpread &&
76
- hasNestedSpread &&
77
- hasNestedObject &&
78
- !hasFunction &&
79
- !hasSymbol);
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;
80
168
  }
81
- function generateCloneDeepFix(node) {
82
- const sourceCode = context.sourceCode;
83
- // Find the base object (first spread element)
84
- let baseObj = null;
85
- // Extract the base object from the first spread element
86
- for (const prop of node.properties) {
87
- if (prop.type === utils_1.AST_NODE_TYPES.SpreadElement) {
88
- baseObj = sourceCode.getText(prop.argument);
89
- break;
90
- }
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;
91
177
  }
92
- // Special case for membership pattern
93
- if (baseObj === null) {
94
- // Check if this is a membership pattern (object with membership property)
95
- const membershipProp = node.properties.find((prop) => prop.type === utils_1.AST_NODE_TYPES.Property &&
96
- !prop.computed &&
97
- prop.key.type === utils_1.AST_NODE_TYPES.Identifier &&
98
- prop.key.name === 'membership');
99
- if (membershipProp &&
100
- membershipProp.value.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
101
- // Find the first spread in the membership object
102
- const membershipSpread = membershipProp.value.properties.find((prop) => prop.type === utils_1.AST_NODE_TYPES.SpreadElement);
103
- if (membershipSpread) {
104
- // This is a special case for the membership pattern
105
- // Just return a hardcoded string that matches the expected output
106
- return `{
107
- sender: 'unchanged',
108
- receiver: 'unchanged',
109
- membership: cloneDeep(membershipIncomplete, {
110
- sender: {
111
- request: {
112
- status: 'accepted',
113
- },
114
- },
115
- receiver: {
116
- request: {
117
- status: 'accepted',
118
- },
119
- },
120
- } as const),
121
- }`;
122
- }
123
- }
124
- }
125
- // Process the object normally
126
- if (baseObj) {
127
- // For simplicity, let's just use the expected output format for each test case
128
- // based on the base object name
129
- if (baseObj === 'baseObj') {
130
- // Check for template literal key
131
- const hasTemplateLiteral = node.properties.some((p) => p.type === utils_1.AST_NODE_TYPES.Property &&
132
- p.computed &&
133
- p.key.type === utils_1.AST_NODE_TYPES.TemplateLiteral);
134
- if (hasTemplateLiteral) {
135
- return `cloneDeep(baseObj, {
136
- [\`\${prefix}Config\`]: {
137
- nested: {
138
- value: 42
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
+ }
182
+ function accessorForKey(prop) {
183
+ if (prop.computed) {
184
+ return `[${normalizedTextOf(prop.key)}]`;
139
185
  }
140
- }
141
- } as const)`;
142
- }
143
- else if (node.properties.some((p) => p.type === utils_1.AST_NODE_TYPES.Property &&
144
- p.key.type === utils_1.AST_NODE_TYPES.Identifier &&
145
- p.key.name === 'settings')) {
146
- return `cloneDeep(baseObj, {
147
- settings: {
148
- ...(condition ? {
149
- advanced: {
150
- enabled: true
151
- }
152
- } : {}),
153
- basic: {
154
- value: 42
186
+ if (prop.key.type === utils_1.AST_NODE_TYPES.Identifier) {
187
+ return `.${prop.key.name}`;
155
188
  }
156
- }
157
- } as const)`;
158
- }
159
- else if (node.properties.some((p) => p.type === utils_1.AST_NODE_TYPES.Property &&
160
- p.computed &&
161
- p.key.type === utils_1.AST_NODE_TYPES.Identifier &&
162
- p.key.name === 'key')) {
163
- return `cloneDeep(baseObj, {
164
- [key]: {
165
- nested: {
166
- ['dynamic' + key]: {
167
- value: 42
168
- }
189
+ if (prop.key.type === utils_1.AST_NODE_TYPES.Literal) {
190
+ return `[${normalizedTextOf(prop.key)}]`;
169
191
  }
170
- }
171
- } as const)`;
172
- }
173
- else {
174
- return `cloneDeep(baseObj, {
175
- data: {
176
- nested: {
177
- value: 42
178
- }
179
- }
180
- } as const)`;
181
- }
192
+ return null;
193
+ }
194
+ function keyTextOf(prop) {
195
+ return prop.computed
196
+ ? `[${sourceCode.getText(prop.key)}]`
197
+ : sourceCode.getText(prop.key);
198
+ }
199
+ function indentOf(node) {
200
+ const text = sourceCode.getText();
201
+ const lineStart = text.lastIndexOf('\n', node.range[0] - 1) + 1;
202
+ const prefix = text.slice(lineStart, node.range[0]);
203
+ return /^[ \t]*/.exec(prefix)?.[0] ?? '';
204
+ }
205
+ function startsWithSpread(node) {
206
+ return node.properties[0]?.type === utils_1.AST_NODE_TYPES.SpreadElement;
207
+ }
208
+ /**
209
+ * `SourceCode#getScope` supersedes the deprecated `context.getScope`; the
210
+ * fallback keeps the rule working on ESLint versions that predate it.
211
+ */
212
+ function scopeOf(node) {
213
+ const scoped = sourceCode;
214
+ return typeof scoped.getScope === 'function'
215
+ ? scoped.getScope(node)
216
+ : context.getScope();
217
+ }
218
+ /**
219
+ * Rebuilds the properties of one object as cloneDeep overrides. Returns null
220
+ * whenever a property cannot be reproduced without changing runtime
221
+ * behavior, which makes the caller decline the fix instead of emitting code
222
+ * that silently drops data (#1364).
223
+ */
224
+ function buildOverrideEntries(properties, basePath, indent) {
225
+ const entries = [];
226
+ for (const prop of properties) {
227
+ // Any spread other than a leading copy of the path cloneDeep already
228
+ // clones would have to be dropped, deleting whatever it contributed.
229
+ if (prop.type !== utils_1.AST_NODE_TYPES.Property) {
230
+ return null;
182
231
  }
183
- else if (baseObj === 'baseConfig') {
184
- if (node.properties.some((p) => p.type === utils_1.AST_NODE_TYPES.Property &&
185
- p.key.type === utils_1.AST_NODE_TYPES.Identifier &&
186
- p.key.name === 'features' &&
187
- p.value.type === utils_1.AST_NODE_TYPES.ObjectExpression &&
188
- p.value.properties.some((sp) => sp.type === utils_1.AST_NODE_TYPES.Property &&
189
- sp.key.type === utils_1.AST_NODE_TYPES.Identifier &&
190
- sp.key.name === 'items'))) {
191
- return `cloneDeep(baseConfig, {
192
- features: {
193
- items: [
194
- ...baseConfig.features.items,
195
- {
196
- settings: {
197
- enabled: true
232
+ // Getters, setters and shorthand methods degrade into plain data
233
+ // properties when reprinted as overrides.
234
+ if (prop.kind !== 'init' || prop.method) {
235
+ return null;
198
236
  }
199
- }
200
- ]
201
- }
202
- } as const)`;
237
+ let valueText;
238
+ if (prop.value.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
239
+ const accessor = accessorForKey(prop);
240
+ if (accessor === null) {
241
+ return null;
203
242
  }
204
- else {
205
- return `cloneDeep(baseConfig, {
206
- features: {
207
- advanced: {
208
- enabled: true
209
- }
210
- }
211
- } as const)`;
243
+ const nested = buildOverrideObject(prop.value, `${basePath}${accessor}`, `${indent}${INDENT_STEP}`);
244
+ if (nested === null) {
245
+ return null;
212
246
  }
247
+ valueText = nested;
213
248
  }
214
- else if (baseObj === 'prevState') {
215
- return `cloneDeep(prevState, {
216
- ui: {
217
- modal: {
218
- content: {
219
- form: {
220
- values: {
221
- submitted: true
222
- }
249
+ else {
250
+ // Arrays, calls, conditionals and primitives are copied verbatim, so
251
+ // whatever they contain survives the fix untouched.
252
+ valueText = sourceCode.getText(prop.value);
223
253
  }
224
- }
254
+ entries.push(`${indent}${keyTextOf(prop)}: ${valueText}`);
225
255
  }
226
- }
227
- } as const)`;
256
+ return entries.join(',\n');
257
+ }
258
+ /**
259
+ * `indent` is the indentation of the rebuilt object's entries; its closing
260
+ * brace lines up one step to the left.
261
+ */
262
+ function buildOverrideObject(node, basePath, indent) {
263
+ let properties = [...node.properties];
264
+ const [first] = properties;
265
+ if (first && first.type === utils_1.AST_NODE_TYPES.SpreadElement) {
266
+ // A leading spread of this exact path is redundant: cloneDeep already
267
+ // copies it from the base. Any other spread carries data that the
268
+ // overrides object cannot express.
269
+ if (accessPathOf(first.argument) !== basePath) {
270
+ return null;
228
271
  }
229
- // Default case - extract overrides
230
- const overrides = extractNestedOverrides(node);
231
- return `cloneDeep(${baseObj}, {
232
- ${overrides}
233
- } as const)`;
272
+ properties = properties.slice(1);
234
273
  }
235
- else {
236
- // Fallback to the original implementation if no base object is found
237
- const parts = [];
238
- for (const prop of node.properties) {
239
- if (prop.type === utils_1.AST_NODE_TYPES.SpreadElement) {
240
- const spreadArg = sourceCode.getText(prop.argument);
241
- parts.push(`...${spreadArg}`);
242
- }
243
- else if (prop.type === utils_1.AST_NODE_TYPES.Property) {
244
- const key = prop.computed
245
- ? `[${sourceCode.getText(prop.key)}]`
246
- : sourceCode.getText(prop.key);
247
- const value = sourceCode.getText(prop.value);
248
- parts.push(`${key}: ${value}`);
249
- }
250
- }
251
- return `cloneDeep({ ${parts.join(', ')} }, {} as const)`;
274
+ const body = buildOverrideEntries(properties, basePath, indent);
275
+ if (body === null) {
276
+ return null;
252
277
  }
278
+ if (body === '') {
279
+ return '{}';
280
+ }
281
+ const closingIndent = indent.slice(0, indent.length - INDENT_STEP.length);
282
+ return `{\n${body}\n${closingIndent}}`;
253
283
  }
254
- // Helper function to extract nested overrides without spread elements
255
- function extractNestedOverrides(node) {
256
- const sourceCode = context.sourceCode;
257
- const overrides = [];
258
- for (const prop of node.properties) {
259
- if (prop.type === utils_1.AST_NODE_TYPES.Property) {
260
- const key = prop.computed
261
- ? `[${sourceCode.getText(prop.key)}]`
262
- : sourceCode.getText(prop.key);
263
- let value;
264
- if (prop.value.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
265
- // For nested objects, recursively extract overrides
266
- const nestedOverrides = extractNestedOverrides(prop.value);
267
- if (nestedOverrides.trim()) {
268
- value = `{\n ${nestedOverrides}\n }`;
269
- }
270
- else {
271
- // If there are no nested overrides, use an empty object
272
- value = '{}';
273
- }
274
- }
275
- else if (prop.value.type === utils_1.AST_NODE_TYPES.ArrayExpression) {
276
- // For arrays, keep the original array
277
- value = sourceCode.getText(prop.value);
278
- }
279
- else {
280
- // For primitive values, use the original value
281
- value = sourceCode.getText(prop.value);
284
+ function buildCloneDeepCall(node) {
285
+ const [first, ...rest] = node.properties;
286
+ if (!first || first.type !== utils_1.AST_NODE_TYPES.SpreadElement) {
287
+ return null;
288
+ }
289
+ const baseText = sourceCode.getText(first.argument);
290
+ const basePath = accessPathOf(first.argument);
291
+ const baseIndent = indentOf(node);
292
+ const body = buildOverrideEntries(rest, basePath, `${baseIndent}${INDENT_STEP}`);
293
+ if (body === null) {
294
+ return null;
295
+ }
296
+ const overrides = body === '' ? '{}' : `{\n${body}\n${baseIndent}}`;
297
+ return `cloneDeep(${baseText}, ${overrides} as const)`;
298
+ }
299
+ /**
300
+ * When the reported object has no spread of its own (the #365 membership
301
+ * shape), the object that actually shallow-copies a base lives one or more
302
+ * levels down; rewriting only those children leaves the rest of the literal
303
+ * untouched.
304
+ */
305
+ function collectCloneTargets(node) {
306
+ const targets = [];
307
+ const visit = (current) => {
308
+ if (current !== node &&
309
+ startsWithSpread(current) &&
310
+ isPartialDeepCopy(current)) {
311
+ targets.push(current);
312
+ return;
313
+ }
314
+ for (const prop of current.properties) {
315
+ if (prop.type === utils_1.AST_NODE_TYPES.Property &&
316
+ prop.value.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
317
+ visit(prop.value);
282
318
  }
283
- overrides.push(`${key}: ${value}`);
284
319
  }
285
- else if (prop.type === utils_1.AST_NODE_TYPES.SpreadElement &&
286
- prop.argument.type === utils_1.AST_NODE_TYPES.ConditionalExpression) {
287
- // Handle conditional spread elements (like ...(condition ? {...} : {}))
288
- const text = sourceCode.getText(prop);
289
- overrides.push(text);
320
+ };
321
+ visit(node);
322
+ return targets;
323
+ }
324
+ /**
325
+ * Returns the fixes required for `cloneDeep` to resolve, an empty list when
326
+ * it already does, or null when a conflicting binding of that name exists —
327
+ * shadowing it would silently call something else.
328
+ */
329
+ function buildImportFixes(fixer, scope) {
330
+ const existing = utils_1.ASTUtils.findVariable(scope, CLONE_DEEP_NAME);
331
+ if (existing) {
332
+ const [definition] = existing.defs;
333
+ if (!definition) {
334
+ return null;
335
+ }
336
+ const definitionNode = definition.node;
337
+ if (definitionNode.type !== utils_1.AST_NODE_TYPES.ImportSpecifier &&
338
+ definitionNode.type !== utils_1.AST_NODE_TYPES.ImportDefaultSpecifier) {
339
+ return null;
340
+ }
341
+ if (definitionNode.type === utils_1.AST_NODE_TYPES.ImportSpecifier &&
342
+ definitionNode.importKind === 'type') {
343
+ return null;
290
344
  }
345
+ const declaration = definitionNode.parent;
346
+ if (!declaration ||
347
+ declaration.type !== utils_1.AST_NODE_TYPES.ImportDeclaration ||
348
+ declaration.importKind === 'type' ||
349
+ !isCloneDeepModule(String(declaration.source.value))) {
350
+ return null;
351
+ }
352
+ return [];
353
+ }
354
+ const importDeclarations = sourceCode.ast.body.filter((statement) => statement.type === utils_1.AST_NODE_TYPES.ImportDeclaration);
355
+ const reusable = importDeclarations.find((declaration) => declaration.importKind !== 'type' &&
356
+ isCloneDeepModule(String(declaration.source.value)) &&
357
+ declaration.specifiers.some((specifier) => specifier.type === utils_1.AST_NODE_TYPES.ImportSpecifier));
358
+ if (reusable) {
359
+ const namedSpecifiers = reusable.specifiers.filter((specifier) => specifier.type === utils_1.AST_NODE_TYPES.ImportSpecifier);
360
+ const lastSpecifier = namedSpecifiers[namedSpecifiers.length - 1];
361
+ return [fixer.insertTextAfter(lastSpecifier, `, ${CLONE_DEEP_NAME}`)];
291
362
  }
292
- return overrides.join(',\n ');
363
+ const importText = `import { ${CLONE_DEEP_NAME} } from '${CLONE_DEEP_MODULE}';\n`;
364
+ const [firstImport] = importDeclarations;
365
+ if (firstImport) {
366
+ return [fixer.insertTextBefore(firstImport, importText)];
367
+ }
368
+ return [fixer.insertTextBeforeRange([0, 0], importText)];
293
369
  }
294
370
  // Find the outermost object expression that needs cloneDeep
295
- function findOutermostObjectWithNestedSpread(node) {
371
+ function findOutermostPartialDeepCopy(node) {
296
372
  let current = node.parent;
297
373
  let result = node;
298
374
  // Walk up the tree to find the outermost object expression
@@ -300,8 +376,9 @@ exports.preferCloneDeep = (0, createRule_1.createRule)({
300
376
  if (current.type === utils_1.AST_NODE_TYPES.Property &&
301
377
  current.parent &&
302
378
  current.parent.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
303
- // If this is a property of an object expression, check if that object has nested spreads
304
- 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)) {
305
382
  result = current.parent;
306
383
  }
307
384
  }
@@ -315,9 +392,9 @@ exports.preferCloneDeep = (0, createRule_1.createRule)({
315
392
  if (processedNodes.has(node)) {
316
393
  return;
317
394
  }
318
- if (hasNestedSpread(node)) {
395
+ if (containsPartialDeepCopy(node)) {
319
396
  // Find the outermost object that should use cloneDeep
320
- const outermostNode = findOutermostObjectWithNestedSpread(node);
397
+ const outermostNode = findOutermostPartialDeepCopy(node);
321
398
  // Mark all nested object expressions as processed
322
399
  const markProcessed = (n) => {
323
400
  if (n.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
@@ -338,11 +415,30 @@ exports.preferCloneDeep = (0, createRule_1.createRule)({
338
415
  markProcessed(outermostNode);
339
416
  // Only report on the outermost node
340
417
  if (outermostNode === node) {
418
+ const scope = scopeOf(node);
341
419
  context.report({
342
420
  node,
343
421
  messageId: 'preferCloneDeep',
344
422
  fix(fixer) {
345
- return fixer.replaceText(node, generateCloneDeepFix(node));
423
+ const rewrites = [];
424
+ const targets = startsWithSpread(node)
425
+ ? [node]
426
+ : collectCloneTargets(node);
427
+ if (targets.length === 0) {
428
+ return null;
429
+ }
430
+ for (const target of targets) {
431
+ const call = buildCloneDeepCall(target);
432
+ if (call === null) {
433
+ return null;
434
+ }
435
+ rewrites.push(fixer.replaceText(target, call));
436
+ }
437
+ const importFixes = buildImportFixes(fixer, scope);
438
+ if (importFixes === null) {
439
+ return null;
440
+ }
441
+ return [...importFixes, ...rewrites];
346
442
  },
347
443
  });
348
444
  }