@blumintinc/eslint-plugin-blumint 1.20.23 → 1.20.24
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/use-latest-callback.js +93 -63
- package/package.json +1 -1
- package/release-manifest.json +14 -0
package/lib/index.js
CHANGED
|
@@ -197,10 +197,21 @@ exports.useLatestCallback = (0, createRule_1.createRule)({
|
|
|
197
197
|
return;
|
|
198
198
|
}
|
|
199
199
|
const specifiers = useCallbackSpecifiersOf(statement);
|
|
200
|
+
// A conversion nested inside another conversion cannot join the atomic
|
|
201
|
+
// batch: the outer replacement re-emits the inner call's original text,
|
|
202
|
+
// so fixing both at once would produce overlapping edits. The inner
|
|
203
|
+
// call is reported without a fix; because its callee then counts as a
|
|
204
|
+
// surviving reference, the react import is preserved and a later pass
|
|
205
|
+
// converts it against the rewritten text.
|
|
206
|
+
const isNestedConversion = (candidate) => conversions.some((other) => other.node !== candidate.node &&
|
|
207
|
+
other.node.range[0] <= candidate.node.range[0] &&
|
|
208
|
+
candidate.node.range[1] <= other.node.range[1]);
|
|
209
|
+
const batchedConversions = conversions.filter((conversion) => !isNestedConversion(conversion));
|
|
210
|
+
const deferredConversions = conversions.filter(isNestedConversion);
|
|
200
211
|
// A reference the fix does not rewrite (a JSX-returning call, an
|
|
201
212
|
// argument-less call, or `useCallback` used as a value) keeps needing
|
|
202
213
|
// react's binding, so the import must be preserved verbatim.
|
|
203
|
-
const convertedCallees = new Set(
|
|
214
|
+
const convertedCallees = new Set(batchedConversions
|
|
204
215
|
.map((conversion) => conversion.callee)
|
|
205
216
|
.filter((callee) => !!callee));
|
|
206
217
|
const hasSurvivingReference = context
|
|
@@ -231,7 +242,84 @@ exports.useLatestCallback = (0, createRule_1.createRule)({
|
|
|
231
242
|
: useCallbackLocalName === 'useCallback'
|
|
232
243
|
? 'useLatestCallback'
|
|
233
244
|
: useCallbackLocalName;
|
|
234
|
-
|
|
245
|
+
const importText = `import ${recommendedHook} from 'use-latest-callback';`;
|
|
246
|
+
// The react import statement participates in the change set only when
|
|
247
|
+
// it binds useCallback or anchors a React.useCallback member call.
|
|
248
|
+
const touchesImport = specifiers.length > 0 || hasReactMemberUseCallback;
|
|
249
|
+
const importFixes = (fixer) => {
|
|
250
|
+
if (!touchesImport) {
|
|
251
|
+
return [];
|
|
252
|
+
}
|
|
253
|
+
// A surviving reference (or a member-only file) leaves the react
|
|
254
|
+
// import untouched; only the new import is added when missing.
|
|
255
|
+
if (hasSurvivingReference || specifiers.length === 0) {
|
|
256
|
+
if (hasUseLatestCallbackImport) {
|
|
257
|
+
return [];
|
|
258
|
+
}
|
|
259
|
+
return [fixer.insertTextBefore(statement, `${importText}\n`)];
|
|
260
|
+
}
|
|
261
|
+
const defaultOrNamespace = statement.specifiers.find((s) => s.type === utils_1.AST_NODE_TYPES.ImportDefaultSpecifier ||
|
|
262
|
+
s.type === utils_1.AST_NODE_TYPES.ImportNamespaceSpecifier);
|
|
263
|
+
const remainingNamed = statement.specifiers.filter((s) => s.type === utils_1.AST_NODE_TYPES.ImportSpecifier &&
|
|
264
|
+
s.imported.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
265
|
+
s.imported.name !== 'useCallback');
|
|
266
|
+
const prefix = statement.importKind && statement.importKind !== 'value'
|
|
267
|
+
? 'import type'
|
|
268
|
+
: 'import';
|
|
269
|
+
if (remainingNamed.length === 0 && !defaultOrNamespace) {
|
|
270
|
+
if (!hasUseLatestCallbackImport) {
|
|
271
|
+
return [fixer.replaceText(statement, importText)];
|
|
272
|
+
}
|
|
273
|
+
return [fixer.remove(statement)];
|
|
274
|
+
}
|
|
275
|
+
const parts = [];
|
|
276
|
+
if (defaultOrNamespace) {
|
|
277
|
+
parts.push(sourceCode.getText(defaultOrNamespace));
|
|
278
|
+
}
|
|
279
|
+
if (remainingNamed.length > 0) {
|
|
280
|
+
parts.push(`{ ${remainingNamed
|
|
281
|
+
.map((s) => sourceCode.getText(s))
|
|
282
|
+
.join(', ')} }`);
|
|
283
|
+
}
|
|
284
|
+
const replacement = `${prefix} ${parts.join(', ')} from 'react';`;
|
|
285
|
+
const fixes = [];
|
|
286
|
+
if (!hasUseLatestCallbackImport) {
|
|
287
|
+
fixes.push(fixer.insertTextBefore(statement, `${importText}\n`));
|
|
288
|
+
}
|
|
289
|
+
fixes.push(fixer.replaceText(statement, replacement));
|
|
290
|
+
return fixes;
|
|
291
|
+
};
|
|
292
|
+
const conversionFix = (fixer, conversion) => {
|
|
293
|
+
const callbackText = sourceCode.getText(conversion.node.arguments[0]);
|
|
294
|
+
const typeParams = conversion.node.typeParameters
|
|
295
|
+
? sourceCode.getText(conversion.node.typeParameters)
|
|
296
|
+
: '';
|
|
297
|
+
// Replace useCallback with useLatestCallback and remove the dependency array
|
|
298
|
+
return fixer.replaceText(conversion.node, `${recommendedHook}${typeParams}(${callbackText})`);
|
|
299
|
+
};
|
|
300
|
+
// Every call-site conversion and the import rewrite ride on ONE fix
|
|
301
|
+
// from ONE report. ESLint discards a multi-part fix wholesale when any
|
|
302
|
+
// part conflicts with another rule's fix and retries it on the next
|
|
303
|
+
// pass against the updated text, whereas fixes split across reports
|
|
304
|
+
// land piecemeal: the disjoint import rewrite would apply even when
|
|
305
|
+
// the call-site conversion is deferred, permanently stranding a
|
|
306
|
+
// `useCallback(...)` call with no import (issue #1400).
|
|
307
|
+
const [fixOwner, ...followers] = batchedConversions;
|
|
308
|
+
context.report({
|
|
309
|
+
node: fixOwner.node,
|
|
310
|
+
messageId: 'useLatestCallback',
|
|
311
|
+
data: {
|
|
312
|
+
currentHook: fixOwner.currentHook,
|
|
313
|
+
recommendedHook,
|
|
314
|
+
},
|
|
315
|
+
fix(fixer) {
|
|
316
|
+
return [
|
|
317
|
+
...batchedConversions.map((conversion) => conversionFix(fixer, conversion)),
|
|
318
|
+
...importFixes(fixer),
|
|
319
|
+
];
|
|
320
|
+
},
|
|
321
|
+
});
|
|
322
|
+
for (const conversion of [...followers, ...deferredConversions]) {
|
|
235
323
|
context.report({
|
|
236
324
|
node: conversion.node,
|
|
237
325
|
messageId: 'useLatestCallback',
|
|
@@ -239,34 +327,13 @@ exports.useLatestCallback = (0, createRule_1.createRule)({
|
|
|
239
327
|
currentHook: conversion.currentHook,
|
|
240
328
|
recommendedHook,
|
|
241
329
|
},
|
|
242
|
-
fix(fixer) {
|
|
243
|
-
const callbackText = sourceCode.getText(conversion.node.arguments[0]);
|
|
244
|
-
const typeParams = conversion.node.typeParameters
|
|
245
|
-
? sourceCode.getText(conversion.node.typeParameters)
|
|
246
|
-
: '';
|
|
247
|
-
// Replace useCallback with useLatestCallback and remove the dependency array
|
|
248
|
-
return fixer.replaceText(conversion.node, `${recommendedHook}${typeParams}(${callbackText})`);
|
|
249
|
-
},
|
|
250
330
|
});
|
|
251
331
|
}
|
|
252
|
-
if (
|
|
332
|
+
if (!touchesImport) {
|
|
253
333
|
return;
|
|
254
334
|
}
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
if (hasUseLatestCallbackImport) {
|
|
258
|
-
return; // The react import stays as is and the new import exists
|
|
259
|
-
}
|
|
260
|
-
context.report({
|
|
261
|
-
node: statement,
|
|
262
|
-
messageId: 'useLatestCallback',
|
|
263
|
-
data: {
|
|
264
|
-
currentHook: useCallbackLocalName,
|
|
265
|
-
recommendedHook,
|
|
266
|
-
},
|
|
267
|
-
fix: (fixer) => fixer.insertTextBefore(statement, `${importText}\n`),
|
|
268
|
-
});
|
|
269
|
-
return;
|
|
335
|
+
if (hasSurvivingReference && hasUseLatestCallbackImport) {
|
|
336
|
+
return; // The react import stays as is and the new import exists
|
|
270
337
|
}
|
|
271
338
|
context.report({
|
|
272
339
|
node: statement,
|
|
@@ -275,43 +342,6 @@ exports.useLatestCallback = (0, createRule_1.createRule)({
|
|
|
275
342
|
currentHook: useCallbackLocalName,
|
|
276
343
|
recommendedHook,
|
|
277
344
|
},
|
|
278
|
-
fix(fixer) {
|
|
279
|
-
if (specifiers.length === 0) {
|
|
280
|
-
if (hasUseLatestCallbackImport)
|
|
281
|
-
return null;
|
|
282
|
-
return fixer.insertTextBefore(statement, `${importText}\n`);
|
|
283
|
-
}
|
|
284
|
-
const defaultOrNamespace = statement.specifiers.find((s) => s.type === utils_1.AST_NODE_TYPES.ImportDefaultSpecifier ||
|
|
285
|
-
s.type === utils_1.AST_NODE_TYPES.ImportNamespaceSpecifier);
|
|
286
|
-
const remainingNamed = statement.specifiers.filter((s) => s.type === utils_1.AST_NODE_TYPES.ImportSpecifier &&
|
|
287
|
-
s.imported.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
288
|
-
s.imported.name !== 'useCallback');
|
|
289
|
-
const prefix = statement.importKind && statement.importKind !== 'value'
|
|
290
|
-
? 'import type'
|
|
291
|
-
: 'import';
|
|
292
|
-
if (remainingNamed.length === 0 && !defaultOrNamespace) {
|
|
293
|
-
if (!hasUseLatestCallbackImport) {
|
|
294
|
-
return fixer.replaceText(statement, importText);
|
|
295
|
-
}
|
|
296
|
-
return fixer.remove(statement);
|
|
297
|
-
}
|
|
298
|
-
const parts = [];
|
|
299
|
-
if (defaultOrNamespace) {
|
|
300
|
-
parts.push(sourceCode.getText(defaultOrNamespace));
|
|
301
|
-
}
|
|
302
|
-
if (remainingNamed.length > 0) {
|
|
303
|
-
parts.push(`{ ${remainingNamed
|
|
304
|
-
.map((s) => sourceCode.getText(s))
|
|
305
|
-
.join(', ')} }`);
|
|
306
|
-
}
|
|
307
|
-
const replacement = `${prefix} ${parts.join(', ')} from 'react';`;
|
|
308
|
-
const fixes = [];
|
|
309
|
-
if (!hasUseLatestCallbackImport) {
|
|
310
|
-
fixes.push(fixer.insertTextBefore(statement, `${importText}\n`));
|
|
311
|
-
}
|
|
312
|
-
fixes.push(fixer.replaceText(statement, replacement));
|
|
313
|
-
return fixes;
|
|
314
|
-
},
|
|
315
345
|
});
|
|
316
346
|
},
|
|
317
347
|
};
|
package/package.json
CHANGED
package/release-manifest.json
CHANGED
|
@@ -1,4 +1,18 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "1.20.24",
|
|
4
|
+
"date": "2026-07-30T02:00:54.523Z",
|
|
5
|
+
"rules": [
|
|
6
|
+
{
|
|
7
|
+
"name": "use-latest-callback",
|
|
8
|
+
"changeType": "fix",
|
|
9
|
+
"issues": [
|
|
10
|
+
1400
|
|
11
|
+
],
|
|
12
|
+
"summary": "apply the import rewrite and call conversions atomically (closes #1400)"
|
|
13
|
+
}
|
|
14
|
+
]
|
|
15
|
+
},
|
|
2
16
|
{
|
|
3
17
|
"version": "1.20.23",
|
|
4
18
|
"date": "2026-07-30T01:37:58.854Z",
|