@blumintinc/eslint-plugin-blumint 1.20.94 → 1.20.96

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.
@@ -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;
@@ -152,6 +152,232 @@ function hasOverloadSignatures(node) {
152
152
  return !hasBody && key.name === targetName;
153
153
  });
154
154
  }
155
+ function getClassOf(node) {
156
+ const classBody = node.parent;
157
+ if (!classBody || classBody.type !== utils_1.AST_NODE_TYPES.ClassBody) {
158
+ return null;
159
+ }
160
+ const classNode = classBody.parent;
161
+ if (classNode &&
162
+ (classNode.type === utils_1.AST_NODE_TYPES.ClassDeclaration ||
163
+ classNode.type === utils_1.AST_NODE_TYPES.ClassExpression)) {
164
+ return classNode;
165
+ }
166
+ return null;
167
+ }
168
+ /**
169
+ * A member typed as a function is as binding as a method signature: a getter
170
+ * returning the function's *result* is not assignable to the function type, so
171
+ * such a member is a contract match too.
172
+ */
173
+ function isFunctionTypeNode(node) {
174
+ if (!node)
175
+ return false;
176
+ switch (node.type) {
177
+ case utils_1.AST_NODE_TYPES.TSFunctionType:
178
+ case utils_1.AST_NODE_TYPES.TSConstructorType:
179
+ return true;
180
+ case utils_1.AST_NODE_TYPES.TSUnionType:
181
+ case utils_1.AST_NODE_TYPES.TSIntersectionType:
182
+ return node.types.some(isFunctionTypeNode);
183
+ default:
184
+ return false;
185
+ }
186
+ }
187
+ function knownMemberName(key, computed) {
188
+ if (!key || computed)
189
+ return null;
190
+ if (key.type === utils_1.AST_NODE_TYPES.Identifier)
191
+ return key.name;
192
+ if (key.type === utils_1.AST_NODE_TYPES.Literal && typeof key.value === 'string') {
193
+ return key.value;
194
+ }
195
+ return null;
196
+ }
197
+ function findVariableInScopeChain(scope, name) {
198
+ let current = scope;
199
+ while (current) {
200
+ const variable = current.variables.find((entry) => entry.name === name);
201
+ if (variable) {
202
+ return variable;
203
+ }
204
+ current = current.upper;
205
+ }
206
+ return null;
207
+ }
208
+ function collectTypeElementMembers(elements, out) {
209
+ for (const element of elements) {
210
+ if (element.type === utils_1.AST_NODE_TYPES.TSMethodSignature) {
211
+ // A `get`/`set` signature already describes property access, so a getter
212
+ // implementation satisfies it — only call signatures bind.
213
+ if (element.kind === 'get' || element.kind === 'set')
214
+ continue;
215
+ const name = knownMemberName(element.key, element.computed);
216
+ if (name)
217
+ out.instance.add(name);
218
+ continue;
219
+ }
220
+ if (element.type === utils_1.AST_NODE_TYPES.TSPropertySignature &&
221
+ isFunctionTypeNode(element.typeAnnotation?.typeAnnotation)) {
222
+ const name = knownMemberName(element.key, element.computed);
223
+ if (name)
224
+ out.instance.add(name);
225
+ }
226
+ }
227
+ }
228
+ function collectClassContractMembers(classNode, out) {
229
+ for (const member of classNode.body.body) {
230
+ if (member.type === utils_1.AST_NODE_TYPES.StaticBlock)
231
+ continue;
232
+ const isStatic = member.static ?? false;
233
+ const target = isStatic ? out.staticSide : out.instance;
234
+ if (member.type === utils_1.AST_NODE_TYPES.MethodDefinition ||
235
+ member.type === utils_1.AST_NODE_TYPES.TSAbstractMethodDefinition) {
236
+ if (member.kind !== 'method')
237
+ continue;
238
+ const name = knownMemberName(member.key, member.computed);
239
+ if (name)
240
+ target.add(name);
241
+ continue;
242
+ }
243
+ if ((member.type === utils_1.AST_NODE_TYPES.PropertyDefinition ||
244
+ member.type === utils_1.AST_NODE_TYPES.TSAbstractPropertyDefinition) &&
245
+ isFunctionTypeNode(member.typeAnnotation?.typeAnnotation)) {
246
+ const name = knownMemberName(member.key, member.computed);
247
+ if (name)
248
+ target.add(name);
249
+ }
250
+ }
251
+ }
252
+ function collectContractFromName(name, scope, seen, out) {
253
+ if (seen.has(name))
254
+ return;
255
+ seen.add(name);
256
+ const variable = findVariableInScopeChain(scope, name);
257
+ // No binding at all (a global/ambient type) or a binding with no declaration
258
+ // node (a lib type) leaves the contract unknowable from this file.
259
+ if (!variable || variable.defs.length === 0) {
260
+ out.resolved = false;
261
+ return;
262
+ }
263
+ for (const def of variable.defs) {
264
+ const declaration = def.node;
265
+ switch (declaration.type) {
266
+ case utils_1.AST_NODE_TYPES.TSInterfaceDeclaration:
267
+ collectContractFromInterface(declaration, scope, seen, out);
268
+ break;
269
+ case utils_1.AST_NODE_TYPES.TSTypeAliasDeclaration:
270
+ collectContractFromTypeNode(declaration.typeAnnotation, scope, seen, out);
271
+ break;
272
+ case utils_1.AST_NODE_TYPES.ClassDeclaration:
273
+ case utils_1.AST_NODE_TYPES.ClassExpression:
274
+ collectContractFromClass(declaration, scope, seen, out);
275
+ break;
276
+ default:
277
+ // An import binding, an enum, a value — nothing whose members this file
278
+ // can enumerate.
279
+ out.resolved = false;
280
+ }
281
+ }
282
+ }
283
+ function collectContractFromHeritage(heritage, scope, seen, out) {
284
+ // A qualified reference (`ns.Type`) names a declaration this lookup cannot
285
+ // reach, so the contract stays unresolved.
286
+ if (heritage.expression.type !== utils_1.AST_NODE_TYPES.Identifier) {
287
+ out.resolved = false;
288
+ return;
289
+ }
290
+ collectContractFromName(heritage.expression.name, scope, seen, out);
291
+ }
292
+ function collectContractFromInterface(declaration, scope, seen, out) {
293
+ collectTypeElementMembers(declaration.body.body, out);
294
+ for (const heritage of [
295
+ ...(declaration.extends ?? []),
296
+ ...(declaration.implements ?? []),
297
+ ]) {
298
+ collectContractFromHeritage(heritage, scope, seen, out);
299
+ }
300
+ }
301
+ function collectContractFromClass(declaration, scope, seen, out) {
302
+ collectClassContractMembers(declaration, out);
303
+ if (declaration.superClass) {
304
+ if (declaration.superClass.type === utils_1.AST_NODE_TYPES.Identifier) {
305
+ collectContractFromName(declaration.superClass.name, scope, seen, out);
306
+ }
307
+ else {
308
+ // A mixin call or member expression base class: unknowable members.
309
+ out.resolved = false;
310
+ }
311
+ }
312
+ for (const heritage of declaration.implements ?? []) {
313
+ collectContractFromHeritage(heritage, scope, seen, out);
314
+ }
315
+ }
316
+ function collectContractFromTypeNode(typeNode, scope, seen, out) {
317
+ switch (typeNode.type) {
318
+ case utils_1.AST_NODE_TYPES.TSTypeLiteral:
319
+ collectTypeElementMembers(typeNode.members, out);
320
+ return;
321
+ case utils_1.AST_NODE_TYPES.TSIntersectionType:
322
+ for (const part of typeNode.types) {
323
+ collectContractFromTypeNode(part, scope, seen, out);
324
+ }
325
+ return;
326
+ case utils_1.AST_NODE_TYPES.TSTypeReference:
327
+ if (typeNode.typeName.type === utils_1.AST_NODE_TYPES.Identifier) {
328
+ collectContractFromName(typeNode.typeName.name, scope, seen, out);
329
+ return;
330
+ }
331
+ out.resolved = false;
332
+ return;
333
+ default:
334
+ // Mapped, conditional, indexed-access and utility-type shapes describe
335
+ // members this rule cannot enumerate syntactically.
336
+ out.resolved = false;
337
+ }
338
+ }
339
+ /**
340
+ * Resolves what the class's `extends`/`implements` clauses oblige its members
341
+ * to look like, using only declarations visible in the file being linted.
342
+ */
343
+ function resolveHeritageContract(classNode, scope) {
344
+ const heritageNames = [];
345
+ let hasOpaqueHeritage = false;
346
+ if (classNode.superClass) {
347
+ if (classNode.superClass.type === utils_1.AST_NODE_TYPES.Identifier) {
348
+ heritageNames.push(classNode.superClass.name);
349
+ }
350
+ else {
351
+ hasOpaqueHeritage = true;
352
+ }
353
+ }
354
+ for (const implemented of classNode.implements ?? []) {
355
+ if (implemented.expression.type === utils_1.AST_NODE_TYPES.Identifier) {
356
+ heritageNames.push(implemented.expression.name);
357
+ }
358
+ else {
359
+ hasOpaqueHeritage = true;
360
+ }
361
+ }
362
+ if (!heritageNames.length && !hasOpaqueHeritage) {
363
+ return { status: 'none' };
364
+ }
365
+ if (hasOpaqueHeritage) {
366
+ return { status: 'unresolvable' };
367
+ }
368
+ const members = {
369
+ instance: new Set(),
370
+ staticSide: new Set(),
371
+ resolved: true,
372
+ };
373
+ const seen = new Set();
374
+ for (const name of heritageNames) {
375
+ collectContractFromName(name, scope, seen, members);
376
+ }
377
+ return members.resolved
378
+ ? { status: 'resolved', members }
379
+ : { status: 'unresolvable' };
380
+ }
155
381
  exports.preferGetterOverParameterlessMethod = (0, createRule_1.createRule)({
156
382
  name: 'prefer-getter-over-parameterless-method',
157
383
  meta: {
@@ -535,6 +761,37 @@ exports.preferGetterOverParameterlessMethod = (0, createRule_1.createRule)({
535
761
  }
536
762
  }
537
763
  }
764
+ const heritageContracts = new WeakMap();
765
+ /**
766
+ * A method that satisfies an `implements` clause or overrides a base-class
767
+ * member cannot become a getter: the heritage type declares a *method*, so
768
+ * the conversion the rule prescribes is a TS2416/TS2417 compile error. When
769
+ * every heritage reference resolves in-file the exemption is per method —
770
+ * only names the contract declares are spared. When any reference leaves
771
+ * the file (an import, a global, a third-party `.d.ts`, a mixin call) the
772
+ * contract's members are unknowable, so no method of that class can be
773
+ * proven safe and the whole class is spared: a false negative is preferable
774
+ * to prescribing a remedy that does not compile.
775
+ */
776
+ function isConstrainedByHeritage(node) {
777
+ const classNode = getClassOf(node);
778
+ if (!classNode)
779
+ return false;
780
+ let contract = heritageContracts.get(classNode);
781
+ if (!contract) {
782
+ contract = resolveHeritageContract(classNode, context.getScope());
783
+ heritageContracts.set(classNode, contract);
784
+ }
785
+ if (contract.status === 'none')
786
+ return false;
787
+ if (contract.status === 'unresolvable')
788
+ return true;
789
+ const name = node.key.name;
790
+ const isStatic = node.static ?? false;
791
+ return isStatic
792
+ ? contract.members.staticSide.has(name)
793
+ : contract.members.instance.has(name);
794
+ }
538
795
  const callUsedNamesByClass = new WeakMap();
539
796
  const callUsedNamesInFile = new Set();
540
797
  const candidates = [];
@@ -594,6 +851,8 @@ exports.preferGetterOverParameterlessMethod = (0, createRule_1.createRule)({
594
851
  return;
595
852
  if (!node.value.body)
596
853
  return;
854
+ if (isConstrainedByHeritage(node))
855
+ return;
597
856
  const name = node.key.name;
598
857
  if (ignoredMethods.has(name))
599
858
  return;