@blumintinc/eslint-plugin-blumint 1.20.95 → 1.20.97

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.
@@ -439,11 +439,7 @@ function contractCoversName(typeName, memberName, index, visited) {
439
439
  }
440
440
  const alias = index.typeAliases.get(typeName);
441
441
  if (alias) {
442
- // An alias to anything but a type literal (an intersection, a mapped type,
443
- // a reference to an imported type) hides its member list from a syntactic
444
- // reader, so it is treated as unreadable.
445
- return (alias.typeAnnotation.type !== utils_1.AST_NODE_TYPES.TSTypeLiteral ||
446
- membersDeclareName(alias.typeAnnotation.members, memberName));
442
+ return aliasCoversName(alias.typeAnnotation, memberName, index, visited);
447
443
  }
448
444
  const classDeclaration = index.classes.get(typeName);
449
445
  if (classDeclaration) {
@@ -454,6 +450,57 @@ function contractCoversName(typeName, memberName, index, visited) {
454
450
  // and its members are unreadable here.
455
451
  return true;
456
452
  }
453
+ /**
454
+ * Reports whether the type an alias names accounts for `memberName`.
455
+ *
456
+ * A type literal lists its members outright. An intersection is readable as far
457
+ * as its constituents are, which matters because `prefer-type-over-interface`
458
+ * ships in the same recommended config and rewrites
459
+ * `interface S extends Base { … }` to `type S = Base & { … }` by `--fix`:
460
+ * treating every non-literal alias as unreadable would let that sibling fix
461
+ * retire the member check for every contract written as an interface with a
462
+ * heritage clause (#1679). Anything else — a union, a mapped or conditional
463
+ * type, a reference whose target lives in another module — hides its member
464
+ * list from a syntactic reader and stays unreadable, so the member keeps its
465
+ * exemption.
466
+ */
467
+ function aliasCoversName(typeNode, memberName, index, visited) {
468
+ switch (typeNode.type) {
469
+ case utils_1.AST_NODE_TYPES.TSTypeLiteral:
470
+ return membersDeclareName(typeNode.members, memberName);
471
+ case utils_1.AST_NODE_TYPES.TSIntersectionType:
472
+ return intersectionCoversName(typeNode, memberName, index, visited);
473
+ default:
474
+ return true;
475
+ }
476
+ }
477
+ /**
478
+ * Reports whether an intersection accounts for `memberName`. An intersection
479
+ * contributes every member of every constituent, so one constituent declaring
480
+ * the name settles the question — and a constituent whose members cannot be
481
+ * read settles it too, in the exempting direction: the name may well be that
482
+ * hidden part's, and this plugin prefers a false negative over a false positive
483
+ * (#1350). The member is only left to the class author when every constituent
484
+ * is readable and none of them declares it.
485
+ */
486
+ function intersectionCoversName(intersection, memberName, index, visited) {
487
+ return intersection.types.some((constituent) => constituentCoversName(constituent, memberName, index, visited));
488
+ }
489
+ /**
490
+ * Answers `intersectionCoversName` for one constituent. A reference is followed
491
+ * through the file's declarations by the same resolver a heritage clause uses,
492
+ * so a contract split across a local base and an inline literal reads as a
493
+ * whole; a reference the file does not declare, and a qualified or otherwise
494
+ * opaque constituent, count as unreadable.
495
+ */
496
+ function constituentCoversName(typeNode, memberName, index, visited) {
497
+ if (typeNode.type === utils_1.AST_NODE_TYPES.TSTypeReference) {
498
+ return typeNode.typeName.type === utils_1.AST_NODE_TYPES.Identifier
499
+ ? contractCoversName(typeNode.typeName.name, memberName, index, visited)
500
+ : true;
501
+ }
502
+ return aliasCoversName(typeNode, memberName, index, visited);
503
+ }
457
504
  function heritageCoversName(heritage, memberName, index, visited) {
458
505
  return heritage.some((clause) => {
459
506
  const typeName = heritageTypeName(clause.expression);
@@ -2,6 +2,7 @@ type Options = [
2
2
  {
3
3
  coordinatorPatterns?: string[];
4
4
  rateLimitedPatterns?: string[];
5
+ ignoreTestFiles?: boolean;
5
6
  }
6
7
  ];
7
8
  export declare const parallelizeLoopAwaits: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<"parallelizeLoopAwaits", Options, import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleListener>;
@@ -3,6 +3,25 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.parallelizeLoopAwaits = void 0;
4
4
  const utils_1 = require("@typescript-eslint/utils");
5
5
  const createRule_1 = require("../utils/createRule");
6
+ // Anchored at the end of the path so multi-part suffixes such as
7
+ // `EventRegistry.integration.test.ts` are recognized while production modules
8
+ // that merely contain the word (`testHelpers.ts`, `latest.ts`, `contest/Thing.ts`)
9
+ // keep their enforcement.
10
+ const TEST_FILE_SUFFIX = /\.(test|spec)\.[cm]?[jt]sx?$/;
11
+ // Jest convention directories hold test-only modules regardless of file name.
12
+ const TEST_FILE_DIRECTORY = /(^|\/)(__tests__|__mocks__)\//;
13
+ /**
14
+ * A test suite serves no requests and is not latency-critical, so the rule's
15
+ * rationale — that sequential awaits make network and I/O latency add up — does
16
+ * not apply to it. A loop in a suite instead replays one entrypoint to exercise
17
+ * behavior that accumulates across calls: each iteration must observe the state
18
+ * the previous iteration stored, and that state usually lives in a mock closure
19
+ * the loop body never names. The dependency is a side effect rather than a
20
+ * value, so it is invisible to every syntactic barrier below, and
21
+ * `Promise.all` would let all iterations observe the same initial state
22
+ * (issues #1395, #1687).
23
+ */
24
+ const isTestFile = (filename) => TEST_FILE_SUFFIX.test(filename) || TEST_FILE_DIRECTORY.test(filename);
6
25
  const DEFAULT_COORDINATOR_PATTERNS = [
7
26
  'batchManager',
8
27
  'batch',
@@ -23,8 +42,16 @@ const defaultOptions = [
23
42
  {
24
43
  coordinatorPatterns: DEFAULT_COORDINATOR_PATTERNS,
25
44
  rateLimitedPatterns: DEFAULT_RATE_LIMITED_PATTERNS,
45
+ ignoreTestFiles: true,
26
46
  },
27
47
  ];
48
+ const LOOP_NODE_TYPES = new Set([
49
+ utils_1.AST_NODE_TYPES.ForOfStatement,
50
+ utils_1.AST_NODE_TYPES.ForInStatement,
51
+ utils_1.AST_NODE_TYPES.ForStatement,
52
+ utils_1.AST_NODE_TYPES.WhileStatement,
53
+ utils_1.AST_NODE_TYPES.DoWhileStatement,
54
+ ]);
28
55
  exports.parallelizeLoopAwaits = (0, createRule_1.createRule)({
29
56
  name: 'parallelize-loop-awaits',
30
57
  meta: {
@@ -48,6 +75,16 @@ exports.parallelizeLoopAwaits = (0, createRule_1.createRule)({
48
75
  items: { type: 'string' },
49
76
  default: DEFAULT_RATE_LIMITED_PATTERNS,
50
77
  },
78
+ // Deliberately carries no schema `default`. ESLint validates rule
79
+ // options with an ajv instance configured `useDefaults: true`, which
80
+ // writes schema defaults INTO the supplied options object before
81
+ // `defaultOptions` are merged, so a schema default here would decide
82
+ // the value for every consumer who passes an options object at all.
83
+ // `defaultOptions` plus the `?? true` read below is the single source
84
+ // of truth.
85
+ ignoreTestFiles: {
86
+ type: 'boolean',
87
+ },
51
88
  },
52
89
  additionalProperties: false,
53
90
  },
@@ -58,6 +95,14 @@ exports.parallelizeLoopAwaits = (0, createRule_1.createRule)({
58
95
  },
59
96
  defaultOptions,
60
97
  create(context, [options]) {
98
+ // Normalize Windows backslash separators so the forward-slash directory
99
+ // check matches on every platform. Without this, `getFilename()` returns
100
+ // `C:\repo\src\__tests__\Foo.ts` on Windows and the exemption silently
101
+ // fails there.
102
+ const filename = context.getFilename().replace(/\\/g, '/');
103
+ if ((options?.ignoreTestFiles ?? true) && isTestFile(filename)) {
104
+ return {};
105
+ }
61
106
  const coordinatorPatterns = options?.coordinatorPatterns ?? DEFAULT_COORDINATOR_PATTERNS;
62
107
  const rateLimitedPatterns = options?.rateLimitedPatterns ?? DEFAULT_RATE_LIMITED_PATTERNS;
63
108
  /**
@@ -72,12 +117,11 @@ exports.parallelizeLoopAwaits = (0, createRule_1.createRule)({
72
117
  return lower === pat || lower.startsWith(pat) || lower.endsWith(pat);
73
118
  }
74
119
  /**
75
- * Recursively collects all identifier names referenced in a node.
76
- * Does not cross nested function boundaries.
120
+ * Recursively collects the names a node REFERENCES. With `stopAtFunctions`
121
+ * the walk halts at a nested function, so a name written inside a callback
122
+ * describes that callback's scope rather than the surrounding loop's.
77
123
  */
78
124
  function collectIdentifiers(node, names, stopAtFunctions = false) {
79
- if (stopAtFunctions && node.type !== node.type)
80
- return; // no-op placeholder
81
125
  if (node.type === utils_1.AST_NODE_TYPES.Identifier) {
82
126
  names.add(node.name);
83
127
  return;
@@ -89,6 +133,20 @@ exports.parallelizeLoopAwaits = (0, createRule_1.createRule)({
89
133
  node.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression)) {
90
134
  return;
91
135
  }
136
+ // A non-computed property key is a label, not a reference: the `lock` in
137
+ // `await send({ lock: true })` names a field of the payload and binds
138
+ // nothing, so letting it reach the coordinator match would exempt the loop
139
+ // on the strength of a string. A computed key (`{ [lock]: true }`) is an
140
+ // expression that really does read the surrounding scope, and shorthand
141
+ // (`{ lock }`) carries the same identifier as its VALUE, so both keep
142
+ // their say. (#1688)
143
+ if (node.type === utils_1.AST_NODE_TYPES.Property) {
144
+ if (node.computed) {
145
+ collectIdentifiers(node.key, names, stopAtFunctions);
146
+ }
147
+ collectIdentifiers(node.value, names, stopAtFunctions);
148
+ return;
149
+ }
92
150
  for (const key in node) {
93
151
  if (key === 'parent' ||
94
152
  key === 'range' ||
@@ -456,9 +514,9 @@ exports.parallelizeLoopAwaits = (0, createRule_1.createRule)({
456
514
  }
457
515
  }
458
516
  /**
459
- * Walks the loop body looking for the first AwaitExpression that is NOT
460
- * inside a nested async function (a different async scope). Returns the
461
- * first such AwaitExpression found, or null if none exists.
517
+ * Walks the loop body looking for the first AwaitExpression that belongs to
518
+ * THIS loop — one that sits in neither a nested function nor a nested loop.
519
+ * Returns the first such AwaitExpression found, or null if none exists.
462
520
  */
463
521
  function findDirectAwait(node, isRoot) {
464
522
  // Do not cross into nested functions; their awaits belong to a
@@ -469,6 +527,14 @@ exports.parallelizeLoopAwaits = (0, createRule_1.createRule)({
469
527
  node.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression)) {
470
528
  return null;
471
529
  }
530
+ // An await inside a nested loop belongs to that loop, which gets its own
531
+ // visit and its own verdict. Claiming it here would anchor a second report
532
+ // on the very same await, and would judge it against the wrong body: the
533
+ // enclosing loop's barriers say nothing about whether the inner
534
+ // iterations can run together. The innermost loop owns the report. (#1688)
535
+ if (!isRoot && LOOP_NODE_TYPES.has(node.type)) {
536
+ return null;
537
+ }
472
538
  if (node.type === utils_1.AST_NODE_TYPES.AwaitExpression) {
473
539
  return node;
474
540
  }
@@ -498,6 +564,135 @@ exports.parallelizeLoopAwaits = (0, createRule_1.createRule)({
498
564
  }
499
565
  return null;
500
566
  }
567
+ /**
568
+ * Reports whether a node contains a CallExpression anywhere inside it.
569
+ * Nested functions are deliberately traversed: a call written inside a
570
+ * callback in a loop clause (`items.some(() => check())`) still makes the
571
+ * clause's value depend on invoking something.
572
+ */
573
+ function containsCallExpression(node) {
574
+ if (node.type === utils_1.AST_NODE_TYPES.CallExpression) {
575
+ return true;
576
+ }
577
+ for (const key in node) {
578
+ if (key === 'parent' ||
579
+ key === 'range' ||
580
+ key === 'loc' ||
581
+ key === 'type')
582
+ continue;
583
+ const child = node[key];
584
+ if (child && typeof child === 'object') {
585
+ if (Array.isArray(child)) {
586
+ for (const item of child) {
587
+ if (item && typeof item === 'object' && 'type' in item) {
588
+ if (containsCallExpression(item))
589
+ return true;
590
+ }
591
+ }
592
+ }
593
+ else if ('type' in child) {
594
+ if (containsCallExpression(child))
595
+ return true;
596
+ }
597
+ }
598
+ }
599
+ return false;
600
+ }
601
+ /**
602
+ * Reports whether the loop's own continuation machinery invokes a
603
+ * function — a call in a `while` test, or in a `for` test or update clause.
604
+ *
605
+ * Such a loop runs until an observation comes back a certain way, so the
606
+ * iteration count is a function of what each iteration does:
607
+ * `while (!hasSettled()) { await tick(); }` and
608
+ * `for (let i = 0; i < 100 && !findTimer(); i += 1)` both re-read state the
609
+ * awaited work advances. `Promise.all` has to know the iteration count up
610
+ * front, so there is no parallel form of these loops at all. Only the
611
+ * clauses re-evaluated on every iteration count; a `for` loop's `init` and a
612
+ * `for...of` loop's `right` run once, so a call there says nothing about
613
+ * cross-iteration coupling (`for (const [k, v] of map.entries())` keeps its
614
+ * enforcement). A `do...while` test is re-evaluated exactly like a `while`
615
+ * test, so it carries the same meaning. (#1687, #1688)
616
+ */
617
+ function isConditionCoupled(loopNode) {
618
+ if (loopNode.type === utils_1.AST_NODE_TYPES.WhileStatement ||
619
+ loopNode.type === utils_1.AST_NODE_TYPES.DoWhileStatement) {
620
+ return containsCallExpression(loopNode.test);
621
+ }
622
+ if (loopNode.type === utils_1.AST_NODE_TYPES.ForStatement) {
623
+ return ((!!loopNode.test && containsCallExpression(loopNode.test)) ||
624
+ (!!loopNode.update && containsCallExpression(loopNode.update)));
625
+ }
626
+ return false;
627
+ }
628
+ /**
629
+ * Collects every identifier named in the loop's head — the `for...of`/
630
+ * `for...in` left and right, the `for` init, test and update, or the
631
+ * `while`/`do...while` test. These are the names an iteration can hand to
632
+ * the body.
633
+ */
634
+ function collectLoopHeadIdentifiers(loopNode) {
635
+ const names = new Set();
636
+ const clauses = loopNode.type === utils_1.AST_NODE_TYPES.ForStatement
637
+ ? [loopNode.init, loopNode.test, loopNode.update]
638
+ : loopNode.type === utils_1.AST_NODE_TYPES.WhileStatement ||
639
+ loopNode.type === utils_1.AST_NODE_TYPES.DoWhileStatement
640
+ ? [loopNode.test]
641
+ : [loopNode.left, loopNode.right];
642
+ for (const clause of clauses) {
643
+ if (clause)
644
+ collectIdentifiers(clause, names, false);
645
+ }
646
+ return names;
647
+ }
648
+ /**
649
+ * Reports whether the loop body is a single discarded `await` of a call
650
+ * that consumes nothing the iteration produces, e.g.
651
+ * `for (let i = 0; i < 21; i += 1) { await postSuggestion(); }`.
652
+ *
653
+ * Such a loop passes nothing from the iteration into the call and keeps
654
+ * nothing the call returns, so the only reason to write it is an ordered
655
+ * side effect the callee owns — replaying one entrypoint so each run
656
+ * observes what the previous run stored. Every barrier below reads syntax
657
+ * inside the body, and this body has none to read: no assignment, no
658
+ * binding, no control flow, and no identifier but the callee. Reporting it
659
+ * would be a verdict passed on zero evidence, and the plugin prefers a
660
+ * false negative to a false positive.
661
+ *
662
+ * A call that names anything from the loop head is excluded, because that
663
+ * name IS the evidence: `for (const doc of snap.docs) { await
664
+ * doc.ref.delete(); }` takes no arguments either, yet each iteration
665
+ * addresses its own document and the loop is exactly the shape the rule
666
+ * exists to flag. (#1687)
667
+ */
668
+ function isBareDiscardedZeroArgCall(loopNode) {
669
+ const { body } = loopNode;
670
+ const statements = body.type === utils_1.AST_NODE_TYPES.BlockStatement ? body.body : [body];
671
+ if (statements.length !== 1)
672
+ return false;
673
+ const [statement] = statements;
674
+ if (statement.type !== utils_1.AST_NODE_TYPES.ExpressionStatement)
675
+ return false;
676
+ if (statement.expression.type !== utils_1.AST_NODE_TYPES.AwaitExpression) {
677
+ return false;
678
+ }
679
+ const { argument } = statement.expression;
680
+ const callExpr = argument.type === utils_1.AST_NODE_TYPES.ChainExpression
681
+ ? argument.expression
682
+ : argument;
683
+ if (callExpr.type !== utils_1.AST_NODE_TYPES.CallExpression ||
684
+ callExpr.arguments.length !== 0) {
685
+ return false;
686
+ }
687
+ const headNames = collectLoopHeadIdentifiers(loopNode);
688
+ const callNames = new Set();
689
+ collectIdentifiers(callExpr, callNames, false);
690
+ for (const name of callNames) {
691
+ if (headNames.has(name))
692
+ return false;
693
+ }
694
+ return true;
695
+ }
501
696
  /**
502
697
  * Central analysis for a loop node. Returns the AwaitExpression to
503
698
  * report on, or null if the loop should not be flagged.
@@ -506,6 +701,21 @@ exports.parallelizeLoopAwaits = (0, createRule_1.createRule)({
506
701
  const body = loopNode.body;
507
702
  if (!body)
508
703
  return null;
704
+ // Exclusion: `for await (const x of stream)` consumes an async iterable,
705
+ // which the language pulls one value at a time; the sequencing is the
706
+ // construct's meaning rather than an oversight.
707
+ if (loopNode.type === utils_1.AST_NODE_TYPES.ForOfStatement &&
708
+ loopNode.await === true) {
709
+ return null;
710
+ }
711
+ // Exclusion: the loop's continuation condition or update invokes a
712
+ // function, so how many iterations run depends on what they do
713
+ if (isConditionCoupled(loopNode))
714
+ return null;
715
+ // Exclusion: the body is a lone discarded await of a zero-argument call,
716
+ // which carries no evidence either way
717
+ if (isBareDiscardedZeroArgCall(loopNode))
718
+ return null;
509
719
  // Find an await directly inside the loop body (not in nested async fns)
510
720
  const awaitExpr = findDirectAwait(body, true);
511
721
  if (!awaitExpr)
@@ -539,43 +749,23 @@ exports.parallelizeLoopAwaits = (0, createRule_1.createRule)({
539
749
  }
540
750
  return awaitExpr;
541
751
  }
752
+ function checkLoop(loopNode) {
753
+ const awaitExpr = analyzeLoop(loopNode);
754
+ if (awaitExpr) {
755
+ context.report({
756
+ node: awaitExpr,
757
+ messageId: 'parallelizeLoopAwaits',
758
+ });
759
+ }
760
+ }
542
761
  return {
543
- ForOfStatement(node) {
544
- const awaitExpr = analyzeLoop(node);
545
- if (awaitExpr) {
546
- context.report({
547
- node: awaitExpr,
548
- messageId: 'parallelizeLoopAwaits',
549
- });
550
- }
551
- },
552
- ForInStatement(node) {
553
- const awaitExpr = analyzeLoop(node);
554
- if (awaitExpr) {
555
- context.report({
556
- node: awaitExpr,
557
- messageId: 'parallelizeLoopAwaits',
558
- });
559
- }
560
- },
561
- ForStatement(node) {
562
- const awaitExpr = analyzeLoop(node);
563
- if (awaitExpr) {
564
- context.report({
565
- node: awaitExpr,
566
- messageId: 'parallelizeLoopAwaits',
567
- });
568
- }
569
- },
570
- WhileStatement(node) {
571
- const awaitExpr = analyzeLoop(node);
572
- if (awaitExpr) {
573
- context.report({
574
- node: awaitExpr,
575
- messageId: 'parallelizeLoopAwaits',
576
- });
577
- }
578
- },
762
+ ForOfStatement: checkLoop,
763
+ ForInStatement: checkLoop,
764
+ ForStatement: checkLoop,
765
+ WhileStatement: checkLoop,
766
+ // A `do...while` repeats one body per iteration exactly as the other four
767
+ // forms do; it is the same target and earns the same analysis. (#1688)
768
+ DoWhileStatement: checkLoop,
579
769
  };
580
770
  },
581
771
  });
@@ -1,3 +1,4 @@
1
+ import { TSESLint } from '@typescript-eslint/utils';
1
2
  type OptionShape = {
2
3
  stripPrefixes?: string[];
3
4
  ignoredMethods?: string[];
@@ -10,5 +11,5 @@ type OptionShape = {
10
11
  };
11
12
  type Options = [OptionShape];
12
13
  type MessageIds = 'preferGetter' | 'preferGetterSideEffect';
13
- export declare const preferGetterOverParameterlessMethod: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<MessageIds, Options, import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleListener>;
14
+ export declare const preferGetterOverParameterlessMethod: TSESLint.RuleModule<MessageIds, Options, TSESLint.RuleListener>;
14
15
  export default preferGetterOverParameterlessMethod;