@blumintinc/eslint-plugin-blumint 1.20.87 → 1.20.88
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 +65 -0
- package/package.json +1 -1
- package/release-manifest.json +14 -0
package/lib/index.js
CHANGED
|
@@ -263,6 +263,55 @@ const brokenOpenCallText = (sourceCode, call, callback, head, indentUnit) => {
|
|
|
263
263
|
const comma = wantsTrailingComma(sourceCode, call) ? ',' : '';
|
|
264
264
|
return `${head}(\n${calleeIndent}${moved}${comma}\n${callIndent})`;
|
|
265
265
|
};
|
|
266
|
+
/** Whether a range sits wholly inside one of the ranges a fix deletes. */
|
|
267
|
+
const fallsInside = (ranges, range) => ranges.some(([start, end]) => start <= range[0] && range[1] <= end);
|
|
268
|
+
/**
|
|
269
|
+
* Whether the binding belongs to a function rather than to the module. A
|
|
270
|
+
* module-scope binding is left alone because this file cannot settle whether it
|
|
271
|
+
* is dead: it may be exported, re-exported, or declared for a side effect,
|
|
272
|
+
* whereas a binding declared inside a component or hook is reachable only from
|
|
273
|
+
* the references this file spells out.
|
|
274
|
+
*/
|
|
275
|
+
const isFunctionLocal = (variable) => {
|
|
276
|
+
for (let scope = variable.scope; scope; scope = scope.upper) {
|
|
277
|
+
if (scope.type === 'function') {
|
|
278
|
+
return true;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return false;
|
|
282
|
+
};
|
|
283
|
+
/** The bindings a reference inside one of the deleted ranges resolves to. */
|
|
284
|
+
const variablesReferencedIn = (root, deletedRanges) => {
|
|
285
|
+
const referenced = new Set();
|
|
286
|
+
const walk = (scope) => {
|
|
287
|
+
for (const reference of scope.references) {
|
|
288
|
+
if (reference.resolved &&
|
|
289
|
+
fallsInside(deletedRanges, reference.identifier.range)) {
|
|
290
|
+
referenced.add(reference.resolved);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
scope.childScopes.forEach(walk);
|
|
294
|
+
};
|
|
295
|
+
walk(root);
|
|
296
|
+
return referenced;
|
|
297
|
+
};
|
|
298
|
+
/**
|
|
299
|
+
* Whether deleting the given ranges would strip the last read of a binding
|
|
300
|
+
* declared inside a function, leaving a declaration — and whatever imports feed
|
|
301
|
+
* it — that nothing uses.
|
|
302
|
+
*
|
|
303
|
+
* A dependency array can hold the sole reference to a value computed for it
|
|
304
|
+
* alone, which is precisely what `no-array-length-in-deps` produces when it
|
|
305
|
+
* hoists `const listHash = useMemo(() => stableHash(list), [list])` and points
|
|
306
|
+
* the dependency at it. Dropping the array then strands that statement, turning
|
|
307
|
+
* a clean file into one `no-unused-vars` rejects with a violation this plugin
|
|
308
|
+
* cannot itself fix (issue #1652). Only a write left behind counts as no use at
|
|
309
|
+
* all, matching how an unused-variable check reads the result.
|
|
310
|
+
*/
|
|
311
|
+
const orphansLocalBinding = (root, deletedRanges) => [...variablesReferencedIn(root, deletedRanges)]
|
|
312
|
+
.filter((variable) => variable.defs.length > 0 && isFunctionLocal(variable))
|
|
313
|
+
.some((variable) => variable.references.every((reference) => !reference.isRead() ||
|
|
314
|
+
fallsInside(deletedRanges, reference.identifier.range)));
|
|
266
315
|
exports.useLatestCallback = (0, createRule_1.createRule)({
|
|
267
316
|
name: 'use-latest-callback',
|
|
268
317
|
meta: {
|
|
@@ -676,6 +725,14 @@ exports.useLatestCallback = (0, createRule_1.createRule)({
|
|
|
676
725
|
}
|
|
677
726
|
return texts;
|
|
678
727
|
};
|
|
728
|
+
// Everything a rewritten call carries past its callback argument — the
|
|
729
|
+
// dependency array, and any further argument — is absent from the
|
|
730
|
+
// replacement text, so those ranges are what the fix deletes.
|
|
731
|
+
const droppedRanges = batchedConversions.map((conversion) => [
|
|
732
|
+
conversion.node.arguments[0].range[1],
|
|
733
|
+
conversion.node.range[1],
|
|
734
|
+
]);
|
|
735
|
+
const programScope = ASTHelpers_1.ASTHelpers.getScope(context, program);
|
|
679
736
|
// Every call-site conversion and the import rewrite ride on ONE fix
|
|
680
737
|
// from ONE report. ESLint discards a multi-part fix wholesale when any
|
|
681
738
|
// part conflicts with another rule's fix and retries it on the next
|
|
@@ -700,6 +757,14 @@ exports.useLatestCallback = (0, createRule_1.createRule)({
|
|
|
700
757
|
if (!batchedConversions.every(reachesHook)) {
|
|
701
758
|
return null;
|
|
702
759
|
}
|
|
760
|
+
// Deleting the dependency arrays must not strand a binding that
|
|
761
|
+
// exists only to be listed in one. The batch is atomic, so a single
|
|
762
|
+
// orphaned binding withholds all of it, and the violation still
|
|
763
|
+
// stands as a report: the author drops the dead declaration
|
|
764
|
+
// together with the array.
|
|
765
|
+
if (orphansLocalBinding(programScope, droppedRanges)) {
|
|
766
|
+
return null;
|
|
767
|
+
}
|
|
703
768
|
const texts = conversionTexts();
|
|
704
769
|
return [
|
|
705
770
|
...batchedConversions.map((conversion) => fixer.replaceText(conversion.node, texts.get(conversion.node))),
|
package/package.json
CHANGED
package/release-manifest.json
CHANGED
|
@@ -1,4 +1,18 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "1.20.88",
|
|
4
|
+
"date": "2026-08-03T09:05:50.265Z",
|
|
5
|
+
"rules": [
|
|
6
|
+
{
|
|
7
|
+
"name": "use-latest-callback",
|
|
8
|
+
"changeType": "fix",
|
|
9
|
+
"issues": [
|
|
10
|
+
1652
|
|
11
|
+
],
|
|
12
|
+
"summary": "decline the fix when dropping the deps array orphans a local binding (closes #1652)"
|
|
13
|
+
}
|
|
14
|
+
]
|
|
15
|
+
},
|
|
2
16
|
{
|
|
3
17
|
"version": "1.20.87",
|
|
4
18
|
"date": "2026-08-03T08:16:33.082Z",
|