@blumintinc/eslint-plugin-blumint 1.20.188 → 1.20.190
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
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { TSESLint } from '@typescript-eslint/utils';
|
|
1
2
|
type MessageIds = 'avoidEntireObject' | 'removeUnusedDependency';
|
|
2
|
-
export declare const noEntireObjectHookDeps:
|
|
3
|
+
export declare const noEntireObjectHookDeps: TSESLint.RuleModule<MessageIds, [], TSESLint.RuleListener>;
|
|
3
4
|
export {};
|
|
@@ -1167,6 +1167,48 @@ function getObjectUsagesInHook(hookBody, objectName, typeInfo, bodyIsDeferred =
|
|
|
1167
1167
|
notUsed,
|
|
1168
1168
|
};
|
|
1169
1169
|
}
|
|
1170
|
+
/** The run of spaces/tabs opening the line that `offset` sits on. */
|
|
1171
|
+
function indentAt(text, offset) {
|
|
1172
|
+
const lineStart = text.lastIndexOf('\n', offset - 1) + 1;
|
|
1173
|
+
return /^[ \t]*/.exec(text.slice(lineStart, offset))?.[0] ?? '';
|
|
1174
|
+
}
|
|
1175
|
+
/**
|
|
1176
|
+
* Drop `span`, but re-emit any comment standing inside it.
|
|
1177
|
+
*
|
|
1178
|
+
* Removing a dependency means removing its separator too, so the span reaches
|
|
1179
|
+
* to a NEIGHBOURING element and therefore covers the margin between the two.
|
|
1180
|
+
* That margin is not the fixer's to delete: it can hold an
|
|
1181
|
+
* `eslint-disable-next-line` protecting the dependency that survives, and
|
|
1182
|
+
* dropping that directive silently re-enables another rule (#2208). Declining
|
|
1183
|
+
* the fix instead would only trade the lost comment for a transform that a
|
|
1184
|
+
* comment decides (#1877), so the comment is carried rather than obeyed.
|
|
1185
|
+
*
|
|
1186
|
+
* Each carried comment is re-emitted on a line of its own: a `//` comment
|
|
1187
|
+
* swallows whatever follows it on the same line, so folding one inline would
|
|
1188
|
+
* comment out the dependency that was meant to survive.
|
|
1189
|
+
*/
|
|
1190
|
+
function removeCarryingComments(fixer, sourceCode, span, anchor) {
|
|
1191
|
+
const carried = sourceCode
|
|
1192
|
+
.getAllComments()
|
|
1193
|
+
.filter((comment) => comment.range[0] >= span[0] && comment.range[1] <= span[1]);
|
|
1194
|
+
// With nothing to preserve the span is pure separator and whitespace, so the
|
|
1195
|
+
// plain removal keeps the comment-free output exactly as it has always been.
|
|
1196
|
+
if (carried.length === 0) {
|
|
1197
|
+
return fixer.removeRange(span);
|
|
1198
|
+
}
|
|
1199
|
+
const text = sourceCode.getText();
|
|
1200
|
+
// Anchor the indentation on the end the surviving code sits against.
|
|
1201
|
+
const indent = indentAt(text, anchor === 'toNextElement' ? span[1] : carried[0].range[0]);
|
|
1202
|
+
const body = carried
|
|
1203
|
+
.map((comment) => text.slice(comment.range[0], comment.range[1]))
|
|
1204
|
+
.join(`\n${indent}`);
|
|
1205
|
+
// Both spellings close on a fresh line: whatever the span abutted — the next
|
|
1206
|
+
// dependency, or the separator trailing the last one — would otherwise land
|
|
1207
|
+
// on the final carried comment's line and be commented out.
|
|
1208
|
+
return fixer.replaceTextRange(span, anchor === 'toNextElement'
|
|
1209
|
+
? `${body}\n${indent}`
|
|
1210
|
+
: `\n${indent}${body}\n${indent}`);
|
|
1211
|
+
}
|
|
1170
1212
|
exports.noEntireObjectHookDeps = (0, createRule_1.createRule)({
|
|
1171
1213
|
name: 'no-entire-object-hook-deps',
|
|
1172
1214
|
meta: {
|
|
@@ -1213,6 +1255,70 @@ exports.noEntireObjectHookDeps = (0, createRule_1.createRule)({
|
|
|
1213
1255
|
return typeInfo;
|
|
1214
1256
|
}
|
|
1215
1257
|
const sourceCode = context.getSourceCode();
|
|
1258
|
+
/**
|
|
1259
|
+
* The variable each reference resolves to, keyed by the referencing
|
|
1260
|
+
* identifier NODE rather than by name.
|
|
1261
|
+
*
|
|
1262
|
+
* why: identity keying is what makes the orphan check below immune to
|
|
1263
|
+
* shadowing — a name lookup would resolve an inner `hydrated` against an
|
|
1264
|
+
* outer binding of the same name and read its uses as survivors.
|
|
1265
|
+
*/
|
|
1266
|
+
let variableByReference = null;
|
|
1267
|
+
function resolveBinding(identifier) {
|
|
1268
|
+
if (!variableByReference) {
|
|
1269
|
+
variableByReference = new Map();
|
|
1270
|
+
for (const scope of sourceCode.scopeManager?.scopes ?? []) {
|
|
1271
|
+
for (const variable of scope.variables) {
|
|
1272
|
+
for (const reference of variable.references) {
|
|
1273
|
+
variableByReference.set(reference.identifier, variable);
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
return variableByReference.get(identifier) ?? null;
|
|
1279
|
+
}
|
|
1280
|
+
/**
|
|
1281
|
+
* Whether deleting `element` from the dependency array would leave its
|
|
1282
|
+
* binding with no reader left in the file.
|
|
1283
|
+
*
|
|
1284
|
+
* why: a value declared and then read ONLY inside a dependency array is by
|
|
1285
|
+
* construction load-bearing — the declaration would be pointless otherwise
|
|
1286
|
+
* — so removing the entry both discards a deliberate recompute trigger and
|
|
1287
|
+
* strands the declaration. The consumer runs `no-unused-vars` as an error
|
|
1288
|
+
* and builds with `noUnusedLocals`, so the rewrite turns a green file red
|
|
1289
|
+
* on their machine while staying green here. Every sibling instance of this
|
|
1290
|
+
* class was fixed by deleting the stranded declaration too, but that remedy
|
|
1291
|
+
* is unavailable here: the declaration is a hook CALL, and dropping it
|
|
1292
|
+
* changes the component's hook order. Declining the edit is the only safe
|
|
1293
|
+
* remedy, so the report stands and the autofix steps aside.
|
|
1294
|
+
*
|
|
1295
|
+
* Parameters are deliberately exempt. An unread parameter is not an unused
|
|
1296
|
+
* BINDING to either instrument — `no-unused-vars` runs with `args: 'none'`
|
|
1297
|
+
* and `noUnusedLocals` does not cover parameters — so declining there would
|
|
1298
|
+
* withhold a fix without preventing any breakage, and it would silently
|
|
1299
|
+
* settle the reporting question #1621 defers.
|
|
1300
|
+
*/
|
|
1301
|
+
function wouldStrandBinding(element) {
|
|
1302
|
+
const identifier = unwrapExpression(element);
|
|
1303
|
+
if (identifier.type !== utils_1.AST_NODE_TYPES.Identifier)
|
|
1304
|
+
return false;
|
|
1305
|
+
const variable = resolveBinding(identifier);
|
|
1306
|
+
if (!variable || variable.defs.length === 0)
|
|
1307
|
+
return false;
|
|
1308
|
+
if (variable.defs.some((def) => def.type === 'Parameter')) {
|
|
1309
|
+
return false;
|
|
1310
|
+
}
|
|
1311
|
+
const [start, end] = element.range;
|
|
1312
|
+
// why: a declarator's own initializer counts as a WRITE reference, so a
|
|
1313
|
+
// survivor test that accepts any reference never fires for `const x = …`
|
|
1314
|
+
// — the shape this check exists for (#1868 is the same trap).
|
|
1315
|
+
return !variable.references.some((reference) => {
|
|
1316
|
+
if (!reference.isRead())
|
|
1317
|
+
return false;
|
|
1318
|
+
const [from, to] = reference.identifier.range;
|
|
1319
|
+
return from < start || to > end;
|
|
1320
|
+
});
|
|
1321
|
+
}
|
|
1216
1322
|
// why: scanning every comment once per file rather than once per hook call
|
|
1217
1323
|
// keeps the check off the hot path of files with many hooks.
|
|
1218
1324
|
let manuallyManagedLines = null;
|
|
@@ -1331,6 +1437,10 @@ exports.noEntireObjectHookDeps = (0, createRule_1.createRule)({
|
|
|
1331
1437
|
const elementIndex = depsArg.elements.indexOf(element);
|
|
1332
1438
|
if (elementIndex === -1)
|
|
1333
1439
|
return null;
|
|
1440
|
+
// The report stands either way; only the rewrite is withheld.
|
|
1441
|
+
if (wouldStrandBinding(element)) {
|
|
1442
|
+
return null;
|
|
1443
|
+
}
|
|
1334
1444
|
// If this is the only element, just remove it
|
|
1335
1445
|
if (depsArg.elements.length === 1) {
|
|
1336
1446
|
return fixer.remove(element);
|
|
@@ -1339,21 +1449,19 @@ exports.noEntireObjectHookDeps = (0, createRule_1.createRule)({
|
|
|
1339
1449
|
if (elementIndex === depsArg.elements.length - 1) {
|
|
1340
1450
|
const prevElement = depsArg.elements[elementIndex - 1];
|
|
1341
1451
|
if (prevElement) {
|
|
1342
|
-
|
|
1452
|
+
return removeCarryingComments(fixer, sourceCode, [
|
|
1343
1453
|
prevElement.range[1],
|
|
1344
1454
|
element.range[1],
|
|
1345
|
-
];
|
|
1346
|
-
return fixer.removeRange(range);
|
|
1455
|
+
], 'fromPrevElement');
|
|
1347
1456
|
}
|
|
1348
1457
|
}
|
|
1349
1458
|
// Otherwise, remove the element and the following comma
|
|
1350
1459
|
const nextElement = depsArg.elements[elementIndex + 1];
|
|
1351
1460
|
if (nextElement) {
|
|
1352
|
-
|
|
1461
|
+
return removeCarryingComments(fixer, sourceCode, [
|
|
1353
1462
|
element.range[0],
|
|
1354
1463
|
nextElement.range[0],
|
|
1355
|
-
];
|
|
1356
|
-
return fixer.removeRange(range);
|
|
1464
|
+
], 'toNextElement');
|
|
1357
1465
|
}
|
|
1358
1466
|
// Fallback to just removing the element
|
|
1359
1467
|
return fixer.remove(element);
|
package/package.json
CHANGED
package/release-manifest.json
CHANGED
|
@@ -1,4 +1,32 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "1.20.190",
|
|
4
|
+
"date": "2026-08-29T13:41:20.482Z",
|
|
5
|
+
"rules": [
|
|
6
|
+
{
|
|
7
|
+
"name": "no-entire-object-hook-deps",
|
|
8
|
+
"changeType": "fix",
|
|
9
|
+
"issues": [
|
|
10
|
+
2209
|
|
11
|
+
],
|
|
12
|
+
"summary": "decline a removal that strands its binding (closes #2209)"
|
|
13
|
+
}
|
|
14
|
+
]
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"version": "1.20.189",
|
|
18
|
+
"date": "2026-08-29T11:57:05.586Z",
|
|
19
|
+
"rules": [
|
|
20
|
+
{
|
|
21
|
+
"name": "no-entire-object-hook-deps",
|
|
22
|
+
"changeType": "fix",
|
|
23
|
+
"issues": [
|
|
24
|
+
2208
|
|
25
|
+
],
|
|
26
|
+
"summary": "carry comments in the removed dep's margin (closes #2208)"
|
|
27
|
+
}
|
|
28
|
+
]
|
|
29
|
+
},
|
|
2
30
|
{
|
|
3
31
|
"version": "1.20.188",
|
|
4
32
|
"date": "2026-08-29T07:11:55.294Z",
|