eslint-plugin-no-mistakes 0.95.0 → 0.96.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/README.md +5 -0
- package/package.json +1 -1
- package/src/index.js +3 -0
- package/src/rules/argv-main-fragments.js +25 -0
- package/src/rules/argv-main-values.js +65 -7
- package/src/rules/async-call-disposition.js +39 -154
- package/src/rules/async-disposition-bindings.js +131 -0
- package/src/rules/async-disposition-expressions.js +166 -0
- package/src/rules/async-disposition-flow.js +151 -0
- package/src/rules/async-disposition-mock-bindings.js +39 -0
- package/src/rules/async-disposition-mocks.js +142 -0
- package/src/rules/configured-runtime-import.js +28 -0
- package/src/rules/gh-api-argument-safety.js +6 -30
- package/src/rules/gh-api-wrappers.js +52 -0
- package/src/rules/logical-call.js +75 -0
- package/src/rules/module-mock-helpers.js +1 -62
- package/src/rules/no-banned-import-outside-allowed-paths-calls.js +39 -0
- package/src/rules/no-banned-import-outside-allowed-paths.js +3 -29
- package/src/rules/no-consecutive-unbatched-reads.js +136 -0
- package/src/rules/no-object-mutation-ast.js +13 -0
- package/src/rules/no-object-mutation-bind.js +10 -3
- package/src/rules/no-object-mutation-call.js +22 -10
- package/src/rules/no-object-mutation-expr.js +9 -2
- package/src/rules/no-object-mutation-fn.js +8 -0
- package/src/rules/no-object-mutation-forward.js +2 -11
- package/src/rules/no-object-mutation-invoke.js +40 -1
- package/src/rules/no-object-mutation-join.js +5 -0
- package/src/rules/no-object-mutation-method.js +65 -0
- package/src/rules/no-object-mutation-read.js +17 -1
- package/src/rules/no-object-mutation-scan.js +8 -1
- package/src/rules/no-object-mutation-values.js +4 -0
- package/src/rules/no-object-mutation-write.js +1 -0
- package/src/rules/playwright-assertion-timeout-cap.js +13 -18
- package/src/rules/playwright-scroll-call.js +8 -6
- package/src/rules/playwright-static-call-alias.js +65 -0
- package/src/rules/playwright-static-call.js +20 -74
- package/src/rules/postgres-executor-calls.js +157 -0
- package/src/rules/postgres-executor.js +19 -3
- package/src/rules/postgres-fanout-calls.js +72 -0
- package/src/rules/postgres-no-manual-transaction.js +5 -2
- package/src/rules/postgres-no-nonliteral-sql-registry-key.js +2 -2
- package/src/rules/postgres-no-unbounded-query-fanout.js +9 -3
- package/src/rules/postgres-runtime-helpers.js +9 -5
- package/src/rules/require-top-level-registration.js +64 -0
- package/src/rules/required-call-selector-aliases.js +45 -0
- package/src/rules/required-call-selector-compile.js +9 -3
- package/src/rules/required-call-selectors.js +24 -5
- package/src/rules/restricted-api-usage.js +195 -0
- package/src/rules/test-message-expect.js +10 -7
- package/src/rules/test-no-delayed-rejects-expect.js +59 -0
- package/src/rules/test-no-delayed-rejects.js +2 -26
- package/src/rules/test-no-skips-bindings.js +14 -4
- package/src/rules/test-no-skips.js +9 -5
- package/src/rules/ts-no-function-aliases-ast.js +65 -0
- package/src/rules/ts-no-function-aliases-contract.js +92 -0
- package/src/rules/ts-no-function-aliases-forwarding.js +46 -0
- package/src/rules/ts-no-function-aliases-legacy.js +79 -0
- package/src/rules/ts-no-function-aliases-reachable.js +76 -0
- package/src/rules/ts-no-function-aliases-self.js +132 -0
- package/src/rules/ts-no-function-aliases-signature.js +65 -0
- package/src/rules/ts-no-function-aliases.js +66 -95
package/README.md
CHANGED
|
@@ -57,3 +57,8 @@ See [options and examples](../../docs/eslint-rules/postgres-no-async-sql-builder
|
|
|
57
57
|
`require-options-on-imported-call` requires a static options object on configured
|
|
58
58
|
imports, globals with no local binding, and terminal methods with a matching receiver shape.
|
|
59
59
|
See [options and examples](../../docs/eslint-rules/require-options-on-imported-call.md).
|
|
60
|
+
|
|
61
|
+
`restricted-api-usage` is an opt-in import/export-name and call-name policy.
|
|
62
|
+
See [options and examples](../../docs/eslint-rules/restricted-api-usage.md).
|
|
63
|
+
|
|
64
|
+
Opt-in client contracts: [`no-consecutive-unbatched-reads`](../../docs/eslint-rules/no-consecutive-unbatched-reads.md) configures adjacent awaited read batching; [`require-top-level-registration`](../../docs/eslint-rules/require-top-level-registration.md) configures owner-module lifecycle registration. Both support ESLint/Oxlint and have no product defaults.
|
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
const rules = {
|
|
4
|
+
"restricted-api-usage": require("./rules/restricted-api-usage"),
|
|
5
|
+
"no-consecutive-unbatched-reads": require("./rules/no-consecutive-unbatched-reads"),
|
|
6
|
+
"require-top-level-registration": require("./rules/require-top-level-registration"),
|
|
4
7
|
"forbidden-member-chains": require("./rules/forbidden-member-chains"),
|
|
5
8
|
"gh-api-argument-safety": require("./rules/gh-api-argument-safety"),
|
|
6
9
|
"no-argv-main-guard": require("./rules/no-argv-main-guard"),
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
const ARGV = 1;
|
|
3
|
+
// Strings and [prefix, suffix] fragments stay separate from path identities.
|
|
4
|
+
// Keeping the fragment until the comparison lets nested concatenations fold once.
|
|
5
|
+
function identity(value) {
|
|
6
|
+
if (Array.isArray(value)) return /^file:\/\/\/?$/i.test(value[0]) && value[1] === "" ? ARGV : 0;
|
|
7
|
+
return typeof value === "number" ? value : 0;
|
|
8
|
+
}
|
|
9
|
+
function concatenate(left, right) {
|
|
10
|
+
if (left === ARGV) left = ["", ""];
|
|
11
|
+
if (right === ARGV) right = ["", ""];
|
|
12
|
+
// Accepted prefixes have at most eight characters; reject before allocating.
|
|
13
|
+
if (typeof left === "string" && typeof right === "string")
|
|
14
|
+
return left.length + right.length <= 8 ? left + right : 0;
|
|
15
|
+
if (typeof left === "string" && Array.isArray(right))
|
|
16
|
+
return left.length + right[0].length <= 8 ? [left + right[0], right[1]] : 0;
|
|
17
|
+
if (Array.isArray(left) && typeof right === "string") return right === "" ? left : 0;
|
|
18
|
+
return 0;
|
|
19
|
+
}
|
|
20
|
+
function fileUrlArgument(target, read) {
|
|
21
|
+
if (target?.type !== "NewExpression" || target.arguments.length !== 1) return 0;
|
|
22
|
+
const argument = read(target.arguments[0]);
|
|
23
|
+
return Array.isArray(argument) && identity(argument) === ARGV ? argument : 0;
|
|
24
|
+
}
|
|
25
|
+
module.exports = { ARGV, identity, concatenate, fileUrlArgument };
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
const { unwrapExpression } = require("./async-ast");
|
|
3
3
|
const { resolveVariable, memberPropertyName, isLocalRequire } = require("./async-target-bindings");
|
|
4
4
|
const { literalString, logicalCall } = require("./module-mock-helpers");
|
|
5
|
+
const { ARGV, identity, concatenate, fileUrlArgument } = require("./argv-main-fragments");
|
|
5
6
|
|
|
6
7
|
function globalName(node, name, context) {
|
|
7
8
|
if (node?.type !== "Identifier" || node.name !== name) return false;
|
|
@@ -29,16 +30,18 @@ function nodeProcess(node, context) {
|
|
|
29
30
|
["process", "node:process"].includes(literalString(init.arguments[0]))
|
|
30
31
|
);
|
|
31
32
|
}
|
|
32
|
-
const ARGV = 1;
|
|
33
33
|
const MAIN = 2;
|
|
34
34
|
const PROCESS = 3;
|
|
35
35
|
const ARGUMENTS = 4;
|
|
36
36
|
function createClassifier(context, wrappers) {
|
|
37
37
|
const stable = new WeakMap();
|
|
38
38
|
const aliases = new WeakMap();
|
|
39
|
+
const value = (node, depth) => identity(classify(node, depth));
|
|
39
40
|
function classify(input, depth = 0) {
|
|
40
41
|
const node = unwrapExpression(input);
|
|
41
42
|
if (!node || depth > 32) return 0;
|
|
43
|
+
const text = literalString(node);
|
|
44
|
+
if (text !== null) return text;
|
|
42
45
|
if (node.type === "Identifier") {
|
|
43
46
|
const variable = resolveVariable(node, context);
|
|
44
47
|
if (!variable) {
|
|
@@ -67,7 +70,7 @@ function createClassifier(context, wrappers) {
|
|
|
67
70
|
def.node?.id?.type === "ObjectPattern"
|
|
68
71
|
) {
|
|
69
72
|
const key = objectPatternKey(def.node.id, node.name);
|
|
70
|
-
if (key === "argv" &&
|
|
73
|
+
if (key === "argv" && value(def.node.init, depth + 1) === PROCESS) return ARGUMENTS;
|
|
71
74
|
return 0;
|
|
72
75
|
}
|
|
73
76
|
if (
|
|
@@ -83,6 +86,29 @@ function createClassifier(context, wrappers) {
|
|
|
83
86
|
}
|
|
84
87
|
return aliases.get(variable);
|
|
85
88
|
}
|
|
89
|
+
if (node.type === "BinaryExpression" && node.operator === "+") {
|
|
90
|
+
return concatenate(classify(node.left, depth + 1), classify(node.right, depth + 1));
|
|
91
|
+
}
|
|
92
|
+
if (
|
|
93
|
+
node.type === "LogicalExpression" &&
|
|
94
|
+
node.operator === "??" &&
|
|
95
|
+
classify(node.right, depth + 1) === ""
|
|
96
|
+
) {
|
|
97
|
+
return classify(node.left, depth + 1);
|
|
98
|
+
}
|
|
99
|
+
if (node.type === "TemplateLiteral") {
|
|
100
|
+
// Only prefix substitutions may be constant; a suffix substitution is not an argv guard.
|
|
101
|
+
const last = node.expressions.length - 1;
|
|
102
|
+
const argument = classify(node.expressions[last], depth + 1);
|
|
103
|
+
if (identity(argument) !== ARGV || node.quasis[last + 1].value.cooked !== "") return 0;
|
|
104
|
+
let prefix = node.quasis[0].value.cooked;
|
|
105
|
+
for (let i = 0; i < last; i++) {
|
|
106
|
+
const part = classify(node.expressions[i], depth + 1);
|
|
107
|
+
if (typeof part !== "string") return 0;
|
|
108
|
+
prefix = concatenate(prefix, concatenate(part, node.quasis[i + 1].value.cooked));
|
|
109
|
+
}
|
|
110
|
+
return concatenate(prefix, argument);
|
|
111
|
+
}
|
|
86
112
|
if (node.type === "MemberExpression") {
|
|
87
113
|
const name = memberPropertyName(node);
|
|
88
114
|
if (
|
|
@@ -92,7 +118,12 @@ function createClassifier(context, wrappers) {
|
|
|
92
118
|
["url", "filename"].includes(name)
|
|
93
119
|
)
|
|
94
120
|
return MAIN;
|
|
95
|
-
const
|
|
121
|
+
const target = unwrapExpression(node.object);
|
|
122
|
+
if (name === "href" && globalName(target?.callee, "URL", context)) {
|
|
123
|
+
const constructed = fileUrlArgument(target, (argument) => classify(argument, depth + 1));
|
|
124
|
+
if (constructed) return constructed;
|
|
125
|
+
}
|
|
126
|
+
const object = value(node.object, depth + 1);
|
|
96
127
|
if (object === PROCESS && name === "argv") return ARGUMENTS;
|
|
97
128
|
if (object === ARGUMENTS && node.computed && staticIndexOne(node.property)) return ARGV;
|
|
98
129
|
// A URL conversion's href preserves the same module/argv identity.
|
|
@@ -110,20 +141,47 @@ function createClassifier(context, wrappers) {
|
|
|
110
141
|
if (
|
|
111
142
|
callee?.type === "MemberExpression" &&
|
|
112
143
|
memberPropertyName(callee) === "at" &&
|
|
113
|
-
|
|
144
|
+
value(callee.object, depth + 1) === ARGUMENTS &&
|
|
114
145
|
invoked.arguments.length === 1 &&
|
|
115
146
|
staticIndexOne(invoked.arguments[0])
|
|
116
147
|
)
|
|
117
148
|
return ARGV;
|
|
149
|
+
if (
|
|
150
|
+
!invoked.forwarded &&
|
|
151
|
+
callee?.type === "MemberExpression" &&
|
|
152
|
+
memberPropertyName(callee) === "toString" &&
|
|
153
|
+
invoked.arguments.length === 0
|
|
154
|
+
) {
|
|
155
|
+
const url = unwrapExpression(callee.object);
|
|
156
|
+
if (globalName(url?.callee, "URL", context)) {
|
|
157
|
+
const constructed = fileUrlArgument(url, (argument) => classify(argument, depth + 1));
|
|
158
|
+
if (constructed) return constructed;
|
|
159
|
+
}
|
|
160
|
+
const classified = classify(callee.object, depth + 1);
|
|
161
|
+
return typeof classified === "string" || [ARGV, MAIN].includes(identity(classified))
|
|
162
|
+
? classified
|
|
163
|
+
: 0;
|
|
164
|
+
}
|
|
165
|
+
if (
|
|
166
|
+
!invoked.forwarded &&
|
|
167
|
+
callee?.type === "MemberExpression" &&
|
|
168
|
+
memberPropertyName(callee) === "concat" &&
|
|
169
|
+
invoked.arguments.every((argument) => argument.type !== "SpreadElement")
|
|
170
|
+
)
|
|
171
|
+
return invoked.arguments.reduce(
|
|
172
|
+
(folded, argument) => concatenate(folded, classify(argument, depth + 1)),
|
|
173
|
+
classify(callee.object, depth + 1),
|
|
174
|
+
);
|
|
118
175
|
if (invoked.arguments.length !== 1 || invoked.arguments[0].type === "SpreadElement") return 0;
|
|
176
|
+
if (globalName(callee, "String", context)) return classify(invoked.arguments[0], depth + 1);
|
|
119
177
|
if (globalName(callee, "Boolean", context) || wrappers.resolveCallTarget(node)) {
|
|
120
|
-
const
|
|
121
|
-
return
|
|
178
|
+
const argument = value(invoked.arguments[0], depth + 1);
|
|
179
|
+
return argument === ARGV || argument === MAIN ? argument : 0;
|
|
122
180
|
}
|
|
123
181
|
}
|
|
124
182
|
return 0;
|
|
125
183
|
}
|
|
126
|
-
return (node) =>
|
|
184
|
+
return (node) => value(node, 0);
|
|
127
185
|
}
|
|
128
186
|
function staticIndexOne(node) {
|
|
129
187
|
if (!node) return false;
|
|
@@ -1,166 +1,50 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
const { rule } = require("../helpers");
|
|
4
|
+
const { ruleSuppression } = require("../rule-suppression");
|
|
4
5
|
const {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
6
|
+
collectedPromise,
|
|
7
|
+
isPromiseAllCall,
|
|
8
|
+
retainedCollection,
|
|
9
|
+
retainedPromise,
|
|
10
|
+
} = require("./async-disposition-bindings");
|
|
11
|
+
const {
|
|
12
|
+
directlyDisposed,
|
|
13
|
+
observedExpression,
|
|
14
|
+
promiseAllDisposed,
|
|
15
|
+
mappedPromise,
|
|
16
|
+
} = require("./async-disposition-expressions");
|
|
11
17
|
const { targetOptionsSchema } = require("./async-schema");
|
|
12
|
-
const { createTargetMatcher
|
|
18
|
+
const { createTargetMatcher } = require("./async-targets");
|
|
13
19
|
|
|
14
|
-
function
|
|
15
|
-
|
|
16
|
-
|
|
20
|
+
function isDisposed(node, context, seen = new Set()) {
|
|
21
|
+
if (seen.has(node)) return false;
|
|
22
|
+
seen.add(node);
|
|
23
|
+
const observed = observedExpression(node, context);
|
|
24
|
+
const disposed = (value) => isDisposed(value, context, seen);
|
|
17
25
|
return (
|
|
18
|
-
node
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
26
|
+
directlyDisposed(node, context) ||
|
|
27
|
+
directlyDisposed(observed, context) ||
|
|
28
|
+
promiseAllDisposed(observed, context) ||
|
|
29
|
+
retainedPromise(observed, context, disposed) ||
|
|
30
|
+
mappedPromise(observed, (value) =>
|
|
31
|
+
retainedCollection(
|
|
32
|
+
value,
|
|
33
|
+
context,
|
|
34
|
+
(aggregate) => isPromiseAllCall(aggregate, context),
|
|
35
|
+
disposed,
|
|
36
|
+
),
|
|
37
|
+
) ||
|
|
38
|
+
collectedPromise(
|
|
39
|
+
observed,
|
|
40
|
+
context,
|
|
41
|
+
(value) => isPromiseAllCall(value, context),
|
|
42
|
+
disposed,
|
|
43
|
+
node.type === "CallExpression",
|
|
44
|
+
)
|
|
23
45
|
);
|
|
24
46
|
}
|
|
25
47
|
|
|
26
|
-
function directlyDisposed(node) {
|
|
27
|
-
const expression = unwrapTransparentParent(node);
|
|
28
|
-
const parent = expression.parent;
|
|
29
|
-
if (parent?.type === "ReturnStatement") {
|
|
30
|
-
const fn = findContainingFunction(parent);
|
|
31
|
-
return !isCallArgument(fn);
|
|
32
|
-
}
|
|
33
|
-
if (parent?.type === "ArrowFunctionExpression" && parent.body === expression) {
|
|
34
|
-
if (isCallCallee(parent)) return directlyDisposed(parent.parent);
|
|
35
|
-
return !isCallArgument(parent);
|
|
36
|
-
}
|
|
37
|
-
return (
|
|
38
|
-
parent?.type === "AwaitExpression" ||
|
|
39
|
-
(parent?.type === "UnaryExpression" && parent.operator === "void")
|
|
40
|
-
);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function isCallArgument(node) {
|
|
44
|
-
return Boolean(node?.parent?.type === "CallExpression" && node.parent.arguments.includes(node));
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function isCallCallee(node) {
|
|
48
|
-
return node?.parent?.type === "CallExpression" && node.parent.callee === node;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function isPromiseAllIterable(node, promiseAll) {
|
|
52
|
-
return promiseAll.arguments[0] === node && (node.type === "ArrayExpression" || isMapCall(node));
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function isMapCall(node) {
|
|
56
|
-
return (
|
|
57
|
-
node?.type === "CallExpression" &&
|
|
58
|
-
node.callee.type === "MemberExpression" &&
|
|
59
|
-
memberPropertyName(node.callee) === "map"
|
|
60
|
-
);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function isMapCallback(fn) {
|
|
64
|
-
const call = fn.parent;
|
|
65
|
-
return isMapCall(call) && call.arguments.includes(fn);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function expressionReturnedFromCallback(node, fn) {
|
|
69
|
-
let current = node;
|
|
70
|
-
let returnStatement = null;
|
|
71
|
-
while (current && current !== fn) {
|
|
72
|
-
if (current.parent === fn && fn.body === current && fn.body.type !== "BlockStatement") {
|
|
73
|
-
return true;
|
|
74
|
-
}
|
|
75
|
-
if (current.parent?.type === "ReturnStatement") {
|
|
76
|
-
returnStatement = current.parent;
|
|
77
|
-
}
|
|
78
|
-
current = current.parent;
|
|
79
|
-
}
|
|
80
|
-
return Boolean(returnStatement);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function canReachPromiseAll(node, promiseAll) {
|
|
84
|
-
let current = node;
|
|
85
|
-
while (current && current !== promiseAll) {
|
|
86
|
-
const parent = current.parent;
|
|
87
|
-
if (isTransparentExpression(parent)) {
|
|
88
|
-
current = parent;
|
|
89
|
-
continue;
|
|
90
|
-
}
|
|
91
|
-
if (parent?.type === "MemberExpression") return false;
|
|
92
|
-
if (parent === promiseAll) return isPromiseAllIterable(current, promiseAll);
|
|
93
|
-
if (isFunction(parent)) {
|
|
94
|
-
if (!expressionReturnedFromCallback(node, parent) || !isMapCallback(parent)) {
|
|
95
|
-
return false;
|
|
96
|
-
}
|
|
97
|
-
current = parent.parent;
|
|
98
|
-
continue;
|
|
99
|
-
}
|
|
100
|
-
if (parent?.type === "ArrayExpression") {
|
|
101
|
-
if (parent.parent !== promiseAll || promiseAll.arguments[0] !== parent) return false;
|
|
102
|
-
current = parent;
|
|
103
|
-
continue;
|
|
104
|
-
}
|
|
105
|
-
if (parent?.type === "ObjectExpression") return false;
|
|
106
|
-
if (
|
|
107
|
-
parent?.type === "CallExpression" &&
|
|
108
|
-
parent !== promiseAll &&
|
|
109
|
-
parent.arguments.includes(current)
|
|
110
|
-
) {
|
|
111
|
-
return false;
|
|
112
|
-
}
|
|
113
|
-
current = parent;
|
|
114
|
-
}
|
|
115
|
-
return current === promiseAll;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
function observedExpression(node) {
|
|
119
|
-
let current = node;
|
|
120
|
-
while (current.parent) {
|
|
121
|
-
const parent = current.parent;
|
|
122
|
-
if (isTransparentExpression(parent)) {
|
|
123
|
-
current = parent;
|
|
124
|
-
continue;
|
|
125
|
-
}
|
|
126
|
-
if (
|
|
127
|
-
parent.type === "MemberExpression" &&
|
|
128
|
-
parent.object === current &&
|
|
129
|
-
parent.parent?.type === "CallExpression" &&
|
|
130
|
-
parent.parent.callee === parent
|
|
131
|
-
) {
|
|
132
|
-
current = parent;
|
|
133
|
-
continue;
|
|
134
|
-
}
|
|
135
|
-
if (parent.type === "CallExpression" && parent.callee === current) {
|
|
136
|
-
current = parent;
|
|
137
|
-
continue;
|
|
138
|
-
}
|
|
139
|
-
break;
|
|
140
|
-
}
|
|
141
|
-
return current;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
function promiseAllDisposed(node) {
|
|
145
|
-
let current = node.parent;
|
|
146
|
-
while (current) {
|
|
147
|
-
if (isPromiseAllCall(current) && canReachPromiseAll(node, current)) {
|
|
148
|
-
return (
|
|
149
|
-
directlyDisposed(current) ||
|
|
150
|
-
directlyDisposed(observedExpression(current)) ||
|
|
151
|
-
promiseAllDisposed(current)
|
|
152
|
-
);
|
|
153
|
-
}
|
|
154
|
-
current = current.parent;
|
|
155
|
-
}
|
|
156
|
-
return false;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
function isDisposed(node) {
|
|
160
|
-
const observed = observedExpression(node);
|
|
161
|
-
return directlyDisposed(node) || directlyDisposed(observed) || promiseAllDisposed(node);
|
|
162
|
-
}
|
|
163
|
-
|
|
164
48
|
module.exports = rule(
|
|
165
49
|
{
|
|
166
50
|
type: "problem",
|
|
@@ -178,12 +62,13 @@ module.exports = rule(
|
|
|
178
62
|
},
|
|
179
63
|
(context) => {
|
|
180
64
|
const matcher = createTargetMatcher(context);
|
|
65
|
+
const suppressed = ruleSuppression(context, "async-call-disposition");
|
|
181
66
|
if (!matcher.hasTargets) return {};
|
|
182
67
|
|
|
183
68
|
return {
|
|
184
69
|
...matcher.visitors,
|
|
185
70
|
CallExpression(node) {
|
|
186
|
-
if (!matcher.isTargetCall(node) || isDisposed(node)) return;
|
|
71
|
+
if (!matcher.isTargetCall(node) || suppressed(node) || isDisposed(node, context)) return;
|
|
187
72
|
context.report({
|
|
188
73
|
node,
|
|
189
74
|
messageId: "disposition",
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { unwrapExpression, unwrapTransparentParent } = require("./async-ast");
|
|
4
|
+
const { resolveVariable, stableConstInitializer } = require("./async-target-bindings");
|
|
5
|
+
const { laterDrain, laterInBlock } = require("./async-disposition-flow");
|
|
6
|
+
const { propertiesWritten } = require("./async-disposition-mocks");
|
|
7
|
+
const { memberPropertyName } = require("./async-targets");
|
|
8
|
+
|
|
9
|
+
function globalPromise(node, context) {
|
|
10
|
+
const variable = resolveVariable(node, context);
|
|
11
|
+
return !variable?.defs.length && !propertiesWritten(node, context);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function isPromiseMethodCall(node, context, methods) {
|
|
15
|
+
const callee = unwrapExpression(node?.callee);
|
|
16
|
+
const object = unwrapExpression(callee?.object);
|
|
17
|
+
return (
|
|
18
|
+
node?.type === "CallExpression" &&
|
|
19
|
+
callee?.type === "MemberExpression" &&
|
|
20
|
+
object?.type === "Identifier" &&
|
|
21
|
+
object.name === "Promise" &&
|
|
22
|
+
globalPromise(object, context) &&
|
|
23
|
+
methods.includes(memberPropertyName(callee))
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function isPromiseAllCall(node, context) {
|
|
28
|
+
return isPromiseMethodCall(node, context, ["all", "allSettled", "race", "any"]);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function standalonePush(call) {
|
|
32
|
+
return (
|
|
33
|
+
call.parent.type === "ExpressionStatement" ||
|
|
34
|
+
(call.parent.type === "ArrowFunctionExpression" && call.parent.body === call)
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function pushedArray(node) {
|
|
39
|
+
const value = unwrapTransparentParent(node);
|
|
40
|
+
const push = value.parent;
|
|
41
|
+
const callee = unwrapExpression(push?.callee);
|
|
42
|
+
if (
|
|
43
|
+
push?.type !== "CallExpression" ||
|
|
44
|
+
!push.arguments.includes(value) ||
|
|
45
|
+
callee?.type !== "MemberExpression" ||
|
|
46
|
+
memberPropertyName(callee) !== "push" ||
|
|
47
|
+
callee.object.type !== "Identifier" ||
|
|
48
|
+
!standalonePush(push)
|
|
49
|
+
)
|
|
50
|
+
return null;
|
|
51
|
+
return { id: callee.object, push };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function collectedPromise(node, context, isAggregate, disposed, createdHere) {
|
|
55
|
+
const found = pushedArray(node);
|
|
56
|
+
if (!found) return false;
|
|
57
|
+
const init = unwrapExpression(stableConstInitializer(found.id, context, new Set()));
|
|
58
|
+
if (init?.type !== "ArrayExpression" || init.elements.length !== 0) return false;
|
|
59
|
+
const variable = resolveVariable(found.id, context);
|
|
60
|
+
let drained = false;
|
|
61
|
+
for (const reference of variable.references) {
|
|
62
|
+
if (reference.init) continue;
|
|
63
|
+
const id = reference.identifier;
|
|
64
|
+
const parent = unwrapTransparentParent(id).parent;
|
|
65
|
+
if (isAggregate(parent) && parent.arguments[0] === id) {
|
|
66
|
+
if (laterDrain(found.push, parent, createdHere) && disposed(parent)) drained = true;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
const member = parent;
|
|
70
|
+
const call = member?.parent;
|
|
71
|
+
if (
|
|
72
|
+
member?.type !== "MemberExpression" ||
|
|
73
|
+
member.object !== id ||
|
|
74
|
+
memberPropertyName(member) !== "push" ||
|
|
75
|
+
call?.type !== "CallExpression" ||
|
|
76
|
+
call.callee !== member ||
|
|
77
|
+
!standalonePush(call)
|
|
78
|
+
)
|
|
79
|
+
return false;
|
|
80
|
+
// A sibling push cannot remove values already collected. Its producer
|
|
81
|
+
// independently proves whether it reaches a handled drain.
|
|
82
|
+
}
|
|
83
|
+
return drained;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function retainedCollection(node, context, isAggregate, disposed) {
|
|
87
|
+
const expression = unwrapTransparentParent(node);
|
|
88
|
+
const declaration = expression.parent;
|
|
89
|
+
if (
|
|
90
|
+
declaration?.type !== "VariableDeclarator" ||
|
|
91
|
+
declaration.init !== expression ||
|
|
92
|
+
declaration.id.type !== "Identifier" ||
|
|
93
|
+
!stableConstInitializer(declaration.id, context, new Set())
|
|
94
|
+
)
|
|
95
|
+
return false;
|
|
96
|
+
const variable = resolveVariable(declaration.id, context);
|
|
97
|
+
let drained = false;
|
|
98
|
+
for (const reference of variable.references) {
|
|
99
|
+
if (reference.init) continue;
|
|
100
|
+
const id = unwrapTransparentParent(reference.identifier);
|
|
101
|
+
const parent = id.parent;
|
|
102
|
+
if (!isAggregate(parent) || parent.arguments[0] !== id) return false;
|
|
103
|
+
if (laterDrain(node, parent) && disposed(parent)) drained = true;
|
|
104
|
+
}
|
|
105
|
+
return drained;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function retainedPromise(node, context, disposed) {
|
|
109
|
+
const expression = unwrapTransparentParent(node);
|
|
110
|
+
const declaration = expression.parent;
|
|
111
|
+
if (
|
|
112
|
+
declaration?.type !== "VariableDeclarator" ||
|
|
113
|
+
declaration.init !== expression ||
|
|
114
|
+
declaration.id.type !== "Identifier"
|
|
115
|
+
)
|
|
116
|
+
return false;
|
|
117
|
+
if (!stableConstInitializer(declaration.id, context, new Set())) return false;
|
|
118
|
+
const variable = resolveVariable(declaration.id, context);
|
|
119
|
+
return variable.references.some(
|
|
120
|
+
(reference) =>
|
|
121
|
+
!reference.init && laterInBlock(node, reference.identifier) && disposed(reference.identifier),
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
module.exports = {
|
|
126
|
+
collectedPromise,
|
|
127
|
+
isPromiseAllCall,
|
|
128
|
+
isPromiseMethodCall,
|
|
129
|
+
retainedCollection,
|
|
130
|
+
retainedPromise,
|
|
131
|
+
};
|