eslint-plugin-no-mistakes 0.96.0 → 0.98.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 +1 -1
- package/src/rules/async-call-disposition.js +24 -9
- package/src/rules/async-disposition-consumers.js +41 -0
- package/src/rules/async-disposition-expressions.js +13 -8
- package/src/rules/async-disposition-mocks.js +33 -6
- package/src/rules/async-schema.js +19 -1
- package/src/rules/async-targets.js +10 -2
- package/src/rules/no-object-mutation-call.js +4 -2
- package/src/rules/no-object-mutation-expr.js +1 -1
- package/src/rules/no-object-mutation-fn.js +7 -2
- package/src/rules/no-object-mutation-invoke.js +36 -1
- package/src/rules/no-object-mutation-reduce.js +92 -0
- package/src/rules/no-object-mutation-scan.js +1 -1
- package/src/rules/no-vitest-sequential.js +5 -16
- package/src/rules/nullable-option-defaults-meta.js +25 -0
- package/src/rules/nullable-option-scalar-alias.js +24 -0
- package/src/rules/playwright-interactive-names.js +1 -0
- package/src/rules/playwright-interactive-slots.js +55 -0
- package/src/rules/playwright-require-interactive-test-id.js +61 -9
- package/src/rules/postgres-no-async-sql-builder-types.js +1 -0
- package/src/rules/postgres-no-executor-adapter-source.js +41 -0
- package/src/rules/postgres-no-executor-adapter-target.js +142 -0
- package/src/rules/postgres-no-executor-adapter.js +18 -7
- package/src/rules/postgres-no-manual-transaction.js +35 -1
- package/src/rules/postgres-scoped-executors.js +1 -1
- package/src/rules/react-no-use-promise-resolve-alias.js +168 -0
- package/src/rules/react-no-use-promise-resolve.js +33 -1
- package/src/rules/test-message-captured-value.js +96 -0
- package/src/rules/test-message-expect.js +2 -1
- package/src/rules/test-no-delayed-rejects-promise-alias.js +69 -0
- package/src/rules/test-no-delayed-rejects.js +4 -33
- package/src/rules/test-no-error-message-matching.js +9 -4
- package/src/rules/test-no-shared-state-callees.js +20 -0
- package/src/rules/test-no-shared-state-helpers.js +8 -3
- package/src/rules/test-no-shared-state-rule-helpers.js +5 -3
- package/src/rules/test-no-shared-state.js +10 -5
- package/src/rules/test-no-skips-bindings.js +29 -19
- package/src/rules/test-no-skips-context.js +43 -0
- package/src/rules/test-no-skips.js +11 -2
- package/src/rules/ts-no-const-aliases.js +2 -4
- package/src/rules/ts-no-function-aliases-ast.js +7 -1
- package/src/rules/ts-no-function-aliases-cast-proof.js +145 -0
- package/src/rules/ts-no-function-aliases-contract.js +8 -30
- package/src/rules/ts-no-function-aliases.js +7 -1
- package/src/rules/ts-preserve-null-option-defaults.js +137 -154
package/package.json
CHANGED
|
@@ -14,18 +14,19 @@ const {
|
|
|
14
14
|
promiseAllDisposed,
|
|
15
15
|
mappedPromise,
|
|
16
16
|
} = require("./async-disposition-expressions");
|
|
17
|
-
const {
|
|
17
|
+
const { dispositionOptionsSchema } = require("./async-schema");
|
|
18
18
|
const { createTargetMatcher } = require("./async-targets");
|
|
19
|
+
const { createAwaitingConsumers } = require("./async-disposition-consumers");
|
|
19
20
|
|
|
20
|
-
function isDisposed(node, context, seen = new Set()) {
|
|
21
|
+
function isDisposed(node, context, returnsThroughConsumer, seen = new Set()) {
|
|
21
22
|
if (seen.has(node)) return false;
|
|
22
23
|
seen.add(node);
|
|
23
24
|
const observed = observedExpression(node, context);
|
|
24
|
-
const disposed = (value) => isDisposed(value, context, seen);
|
|
25
|
+
const disposed = (value) => isDisposed(value, context, returnsThroughConsumer, seen);
|
|
25
26
|
return (
|
|
26
|
-
directlyDisposed(node, context) ||
|
|
27
|
-
directlyDisposed(observed, context) ||
|
|
28
|
-
promiseAllDisposed(observed, context) ||
|
|
27
|
+
directlyDisposed(node, context, returnsThroughConsumer) ||
|
|
28
|
+
directlyDisposed(observed, context, returnsThroughConsumer) ||
|
|
29
|
+
promiseAllDisposed(observed, context, returnsThroughConsumer) ||
|
|
29
30
|
retainedPromise(observed, context, disposed) ||
|
|
30
31
|
mappedPromise(observed, (value) =>
|
|
31
32
|
retainedCollection(
|
|
@@ -53,7 +54,7 @@ module.exports = rule(
|
|
|
53
54
|
recommended: false,
|
|
54
55
|
},
|
|
55
56
|
hasSuggestions: true,
|
|
56
|
-
schema:
|
|
57
|
+
schema: dispositionOptionsSchema,
|
|
57
58
|
messages: {
|
|
58
59
|
disposition:
|
|
59
60
|
"Handle this async promise explicitly with await, return, Promise.all(...), or void.",
|
|
@@ -62,13 +63,27 @@ module.exports = rule(
|
|
|
62
63
|
},
|
|
63
64
|
(context) => {
|
|
64
65
|
const matcher = createTargetMatcher(context);
|
|
66
|
+
const consumers = createAwaitingConsumers(context);
|
|
65
67
|
const suppressed = ruleSuppression(context, "async-call-disposition");
|
|
66
68
|
if (!matcher.hasTargets) return {};
|
|
67
69
|
|
|
68
70
|
return {
|
|
69
|
-
...
|
|
71
|
+
...Object.fromEntries(
|
|
72
|
+
Object.keys(matcher.visitors).map((key) => [
|
|
73
|
+
key,
|
|
74
|
+
(node) => {
|
|
75
|
+
matcher.visitors[key](node);
|
|
76
|
+
consumers.visitors[key](node);
|
|
77
|
+
},
|
|
78
|
+
]),
|
|
79
|
+
),
|
|
70
80
|
CallExpression(node) {
|
|
71
|
-
if (
|
|
81
|
+
if (
|
|
82
|
+
!matcher.isTargetCall(node) ||
|
|
83
|
+
suppressed(node) ||
|
|
84
|
+
isDisposed(node, context, consumers.returnsThroughConsumer)
|
|
85
|
+
)
|
|
86
|
+
return;
|
|
72
87
|
context.report({
|
|
73
88
|
node,
|
|
74
89
|
messageId: "disposition",
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { unwrapTransparentParent } = require("./async-ast");
|
|
4
|
+
const { compileTargets, targetMatches } = require("./async-patterns");
|
|
5
|
+
const { propertiesWritten } = require("./async-disposition-mocks");
|
|
6
|
+
const { createTargetMatcher } = require("./async-targets");
|
|
7
|
+
|
|
8
|
+
function createAwaitingConsumers(context) {
|
|
9
|
+
const matcher = createTargetMatcher(
|
|
10
|
+
context,
|
|
11
|
+
"awaitingConsumers",
|
|
12
|
+
(id) => !propertiesWritten(id, context, new Set(), null, true),
|
|
13
|
+
true,
|
|
14
|
+
);
|
|
15
|
+
const consumers = (context.options?.[0]?.awaitingConsumers || []).map((consumer) => ({
|
|
16
|
+
targets: compileTargets({ targets: [consumer] }, "targets"),
|
|
17
|
+
indexes: consumer.callbackArgumentIndexes,
|
|
18
|
+
}));
|
|
19
|
+
return {
|
|
20
|
+
visitors: matcher.visitors,
|
|
21
|
+
returnsThroughConsumer(fn) {
|
|
22
|
+
const callback = unwrapTransparentParent(fn);
|
|
23
|
+
const target = matcher.resolveCallTarget(callback.parent);
|
|
24
|
+
if (!target) return false;
|
|
25
|
+
const index = target.arguments.indexOf(callback);
|
|
26
|
+
if (
|
|
27
|
+
index < 0 ||
|
|
28
|
+
target.arguments.slice(0, index).some((arg) => arg.type === "SpreadElement")
|
|
29
|
+
) {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
return consumers.some(
|
|
33
|
+
(consumer) =>
|
|
34
|
+
consumer.indexes.includes(index) &&
|
|
35
|
+
targetMatches(consumer.targets, target.source, target.calleeName),
|
|
36
|
+
);
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
module.exports = { createAwaitingConsumers };
|
|
@@ -11,16 +11,21 @@ const {
|
|
|
11
11
|
} = require("./async-ast");
|
|
12
12
|
const { memberPropertyName } = require("./async-targets");
|
|
13
13
|
|
|
14
|
-
function directlyDisposed(node, context) {
|
|
14
|
+
function directlyDisposed(node, context, returnsThroughConsumer) {
|
|
15
15
|
const expression = unwrapTransparentParent(node);
|
|
16
16
|
const parent = expression.parent;
|
|
17
17
|
if (parent?.type === "ReturnStatement") {
|
|
18
18
|
const fn = findContainingFunction(parent);
|
|
19
|
-
return !isCallArgument(fn) || returnsThroughMock(fn, context);
|
|
19
|
+
return !isCallArgument(fn) || returnsThroughMock(fn, context) || returnsThroughConsumer(fn);
|
|
20
20
|
}
|
|
21
21
|
if (parent?.type === "ArrowFunctionExpression" && parent.body === expression) {
|
|
22
|
-
if (isCallCallee(parent))
|
|
23
|
-
|
|
22
|
+
if (isCallCallee(parent))
|
|
23
|
+
return directlyDisposed(parent.parent, context, returnsThroughConsumer);
|
|
24
|
+
return (
|
|
25
|
+
!isCallArgument(parent) ||
|
|
26
|
+
returnsThroughMock(parent, context) ||
|
|
27
|
+
returnsThroughConsumer(parent)
|
|
28
|
+
);
|
|
24
29
|
}
|
|
25
30
|
return (
|
|
26
31
|
parent?.type === "AwaitExpression" ||
|
|
@@ -130,14 +135,14 @@ function observedExpression(node, context) {
|
|
|
130
135
|
return current;
|
|
131
136
|
}
|
|
132
137
|
|
|
133
|
-
function promiseAllDisposed(node, context) {
|
|
138
|
+
function promiseAllDisposed(node, context, returnsThroughConsumer) {
|
|
134
139
|
let current = node.parent;
|
|
135
140
|
while (current) {
|
|
136
141
|
if (isPromiseAllCall(current, context) && canReachPromiseAll(node, current)) {
|
|
137
142
|
return (
|
|
138
|
-
directlyDisposed(current, context) ||
|
|
139
|
-
directlyDisposed(observedExpression(current, context), context) ||
|
|
140
|
-
promiseAllDisposed(current, context)
|
|
143
|
+
directlyDisposed(current, context, returnsThroughConsumer) ||
|
|
144
|
+
directlyDisposed(observedExpression(current, context), context, returnsThroughConsumer) ||
|
|
145
|
+
promiseAllDisposed(current, context, returnsThroughConsumer)
|
|
141
146
|
);
|
|
142
147
|
}
|
|
143
148
|
current = current.parent;
|
|
@@ -9,9 +9,34 @@ const {
|
|
|
9
9
|
const { mockInitializer } = require("./async-disposition-mock-bindings");
|
|
10
10
|
const { memberPropertyName } = require("./async-targets");
|
|
11
11
|
|
|
12
|
+
// Destructuring and loop targets also replace a consumer method's identity.
|
|
13
|
+
function propertyTargetWritten(value) {
|
|
14
|
+
if (value.type !== "MemberExpression") return false;
|
|
15
|
+
let target = value;
|
|
16
|
+
while (
|
|
17
|
+
["ObjectPattern", "ArrayPattern", "RestElement"].includes(target.parent.type) ||
|
|
18
|
+
(target.parent.type === "Property" && target.parent.value === target) ||
|
|
19
|
+
(target.parent.type === "AssignmentPattern" && target.parent.left === target)
|
|
20
|
+
)
|
|
21
|
+
target = target.parent;
|
|
22
|
+
const parent = target.parent;
|
|
23
|
+
return (
|
|
24
|
+
(["AssignmentExpression", "ForOfStatement", "ForInStatement"].includes(parent.type) &&
|
|
25
|
+
parent.left === target) ||
|
|
26
|
+
parent.type === "UpdateExpression" ||
|
|
27
|
+
(parent.type === "UnaryExpression" && parent.operator === "delete")
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
12
31
|
// Property writes invalidate provenance, including writes through stable local aliases.
|
|
13
32
|
// This follows syntax in this file, not arbitrary callees' side effects.
|
|
14
|
-
function propertiesWritten(
|
|
33
|
+
function propertiesWritten(
|
|
34
|
+
node,
|
|
35
|
+
context,
|
|
36
|
+
seen = new Set(),
|
|
37
|
+
initialization = null,
|
|
38
|
+
propertiesOnly = false,
|
|
39
|
+
) {
|
|
15
40
|
const variable = resolveVariable(node, context);
|
|
16
41
|
const key = variable || node.name;
|
|
17
42
|
if (seen.has(key)) return false;
|
|
@@ -40,9 +65,11 @@ function propertiesWritten(node, context, seen = new Set(), initialization = nul
|
|
|
40
65
|
const parent = value.parent;
|
|
41
66
|
if (parent === initialization && parent.left === value) return false;
|
|
42
67
|
if (
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
68
|
+
propertiesOnly
|
|
69
|
+
? propertyTargetWritten(value)
|
|
70
|
+
: (parent.type === "AssignmentExpression" && parent.left === value) ||
|
|
71
|
+
parent.type === "UpdateExpression" ||
|
|
72
|
+
(parent.type === "UnaryExpression" && parent.operator === "delete")
|
|
46
73
|
)
|
|
47
74
|
return true;
|
|
48
75
|
if (
|
|
@@ -51,13 +78,13 @@ function propertiesWritten(node, context, seen = new Set(), initialization = nul
|
|
|
51
78
|
parent.id.type === "Identifier" &&
|
|
52
79
|
["const", "let"].includes(parent.parent.kind)
|
|
53
80
|
)
|
|
54
|
-
return propertiesWritten(parent.id, context, seen, initialization);
|
|
81
|
+
return propertiesWritten(parent.id, context, seen, initialization, propertiesOnly);
|
|
55
82
|
if (
|
|
56
83
|
parent.type === "AssignmentExpression" &&
|
|
57
84
|
parent.right === value &&
|
|
58
85
|
parent.left.type === "Identifier"
|
|
59
86
|
)
|
|
60
|
-
return propertiesWritten(parent.left, context, seen, parent);
|
|
87
|
+
return propertiesWritten(parent.left, context, seen, parent, propertiesOnly);
|
|
61
88
|
return false;
|
|
62
89
|
});
|
|
63
90
|
}
|
|
@@ -25,4 +25,22 @@ function optionsSchema(propertyName) {
|
|
|
25
25
|
const handlerOptionsSchema = optionsSchema("handlers");
|
|
26
26
|
const targetOptionsSchema = optionsSchema("targets");
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
const dispositionOptionsSchema = optionsSchema("targets");
|
|
29
|
+
dispositionOptionsSchema[0].properties.awaitingConsumers = {
|
|
30
|
+
type: "array",
|
|
31
|
+
items: {
|
|
32
|
+
...targetOptionsSchema[0].properties.targets.items,
|
|
33
|
+
properties: {
|
|
34
|
+
...targetOptionsSchema[0].properties.targets.items.properties,
|
|
35
|
+
callbackArgumentIndexes: {
|
|
36
|
+
type: "array",
|
|
37
|
+
minItems: 1,
|
|
38
|
+
uniqueItems: true,
|
|
39
|
+
items: { type: "integer", minimum: 0 },
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
required: ["sourceSpecifierPatterns", "calleeNamePatterns", "callbackArgumentIndexes"],
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
module.exports = { handlerOptionsSchema, targetOptionsSchema, dispositionOptionsSchema };
|
|
@@ -33,7 +33,12 @@ function requireSource(node, context) {
|
|
|
33
33
|
return source;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
function createTargetMatcher(
|
|
36
|
+
function createTargetMatcher(
|
|
37
|
+
context,
|
|
38
|
+
optionKey = "targets",
|
|
39
|
+
bindingIsStable = null,
|
|
40
|
+
strictLogicalCalls = false,
|
|
41
|
+
) {
|
|
37
42
|
const targets = compileTargets(context.options?.[0] || {}, optionKey);
|
|
38
43
|
const sourceSpecifierPatterns = targets.flatMap((target) => target.sourceSpecifierPatterns);
|
|
39
44
|
const directBindings = new Map();
|
|
@@ -43,6 +48,7 @@ function createTargetMatcher(context, optionKey = "targets") {
|
|
|
43
48
|
if (
|
|
44
49
|
id?.type !== "Identifier" ||
|
|
45
50
|
isReassigned(id, context) ||
|
|
51
|
+
(bindingIsStable && !bindingIsStable(id)) ||
|
|
46
52
|
!targetMatches(targets, source, calleeName)
|
|
47
53
|
) {
|
|
48
54
|
return;
|
|
@@ -55,6 +61,7 @@ function createTargetMatcher(context, optionKey = "targets") {
|
|
|
55
61
|
if (
|
|
56
62
|
id.type !== "Identifier" ||
|
|
57
63
|
isReassigned(id, context) ||
|
|
64
|
+
(bindingIsStable && !bindingIsStable(id)) ||
|
|
58
65
|
!matchesAny(source, sourceSpecifierPatterns)
|
|
59
66
|
) {
|
|
60
67
|
return;
|
|
@@ -153,6 +160,7 @@ function createTargetMatcher(context, optionKey = "targets") {
|
|
|
153
160
|
if (!name) return null;
|
|
154
161
|
let object = unwrapExpression(node.object);
|
|
155
162
|
for (;;) {
|
|
163
|
+
if (object?.type === "Identifier" && bindingIsStable && !bindingIsStable(object)) return null;
|
|
156
164
|
const source =
|
|
157
165
|
requireSource(object, context) ||
|
|
158
166
|
(object?.type === "Identifier"
|
|
@@ -181,7 +189,7 @@ function createTargetMatcher(context, optionKey = "targets") {
|
|
|
181
189
|
if (node?.type !== "CallExpression") return null;
|
|
182
190
|
const direct = resolveCallee(node.callee);
|
|
183
191
|
if (direct) return { ...direct, arguments: node.arguments };
|
|
184
|
-
const invoked = logicalCall(node);
|
|
192
|
+
const invoked = logicalCall(node, strictLogicalCalls ? resolveCallee : undefined);
|
|
185
193
|
if (!invoked) return null;
|
|
186
194
|
const forwarded = resolveCallee(invoked.callee);
|
|
187
195
|
return forwarded ? { ...forwarded, arguments: invoked.arguments } : null;
|
|
@@ -79,13 +79,15 @@ function visitCall(state, node, scope, role) {
|
|
|
79
79
|
return unknown();
|
|
80
80
|
}
|
|
81
81
|
const known = visitKnownCall(state, node, scope, role);
|
|
82
|
-
if (known !== undefined) {
|
|
82
|
+
if (known !== undefined && !known.fallbackCalleeTag) {
|
|
83
83
|
return known;
|
|
84
84
|
}
|
|
85
85
|
const callee = unwrap(node.callee);
|
|
86
86
|
const forward = callee.type === "MemberExpression" ? memberKey(callee) : null;
|
|
87
87
|
const forwarded = forward === "call" || forward === "apply";
|
|
88
|
-
const calleeTag =
|
|
88
|
+
const calleeTag =
|
|
89
|
+
known?.fallbackCalleeTag ||
|
|
90
|
+
api.visit(state, forwarded ? callee.object : node.callee, scope, "read");
|
|
89
91
|
if (calleeTag.mutator) {
|
|
90
92
|
return visitAliasedMutation(state, node, scope, calleeTag.mutator, forwarded ? forward : null);
|
|
91
93
|
}
|
|
@@ -104,7 +104,7 @@ function visitExpression(state, node, scope, role) {
|
|
|
104
104
|
memberKey(node),
|
|
105
105
|
node.object,
|
|
106
106
|
);
|
|
107
|
-
if (role === "escape"
|
|
107
|
+
if (role === "escape") api.releaseEscaping(state, member);
|
|
108
108
|
return member;
|
|
109
109
|
}
|
|
110
110
|
case "ObjectExpression":
|
|
@@ -76,8 +76,13 @@ function walkFunctionBody(state, node, scope, tag) {
|
|
|
76
76
|
fnEnv.inClass = arrow ? Boolean(scope.inClass) : classMethod;
|
|
77
77
|
fnEnv.hasArguments = arrow ? Boolean(scope.hasArguments) : true;
|
|
78
78
|
fnEnv.receiver = arrow ? scope.receiver || null : tag.receiver || null;
|
|
79
|
-
|
|
80
|
-
|
|
79
|
+
if (tag.reduceSeed) tag.reduceSeed.ownerEnv = fnEnv;
|
|
80
|
+
for (const [index, param] of node.params.entries()) {
|
|
81
|
+
if (index === 0 && tag.reduceSeed) {
|
|
82
|
+
bindName(param.name, tag.reduceSeed, fnEnv, "param");
|
|
83
|
+
} else {
|
|
84
|
+
bindPattern(state, param, shared("parameter"), fnEnv, "param");
|
|
85
|
+
}
|
|
81
86
|
}
|
|
82
87
|
const body = functionStatements(node);
|
|
83
88
|
if (Array.isArray(body)) {
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
const api = require("./no-object-mutation-runtime");
|
|
4
|
+
const { callFunction } = require("./no-object-mutation-fn");
|
|
5
|
+
const { reducerCallback } = require("./no-object-mutation-reduce");
|
|
4
6
|
const { iteratedContents } = require("./no-object-mutation-read");
|
|
5
|
-
const { methodName, unwrap } = require("./no-object-mutation-syntax");
|
|
7
|
+
const { methodName, staticKey, unwrap } = require("./no-object-mutation-syntax");
|
|
6
8
|
const {
|
|
7
9
|
callable,
|
|
8
10
|
createFresh,
|
|
9
11
|
joinValues,
|
|
10
12
|
lookup,
|
|
13
|
+
readMember,
|
|
11
14
|
remember,
|
|
12
15
|
unknown,
|
|
13
16
|
} = require("./no-object-mutation-values");
|
|
@@ -21,6 +24,38 @@ function memberCall(node, name) {
|
|
|
21
24
|
}
|
|
22
25
|
|
|
23
26
|
function visitKnownCall(state, node, scope, role) {
|
|
27
|
+
const reducer = reducerCallback(node);
|
|
28
|
+
if (reducer) {
|
|
29
|
+
const callee = unwrap(node.callee);
|
|
30
|
+
const receiver = api.visit(state, callee.object, scope, "read");
|
|
31
|
+
const method = readMember(receiver, methodName(callee));
|
|
32
|
+
if (
|
|
33
|
+
callable(method) ||
|
|
34
|
+
(receiver.type === "fresh" &&
|
|
35
|
+
(receiver.fields.has(methodName(callee)) ||
|
|
36
|
+
receiver.items.some(
|
|
37
|
+
(item) =>
|
|
38
|
+
item.node?.parent?.key && staticKey(item.node.parent.key) === methodName(callee),
|
|
39
|
+
)))
|
|
40
|
+
) {
|
|
41
|
+
// Reuse the evaluated receiver in the ordinary call path. A custom
|
|
42
|
+
// method may supply a shared accumulator or retain its callback.
|
|
43
|
+
return { fallbackCalleeTag: method };
|
|
44
|
+
}
|
|
45
|
+
const seed = api.visit(state, node.arguments[1], scope, "read");
|
|
46
|
+
const callback = api.visit(state, reducer, scope, "read");
|
|
47
|
+
callback.reduceSeed = seed;
|
|
48
|
+
const escapeEpoch = state.escapeEpoch;
|
|
49
|
+
callFunction(state, callback);
|
|
50
|
+
if (state.escapeEpoch !== escapeEpoch) {
|
|
51
|
+
// A leak at the end of one iteration is visible at the next entry.
|
|
52
|
+
callback.walked = false;
|
|
53
|
+
callFunction(state, callback);
|
|
54
|
+
}
|
|
55
|
+
seed.ownerEnv = scope.ownerEnv;
|
|
56
|
+
if (role === "escape") api.releaseEscaping(state, seed);
|
|
57
|
+
return seed;
|
|
58
|
+
}
|
|
24
59
|
const values = memberCall(node, "values");
|
|
25
60
|
if (values && isGlobalObject(values.object, scope)) {
|
|
26
61
|
const target = node.arguments[0];
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { methodName, unwrap } = require("./no-object-mutation-syntax");
|
|
4
|
+
const { ARRAY_METHODS } = require("./no-object-mutation-ast");
|
|
5
|
+
|
|
6
|
+
// Limit the exemption to an inline, synchronous accumulator builder. Every
|
|
7
|
+
// callback exit must preserve the same accumulator for the next iteration.
|
|
8
|
+
function reducerCallback(node) {
|
|
9
|
+
const callee = unwrap(node.callee);
|
|
10
|
+
if (callee?.type !== "MemberExpression" || node.arguments.length !== 2) return null;
|
|
11
|
+
if (!["reduce", "reduceRight"].includes(methodName(callee))) return null;
|
|
12
|
+
const seed = unwrap(node.arguments[1]);
|
|
13
|
+
if (!["ObjectExpression", "ArrayExpression"].includes(seed?.type)) return null;
|
|
14
|
+
const callback = unwrap(node.arguments[0]);
|
|
15
|
+
if (!["ArrowFunctionExpression", "FunctionExpression"].includes(callback?.type)) return null;
|
|
16
|
+
if (callback.async || callback.generator || callback.params[0]?.type !== "Identifier")
|
|
17
|
+
return null;
|
|
18
|
+
const name = callback.params[0].name;
|
|
19
|
+
const body = callback.body;
|
|
20
|
+
if (body.type !== "BlockStatement") return null;
|
|
21
|
+
if (body.body.at(-1)?.type !== "ReturnStatement") return null;
|
|
22
|
+
return preservesAccumulator(body, name) ? callback : null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function preservesAccumulator(node, name, nested = false) {
|
|
26
|
+
if (!node || typeof node.type !== "string") return true;
|
|
27
|
+
if (
|
|
28
|
+
["FunctionDeclaration", "FunctionExpression", "ArrowFunctionExpression"].includes(node.type)
|
|
29
|
+
) {
|
|
30
|
+
// Returns in nested closures do not define the reduction result. Still
|
|
31
|
+
// reject rebinding there: a closure can change the next accumulator.
|
|
32
|
+
nested = true;
|
|
33
|
+
}
|
|
34
|
+
if (node.type === "ReturnStatement" && !nested) {
|
|
35
|
+
const value = unwrap(node.argument);
|
|
36
|
+
return value?.type === "Identifier" && value.name === name;
|
|
37
|
+
}
|
|
38
|
+
// Rebinding could pass a shared value into the following iteration. Keep
|
|
39
|
+
// destructuring and shadowing conservative rather than prove control flow.
|
|
40
|
+
if (
|
|
41
|
+
[
|
|
42
|
+
"AssignmentExpression",
|
|
43
|
+
"UpdateExpression",
|
|
44
|
+
"VariableDeclarator",
|
|
45
|
+
"ForInStatement",
|
|
46
|
+
"ForOfStatement",
|
|
47
|
+
].includes(node.type)
|
|
48
|
+
) {
|
|
49
|
+
const target = node.left || node.argument || node.id;
|
|
50
|
+
if (target.type !== "MemberExpression" && mentions(target, name)) return false;
|
|
51
|
+
}
|
|
52
|
+
if (node.type === "Identifier" && node.name === name && !directAccumulatorUse(node)) return false;
|
|
53
|
+
if (node.type === "CatchClause" && mentions(node.param, name)) return false;
|
|
54
|
+
for (const [key, value] of Object.entries(node)) {
|
|
55
|
+
if (["parent", "loc", "range", "tokens"].includes(key)) continue;
|
|
56
|
+
if (Array.isArray(value)) {
|
|
57
|
+
if (!value.every((part) => preservesAccumulator(part, name, nested))) return false;
|
|
58
|
+
} else if (value && typeof value === "object" && !preservesAccumulator(value, name, nested))
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Only direct container writes are exempt. Member reads and aliases can carry
|
|
65
|
+
// shared nested provenance across arbitrarily many iterations, so abstain.
|
|
66
|
+
function directAccumulatorUse(node) {
|
|
67
|
+
const parent = node.parent;
|
|
68
|
+
if (parent.type === "ReturnStatement") return true;
|
|
69
|
+
if (parent.type === "CallExpression") return parent.arguments.includes(node);
|
|
70
|
+
if (parent.type !== "MemberExpression") return false;
|
|
71
|
+
if (parent.property === node && !parent.computed) return true;
|
|
72
|
+
if (parent.object !== node) return false;
|
|
73
|
+
const use = parent.parent;
|
|
74
|
+
if (use.type === "AssignmentExpression" && use.left === parent) return true;
|
|
75
|
+
if (use.type === "UpdateExpression" && use.argument === parent) return true;
|
|
76
|
+
return (
|
|
77
|
+
use.type === "CallExpression" && use.callee === parent && ARRAY_METHODS.has(methodName(parent))
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function mentions(node, name) {
|
|
82
|
+
if (!node || typeof node.type !== "string") return false;
|
|
83
|
+
if (node.type === "Identifier") return node.name === name;
|
|
84
|
+
return Object.entries(node).some(([key, value]) => {
|
|
85
|
+
if (["parent", "loc", "range", "tokens"].includes(key)) return false;
|
|
86
|
+
return Array.isArray(value)
|
|
87
|
+
? value.some((part) => mentions(part, name))
|
|
88
|
+
: mentions(value, name);
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
module.exports = { reducerCallback };
|
|
@@ -113,7 +113,7 @@ function visit(state, node, scope, role) {
|
|
|
113
113
|
return unknown();
|
|
114
114
|
case "ReturnStatement":
|
|
115
115
|
if (node.argument) {
|
|
116
|
-
visit(state, node.argument, scope, "escape");
|
|
116
|
+
visit(state, node.argument, scope, scope.fnTag?.reduceSeed ? "read" : "escape");
|
|
117
117
|
}
|
|
118
118
|
return unknown();
|
|
119
119
|
case "LabeledStatement":
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const { rule } = require("../helpers");
|
|
4
4
|
const { ruleSuppression } = require("../rule-suppression");
|
|
5
|
+
const { unwrapExpression, unwrapSequence: unwrap } = require("./async-ast");
|
|
5
6
|
const { literalString } = require("./module-mock-helpers");
|
|
6
7
|
const { resolveVariable } = require("./test-no-shared-state-aliases");
|
|
7
8
|
|
|
@@ -13,20 +14,13 @@ function isTestFile(filename) {
|
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
function staticName(node, computed) {
|
|
17
|
+
node = unwrapExpression(node);
|
|
16
18
|
if (!node) return null;
|
|
17
19
|
if (computed && node.type === "Identifier") return null;
|
|
18
20
|
if (!computed && typeof node.name === "string" && node.type !== "Literal") return node.name;
|
|
19
21
|
return literalString(node);
|
|
20
22
|
}
|
|
21
23
|
|
|
22
|
-
function unwrap(node) {
|
|
23
|
-
while (node?.type === "ChainExpression") node = node.expression;
|
|
24
|
-
if (node?.type === "SequenceExpression" && node.expressions?.length) {
|
|
25
|
-
return unwrap(node.expressions.at(-1));
|
|
26
|
-
}
|
|
27
|
-
return node;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
24
|
function constInit(node, context, seen) {
|
|
31
25
|
if (!context || node?.type !== "Identifier" || seen.has(node.name)) return null;
|
|
32
26
|
const variable = resolveVariable(node, context);
|
|
@@ -72,14 +66,9 @@ function hasSequentialOption(node) {
|
|
|
72
66
|
value?.type === "ObjectExpression" &&
|
|
73
67
|
value.properties.some((property) => {
|
|
74
68
|
if (property.type !== "Property") return false;
|
|
75
|
-
const key = property.computed
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
return (
|
|
79
|
-
key === "sequential" &&
|
|
80
|
-
property.value?.type === "Literal" &&
|
|
81
|
-
property.value.value === true
|
|
82
|
-
);
|
|
69
|
+
const key = staticName(property.key, property.computed);
|
|
70
|
+
const option = unwrapExpression(property.value);
|
|
71
|
+
return key === "sequential" && option?.type === "Literal" && option.value === true;
|
|
83
72
|
})
|
|
84
73
|
);
|
|
85
74
|
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
module.exports = {
|
|
4
|
+
type: "problem",
|
|
5
|
+
docs: {
|
|
6
|
+
description: "preserve explicit null in nullable option defaults",
|
|
7
|
+
recommended: false,
|
|
8
|
+
},
|
|
9
|
+
schema: [
|
|
10
|
+
{
|
|
11
|
+
type: "object",
|
|
12
|
+
properties: {
|
|
13
|
+
includePathPatterns: { type: "array", items: { type: "string" } },
|
|
14
|
+
excludePathPatterns: { type: "array", items: { type: "string" } },
|
|
15
|
+
optionObjectNames: { type: "array", items: { type: "string" } },
|
|
16
|
+
optionObjectNamePatterns: { type: "array", items: { type: "string" } },
|
|
17
|
+
},
|
|
18
|
+
additionalProperties: false,
|
|
19
|
+
},
|
|
20
|
+
],
|
|
21
|
+
messages: {
|
|
22
|
+
default:
|
|
23
|
+
"Do not default nullable option '{{name}}' with ??, ||, ??=, or ||=. Preserve explicit null and check undefined explicitly.",
|
|
24
|
+
},
|
|
25
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { resolveVariable } = require("./async-target-bindings");
|
|
4
|
+
|
|
5
|
+
// Scope facts are sequential: consult real bindings to reject hoisted/TDZ shadows.
|
|
6
|
+
function scalarConstAlias(node, context) {
|
|
7
|
+
if (node.parent.kind !== "const" || node.init.type !== "Identifier") return false;
|
|
8
|
+
const variable = resolveVariable(node.init, context);
|
|
9
|
+
if (
|
|
10
|
+
!variable ||
|
|
11
|
+
variable.defs.length !== 1 ||
|
|
12
|
+
variable.references.some((reference) => reference.isWrite() && !reference.init)
|
|
13
|
+
)
|
|
14
|
+
return false;
|
|
15
|
+
const def = variable.defs[0];
|
|
16
|
+
// Parameter initializers can run before a later binding is initialized.
|
|
17
|
+
// Follow known nullable parameters only from their function body.
|
|
18
|
+
if (def.type === "Parameter") return def.node.body.range[0] < node.init.range[0];
|
|
19
|
+
return (
|
|
20
|
+
def.type === "Variable" && def.parent.kind === "const" && def.node.range[1] < node.init.range[0]
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
module.exports = { scalarConstAlias };
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { attributeName, isStaticString, literalString, selectorValueNode } = require("../helpers");
|
|
4
|
+
const { unwrapExpression } = require("./async-ast");
|
|
5
|
+
|
|
6
|
+
// A spread invalidates earlier props; a later explicit prop establishes its value again.
|
|
7
|
+
function effectiveAttribute(opening, name) {
|
|
8
|
+
let found = null;
|
|
9
|
+
for (const attribute of opening.attributes) {
|
|
10
|
+
if (attribute.type === "JSXSpreadAttribute") found = null;
|
|
11
|
+
else if (attributeName(attribute) === name) found = attribute;
|
|
12
|
+
}
|
|
13
|
+
return found;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function hasSlotProp(opening, names) {
|
|
17
|
+
return names.some((name) => {
|
|
18
|
+
const attribute = effectiveAttribute(opening, name);
|
|
19
|
+
if (!attribute) return false;
|
|
20
|
+
if (!attribute.value) return true;
|
|
21
|
+
const value = unwrapExpression(selectorValueNode(attribute));
|
|
22
|
+
if (value?.type === "Literal") return Boolean(value.value);
|
|
23
|
+
return isStaticString(value) && Boolean(literalString(value));
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function literalSelectors(opening, names) {
|
|
28
|
+
return names.filter((name) => {
|
|
29
|
+
const attribute = effectiveAttribute(opening, name);
|
|
30
|
+
return attribute && isStaticString(unwrapExpression(selectorValueNode(attribute)));
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function singleElementChild(element) {
|
|
35
|
+
const children = element.children.filter((child) => {
|
|
36
|
+
if (child.type === "JSXExpressionContainer" && child.expression.type === "JSXEmptyExpression")
|
|
37
|
+
return false;
|
|
38
|
+
if (child.type !== "JSXText") return true;
|
|
39
|
+
// JSX drops indentation around newlines. Same-line spaces and nonbreaking
|
|
40
|
+
// spaces are actual text siblings, so a Slot cannot merge onto one element.
|
|
41
|
+
return child.value !== "" && !(/^[ \t\r\n]*$/.test(child.value) && /[\r\n]/.test(child.value));
|
|
42
|
+
});
|
|
43
|
+
return children.length === 1 && children[0].type === "JSXElement" ? children[0] : null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function childKeepsSelector(opening, names) {
|
|
47
|
+
return names.some(
|
|
48
|
+
(name) =>
|
|
49
|
+
!opening.attributes.some(
|
|
50
|
+
(attribute) => attribute.type === "JSXSpreadAttribute" || attributeName(attribute) === name,
|
|
51
|
+
),
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
module.exports = { childKeepsSelector, hasSlotProp, literalSelectors, singleElementChild };
|