@blumintinc/eslint-plugin-blumint 1.21.13 → 1.21.15
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
|
@@ -1404,6 +1404,68 @@ exports.enforceEmptyObjectCheck = (0, createRule_1.createRule)({
|
|
|
1404
1404
|
}
|
|
1405
1405
|
return false;
|
|
1406
1406
|
}
|
|
1407
|
+
/**
|
|
1408
|
+
* The type the binding behind this value DECLARES, if it declares one.
|
|
1409
|
+
*
|
|
1410
|
+
* Parameters and variables are the two declarations that annotate the
|
|
1411
|
+
* value a guard tests; an import is answered ahead of this by
|
|
1412
|
+
* `tracesToImport`. The annotation has to sit on the BINDING: the one on
|
|
1413
|
+
* `const { config }: Props = load()` describes the container, and reading
|
|
1414
|
+
* it as a verdict on a single property would need exactly the resolution
|
|
1415
|
+
* that failed, so a destructured binding keeps the naming heuristic.
|
|
1416
|
+
*/
|
|
1417
|
+
function declaredTypeOf(identifier) {
|
|
1418
|
+
const variable = variableFor(identifier);
|
|
1419
|
+
if (!variable) {
|
|
1420
|
+
return null;
|
|
1421
|
+
}
|
|
1422
|
+
for (const def of variable.defs) {
|
|
1423
|
+
if (def.type !== 'Parameter' && def.type !== 'Variable') {
|
|
1424
|
+
continue;
|
|
1425
|
+
}
|
|
1426
|
+
if (def.name.typeAnnotation) {
|
|
1427
|
+
return def.name.typeAnnotation.typeAnnotation;
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
return null;
|
|
1431
|
+
}
|
|
1432
|
+
/**
|
|
1433
|
+
* Whether a declared type declares NOTHING.
|
|
1434
|
+
*
|
|
1435
|
+
* An explicit `any` is the checker reporting what the source told it, not
|
|
1436
|
+
* a resolution failure, so it leaves the value in the same position as an
|
|
1437
|
+
* unannotated one and the naming heuristic keeps answering. `unknown` is
|
|
1438
|
+
* excluded from this reading on purpose: `Object.keys` rejects an
|
|
1439
|
+
* `unknown` operand, so the fix the heuristic would attach there does not
|
|
1440
|
+
* typecheck.
|
|
1441
|
+
*/
|
|
1442
|
+
function declaresNothing(node) {
|
|
1443
|
+
if (node.type === utils_1.AST_NODE_TYPES.TSUnionType) {
|
|
1444
|
+
return node.types.some(declaresNothing);
|
|
1445
|
+
}
|
|
1446
|
+
return node.type === utils_1.AST_NODE_TYPES.TSAnyKeyword;
|
|
1447
|
+
}
|
|
1448
|
+
/**
|
|
1449
|
+
* Whether a declared type the checker could not resolve nonetheless
|
|
1450
|
+
* SPELLS an index-signature dictionary.
|
|
1451
|
+
*
|
|
1452
|
+
* `Record<K, V>` is an index signature whichever way `K` and `V` resolve,
|
|
1453
|
+
* so the value is a plain data map rather than a class instance and
|
|
1454
|
+
* `Object.keys` measures its emptiness correctly. Reading that from the
|
|
1455
|
+
* source keeps the verdict a complete program gives — `object` — reachable
|
|
1456
|
+
* without one, which is why a union answers yes as soon as one member
|
|
1457
|
+
* spells it, mirroring `isObjectLikeType`. A reference the source does not
|
|
1458
|
+
* pin this way, `Readonly<NextResponse>` among them, carries no such
|
|
1459
|
+
* guarantee.
|
|
1460
|
+
*/
|
|
1461
|
+
function spellsDictionary(node) {
|
|
1462
|
+
if (node.type === utils_1.AST_NODE_TYPES.TSUnionType) {
|
|
1463
|
+
return node.types.some(spellsDictionary);
|
|
1464
|
+
}
|
|
1465
|
+
return (node.type === utils_1.AST_NODE_TYPES.TSTypeReference &&
|
|
1466
|
+
node.typeName.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
1467
|
+
node.typeName.name === 'Record');
|
|
1468
|
+
}
|
|
1407
1469
|
function isLikelyObject(identifier) {
|
|
1408
1470
|
if (checker && parserServices?.esTreeNodeToTSNodeMap) {
|
|
1409
1471
|
try {
|
|
@@ -1424,13 +1486,39 @@ exports.enforceEmptyObjectCheck = (0, createRule_1.createRule)({
|
|
|
1424
1486
|
* absence of evidence became evidence the value can be `{}`.
|
|
1425
1487
|
*
|
|
1426
1488
|
* The heuristic is still the right answer for a value with no
|
|
1427
|
-
* declaration to resolve — removing it outright silences
|
|
1428
|
-
* rule's own
|
|
1429
|
-
*
|
|
1489
|
+
* declaration to resolve — removing it outright silences 56 of this
|
|
1490
|
+
* rule's own 108 fixtures — so the fall-through is carved out only
|
|
1491
|
+
* where evidence EXISTS and went unread: a value from an unresolved
|
|
1492
|
+
* IMPORT (#2252), and a binding that DECLARES a type the checker
|
|
1493
|
+
* could not resolve (#2344). An unresolvable annotation means the
|
|
1494
|
+
* checker could not look, not that the value is loosely typed:
|
|
1495
|
+
* `response: Readonly<NextResponse>` read off its name alone
|
|
1496
|
+
* reported a class instance, whose state lives behind prototype
|
|
1497
|
+
* accessors, so the prescribed `Object.keys` clause holds for every
|
|
1498
|
+
* valid value and inverts the guard it was meant to harden.
|
|
1499
|
+
*
|
|
1500
|
+
* The carve-out costs reach, and the cost is measured rather than
|
|
1501
|
+
* assumed: across the consumer's 14,059 tracked sources it drops 8
|
|
1502
|
+
* reports, and re-running each under a real `ts.Program` — the
|
|
1503
|
+
* verdict this arm exists to preserve — agrees with 6 of them. The
|
|
1504
|
+
* remaining 2 annotate an all-optional imported type
|
|
1505
|
+
* (`Record<string, string[]>` behind a local alias, and an
|
|
1506
|
+
* `algoliasearch-helper` parameter bag), which a resolved program
|
|
1507
|
+
* calls `object` and reports. Nothing at the annotation site
|
|
1508
|
+
* separates those from `Readonly<NextResponse>`: both are bare
|
|
1509
|
+
* references into a module this program did not load, so recovering
|
|
1510
|
+
* them needs the cross-file resolution whose absence defines this
|
|
1511
|
+
* branch. Two silent true positives is the accepted price of six
|
|
1512
|
+
* guard inversions, because a fix that rewrites correct code costs
|
|
1513
|
+
* more than one the rule declines to make.
|
|
1430
1514
|
*/
|
|
1431
1515
|
if (tracesToImport(identifier)) {
|
|
1432
1516
|
return false;
|
|
1433
1517
|
}
|
|
1518
|
+
const declared = declaredTypeOf(identifier);
|
|
1519
|
+
if (declared && !declaresNothing(declared)) {
|
|
1520
|
+
return spellsDictionary(declared);
|
|
1521
|
+
}
|
|
1434
1522
|
}
|
|
1435
1523
|
catch {
|
|
1436
1524
|
// TypeScript parser services can throw when AST-to-TS node mapping fails; fall back to naming heuristic so linting does not crash.
|
|
@@ -423,6 +423,168 @@ function transactionParticipantsOf(body) {
|
|
|
423
423
|
}
|
|
424
424
|
return participants;
|
|
425
425
|
}
|
|
426
|
+
/**
|
|
427
|
+
* Node types that own the statements written inside them. A walk bounded by
|
|
428
|
+
* them answers about what the method DOES rather than about what it merely
|
|
429
|
+
* defines: a `try`/`finally` written inside a callback is that callback's
|
|
430
|
+
* acquire/release pair, and a field written inside one is written on whatever
|
|
431
|
+
* schedule the callback runs on. The decorator caches the method's own promise,
|
|
432
|
+
* so only the method's own steps bear on whether caching is safe.
|
|
433
|
+
*/
|
|
434
|
+
const OWN_STEP_BOUNDARIES = new Set([
|
|
435
|
+
utils_1.AST_NODE_TYPES.ArrowFunctionExpression,
|
|
436
|
+
utils_1.AST_NODE_TYPES.FunctionDeclaration,
|
|
437
|
+
utils_1.AST_NODE_TYPES.FunctionExpression,
|
|
438
|
+
]);
|
|
439
|
+
/**
|
|
440
|
+
* Every node the enclosing function executes as its own step, `root` included.
|
|
441
|
+
* Descent stops at a nested function, which `subtreeOf` enters.
|
|
442
|
+
*/
|
|
443
|
+
function* ownSubtreeOf(root) {
|
|
444
|
+
yield root;
|
|
445
|
+
for (const [key, value] of Object.entries(root)) {
|
|
446
|
+
if (NON_TRAVERSABLE_KEYS.has(key)) {
|
|
447
|
+
continue;
|
|
448
|
+
}
|
|
449
|
+
const children = Array.isArray(value) ? value : [value];
|
|
450
|
+
for (const child of children) {
|
|
451
|
+
if (ASTHelpers_1.ASTHelpers.isNode(child) && !OWN_STEP_BOUNDARIES.has(child.type)) {
|
|
452
|
+
yield* ownSubtreeOf(child);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
/**
|
|
458
|
+
* Whether the block performs a call as a step of the function that owns it.
|
|
459
|
+
* A call written inside a function the block only DEFINES — `finally { const
|
|
460
|
+
* undo = () => release(t); }` — performs nothing, so the bounded walk is what
|
|
461
|
+
* separates a release from a closure that could perform one elsewhere.
|
|
462
|
+
*/
|
|
463
|
+
function performsCall(block) {
|
|
464
|
+
for (const node of ownSubtreeOf(block)) {
|
|
465
|
+
if (node.type === utils_1.AST_NODE_TYPES.CallExpression) {
|
|
466
|
+
return true;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
return false;
|
|
470
|
+
}
|
|
471
|
+
/**
|
|
472
|
+
* Whether the method owns an acquire/release pair: a `try` among its own steps
|
|
473
|
+
* whose `finalizer` calls something.
|
|
474
|
+
*
|
|
475
|
+
* A `finally` that calls something exists to undo an effect the `try`
|
|
476
|
+
* performed. What such a method hands back describes one attempt — a grant held
|
|
477
|
+
* while a queue ticket was outstanding, a read taken while a lock was held —
|
|
478
|
+
* rather than a fact that stays true once the release has run.
|
|
479
|
+
*
|
|
480
|
+
* An empty `finalizer` releases nothing, and one that only writes a local
|
|
481
|
+
* (`finally { done = true; }`) records that the attempt finished instead of
|
|
482
|
+
* undoing it, so neither costs the method its report.
|
|
483
|
+
*/
|
|
484
|
+
function releasesInOwnFinalizer(fn) {
|
|
485
|
+
for (const node of ownSubtreeOf(fn.body)) {
|
|
486
|
+
if (node.type === utils_1.AST_NODE_TYPES.TryStatement &&
|
|
487
|
+
node.finalizer &&
|
|
488
|
+
performsCall(node.finalizer)) {
|
|
489
|
+
return true;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
return false;
|
|
493
|
+
}
|
|
494
|
+
/**
|
|
495
|
+
* The expression under the wrappers that stand between a value and the access
|
|
496
|
+
* path it evaluates. `await this.pending`, `this.pending!`, `this.pending as
|
|
497
|
+
* Ready` and `this?.pending` all read the same field, and a discriminator blind
|
|
498
|
+
* to one of those spellings would answer "hands back something else" about a
|
|
499
|
+
* method that hands back exactly what it wrote.
|
|
500
|
+
*/
|
|
501
|
+
function withoutValueWrappers(node) {
|
|
502
|
+
switch (node.type) {
|
|
503
|
+
case utils_1.AST_NODE_TYPES.AwaitExpression:
|
|
504
|
+
return withoutValueWrappers(node.argument);
|
|
505
|
+
case utils_1.AST_NODE_TYPES.ChainExpression:
|
|
506
|
+
case utils_1.AST_NODE_TYPES.TSAsExpression:
|
|
507
|
+
case utils_1.AST_NODE_TYPES.TSNonNullExpression:
|
|
508
|
+
case utils_1.AST_NODE_TYPES.TSSatisfiesExpression:
|
|
509
|
+
case utils_1.AST_NODE_TYPES.TSTypeAssertion:
|
|
510
|
+
return withoutValueWrappers(node.expression);
|
|
511
|
+
default:
|
|
512
|
+
return node;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
/**
|
|
516
|
+
* The instance field an expression is rooted at: `cached` for `this.cached`,
|
|
517
|
+
* for `this.cached.value` and for `this.cache[id]` alike.
|
|
518
|
+
*
|
|
519
|
+
* Both sides of the discriminator below read the ROOT, so an element write
|
|
520
|
+
* (`this.cache[id] = …`) and the read that answers it (`return this.cache[id]`)
|
|
521
|
+
* meet on the same name. A computed root (`this[key]`) names no field
|
|
522
|
+
* statically and answers nothing, which leaves the method reporting.
|
|
523
|
+
*/
|
|
524
|
+
function rootThisField(node) {
|
|
525
|
+
const expression = withoutValueWrappers(node);
|
|
526
|
+
if (expression.type !== utils_1.AST_NODE_TYPES.MemberExpression) {
|
|
527
|
+
return undefined;
|
|
528
|
+
}
|
|
529
|
+
const object = withoutValueWrappers(expression.object);
|
|
530
|
+
if (object.type !== utils_1.AST_NODE_TYPES.ThisExpression) {
|
|
531
|
+
return rootThisField(object);
|
|
532
|
+
}
|
|
533
|
+
return !expression.computed &&
|
|
534
|
+
expression.property.type === utils_1.AST_NODE_TYPES.Identifier
|
|
535
|
+
? expression.property.name
|
|
536
|
+
: undefined;
|
|
537
|
+
}
|
|
538
|
+
/**
|
|
539
|
+
* Whether the expression reads a field the method wrote. Depth and nesting are
|
|
540
|
+
* immaterial — `this.cached ?? EMPTY` and `() => this.cached` hand the written
|
|
541
|
+
* state back as plainly as a bare `this.cached` does — because the question is
|
|
542
|
+
* whether the result carries that state at all, not how it is spelled.
|
|
543
|
+
*/
|
|
544
|
+
function readsWrittenField(node, fields) {
|
|
545
|
+
for (const descendant of subtreeOf(node)) {
|
|
546
|
+
const field = rootThisField(descendant);
|
|
547
|
+
if (field !== undefined && fields.has(field)) {
|
|
548
|
+
return true;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
return false;
|
|
552
|
+
}
|
|
553
|
+
/**
|
|
554
|
+
* Whether the method writes an instance field and hands back a result that
|
|
555
|
+
* reads none of the fields it wrote.
|
|
556
|
+
*
|
|
557
|
+
* Such a method reports on an effect: its result says what THIS call did, so
|
|
558
|
+
* the next call has to be free to do it again. Writing a field and returning it
|
|
559
|
+
* is the opposite shape — a hand-rolled cache, which is the very thing
|
|
560
|
+
* `@Memoize()` replaces — and the field read is what separates the two. A
|
|
561
|
+
* single returned read is enough, because a method with one cache-hit path
|
|
562
|
+
* hands back state on that path however many effects its other paths report.
|
|
563
|
+
*
|
|
564
|
+
* A method with no value-returning `return` is left alone. Its result is void
|
|
565
|
+
* by inference rather than by declaration, and #1548 fenced inferred void out
|
|
566
|
+
* of this rule deliberately: an unannotated body carries no declaration of
|
|
567
|
+
* intent to honour.
|
|
568
|
+
*/
|
|
569
|
+
function writesFieldItDoesNotReturn(fn) {
|
|
570
|
+
const written = new Set();
|
|
571
|
+
const results = [];
|
|
572
|
+
for (const node of ownSubtreeOf(fn.body)) {
|
|
573
|
+
if (node.type === utils_1.AST_NODE_TYPES.AssignmentExpression) {
|
|
574
|
+
const field = rootThisField(node.left);
|
|
575
|
+
if (field !== undefined) {
|
|
576
|
+
written.add(field);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
else if (node.type === utils_1.AST_NODE_TYPES.ReturnStatement && node.argument) {
|
|
580
|
+
results.push(node.argument);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
if (written.size === 0 || results.length === 0) {
|
|
584
|
+
return false;
|
|
585
|
+
}
|
|
586
|
+
return !results.some((result) => readsWrittenField(result, written));
|
|
587
|
+
}
|
|
426
588
|
/** The statically known name of a method, for matching call sites against it. */
|
|
427
589
|
function methodName(node) {
|
|
428
590
|
if (node.computed) {
|
|
@@ -664,6 +826,37 @@ exports.enforceMemoizeAsync = (0, createRule_1.createRule)({
|
|
|
664
826
|
if (participatesInTransaction(node, node.value)) {
|
|
665
827
|
return;
|
|
666
828
|
}
|
|
829
|
+
// A `finally` that calls something exists to undo an effect the `try`
|
|
830
|
+
// performed: the method takes a resource, works while it holds it, and
|
|
831
|
+
// gives it back on the way out. What it hands back describes that one
|
|
832
|
+
// attempt, not a fact that stays true afterwards. Cached, the
|
|
833
|
+
// acquire/release pair runs once per instance and every later caller
|
|
834
|
+
// receives a value computed while a resource the `finally` has since
|
|
835
|
+
// released was still held — the hazard the resource-handle exemption
|
|
836
|
+
// above names, seen from the side where the method releases the handle
|
|
837
|
+
// itself, so nothing of it reaches the return type for a signature-keyed
|
|
838
|
+
// gate to read. The failure is silent and arrives only under
|
|
839
|
+
// concurrency, so the fixer would apply it unattended under `--fix`,
|
|
840
|
+
// and both report and fix are withheld.
|
|
841
|
+
if (releasesInOwnFinalizer(node.value)) {
|
|
842
|
+
return;
|
|
843
|
+
}
|
|
844
|
+
// A method that writes an instance field and hands back a result
|
|
845
|
+
// reading none of the fields it wrote reports on an effect: the result
|
|
846
|
+
// says what THIS call did — "I reclaimed the slot" — and the next call
|
|
847
|
+
// has to be free to do it again. Cached, the write happens once per
|
|
848
|
+
// instance while every later caller reads the first call's verdict
|
|
849
|
+
// about work that call alone performed, and a caller passing a freshly
|
|
850
|
+
// built argument object instead accumulates one dead entry per call.
|
|
851
|
+
//
|
|
852
|
+
// The read is the discriminator, and it is deliberately the whole test:
|
|
853
|
+
// a method that writes a field and returns it is a hand-rolled cache,
|
|
854
|
+
// which is the shape `@Memoize()` exists to replace, so it keeps both
|
|
855
|
+
// report and fix. Carving out every write to `this` would silence that
|
|
856
|
+
// shape along with these.
|
|
857
|
+
if (writesFieldItDoesNotReturn(node.value)) {
|
|
858
|
+
return;
|
|
859
|
+
}
|
|
667
860
|
const { aliases: memoizeAliases, namespaces: memoizeNamespaces } = memoizeImports();
|
|
668
861
|
const hasMemoizeImport = memoizeAliases.size > 0 || memoizeNamespaces.size > 0;
|
|
669
862
|
// Check if method already has @Memoize or @Memoize() decorator
|