@blumintinc/eslint-plugin-blumint 1.20.91 → 1.20.93
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-date-ttime.d.ts +2 -1
- package/lib/rules/enforce-date-ttime.js +39 -5
- package/lib/rules/no-redundant-annotation-assertion.js +45 -5
- package/lib/rules/prefer-map-over-conditional-dispatch.js +21 -1
- package/lib/rules/prefer-use-deep-compare-memo.js +86 -60
- package/package.json +1 -1
- package/release-manifest.json +44 -0
package/lib/index.js
CHANGED
|
@@ -1 +1,2 @@
|
|
|
1
|
-
|
|
1
|
+
import { TSESLint } from '@typescript-eslint/utils';
|
|
2
|
+
export declare const enforceDateTTime: TSESLint.RuleModule<"enforceDateTTime", [], TSESLint.RuleListener>;
|
|
@@ -27,6 +27,7 @@ exports.enforceDateTTime = void 0;
|
|
|
27
27
|
const utils_1 = require("@typescript-eslint/utils");
|
|
28
28
|
const ts = __importStar(require("typescript"));
|
|
29
29
|
const createRule_1 = require("../utils/createRule");
|
|
30
|
+
const importRemoval_1 = require("../utils/importRemoval");
|
|
30
31
|
exports.enforceDateTTime = (0, createRule_1.createRule)({
|
|
31
32
|
name: 'enforce-date-ttime',
|
|
32
33
|
meta: {
|
|
@@ -43,7 +44,8 @@ exports.enforceDateTTime = (0, createRule_1.createRule)({
|
|
|
43
44
|
},
|
|
44
45
|
defaultOptions: [],
|
|
45
46
|
create(context) {
|
|
46
|
-
const sourceCode = context
|
|
47
|
+
const sourceCode = context
|
|
48
|
+
.sourceCode ?? context.getSourceCode();
|
|
47
49
|
const parserServices = sourceCode?.parserServices ?? context.parserServices;
|
|
48
50
|
const checker = parserServices?.program?.getTypeChecker();
|
|
49
51
|
if (!parserServices || !checker) {
|
|
@@ -139,14 +141,46 @@ exports.enforceDateTTime = (0, createRule_1.createRule)({
|
|
|
139
141
|
});
|
|
140
142
|
}
|
|
141
143
|
else if (!isExactDate(tTimeArg)) {
|
|
142
|
-
// TTime is provided but is not exactly Date
|
|
144
|
+
// TTime is provided but is not exactly Date. Overwriting the argument
|
|
145
|
+
// deletes every name it mentions, so the rewrite and the unbinding of
|
|
146
|
+
// whatever it was the last reference to are one fix: applying either
|
|
147
|
+
// half alone leaves the file worse than applying neither, and since
|
|
148
|
+
// this rule's own report is resolved by the fix, nothing re-reports
|
|
149
|
+
// the debt an orphaned declaration becomes.
|
|
150
|
+
//
|
|
151
|
+
// Orphanhood is judged against this one argument's own range and the
|
|
152
|
+
// file as it stands, never against what the rest of the `--fix` run
|
|
153
|
+
// might also overwrite. A sibling argument naming the same alias may
|
|
154
|
+
// be `eslint-disable`d — which a rule cannot see, since suppression is
|
|
155
|
+
// applied to reports after they are emitted — so an edit assuming its
|
|
156
|
+
// sibling will also go unbinds an import the survivor still
|
|
157
|
+
// references, trading an unused import for a dangling type. Judging
|
|
158
|
+
// one edit at a time is suppression-safe by construction.
|
|
159
|
+
const importRanges = (0, importRemoval_1.planOrphanedImportRemoval)(sourceCode, [
|
|
160
|
+
tTimeArg.range,
|
|
161
|
+
]);
|
|
143
162
|
context.report({
|
|
144
163
|
node: tTimeArg,
|
|
145
164
|
messageId: 'enforceDateTTime',
|
|
146
165
|
data: { typeName },
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
166
|
+
// No plan means the argument holds the last reference to something
|
|
167
|
+
// the helper cannot rewrite — a locally declared alias, or the
|
|
168
|
+
// enclosing declaration's own type parameter — so the argument
|
|
169
|
+
// stays as written: the report without a fixer is the lesser
|
|
170
|
+
// damage. Deleting a declaration is a materially riskier edit than
|
|
171
|
+
// dropping an import specifier, and a type parameter left unread by
|
|
172
|
+
// its own body fails `no-unused-vars` and `noUnusedParameters`
|
|
173
|
+
// exactly as an orphaned alias does, on top of turning a
|
|
174
|
+
// pass-through generic into one whose argument no longer means
|
|
175
|
+
// anything.
|
|
176
|
+
...(importRanges
|
|
177
|
+
? {
|
|
178
|
+
fix: (fixer) => [
|
|
179
|
+
fixer.replaceText(tTimeArg, 'Date'),
|
|
180
|
+
...importRanges.map((range) => fixer.removeRange([range[0], range[1]])),
|
|
181
|
+
],
|
|
182
|
+
}
|
|
183
|
+
: {}),
|
|
150
184
|
});
|
|
151
185
|
}
|
|
152
186
|
},
|
|
@@ -29,6 +29,7 @@ const visitor_keys_1 = require("@typescript-eslint/visitor-keys");
|
|
|
29
29
|
const ts = __importStar(require("typescript"));
|
|
30
30
|
const ASTHelpers_1 = require("../utils/ASTHelpers");
|
|
31
31
|
const createRule_1 = require("../utils/createRule");
|
|
32
|
+
const importRemoval_1 = require("../utils/importRemoval");
|
|
32
33
|
/**
|
|
33
34
|
* Type string formatting flags chosen to keep comparisons stable and predictable.
|
|
34
35
|
* - NoTruncation avoids `...` elisions that can hide important differences.
|
|
@@ -143,10 +144,15 @@ function findTypeAnnotationStart(typeAnnotation, sourceCode) {
|
|
|
143
144
|
}
|
|
144
145
|
return removalStart;
|
|
145
146
|
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
147
|
+
/**
|
|
148
|
+
* The slice a fix deletes to drop `typeAnnotation`: the `:` and the whitespace
|
|
149
|
+
* separating it from the declared name go with the type.
|
|
150
|
+
*/
|
|
151
|
+
function annotationRemovalRange(typeAnnotation, sourceCode) {
|
|
152
|
+
return [
|
|
153
|
+
findTypeAnnotationStart(typeAnnotation, sourceCode),
|
|
154
|
+
typeAnnotation.range[1],
|
|
155
|
+
];
|
|
150
156
|
}
|
|
151
157
|
function typeText(type, checker) {
|
|
152
158
|
return checker.typeToString(type, undefined, typeFormatFlags());
|
|
@@ -338,15 +344,49 @@ exports.noRedundantAnnotationAssertion = (0, createRule_1.createRule)({
|
|
|
338
344
|
return {};
|
|
339
345
|
}
|
|
340
346
|
const checker = parserServices.program.getTypeChecker();
|
|
347
|
+
/**
|
|
348
|
+
* Reports the redundant annotation, taking with it any import it was the
|
|
349
|
+
* only consumer of. The two are one fix: applying either half alone leaves
|
|
350
|
+
* the file worse than applying neither — a stripped annotation with its
|
|
351
|
+
* import left behind fails `no-unused-vars`, and since this rule's own
|
|
352
|
+
* report is resolved by the fix, nothing re-reports the debt.
|
|
353
|
+
*
|
|
354
|
+
* Orphanhood is judged against this one annotation's own removal and the
|
|
355
|
+
* file as it stands, never against what the rest of the `--fix` run might
|
|
356
|
+
* also delete. A sibling annotation naming the same type may be
|
|
357
|
+
* `eslint-disable`d — which a rule cannot see, since suppression is applied
|
|
358
|
+
* to reports after they are emitted — so an edit that assumes its sibling
|
|
359
|
+
* will also go deletes an import the surviving annotation still references,
|
|
360
|
+
* trading an unused import for a dangling type. Judging one edit at a time
|
|
361
|
+
* is suppression-safe by construction: a suppressed report's fix never
|
|
362
|
+
* applies, so it can never have been depended on.
|
|
363
|
+
*
|
|
364
|
+
* A type the annotation names but the helper cannot rewrite — a local alias,
|
|
365
|
+
* an interface, a type parameter — declines the fix outright. Deleting a
|
|
366
|
+
* declaration is a materially riskier edit than dropping an import
|
|
367
|
+
* specifier, and the author is better placed to decide whether the type
|
|
368
|
+
* should go or be used elsewhere.
|
|
369
|
+
*/
|
|
341
370
|
function reportIfRedundant(annotation, assertion, reportNode, fixerTarget) {
|
|
342
371
|
const matchingType = haveMatchingTypes(annotation.typeAnnotation, assertion, checker, parserServices);
|
|
343
372
|
if (!matchingType)
|
|
344
373
|
return;
|
|
374
|
+
const removal = annotationRemovalRange(fixerTarget, sourceCode);
|
|
375
|
+
const importRanges = (0, importRemoval_1.planOrphanedImportRemoval)(sourceCode, [removal]);
|
|
345
376
|
context.report({
|
|
346
377
|
node: reportNode,
|
|
347
378
|
messageId: 'redundantAnnotationAndAssertion',
|
|
348
379
|
data: { type: matchingType },
|
|
349
|
-
|
|
380
|
+
// No plan means no binding can be unbound safely, so the annotation
|
|
381
|
+
// stays too: the report without a fixer is the lesser damage.
|
|
382
|
+
...(importRanges
|
|
383
|
+
? {
|
|
384
|
+
fix: (fixer) => [
|
|
385
|
+
fixer.removeRange([removal[0], removal[1]]),
|
|
386
|
+
...importRanges.map((range) => fixer.removeRange([range[0], range[1]])),
|
|
387
|
+
],
|
|
388
|
+
}
|
|
389
|
+
: {}),
|
|
350
390
|
});
|
|
351
391
|
}
|
|
352
392
|
return {
|
|
@@ -203,6 +203,15 @@ exports.preferMapOverConditionalDispatch = (0, createRule_1.createRule)({
|
|
|
203
203
|
* Resolve a case-test / equality-test node to a literal key. Handles inline
|
|
204
204
|
* literals directly and constant references (e.g. `THIS_DEVICE_STATUS.active`)
|
|
205
205
|
* via the checker.
|
|
206
|
+
*
|
|
207
|
+
* A test the checker had to resolve carries its source text along with the
|
|
208
|
+
* value: the value proves the key is a literal, but emitting the value as
|
|
209
|
+
* the key destroys the expression it came from. For a constant reference
|
|
210
|
+
* that orphans the constant (an imported one then trips `no-unused-vars`)
|
|
211
|
+
* and bakes its value into the call site, discarding the single source of
|
|
212
|
+
* truth the constant exists to provide; for `case -1:` the resolved value
|
|
213
|
+
* does not even print as a legal object key (`{ -1: v }` does not parse).
|
|
214
|
+
* The fixer emits a computed key from the source text instead.
|
|
206
215
|
*/
|
|
207
216
|
function resolveLiteralKey(node) {
|
|
208
217
|
if (node.type === utils_1.AST_NODE_TYPES.Literal) {
|
|
@@ -218,7 +227,11 @@ exports.preferMapOverConditionalDispatch = (0, createRule_1.createRule)({
|
|
|
218
227
|
if (!type) {
|
|
219
228
|
return null;
|
|
220
229
|
}
|
|
221
|
-
|
|
230
|
+
const literal = literalValueOf(type);
|
|
231
|
+
if (!literal) {
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
return { ...literal, sourceText: sourceCode.getText(node) };
|
|
222
235
|
}
|
|
223
236
|
function computeValueTypeText(exprs) {
|
|
224
237
|
const seen = new Set();
|
|
@@ -589,6 +602,13 @@ exports.preferMapOverConditionalDispatch = (0, createRule_1.createRule)({
|
|
|
589
602
|
}
|
|
590
603
|
// ---- Fix construction ---------------------------------------------------
|
|
591
604
|
function formatKey(key) {
|
|
605
|
+
if (key.sourceText !== undefined) {
|
|
606
|
+
// Computed key preserving the constant reference the case tested on.
|
|
607
|
+
// Type-safe by construction: the fix is reached only after the checker
|
|
608
|
+
// resolved this expression to a string/number *literal* type, so the
|
|
609
|
+
// computed key is a literal key and the Record stays exhaustive.
|
|
610
|
+
return `[${key.sourceText}]`;
|
|
611
|
+
}
|
|
592
612
|
if (key.kind === 'number') {
|
|
593
613
|
return String(key.value);
|
|
594
614
|
}
|
|
@@ -6,6 +6,7 @@ const createRule_1 = require("../utils/createRule");
|
|
|
6
6
|
const ASTHelpers_1 = require("../utils/ASTHelpers");
|
|
7
7
|
const importInsertion_1 = require("../utils/importInsertion");
|
|
8
8
|
const importRemoval_1 = require("../utils/importRemoval");
|
|
9
|
+
const disableDirectives_1 = require("../utils/disableDirectives");
|
|
9
10
|
const DEEP_COMPARE_MODULE = '@blumintinc/use-deep-compare';
|
|
10
11
|
const DEEP_COMPARE_HOOK = 'useDeepCompareMemo';
|
|
11
12
|
// Consider these as memoizing hooks producing stable references
|
|
@@ -200,6 +201,50 @@ function bindsHookImport(variable, hookImport) {
|
|
|
200
201
|
variable.defs.length > 0 &&
|
|
201
202
|
variable.defs.every((def) => def.node === hookImport));
|
|
202
203
|
}
|
|
204
|
+
/**
|
|
205
|
+
* Whether the name the rewrite spells resolves, at this call site, to nothing or
|
|
206
|
+
* to the very import this fix would insert. Any other binding makes the edit
|
|
207
|
+
* wrong twice over: the inserted import declares the name a second time
|
|
208
|
+
* (TS2440/TS2300), and a shadowing parameter or local silently routes the call
|
|
209
|
+
* to the wrong value with no diagnostic at all. Such a call site keeps its
|
|
210
|
+
* report and stays out of the batch, so the author migrates it deliberately —
|
|
211
|
+
* and, still spelling `useMemo`, it holds the specifier the batch would
|
|
212
|
+
* otherwise unbind.
|
|
213
|
+
*/
|
|
214
|
+
function emitsResolvableHook(call, hookImport) {
|
|
215
|
+
const existing = ASTHelpers_1.ASTHelpers.findVariableInScope(call.scope, DEEP_COMPARE_HOOK);
|
|
216
|
+
return !existing || bindsHookImport(existing, hookImport);
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* The single fix that converts every call in `calls`, imports the hook once, and
|
|
220
|
+
* unbinds whatever the conversions stop reading.
|
|
221
|
+
*
|
|
222
|
+
* Batching is what makes the unbinding reachable at all. Judged one call at a
|
|
223
|
+
* time, a file with two convertible calls never sees either as the specifier's
|
|
224
|
+
* sole remaining reference, and once both are rewritten the rule no longer
|
|
225
|
+
* reports — so nothing revisits the stranded import. The batch is sound only
|
|
226
|
+
* because it contains exactly the calls this one fix rewrites: siblings the
|
|
227
|
+
* caller has already dropped for being suppressed or unfixable are absent, so no
|
|
228
|
+
* unbinding is ever claimed on the strength of an edit that does not happen.
|
|
229
|
+
*/
|
|
230
|
+
function convertCallsFixes(context, fixer, calls) {
|
|
231
|
+
// The callees are the text this fix deletes, so they are also the text
|
|
232
|
+
// whatever carried the hook stops being read from: `useMemo` for a bare call,
|
|
233
|
+
// `React` for a member call. A binding left with no reference at all is
|
|
234
|
+
// unbound here, in this same fix — stripping its last use and keeping the
|
|
235
|
+
// declaration trades this rule's report for an unused-import one, and nothing
|
|
236
|
+
// re-reports that debt once the rewrite has resolved the original violation.
|
|
237
|
+
const importRemoval = (0, importRemoval_1.planOrphanedImportRemoval)(context.sourceCode, calls.map((call) => call.node.callee.range));
|
|
238
|
+
// No plan means a binding is orphaned yet cannot be unbound safely, so the
|
|
239
|
+
// rewrite stays too: the report without a fixer is the lesser damage.
|
|
240
|
+
if (!importRemoval)
|
|
241
|
+
return null;
|
|
242
|
+
return [
|
|
243
|
+
...calls.map((call) => fixer.replaceText(call.node.callee, DEEP_COMPARE_HOOK)),
|
|
244
|
+
...ensureDeepCompareImportFixes(context, fixer),
|
|
245
|
+
...importRemoval.map((range) => fixer.removeRange([range[0], range[1]])),
|
|
246
|
+
];
|
|
247
|
+
}
|
|
203
248
|
function ensureDeepCompareImportFixes(context, fixer) {
|
|
204
249
|
const sourceCode = context.sourceCode;
|
|
205
250
|
const program = sourceCode.ast;
|
|
@@ -312,6 +357,18 @@ exports.preferUseDeepCompareMemo = (0, createRule_1.createRule)({
|
|
|
312
357
|
defaultOptions: [],
|
|
313
358
|
create(context) {
|
|
314
359
|
const memoizedIds = collectMemoizedIdentifiers(context);
|
|
360
|
+
// Reporting is deferred to Program:exit because the import rewrite depends
|
|
361
|
+
// on knowing every conversion in the file: the `useMemo` specifier may only
|
|
362
|
+
// be unbound once no reference to it survives the fix, and a file where two
|
|
363
|
+
// call sites convert in the same pass has no later pass to notice that.
|
|
364
|
+
const calls = [];
|
|
365
|
+
/**
|
|
366
|
+
* A suppressed report is discarded together with its fix, yet its
|
|
367
|
+
* `useMemo(...)` call stays in the file. Counting such a call as converted
|
|
368
|
+
* would unbind an import the surviving text still spells — trading an unused
|
|
369
|
+
* import for a dangling reference, a lint warning for a compile error.
|
|
370
|
+
*/
|
|
371
|
+
const isReportSuppressed = (0, disableDirectives_1.createSuppressionChecker)(context);
|
|
315
372
|
return {
|
|
316
373
|
CallExpression(node) {
|
|
317
374
|
if (!isUseMemoCallee(node.callee))
|
|
@@ -370,66 +427,35 @@ exports.preferUseDeepCompareMemo = (0, createRule_1.createRule)({
|
|
|
370
427
|
}
|
|
371
428
|
if (!hasUnmemoizedNonPrimitive)
|
|
372
429
|
return;
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
// report for an unused-import one, and nothing re-reports that debt
|
|
403
|
-
// once the rewrite has resolved the original violation.
|
|
404
|
-
//
|
|
405
|
-
// Orphanhood is judged against this one callee's removal and the
|
|
406
|
-
// file as it stands. A second `useMemo` call site keeps the
|
|
407
|
-
// specifier, even one this rule also reports: that sibling may be
|
|
408
|
-
// `eslint-disable`d (suppression is applied after a rule emits its
|
|
409
|
-
// reports, so its fix never runs) or lose its fix to a conflict, and
|
|
410
|
-
// either way the surviving call would spell a name nothing binds. A
|
|
411
|
-
// later pass, reading a source where the sibling is already
|
|
412
|
-
// converted, finds the specifier orphaned and removes it then.
|
|
413
|
-
const importRemoval = (0, importRemoval_1.planOrphanedImportRemoval)(context.sourceCode, [node.callee.range]);
|
|
414
|
-
// No plan means a binding is orphaned yet cannot be unbound safely,
|
|
415
|
-
// so the rewrite stays too: the report without a fixer is the lesser
|
|
416
|
-
// damage.
|
|
417
|
-
if (!importRemoval)
|
|
418
|
-
return null;
|
|
419
|
-
const fixes = [];
|
|
420
|
-
// Replace callee
|
|
421
|
-
if (node.callee.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
422
|
-
fixes.push(fixer.replaceText(node.callee, 'useDeepCompareMemo'));
|
|
423
|
-
}
|
|
424
|
-
else if (node.callee.type === utils_1.AST_NODE_TYPES.MemberExpression) {
|
|
425
|
-
fixes.push(fixer.replaceText(node.callee, 'useDeepCompareMemo'));
|
|
426
|
-
}
|
|
427
|
-
// Ensure import exists
|
|
428
|
-
fixes.push(...ensureDeepCompareImportFixes(context, fixer));
|
|
429
|
-
fixes.push(...importRemoval.map((range) => fixer.removeRange([range[0], range[1]])));
|
|
430
|
-
return fixes;
|
|
431
|
-
},
|
|
432
|
-
});
|
|
430
|
+
calls.push({ node, scope: ASTHelpers_1.ASTHelpers.getScope(context, node) });
|
|
431
|
+
},
|
|
432
|
+
'Program:exit'() {
|
|
433
|
+
if (calls.length === 0)
|
|
434
|
+
return;
|
|
435
|
+
const hookImport = findDeepCompareMemoImport(context.sourceCode.ast);
|
|
436
|
+
// Exactly the calls the carrier's fix rewrites: a suppressed report
|
|
437
|
+
// loses its fix, and one whose scope binds the hook name to something
|
|
438
|
+
// else must not be rewritten at all.
|
|
439
|
+
const converted = calls.filter((call) => !isReportSuppressed(call.node) &&
|
|
440
|
+
emitsResolvableHook(call, hookImport));
|
|
441
|
+
// The carrier is the first violation whose fix actually survives, so a
|
|
442
|
+
// suppressed or unfixable leading violation cannot take the batch down
|
|
443
|
+
// with it. Every other report emits without a fixer: the carrier's one
|
|
444
|
+
// pass already resolves them, and a second fixer would either duplicate
|
|
445
|
+
// its edits or contradict them.
|
|
446
|
+
const [carrier] = converted;
|
|
447
|
+
for (const call of calls) {
|
|
448
|
+
context.report({
|
|
449
|
+
node: call.node,
|
|
450
|
+
messageId: 'preferUseDeepCompareMemo',
|
|
451
|
+
data: {
|
|
452
|
+
hook: 'useMemo',
|
|
453
|
+
},
|
|
454
|
+
fix: call === carrier
|
|
455
|
+
? (fixer) => convertCallsFixes(context, fixer, converted)
|
|
456
|
+
: null,
|
|
457
|
+
});
|
|
458
|
+
}
|
|
433
459
|
},
|
|
434
460
|
};
|
|
435
461
|
},
|
package/package.json
CHANGED
package/release-manifest.json
CHANGED
|
@@ -1,4 +1,48 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "1.20.93",
|
|
4
|
+
"date": "2026-08-03T22:34:05.616Z",
|
|
5
|
+
"rules": [
|
|
6
|
+
{
|
|
7
|
+
"name": "enforce-date-ttime",
|
|
8
|
+
"changeType": "fix",
|
|
9
|
+
"issues": [
|
|
10
|
+
1667
|
|
11
|
+
],
|
|
12
|
+
"summary": "keep the alias it rewrites away (closes #1667)"
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"name": "no-redundant-annotation-assertion",
|
|
16
|
+
"changeType": "fix",
|
|
17
|
+
"issues": [
|
|
18
|
+
1666
|
|
19
|
+
],
|
|
20
|
+
"summary": "keep the type its annotation named (closes #1666)"
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"version": "1.20.92",
|
|
26
|
+
"date": "2026-08-03T19:30:56.799Z",
|
|
27
|
+
"rules": [
|
|
28
|
+
{
|
|
29
|
+
"name": "prefer-map-over-conditional-dispatch",
|
|
30
|
+
"changeType": "fix",
|
|
31
|
+
"issues": [
|
|
32
|
+
1663
|
|
33
|
+
],
|
|
34
|
+
"summary": "emit computed keys for resolved case tests (closes #1663)"
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"name": "prefer-use-deep-compare-memo",
|
|
38
|
+
"changeType": "fix",
|
|
39
|
+
"issues": [
|
|
40
|
+
1662
|
|
41
|
+
],
|
|
42
|
+
"summary": "convert every call site in one edit (closes #1662)"
|
|
43
|
+
}
|
|
44
|
+
]
|
|
45
|
+
},
|
|
2
46
|
{
|
|
3
47
|
"version": "1.20.91",
|
|
4
48
|
"date": "2026-08-03T15:15:42.831Z",
|