@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.
- package/lib/index.js +1 -1
- package/lib/rules/enforce-early-destructuring.js +14 -0
- package/lib/rules/enforce-m3-sentence-case.js +211 -45
- package/lib/rules/enforce-props-naming-consistency.js +127 -8
- package/lib/rules/enforce-querykey-ts.js +170 -16
- package/lib/rules/enforce-react-type-naming.d.ts +2 -1
- package/lib/rules/enforce-react-type-naming.js +46 -17
- package/lib/rules/enforce-snapshot-state-narrowing.d.ts +3 -1
- package/lib/rules/enforce-snapshot-state-narrowing.js +213 -53
- package/lib/rules/no-class-instance-destructuring.js +7 -0
- package/lib/rules/no-excessive-parent-chain.js +43 -10
- package/lib/rules/no-unnecessary-destructuring.js +8 -1
- package/lib/rules/prefer-clone-deep.d.ts +2 -1
- package/lib/rules/prefer-clone-deep.js +332 -236
- package/lib/rules/prefer-destructuring-no-class.js +8 -0
- package/lib/rules/prefer-usecallback-over-usememo-for-functions.d.ts +2 -1
- package/lib/rules/prefer-usecallback-over-usememo-for-functions.js +192 -5
- package/lib/rules/use-custom-memo.d.ts +2 -1
- package/lib/rules/use-custom-memo.js +51 -36
- package/lib/rules/use-latest-callback.js +142 -92
- package/lib/utils/renameFixes.d.ts +69 -0
- package/lib/utils/renameFixes.js +202 -0
- package/package.json +1 -1
- package/release-manifest.json +132 -0
|
@@ -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
|
-
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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 (
|
|
38
|
-
|
|
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
|
-
(
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
(
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
|
59
|
-
//
|
|
60
|
-
//
|
|
61
|
-
if (
|
|
62
|
-
|
|
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 (
|
|
68
|
-
visit(
|
|
150
|
+
else if (current.type === utils_1.AST_NODE_TYPES.Property) {
|
|
151
|
+
visit(current.value, depth + 1);
|
|
69
152
|
}
|
|
70
|
-
else if (
|
|
71
|
-
visit(
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
!
|
|
79
|
-
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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
|
-
|
|
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
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
230
|
-
const overrides = extractNestedOverrides(node);
|
|
231
|
-
return `cloneDeep(${baseObj}, {
|
|
232
|
-
${overrides}
|
|
233
|
-
} as const)`;
|
|
272
|
+
properties = properties.slice(1);
|
|
234
273
|
}
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
//
|
|
304
|
-
|
|
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 (
|
|
395
|
+
if (containsPartialDeepCopy(node)) {
|
|
319
396
|
// Find the outermost object that should use cloneDeep
|
|
320
|
-
const outermostNode =
|
|
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
|
-
|
|
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
|
}
|