eslint-plugin-no-mistakes 0.57.0 → 0.58.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-no-mistakes",
3
- "version": "0.57.0",
3
+ "version": "0.58.0",
4
4
  "description": "ESLint and Oxlint rules that keep code static enough for no-mistakes analyzers and coding agents",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -21,9 +21,9 @@
21
21
  "test": "vitest run --coverage"
22
22
  },
23
23
  "devDependencies": {
24
- "@typescript-eslint/parser": "^8.67.0",
24
+ "@typescript-eslint/parser": "^8.68.0",
25
25
  "@vitest/coverage-v8": "^4.1.11",
26
- "eslint": "^10.9.0",
26
+ "eslint": "^10.9.1",
27
27
  "oxlint": "^1.80.0",
28
28
  "vitest": "^4.1.11"
29
29
  },
package/src/index.js CHANGED
@@ -14,6 +14,7 @@ const rules = {
14
14
  "no-global-fetch-outside-helper": require("./rules/no-global-fetch-outside-helper"),
15
15
  "no-delete-property": require("./rules/no-delete-property"),
16
16
  "no-import-only-test-files": require("./rules/no-import-only-test-files"),
17
+ "no-inline-noop-promise-catch": require("./rules/no-inline-noop-promise-catch"),
17
18
  "no-placeholder-never-type-exports": require("./rules/no-placeholder-never-type-exports"),
18
19
  "no-vitest-sequential": require("./rules/no-vitest-sequential"),
19
20
  "playwright-consistent-attribute": require("./rules/playwright-consistent-attribute"),
@@ -38,6 +39,7 @@ const rules = {
38
39
  "react-no-use-promise-resolve": require("./rules/react-no-use-promise-resolve"),
39
40
  "server-require-nullable-fetch-wrapper": require("./rules/server-require-nullable-fetch-wrapper"),
40
41
  "test-no-error-message-matching": require("./rules/test-no-error-message-matching"),
42
+ "test-no-delayed-rejects": require("./rules/test-no-delayed-rejects"),
41
43
  "test-no-shared-state": require("./rules/test-no-shared-state"),
42
44
  "ts-no-const-aliases": require("./rules/ts-no-const-aliases"),
43
45
  "ts-no-export-renaming": require("./rules/ts-no-export-renaming"),
@@ -96,6 +98,7 @@ plugin.configs.strict = {
96
98
  "no-mistakes/react-no-iife-in-jsx": "error",
97
99
  "no-mistakes/react-no-use-promise-resolve": "error",
98
100
  "no-mistakes/test-no-error-message-matching": "error",
101
+ "no-mistakes/test-no-delayed-rejects": "error",
99
102
  "no-mistakes/test-no-shared-state": "error",
100
103
  "no-mistakes/vitest-mock-test-file-naming": "error",
101
104
  },
@@ -0,0 +1,92 @@
1
+ "use strict";
2
+
3
+ const { unwrapExpression } = require("./async-ast");
4
+ const {
5
+ memberPropertyName,
6
+ repoRelativeFilename,
7
+ stringMatches,
8
+ } = require("./module-mock-helpers");
9
+
10
+ const CHAIN_METHODS = new Set(["catch", "finally", "then"]);
11
+
12
+ function shouldCheckFile(filename, options) {
13
+ const file = repoRelativeFilename(filename);
14
+ const checked = options.checkedPathPatterns ?? [];
15
+ const allowed = options.allowedPathPatterns ?? [];
16
+ if (checked.length > 0 && !stringMatches(file, checked)) return false;
17
+ return !stringMatches(file, allowed);
18
+ }
19
+
20
+ function promiseMethodName(call) {
21
+ const callee = unwrapExpression(call.callee);
22
+ if (callee?.type !== "MemberExpression" || callee.computed) return null;
23
+ return callee.property.name;
24
+ }
25
+
26
+ function rejectionHandler(call) {
27
+ const method = promiseMethodName(call);
28
+ if (method === "catch") return call.arguments[0] ?? null;
29
+ if (method === "then") return call.arguments[1] ?? null;
30
+ return null;
31
+ }
32
+
33
+ function isVoidZero(node) {
34
+ const current = unwrapExpression(node);
35
+ if (current?.type !== "UnaryExpression" || current.operator !== "void") return false;
36
+ const argument = unwrapExpression(current.argument);
37
+ return argument?.type === "Literal" && argument.value === 0;
38
+ }
39
+
40
+ function isNoopExpression(node) {
41
+ const current = unwrapExpression(node);
42
+ if (current.type === "Identifier" && current.name === "undefined") return true;
43
+ return isVoidZero(current);
44
+ }
45
+
46
+ function isNoopStatement(statement) {
47
+ if (statement.type === "EmptyStatement") return true;
48
+ if (statement.type === "ReturnStatement") {
49
+ return statement.argument == null || isNoopExpression(statement.argument);
50
+ }
51
+ return statement.type === "ExpressionStatement" && isNoopExpression(statement.expression);
52
+ }
53
+
54
+ function isInlineFunction(node) {
55
+ if (node?.type === "ArrowFunctionExpression") return true;
56
+ return node?.type === "FunctionExpression" && node.id == null;
57
+ }
58
+
59
+ function isInlineNoopFunction(node) {
60
+ const current = unwrapExpression(node);
61
+ if (!isInlineFunction(current)) return false;
62
+ if (current.body.type !== "BlockStatement") return isNoopExpression(current.body);
63
+ return current.body.body.every(isNoopStatement);
64
+ }
65
+
66
+ function originatingCalleeName(call) {
67
+ let current = unwrapExpression(call);
68
+ while (current?.type === "CallExpression") {
69
+ const method = promiseMethodName(current);
70
+ if (!CHAIN_METHODS.has(method)) break;
71
+ const callee = unwrapExpression(current.callee);
72
+ current = unwrapExpression(callee?.object);
73
+ }
74
+ if (current?.type !== "CallExpression") return null;
75
+ const callee = unwrapExpression(current.callee);
76
+ if (callee?.type === "Identifier") return callee.name;
77
+ if (callee?.type === "MemberExpression") return memberPropertyName(callee);
78
+ return null;
79
+ }
80
+
81
+ function isAllowedCallee(call, options) {
82
+ const name = originatingCalleeName(call);
83
+ return Boolean(name && stringMatches(name, options.allowedCalleeNamePatterns ?? []));
84
+ }
85
+
86
+ module.exports = {
87
+ isAllowedCallee,
88
+ isInlineNoopFunction,
89
+ originatingCalleeName,
90
+ rejectionHandler,
91
+ shouldCheckFile,
92
+ };
@@ -0,0 +1,48 @@
1
+ "use strict";
2
+
3
+ const { rule } = require("../helpers");
4
+ const helpers = require("./no-inline-noop-promise-catch-helpers");
5
+
6
+ const { isAllowedCallee, isInlineNoopFunction, rejectionHandler, shouldCheckFile } = helpers;
7
+
8
+ const patternList = { type: "array", items: { type: "string" } };
9
+
10
+ module.exports = Object.assign(
11
+ rule(
12
+ {
13
+ type: "problem",
14
+ docs: {
15
+ description: "disallow inline Promise catch callbacks that do not handle the rejection",
16
+ recommended: false,
17
+ },
18
+ schema: [
19
+ {
20
+ type: "object",
21
+ properties: {
22
+ checkedPathPatterns: patternList,
23
+ allowedPathPatterns: patternList,
24
+ allowedCalleeNamePatterns: patternList,
25
+ },
26
+ additionalProperties: false,
27
+ },
28
+ ],
29
+ messages: {
30
+ noopCatch:
31
+ "Replace this inline no-op Promise catch with a named handler, logging, reporting, transformation, or rethrow so rejection handling stays reviewable.",
32
+ },
33
+ },
34
+ (context) => {
35
+ const options = context.options?.[0] ?? {};
36
+ if (!shouldCheckFile(context.filename, options)) return {};
37
+ return {
38
+ CallExpression(node) {
39
+ const handler = rejectionHandler(node);
40
+ if (!handler || !isInlineNoopFunction(handler)) return;
41
+ if (isAllowedCallee(node, options)) return;
42
+ context.report({ node: handler, messageId: "noopCatch" });
43
+ },
44
+ };
45
+ },
46
+ ),
47
+ { __test: helpers },
48
+ );
@@ -0,0 +1,180 @@
1
+ "use strict";
2
+
3
+ function contains(ancestor, node) {
4
+ return ancestor.range[0] <= node.range[0] && ancestor.range[1] >= node.range[1];
5
+ }
6
+
7
+ function alwaysExits(statement) {
8
+ if (statement.type === "ReturnStatement" || statement.type === "ThrowStatement") return true;
9
+ if (statement.type === "BlockStatement") return statement.body.some(alwaysExits);
10
+ if (statement.type === "TryStatement") {
11
+ if (statement.finalizer && alwaysExits(statement.finalizer)) return true;
12
+ if (!alwaysExits(statement.block)) return false;
13
+ return (
14
+ !statement.handler || alwaysReturns(statement.block) || alwaysExits(statement.handler.body)
15
+ );
16
+ }
17
+ return (
18
+ statement.type === "IfStatement" &&
19
+ statement.alternate &&
20
+ alwaysExits(statement.consequent) &&
21
+ alwaysExits(statement.alternate)
22
+ );
23
+ }
24
+
25
+ function alwaysReturns(statement) {
26
+ if (statement.type === "ReturnStatement") return true;
27
+ if (statement.type === "BlockStatement") {
28
+ const exit = statement.body.find(alwaysExits);
29
+ return Boolean(exit && alwaysReturns(exit));
30
+ }
31
+ if (statement.type === "TryStatement") {
32
+ if (statement.finalizer && alwaysExits(statement.finalizer)) {
33
+ return alwaysReturns(statement.finalizer);
34
+ }
35
+ if (alwaysReturns(statement.block)) return true;
36
+ return Boolean(
37
+ statement.handler && alwaysExits(statement.block) && alwaysReturns(statement.handler.body),
38
+ );
39
+ }
40
+ return (
41
+ statement.type === "IfStatement" &&
42
+ statement.alternate &&
43
+ alwaysReturns(statement.consequent) &&
44
+ alwaysReturns(statement.alternate)
45
+ );
46
+ }
47
+
48
+ function alwaysThrows(statement) {
49
+ if (statement.type === "ThrowStatement") return true;
50
+ if (statement.type === "BlockStatement") {
51
+ const exit = statement.body.find(alwaysExits);
52
+ return Boolean(exit && alwaysThrows(exit));
53
+ }
54
+ if (statement.type === "TryStatement") {
55
+ if (statement.finalizer && alwaysExits(statement.finalizer)) {
56
+ return alwaysThrows(statement.finalizer);
57
+ }
58
+ return (
59
+ alwaysThrows(statement.block) && (!statement.handler || alwaysThrows(statement.handler.body))
60
+ );
61
+ }
62
+ return (
63
+ statement.type === "IfStatement" &&
64
+ statement.alternate &&
65
+ alwaysThrows(statement.consequent) &&
66
+ alwaysThrows(statement.alternate)
67
+ );
68
+ }
69
+
70
+ function matcherRunsInClause(node, matcher, clause) {
71
+ let current = node;
72
+ while (current.parent) {
73
+ const parent = current.parent;
74
+ if (parent.type === "TryStatement" && clause(parent, current, matcher)) return true;
75
+ current = parent;
76
+ }
77
+ return false;
78
+ }
79
+
80
+ function abruptCompletionReachesMatcher(node, matcher) {
81
+ const runsInFinally = matcherRunsInClause(
82
+ node,
83
+ matcher,
84
+ (parent, current) =>
85
+ parent.finalizer &&
86
+ (current === parent.block || current === parent.handler) &&
87
+ contains(parent.finalizer, matcher),
88
+ );
89
+ const runsInCatch = matcherRunsInClause(
90
+ node,
91
+ matcher,
92
+ (parent, current) =>
93
+ current === parent.block && parent.handler && contains(parent.handler, matcher),
94
+ );
95
+ return runsInFinally || (alwaysThrows(node) && runsInCatch);
96
+ }
97
+
98
+ function caughtThrowCanContinue(node, matcher) {
99
+ if (!alwaysThrows(node)) return false;
100
+ let current = node;
101
+ while (current.parent) {
102
+ const parent = current.parent;
103
+ if (parent.type === "TryStatement" && current === parent.block) {
104
+ if (parent.finalizer && alwaysExits(parent.finalizer)) {
105
+ if (!alwaysThrows(parent.finalizer)) return false;
106
+ } else if (parent.handler) {
107
+ if (contains(parent, matcher)) return false;
108
+ if (!alwaysExits(parent.handler.body)) return true;
109
+ if (!alwaysThrows(parent.handler.body)) return false;
110
+ }
111
+ }
112
+ current = parent;
113
+ }
114
+ return false;
115
+ }
116
+
117
+ function isLoop(node) {
118
+ return (
119
+ node.type === "WhileStatement" ||
120
+ node.type === "DoWhileStatement" ||
121
+ node.type === "ForStatement" ||
122
+ node.type === "ForInStatement" ||
123
+ node.type === "ForOfStatement"
124
+ );
125
+ }
126
+
127
+ function directJumpSkipsMatcher(node, matcher) {
128
+ let current = node.parent;
129
+ while (true) {
130
+ const isTarget = node.label
131
+ ? current.type === "LabeledStatement" && current.label.name === node.label.name
132
+ : isLoop(current) || (node.type === "BreakStatement" && current.type === "SwitchStatement");
133
+ if (isTarget) return contains(current, matcher);
134
+ current = current.parent;
135
+ }
136
+ }
137
+
138
+ function jumpSkipsMatcher(node, matcher, type) {
139
+ if (node.type === type) return directJumpSkipsMatcher(node, matcher);
140
+ if (node.type === "BlockStatement" || node.type === "SwitchCase") {
141
+ const statements = node.type === "BlockStatement" ? node.body : node.consequent;
142
+ return statements.some((statement) => jumpSkipsMatcher(statement, matcher, type));
143
+ }
144
+ if (node.type === "TryStatement") {
145
+ if (node.finalizer && jumpSkipsMatcher(node.finalizer, matcher, type)) return true;
146
+ if (node.finalizer && alwaysExits(node.finalizer)) {
147
+ return jumpSkipsMatcher(node.finalizer, matcher, type);
148
+ }
149
+ if (jumpSkipsMatcher(node.block, matcher, type)) return true;
150
+ return Boolean(
151
+ node.handler &&
152
+ alwaysThrows(node.block) &&
153
+ jumpSkipsMatcher(node.handler.body, matcher, type),
154
+ );
155
+ }
156
+ return Boolean(
157
+ node.type === "IfStatement" &&
158
+ node.alternate &&
159
+ jumpSkipsMatcher(node.consequent, matcher, type) &&
160
+ jumpSkipsMatcher(node.alternate, matcher, type),
161
+ );
162
+ }
163
+
164
+ function breakSkipsMatcher(node, matcher) {
165
+ return jumpSkipsMatcher(node, matcher, "BreakStatement");
166
+ }
167
+
168
+ function continueSkipsMatcher(node, matcher) {
169
+ return jumpSkipsMatcher(node, matcher, "ContinueStatement");
170
+ }
171
+
172
+ module.exports = {
173
+ abruptCompletionReachesMatcher,
174
+ alwaysExits,
175
+ alwaysThrows,
176
+ breakSkipsMatcher,
177
+ continueSkipsMatcher,
178
+ caughtThrowCanContinue,
179
+ contains,
180
+ };
@@ -0,0 +1,83 @@
1
+ "use strict";
2
+
3
+ const { unwrapExpression, unwrapTransparentParent } = require("./async-ast");
4
+ const {
5
+ isNonRejectingHandler,
6
+ isNonRejectingHandlerOrAbsent,
7
+ } = require("./test-no-delayed-rejects-handlers");
8
+
9
+ const PROMISE_CHAIN_METHODS = new Set(["catch", "finally", "then"]);
10
+
11
+ function literalPropertyName(member) {
12
+ if (!member.computed && member.property.type === "Identifier") return member.property.name;
13
+ if (member.computed && member.property.type === "Literal") return member.property.value;
14
+ return null;
15
+ }
16
+
17
+ function isPromiseChainMember(node) {
18
+ return node.type === "MemberExpression" && PROMISE_CHAIN_METHODS.has(literalPropertyName(node));
19
+ }
20
+
21
+ function promiseChainBase(node) {
22
+ let current = unwrapExpression(node);
23
+ while (current.type === "CallExpression" && isPromiseChainMember(current.callee)) {
24
+ current = unwrapExpression(current.callee.object);
25
+ }
26
+ return current;
27
+ }
28
+
29
+ function continuationCall(node) {
30
+ const chainNode = unwrapTransparentParent(node);
31
+ const member = chainNode.parent;
32
+ if (
33
+ member?.type !== "MemberExpression" ||
34
+ member.object !== chainNode ||
35
+ !PROMISE_CHAIN_METHODS.has(literalPropertyName(member)) ||
36
+ member.parent?.type !== "CallExpression" ||
37
+ member.parent.callee !== member
38
+ ) {
39
+ return null;
40
+ }
41
+ return member.parent;
42
+ }
43
+
44
+ function initialCallIsSafe(node) {
45
+ const property = literalPropertyName(node.callee);
46
+ if (property === "catch") return isNonRejectingHandler(node.arguments[0]);
47
+ return (
48
+ property === "then" &&
49
+ isNonRejectingHandlerOrAbsent(node.arguments[0]) &&
50
+ isNonRejectingHandler(node.arguments[1])
51
+ );
52
+ }
53
+
54
+ function applyContinuationSafety(safe, node) {
55
+ const property = literalPropertyName(node.callee);
56
+ if (property === "finally") {
57
+ return safe && isNonRejectingHandlerOrAbsent(node.arguments[0]);
58
+ }
59
+ if (property === "catch") {
60
+ return safe || isNonRejectingHandler(node.arguments[0]);
61
+ }
62
+ if (safe) return isNonRejectingHandlerOrAbsent(node.arguments[0]);
63
+ return isNonRejectingHandler(node.arguments[0]) && isNonRejectingHandler(node.arguments[1]);
64
+ }
65
+
66
+ function chainIsSafelyObserved(node) {
67
+ let safe = initialCallIsSafe(node);
68
+ if (!safe) return false;
69
+ let current = node;
70
+ while (true) {
71
+ const continuation = continuationCall(current);
72
+ if (!continuation) return safe;
73
+ safe = applyContinuationSafety(safe, continuation);
74
+ current = continuation;
75
+ }
76
+ }
77
+
78
+ module.exports = {
79
+ chainIsSafelyObserved,
80
+ isPromiseChainMember,
81
+ literalPropertyName,
82
+ promiseChainBase,
83
+ };
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+
3
+ const {
4
+ abruptCompletionReachesMatcher,
5
+ alwaysExits,
6
+ caughtThrowCanContinue,
7
+ contains,
8
+ continueSkipsMatcher,
9
+ } = require("./test-no-delayed-rejects-abrupt");
10
+
11
+ function finalizerPreventsReach(parent, child, matcher) {
12
+ if (
13
+ parent?.type !== "TryStatement" ||
14
+ child === parent.finalizer ||
15
+ !parent.finalizer ||
16
+ contains(parent, matcher)
17
+ ) {
18
+ return false;
19
+ }
20
+ if (continueSkipsMatcher(parent.finalizer, matcher)) return true;
21
+ return (
22
+ alwaysExits(parent.finalizer) &&
23
+ !contains(parent.finalizer, matcher) &&
24
+ !abruptCompletionReachesMatcher(parent.finalizer, matcher) &&
25
+ !caughtThrowCanContinue(parent.finalizer, matcher)
26
+ );
27
+ }
28
+
29
+ module.exports = { finalizerPreventsReach };
@@ -0,0 +1,201 @@
1
+ "use strict";
2
+
3
+ const {
4
+ abruptCompletionReachesMatcher,
5
+ alwaysExits,
6
+ alwaysThrows,
7
+ breakSkipsMatcher,
8
+ continueSkipsMatcher,
9
+ caughtThrowCanContinue,
10
+ contains,
11
+ } = require("./test-no-delayed-rejects-abrupt");
12
+ const { finalizerPreventsReach } = require("./test-no-delayed-rejects-finalizers");
13
+ const {
14
+ mayThrow,
15
+ possibleCaughtThrowCanContinue,
16
+ thrownCompletionCanReachMatcher,
17
+ } = require("./test-no-delayed-rejects-transfers");
18
+ const { childExecutesBefore } = require("./test-no-delayed-rejects-order");
19
+
20
+ function isLoop(node) {
21
+ return (
22
+ node?.type === "WhileStatement" ||
23
+ node?.type === "DoWhileStatement" ||
24
+ node?.type === "ForStatement" ||
25
+ node?.type === "ForInStatement" ||
26
+ node?.type === "ForOfStatement"
27
+ );
28
+ }
29
+
30
+ function isOptionalCall(node) {
31
+ if (node.type !== "CallExpression") return false;
32
+ if (node.optional) return true;
33
+ let current = node.callee;
34
+ while (current.type === "MemberExpression") {
35
+ if (current.optional) return true;
36
+ current = current.object;
37
+ }
38
+ return false;
39
+ }
40
+
41
+ function hasLoopBackedge(node, functionNode) {
42
+ let current = node.parent;
43
+ while (current && current !== functionNode) {
44
+ if (isLoop(current)) return true;
45
+ current = current.parent;
46
+ }
47
+ return false;
48
+ }
49
+
50
+ function branchesAreExclusive(current, parent, matcher, suspension, functionNode) {
51
+ if (parent?.type === "IfStatement" || parent?.type === "ConditionalExpression") {
52
+ if (hasLoopBackedge(parent, functionNode)) return false;
53
+ return (
54
+ (current === parent.consequent && parent.alternate && contains(parent.alternate, matcher)) ||
55
+ (current === parent.alternate && contains(parent.consequent, matcher))
56
+ );
57
+ }
58
+ if (current.type === "SwitchCase" && parent?.type === "SwitchStatement") {
59
+ if (hasLoopBackedge(parent, functionNode)) return false;
60
+ const currentIndex = parent.cases.indexOf(current);
61
+ const matcherIndex = parent.cases.findIndex((item) => contains(item, matcher));
62
+ if (matcherIndex === -1 || matcherIndex === currentIndex) return false;
63
+ if (matcherIndex < currentIndex) return true;
64
+ const suspensionIndex = current.consequent.findIndex((item) => contains(item, suspension));
65
+ return current.consequent
66
+ .slice(suspensionIndex + 1)
67
+ .some(
68
+ (item) =>
69
+ item.type === "BreakStatement" ||
70
+ alwaysExits(item) ||
71
+ continueSkipsMatcher(item, matcher),
72
+ );
73
+ }
74
+ return false;
75
+ }
76
+
77
+ function canReachMatcher(suspension, matcher, functionNode) {
78
+ if (thrownCompletionCanReachMatcher(suspension, matcher)) return true;
79
+ let current = suspension;
80
+ while (current && current !== functionNode) {
81
+ if (
82
+ (current.type === "ReturnStatement" || current.type === "ThrowStatement") &&
83
+ !contains(current, matcher) &&
84
+ !abruptCompletionReachesMatcher(current, matcher) &&
85
+ !caughtThrowCanContinue(current, matcher)
86
+ ) {
87
+ return false;
88
+ }
89
+ if (!contains(current, matcher) && breakSkipsMatcher(current, matcher)) return false;
90
+ if (!contains(current, matcher) && continueSkipsMatcher(current, matcher)) return false;
91
+ const parent = current.parent;
92
+ if (finalizerPreventsReach(parent, current, matcher)) return false;
93
+ if (branchesAreExclusive(current, parent, matcher, suspension, functionNode)) return false;
94
+ const statements =
95
+ parent?.type === "BlockStatement"
96
+ ? parent.body
97
+ : parent?.type === "SwitchCase"
98
+ ? parent.consequent
99
+ : null;
100
+ if (statements) {
101
+ const currentIndex = statements.indexOf(current);
102
+ if (currentIndex !== -1) {
103
+ const matcherIndex = statements.findIndex((statement) => contains(statement, matcher));
104
+ const end = matcherIndex === -1 ? statements.length : matcherIndex;
105
+ const following = statements.slice(currentIndex + 1, end);
106
+ const exitIndex = following.findIndex(
107
+ (statement) =>
108
+ alwaysExits(statement) ||
109
+ breakSkipsMatcher(statement, matcher) ||
110
+ continueSkipsMatcher(statement, matcher),
111
+ );
112
+ const exit = following[exitIndex];
113
+ const caughtThrow =
114
+ exit &&
115
+ following
116
+ .slice(0, exitIndex + 1)
117
+ .some((statement) => possibleCaughtThrowCanContinue(statement, matcher));
118
+ if (
119
+ exit &&
120
+ !caughtThrow &&
121
+ !abruptCompletionReachesMatcher(exit, matcher) &&
122
+ !caughtThrowCanContinue(exit, matcher)
123
+ ) {
124
+ return false;
125
+ }
126
+ }
127
+ }
128
+ current = parent;
129
+ }
130
+ return true;
131
+ }
132
+
133
+ function throwCanSkipObserver(block, observer, suspension) {
134
+ const observerIndex = block.body.findIndex((statement) => contains(statement, observer));
135
+ return block.body
136
+ .slice(0, observerIndex)
137
+ .some(
138
+ (statement) => mayThrow(statement) && thrownCompletionCanReachMatcher(statement, suspension),
139
+ );
140
+ }
141
+
142
+ function isConditionalBoundary(node, child, observer, suspension) {
143
+ if (node.type === "IfStatement" || node.type === "ConditionalExpression") {
144
+ return child !== node.test;
145
+ }
146
+ if (node.type === "LogicalExpression") return child === node.right;
147
+ if (node.type === "AssignmentPattern") return child === node.right;
148
+ if (node.type === "SwitchStatement") return child !== node.discriminant;
149
+ if (node.type === "TryStatement") {
150
+ if (child === node.handler) return !alwaysThrows(node.block);
151
+ return Boolean(child === node.block && throwCanSkipObserver(node.block, observer, suspension));
152
+ }
153
+ if (node.type === "ForStatement") return child !== node.init && child !== node.test;
154
+ if (node.type === "WhileStatement") return child !== node.test;
155
+ if (node.type === "ForInStatement" || node.type === "ForOfStatement") {
156
+ return child !== node.right;
157
+ }
158
+ return isOptionalCall(node) || node.type === "DoWhileStatement";
159
+ }
160
+
161
+ function statementsFor(container) {
162
+ return container.type === "BlockStatement" ? container.body : container.consequent;
163
+ }
164
+
165
+ function directChildIn(node, container) {
166
+ let current = node;
167
+ while (current.parent && current.parent !== container) current = current.parent;
168
+ return current.parent === container ? current : null;
169
+ }
170
+
171
+ function executesBefore(observer, suspension) {
172
+ if (contains(suspension, observer)) {
173
+ let current = observer;
174
+ while (current !== suspension) {
175
+ const parent = current.parent;
176
+ if (isConditionalBoundary(parent, current, observer, suspension)) {
177
+ return false;
178
+ }
179
+ current = parent;
180
+ }
181
+ return true;
182
+ }
183
+ let current = observer;
184
+ let conditional = false;
185
+ while (current.parent) {
186
+ const parent = current.parent;
187
+ if (!conditional && childExecutesBefore(parent, current, suspension)) return true;
188
+ if ((parent.type === "BlockStatement" || parent.type === "SwitchCase") && !conditional) {
189
+ const suspensionStatement = directChildIn(suspension, parent);
190
+ if (suspensionStatement) {
191
+ const statements = statementsFor(parent);
192
+ if (statements.indexOf(current) < statements.indexOf(suspensionStatement)) return true;
193
+ }
194
+ }
195
+ if (isConditionalBoundary(parent, current, observer, suspension)) conditional = true;
196
+ current = parent;
197
+ }
198
+ return false;
199
+ }
200
+
201
+ module.exports = { canReachMatcher, contains, executesBefore };