@blumintinc/eslint-plugin-blumint 1.20.8 → 1.20.10
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 +250 -192
- package/lib/rules/prefer-destructuring-no-class.js +8 -0
- package/lib/rules/prefer-spread-over-reassembly.d.ts +2 -1
- package/lib/rules/prefer-spread-over-reassembly.js +30 -2
- 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: {
|
|
@@ -78,218 +100,235 @@ exports.preferCloneDeep = (0, createRule_1.createRule)({
|
|
|
78
100
|
!hasFunction &&
|
|
79
101
|
!hasSymbol);
|
|
80
102
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
103
|
+
const sourceCode = context.sourceCode;
|
|
104
|
+
function normalizedTextOf(node) {
|
|
105
|
+
return sourceCode.getText(node).replace(/\s+/g, '');
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* `...(base.a ?? {})`, `...(base?.a)` and `...base.a!` are defensive
|
|
109
|
+
* spellings of `...base.a`; unwrapping them lets the mirror check below
|
|
110
|
+
* recognize the underlying member path.
|
|
111
|
+
*/
|
|
112
|
+
function unwrapSpreadArgument(node) {
|
|
113
|
+
if (node.type === utils_1.AST_NODE_TYPES.ChainExpression) {
|
|
114
|
+
return unwrapSpreadArgument(node.expression);
|
|
91
115
|
}
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
}
|
|
116
|
+
if (node.type === utils_1.AST_NODE_TYPES.TSNonNullExpression) {
|
|
117
|
+
return unwrapSpreadArgument(node.expression);
|
|
124
118
|
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
|
119
|
+
if (node.type === utils_1.AST_NODE_TYPES.LogicalExpression &&
|
|
120
|
+
(node.operator === '??' || node.operator === '||') &&
|
|
121
|
+
node.right.type === utils_1.AST_NODE_TYPES.ObjectExpression &&
|
|
122
|
+
node.right.properties.length === 0) {
|
|
123
|
+
return unwrapSpreadArgument(node.left);
|
|
139
124
|
}
|
|
140
|
-
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
125
|
+
return node;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Canonical, whitespace-insensitive spelling of a member path so that a
|
|
129
|
+
* nested spread can be compared against the path cloneDeep already copies.
|
|
130
|
+
*/
|
|
131
|
+
function accessPathOf(node) {
|
|
132
|
+
const unwrapped = unwrapSpreadArgument(node);
|
|
133
|
+
if (unwrapped.type === utils_1.AST_NODE_TYPES.MemberExpression) {
|
|
134
|
+
const objectPath = accessPathOf(unwrapped.object);
|
|
135
|
+
if (unwrapped.computed) {
|
|
136
|
+
return `${objectPath}[${normalizedTextOf(unwrapped.property)}]`;
|
|
137
|
+
}
|
|
138
|
+
if (unwrapped.property.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
139
|
+
return `${objectPath}.${unwrapped.property.name}`;
|
|
140
|
+
}
|
|
141
|
+
return `${objectPath}.${normalizedTextOf(unwrapped.property)}`;
|
|
155
142
|
}
|
|
156
|
-
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
-
}
|
|
143
|
+
return normalizedTextOf(unwrapped);
|
|
144
|
+
}
|
|
145
|
+
function accessorForKey(prop) {
|
|
146
|
+
if (prop.computed) {
|
|
147
|
+
return `[${normalizedTextOf(prop.key)}]`;
|
|
169
148
|
}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
}
|
|
173
|
-
else {
|
|
174
|
-
return `cloneDeep(baseObj, {
|
|
175
|
-
data: {
|
|
176
|
-
nested: {
|
|
177
|
-
value: 42
|
|
149
|
+
if (prop.key.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
150
|
+
return `.${prop.key.name}`;
|
|
178
151
|
}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
152
|
+
if (prop.key.type === utils_1.AST_NODE_TYPES.Literal) {
|
|
153
|
+
return `[${normalizedTextOf(prop.key)}]`;
|
|
154
|
+
}
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
function keyTextOf(prop) {
|
|
158
|
+
return prop.computed
|
|
159
|
+
? `[${sourceCode.getText(prop.key)}]`
|
|
160
|
+
: sourceCode.getText(prop.key);
|
|
161
|
+
}
|
|
162
|
+
function indentOf(node) {
|
|
163
|
+
const text = sourceCode.getText();
|
|
164
|
+
const lineStart = text.lastIndexOf('\n', node.range[0] - 1) + 1;
|
|
165
|
+
const prefix = text.slice(lineStart, node.range[0]);
|
|
166
|
+
return /^[ \t]*/.exec(prefix)?.[0] ?? '';
|
|
167
|
+
}
|
|
168
|
+
function startsWithSpread(node) {
|
|
169
|
+
return node.properties[0]?.type === utils_1.AST_NODE_TYPES.SpreadElement;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* `SourceCode#getScope` supersedes the deprecated `context.getScope`; the
|
|
173
|
+
* fallback keeps the rule working on ESLint versions that predate it.
|
|
174
|
+
*/
|
|
175
|
+
function scopeOf(node) {
|
|
176
|
+
const scoped = sourceCode;
|
|
177
|
+
return typeof scoped.getScope === 'function'
|
|
178
|
+
? scoped.getScope(node)
|
|
179
|
+
: context.getScope();
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Rebuilds the properties of one object as cloneDeep overrides. Returns null
|
|
183
|
+
* whenever a property cannot be reproduced without changing runtime
|
|
184
|
+
* behavior, which makes the caller decline the fix instead of emitting code
|
|
185
|
+
* that silently drops data (#1364).
|
|
186
|
+
*/
|
|
187
|
+
function buildOverrideEntries(properties, basePath, indent) {
|
|
188
|
+
const entries = [];
|
|
189
|
+
for (const prop of properties) {
|
|
190
|
+
// Any spread other than a leading copy of the path cloneDeep already
|
|
191
|
+
// clones would have to be dropped, deleting whatever it contributed.
|
|
192
|
+
if (prop.type !== utils_1.AST_NODE_TYPES.Property) {
|
|
193
|
+
return null;
|
|
182
194
|
}
|
|
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
|
|
195
|
+
// Getters, setters and shorthand methods degrade into plain data
|
|
196
|
+
// properties when reprinted as overrides.
|
|
197
|
+
if (prop.kind !== 'init' || prop.method) {
|
|
198
|
+
return null;
|
|
198
199
|
}
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
200
|
+
let valueText;
|
|
201
|
+
if (prop.value.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
|
|
202
|
+
const accessor = accessorForKey(prop);
|
|
203
|
+
if (accessor === null) {
|
|
204
|
+
return null;
|
|
203
205
|
}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
advanced: {
|
|
208
|
-
enabled: true
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
} as const)`;
|
|
206
|
+
const nested = buildOverrideObject(prop.value, `${basePath}${accessor}`, `${indent}${INDENT_STEP}`);
|
|
207
|
+
if (nested === null) {
|
|
208
|
+
return null;
|
|
212
209
|
}
|
|
210
|
+
valueText = nested;
|
|
213
211
|
}
|
|
214
|
-
else
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
content: {
|
|
219
|
-
form: {
|
|
220
|
-
values: {
|
|
221
|
-
submitted: true
|
|
222
|
-
}
|
|
212
|
+
else {
|
|
213
|
+
// Arrays, calls, conditionals and primitives are copied verbatim, so
|
|
214
|
+
// whatever they contain survives the fix untouched.
|
|
215
|
+
valueText = sourceCode.getText(prop.value);
|
|
223
216
|
}
|
|
224
|
-
|
|
217
|
+
entries.push(`${indent}${keyTextOf(prop)}: ${valueText}`);
|
|
225
218
|
}
|
|
226
|
-
|
|
227
|
-
}
|
|
219
|
+
return entries.join(',\n');
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* `indent` is the indentation of the rebuilt object's entries; its closing
|
|
223
|
+
* brace lines up one step to the left.
|
|
224
|
+
*/
|
|
225
|
+
function buildOverrideObject(node, basePath, indent) {
|
|
226
|
+
let properties = [...node.properties];
|
|
227
|
+
const [first] = properties;
|
|
228
|
+
if (first && first.type === utils_1.AST_NODE_TYPES.SpreadElement) {
|
|
229
|
+
// A leading spread of this exact path is redundant: cloneDeep already
|
|
230
|
+
// copies it from the base. Any other spread carries data that the
|
|
231
|
+
// overrides object cannot express.
|
|
232
|
+
if (accessPathOf(first.argument) !== basePath) {
|
|
233
|
+
return null;
|
|
228
234
|
}
|
|
229
|
-
|
|
230
|
-
const overrides = extractNestedOverrides(node);
|
|
231
|
-
return `cloneDeep(${baseObj}, {
|
|
232
|
-
${overrides}
|
|
233
|
-
} as const)`;
|
|
235
|
+
properties = properties.slice(1);
|
|
234
236
|
}
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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)`;
|
|
237
|
+
const body = buildOverrideEntries(properties, basePath, indent);
|
|
238
|
+
if (body === null) {
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
if (body === '') {
|
|
242
|
+
return '{}';
|
|
252
243
|
}
|
|
244
|
+
const closingIndent = indent.slice(0, indent.length - INDENT_STEP.length);
|
|
245
|
+
return `{\n${body}\n${closingIndent}}`;
|
|
253
246
|
}
|
|
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
|
-
|
|
247
|
+
function buildCloneDeepCall(node) {
|
|
248
|
+
const [first, ...rest] = node.properties;
|
|
249
|
+
if (!first || first.type !== utils_1.AST_NODE_TYPES.SpreadElement) {
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
const baseText = sourceCode.getText(first.argument);
|
|
253
|
+
const basePath = accessPathOf(first.argument);
|
|
254
|
+
const baseIndent = indentOf(node);
|
|
255
|
+
const body = buildOverrideEntries(rest, basePath, `${baseIndent}${INDENT_STEP}`);
|
|
256
|
+
if (body === null) {
|
|
257
|
+
return null;
|
|
258
|
+
}
|
|
259
|
+
const overrides = body === '' ? '{}' : `{\n${body}\n${baseIndent}}`;
|
|
260
|
+
return `cloneDeep(${baseText}, ${overrides} as const)`;
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* When the reported object has no spread of its own (the #365 membership
|
|
264
|
+
* shape), the object that actually shallow-copies a base lives one or more
|
|
265
|
+
* levels down; rewriting only those children leaves the rest of the literal
|
|
266
|
+
* untouched.
|
|
267
|
+
*/
|
|
268
|
+
function collectCloneTargets(node) {
|
|
269
|
+
const targets = [];
|
|
270
|
+
const visit = (current) => {
|
|
271
|
+
if (current !== node &&
|
|
272
|
+
startsWithSpread(current) &&
|
|
273
|
+
hasNestedSpread(current)) {
|
|
274
|
+
targets.push(current);
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
for (const prop of current.properties) {
|
|
278
|
+
if (prop.type === utils_1.AST_NODE_TYPES.Property &&
|
|
279
|
+
prop.value.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
|
|
280
|
+
visit(prop.value);
|
|
282
281
|
}
|
|
283
|
-
overrides.push(`${key}: ${value}`);
|
|
284
282
|
}
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
283
|
+
};
|
|
284
|
+
visit(node);
|
|
285
|
+
return targets;
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Returns the fixes required for `cloneDeep` to resolve, an empty list when
|
|
289
|
+
* it already does, or null when a conflicting binding of that name exists —
|
|
290
|
+
* shadowing it would silently call something else.
|
|
291
|
+
*/
|
|
292
|
+
function buildImportFixes(fixer, scope) {
|
|
293
|
+
const existing = utils_1.ASTUtils.findVariable(scope, CLONE_DEEP_NAME);
|
|
294
|
+
if (existing) {
|
|
295
|
+
const [definition] = existing.defs;
|
|
296
|
+
if (!definition) {
|
|
297
|
+
return null;
|
|
298
|
+
}
|
|
299
|
+
const definitionNode = definition.node;
|
|
300
|
+
if (definitionNode.type !== utils_1.AST_NODE_TYPES.ImportSpecifier &&
|
|
301
|
+
definitionNode.type !== utils_1.AST_NODE_TYPES.ImportDefaultSpecifier) {
|
|
302
|
+
return null;
|
|
290
303
|
}
|
|
304
|
+
if (definitionNode.type === utils_1.AST_NODE_TYPES.ImportSpecifier &&
|
|
305
|
+
definitionNode.importKind === 'type') {
|
|
306
|
+
return null;
|
|
307
|
+
}
|
|
308
|
+
const declaration = definitionNode.parent;
|
|
309
|
+
if (!declaration ||
|
|
310
|
+
declaration.type !== utils_1.AST_NODE_TYPES.ImportDeclaration ||
|
|
311
|
+
declaration.importKind === 'type' ||
|
|
312
|
+
!isCloneDeepModule(String(declaration.source.value))) {
|
|
313
|
+
return null;
|
|
314
|
+
}
|
|
315
|
+
return [];
|
|
316
|
+
}
|
|
317
|
+
const importDeclarations = sourceCode.ast.body.filter((statement) => statement.type === utils_1.AST_NODE_TYPES.ImportDeclaration);
|
|
318
|
+
const reusable = importDeclarations.find((declaration) => declaration.importKind !== 'type' &&
|
|
319
|
+
isCloneDeepModule(String(declaration.source.value)) &&
|
|
320
|
+
declaration.specifiers.some((specifier) => specifier.type === utils_1.AST_NODE_TYPES.ImportSpecifier));
|
|
321
|
+
if (reusable) {
|
|
322
|
+
const namedSpecifiers = reusable.specifiers.filter((specifier) => specifier.type === utils_1.AST_NODE_TYPES.ImportSpecifier);
|
|
323
|
+
const lastSpecifier = namedSpecifiers[namedSpecifiers.length - 1];
|
|
324
|
+
return [fixer.insertTextAfter(lastSpecifier, `, ${CLONE_DEEP_NAME}`)];
|
|
291
325
|
}
|
|
292
|
-
|
|
326
|
+
const importText = `import { ${CLONE_DEEP_NAME} } from '${CLONE_DEEP_MODULE}';\n`;
|
|
327
|
+
const [firstImport] = importDeclarations;
|
|
328
|
+
if (firstImport) {
|
|
329
|
+
return [fixer.insertTextBefore(firstImport, importText)];
|
|
330
|
+
}
|
|
331
|
+
return [fixer.insertTextBeforeRange([0, 0], importText)];
|
|
293
332
|
}
|
|
294
333
|
// Find the outermost object expression that needs cloneDeep
|
|
295
334
|
function findOutermostObjectWithNestedSpread(node) {
|
|
@@ -338,11 +377,30 @@ exports.preferCloneDeep = (0, createRule_1.createRule)({
|
|
|
338
377
|
markProcessed(outermostNode);
|
|
339
378
|
// Only report on the outermost node
|
|
340
379
|
if (outermostNode === node) {
|
|
380
|
+
const scope = scopeOf(node);
|
|
341
381
|
context.report({
|
|
342
382
|
node,
|
|
343
383
|
messageId: 'preferCloneDeep',
|
|
344
384
|
fix(fixer) {
|
|
345
|
-
|
|
385
|
+
const rewrites = [];
|
|
386
|
+
const targets = startsWithSpread(node)
|
|
387
|
+
? [node]
|
|
388
|
+
: collectCloneTargets(node);
|
|
389
|
+
if (targets.length === 0) {
|
|
390
|
+
return null;
|
|
391
|
+
}
|
|
392
|
+
for (const target of targets) {
|
|
393
|
+
const call = buildCloneDeepCall(target);
|
|
394
|
+
if (call === null) {
|
|
395
|
+
return null;
|
|
396
|
+
}
|
|
397
|
+
rewrites.push(fixer.replaceText(target, call));
|
|
398
|
+
}
|
|
399
|
+
const importFixes = buildImportFixes(fixer, scope);
|
|
400
|
+
if (importFixes === null) {
|
|
401
|
+
return null;
|
|
402
|
+
}
|
|
403
|
+
return [...importFixes, ...rewrites];
|
|
346
404
|
},
|
|
347
405
|
});
|
|
348
406
|
}
|
|
@@ -295,6 +295,14 @@ exports.preferDestructuringNoClass = (0, createRule_1.createRule)({
|
|
|
295
295
|
if (node.id.type !== utils_1.AST_NODE_TYPES.Identifier) {
|
|
296
296
|
return null;
|
|
297
297
|
}
|
|
298
|
+
// The fix re-emits the declaration as `kind { binding } = object`, which has
|
|
299
|
+
// nowhere to carry an explicit annotation on the binding. Relocating it as
|
|
300
|
+
// `const { alpha }: { alpha: string } = obj` would assert something different
|
|
301
|
+
// (a structural constraint on the source object rather than the variable's
|
|
302
|
+
// declared type) and can shift inference, so withhold the fix and report only.
|
|
303
|
+
if (node.id.typeAnnotation) {
|
|
304
|
+
return null;
|
|
305
|
+
}
|
|
298
306
|
const destructuringBinding = getDestructuringBindingText(memberExpr, propertyText, node.id.name);
|
|
299
307
|
if (!destructuringBinding) {
|
|
300
308
|
return null;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { TSESLint } from '@typescript-eslint/utils';
|
|
1
2
|
type Options = [{
|
|
2
3
|
minFields?: number;
|
|
3
4
|
}];
|
|
4
|
-
export declare const preferSpreadOverReassembly:
|
|
5
|
+
export declare const preferSpreadOverReassembly: TSESLint.RuleModule<"preferSpread", Options, TSESLint.RuleListener>;
|
|
5
6
|
export {};
|
|
@@ -265,6 +265,29 @@ function freshPropsName(existingNames) {
|
|
|
265
265
|
i++;
|
|
266
266
|
return `props${i}`;
|
|
267
267
|
}
|
|
268
|
+
/**
|
|
269
|
+
* Rewrites the destructured parameter to a single identifier without touching
|
|
270
|
+
* its type annotation. A parameter node's `range` spans the annotation, so
|
|
271
|
+
* replacing the whole node would delete `: FooProps` and leave an implicit
|
|
272
|
+
* `any`; the replacement is therefore bounded to the pattern's own closing
|
|
273
|
+
* brace, leaving the annotation text (generics, unions, multi-line formatting)
|
|
274
|
+
* exactly as the author wrote it. Returns null when the closing brace cannot be
|
|
275
|
+
* located, so a malformed fix is never emitted.
|
|
276
|
+
*/
|
|
277
|
+
function replacePatternWithName(fixer, sourceCode, pattern, name) {
|
|
278
|
+
if (!pattern.typeAnnotation) {
|
|
279
|
+
return fixer.replaceText(pattern, name);
|
|
280
|
+
}
|
|
281
|
+
// Scanning backwards from the annotation (whose range starts at the `:`)
|
|
282
|
+
// skips any `?` optional marker sitting between the pattern and the colon.
|
|
283
|
+
const closingBrace = sourceCode.getTokenBefore(pattern.typeAnnotation, {
|
|
284
|
+
filter: (token) => token.type === utils_1.AST_TOKEN_TYPES.Punctuator && token.value === '}',
|
|
285
|
+
});
|
|
286
|
+
if (!closingBrace) {
|
|
287
|
+
return null;
|
|
288
|
+
}
|
|
289
|
+
return fixer.replaceTextRange([pattern.range[0], closingBrace.range[1]], name);
|
|
290
|
+
}
|
|
268
291
|
exports.preferSpreadOverReassembly = (0, createRule_1.createRule)({
|
|
269
292
|
name: 'prefer-spread-over-reassembly',
|
|
270
293
|
meta: {
|
|
@@ -356,8 +379,13 @@ exports.preferSpreadOverReassembly = (0, createRule_1.createRule)({
|
|
|
356
379
|
// Choose a fresh name for the props parameter that does not collide
|
|
357
380
|
// with any binding currently in scope.
|
|
358
381
|
const propsName = freshPropsName(namesSet);
|
|
359
|
-
// 1. Replace the destructured parameter with `props` (or fresh name)
|
|
360
|
-
|
|
382
|
+
// 1. Replace the destructured parameter with `props` (or fresh name),
|
|
383
|
+
// keeping any type annotation intact.
|
|
384
|
+
const paramFix = replacePatternWithName(fixer, sourceCode, param, propsName);
|
|
385
|
+
if (!paramFix) {
|
|
386
|
+
return null;
|
|
387
|
+
}
|
|
388
|
+
fixes.push(paramFix);
|
|
361
389
|
if (target.kind === 'jsx') {
|
|
362
390
|
// 2a. Build the new JSX opening element text.
|
|
363
391
|
const attrs = target.openingElement.attributes;
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import { TSESLint } from '@typescript-eslint/utils';
|
|
1
2
|
type Options = [
|
|
2
3
|
{
|
|
3
4
|
allowComplexBodies?: boolean;
|
|
4
5
|
allowFunctionFactories?: boolean;
|
|
5
6
|
}
|
|
6
7
|
];
|
|
7
|
-
export declare const preferUseCallbackOverUseMemoForFunctions:
|
|
8
|
+
export declare const preferUseCallbackOverUseMemoForFunctions: TSESLint.RuleModule<"preferUseCallback", Options, TSESLint.RuleListener>;
|
|
8
9
|
export default preferUseCallbackOverUseMemoForFunctions;
|