@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.
- package/README.md +1 -1
- package/lib/index.js +1 -1
- package/lib/rules/enforce-css-media-queries.d.ts +2 -1
- package/lib/rules/enforce-css-media-queries.js +203 -8
- package/lib/rules/enforce-exported-function-types.js +209 -6
- package/lib/rules/enforce-firestore-doc-ref-generic.js +71 -18
- package/lib/rules/enforce-mui-rounded-icons.d.ts +2 -1
- package/lib/rules/enforce-mui-rounded-icons.js +194 -13
- package/lib/rules/generic-starts-with-t.js +49 -0
- package/lib/rules/global-const-style.js +101 -51
- package/lib/rules/no-always-true-false-conditions.js +231 -62
- package/lib/rules/no-redundant-param-types.js +69 -37
- package/lib/rules/no-unnecessary-verb-suffix.js +52 -5
- package/lib/rules/parallelize-loop-awaits.d.ts +1 -0
- package/lib/rules/parallelize-loop-awaits.js +233 -43
- package/lib/rules/prefer-getter-over-parameterless-method.d.ts +2 -1
- package/lib/rules/prefer-getter-over-parameterless-method.js +259 -0
- package/lib/rules/prefer-type-alias-over-typeof-constant.js +63 -4
- package/lib/rules/require-https-error.js +6 -2
- package/lib/rules/use-custom-link.js +45 -1
- package/lib/rules/use-custom-memo.js +30 -0
- package/lib/rules/use-custom-router.js +32 -2
- package/package.json +1 -1
- package/release-manifest.json +142 -0
|
@@ -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
|
|
76
|
-
*
|
|
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
|
|
460
|
-
*
|
|
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
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
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:
|
|
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;
|