eslint-plugin-no-mistakes 0.98.0 → 0.99.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 +16 -0
- package/package.json +13 -2
- package/src/rules/async-call-disposition.js +29 -21
- package/src/rules/async-consumer-cache-proof.js +146 -0
- package/src/rules/async-consumer-copy-spellings.js +82 -0
- package/src/rules/async-consumer-imported-proof.js +96 -0
- package/src/rules/async-consumer-local-proof.js +86 -0
- package/src/rules/async-consumer-proof.js +113 -0
- package/src/rules/async-consumer-property-carrier.js +136 -0
- package/src/rules/async-consumer-property-proof.js +192 -0
- package/src/rules/async-consumer-receivers.js +161 -0
- package/src/rules/async-consumer-typed-proof.js +96 -0
- package/src/rules/async-disposition-callbacks.js +140 -0
- package/src/rules/async-disposition-consumers.js +86 -22
- package/src/rules/async-disposition-expressions.js +80 -62
- package/src/rules/async-disposition-flow.js +23 -1
- package/src/rules/async-disposition-observers.js +95 -0
- package/src/rules/async-handoff-data.js +168 -0
- package/src/rules/async-handoff-return.js +38 -0
- package/src/rules/async-handoff-safety.js +184 -0
- package/src/rules/async-handoff-sibling.js +41 -0
- package/src/rules/async-handoff-snapshots.js +33 -0
- package/src/rules/async-observer-mutations.js +140 -0
- package/src/rules/async-promise-handoff.js +60 -0
- package/src/rules/async-schema.js +23 -1
- package/src/rules/async-source-declarations.js +97 -0
- package/src/rules/async-source-slots.js +58 -0
- package/src/rules/async-source-store.js +96 -0
- package/src/rules/async-source-types.js +154 -0
- package/src/rules/async-target-sources.js +27 -0
- package/src/rules/async-targets.js +3 -22
- package/src/rules/no-import-only-test-files.js +45 -7
- package/src/rules/no-vitest-sequential.js +9 -1
- package/src/rules/postgres-executor-calls.js +36 -3
- package/src/rules/postgres-executor.js +12 -4
- package/src/rules/postgres-no-correlated-sql-builder-append.js +19 -65
- package/src/rules/postgres-no-correlated-sql-builder-invocation.js +55 -0
- package/src/rules/postgres-no-correlated-sql-builder.js +12 -8
- package/src/rules/postgres-no-executor-adapter-binding.js +1 -0
- package/src/rules/postgres-no-executor-adapter-collection.js +123 -0
- package/src/rules/postgres-no-executor-adapter-escape.js +163 -0
- package/src/rules/postgres-no-executor-adapter-mutation.js +155 -0
- package/src/rules/postgres-no-executor-adapter-option-local.js +78 -0
- package/src/rules/postgres-no-executor-adapter-option-ts.js +173 -0
- package/src/rules/postgres-no-executor-adapter-options.js +46 -0
- package/src/rules/postgres-no-executor-adapter-ref.js +15 -0
- package/src/rules/postgres-no-executor-adapter-source.js +100 -10
- package/src/rules/postgres-no-executor-adapter-target.js +62 -19
- package/src/rules/postgres-no-executor-adapter.js +7 -2
- package/src/rules/postgres-no-manual-transaction.js +5 -3
- package/src/rules/postgres-no-nonliteral-sql-registry-key.js +2 -10
- package/src/rules/postgres-scoped-executors.js +20 -52
- package/src/rules/postgres-scoped-lookup.js +32 -0
- package/src/rules/postgres-scoped-parameters.js +83 -0
- package/src/rules/postgres-transaction-statements.js +3 -3
- package/src/rules/react-no-nullish-react-node.js +53 -27
- package/src/rules/test-message-expect.js +12 -6
- package/src/rules/test-message-runner.js +106 -0
- package/src/rules/test-no-error-message-matching.js +3 -15
- package/src/rules/vitest-mock-test-file-naming.js +36 -3
package/README.md
CHANGED
|
@@ -9,6 +9,22 @@ callers, fetches, or selectors behind aliases and dynamic values. See
|
|
|
9
9
|
npm install --save-dev eslint-plugin-no-mistakes
|
|
10
10
|
```
|
|
11
11
|
|
|
12
|
+
For `async-call-disposition` to inspect imported callback implementations when
|
|
13
|
+
the linter does not provide a TypeScript program (including Oxlint's
|
|
14
|
+
`--type-aware` mode), also install `typescript` and
|
|
15
|
+
`@typescript-eslint/parser` in your project:
|
|
16
|
+
|
|
17
|
+
```sh
|
|
18
|
+
npm install --save-dev typescript @typescript-eslint/parser
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
These are optional peer dependencies. This fallback requires TypeScript 5 or
|
|
22
|
+
later for Bundler module resolution. With older TypeScript or missing or
|
|
23
|
+
incompatible tooling, calls that rely on this cross-file proof are reported as
|
|
24
|
+
unproven. Same-file proofs and explicitly configured awaiting-consumer
|
|
25
|
+
contracts continue to work. When ESLint provides a typed TypeScript program,
|
|
26
|
+
the rule continues to use that program for its typed proof.
|
|
27
|
+
|
|
12
28
|
Configure the plugin in a flat config and scope rules with `files` globs. For
|
|
13
29
|
example, this checks test files for skipped or exclusive tests while allowing
|
|
14
30
|
`todo`:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eslint-plugin-no-mistakes",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.99.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": {
|
|
@@ -25,10 +25,21 @@
|
|
|
25
25
|
"@vitest/coverage-v8": "^5.0.4",
|
|
26
26
|
"eslint": "^10.12.0",
|
|
27
27
|
"oxlint": "^1.87.0",
|
|
28
|
+
"typescript": "^6.0.3",
|
|
28
29
|
"vitest": "^5.0.4"
|
|
29
30
|
},
|
|
30
31
|
"peerDependencies": {
|
|
31
|
-
"eslint": ">=
|
|
32
|
+
"@typescript-eslint/parser": ">=8",
|
|
33
|
+
"eslint": ">=9",
|
|
34
|
+
"typescript": ">=4.8.4"
|
|
35
|
+
},
|
|
36
|
+
"peerDependenciesMeta": {
|
|
37
|
+
"@typescript-eslint/parser": {
|
|
38
|
+
"optional": true
|
|
39
|
+
},
|
|
40
|
+
"typescript": {
|
|
41
|
+
"optional": true
|
|
42
|
+
}
|
|
32
43
|
},
|
|
33
44
|
"engines": {
|
|
34
45
|
"node": "^20.19.0 || ^22.13.0 || >=24"
|
|
@@ -5,37 +5,34 @@ const { ruleSuppression } = require("../rule-suppression");
|
|
|
5
5
|
const {
|
|
6
6
|
collectedPromise,
|
|
7
7
|
isPromiseAllCall,
|
|
8
|
-
retainedCollection,
|
|
9
8
|
retainedPromise,
|
|
10
9
|
} = require("./async-disposition-bindings");
|
|
11
10
|
const {
|
|
12
11
|
directlyDisposed,
|
|
12
|
+
mappedArgumentDisposed,
|
|
13
13
|
observedExpression,
|
|
14
14
|
promiseAllDisposed,
|
|
15
15
|
mappedPromise,
|
|
16
16
|
} = require("./async-disposition-expressions");
|
|
17
17
|
const { dispositionOptionsSchema } = require("./async-schema");
|
|
18
18
|
const { createTargetMatcher } = require("./async-targets");
|
|
19
|
+
const { promiseChainFrom } = require("./async-disposition-observers");
|
|
19
20
|
const { createAwaitingConsumers } = require("./async-disposition-consumers");
|
|
21
|
+
const { createPromiseHandoff } = require("./async-promise-handoff");
|
|
20
22
|
|
|
21
|
-
function isDisposed(node, context,
|
|
23
|
+
function isDisposed(node, context, consumers, seen = new Set(), catchProven = true) {
|
|
22
24
|
if (seen.has(node)) return false;
|
|
23
25
|
seen.add(node);
|
|
24
26
|
const observed = observedExpression(node, context);
|
|
25
|
-
const
|
|
27
|
+
const retainedCatch =
|
|
28
|
+
catchProven && (observed === node || promiseChainFrom(observed, node, context));
|
|
29
|
+
const disposed = (value) => isDisposed(value, context, consumers, seen, retainedCatch);
|
|
26
30
|
return (
|
|
27
|
-
directlyDisposed(node, context,
|
|
28
|
-
directlyDisposed(observed, context,
|
|
29
|
-
promiseAllDisposed(observed, context,
|
|
31
|
+
directlyDisposed(node, context, consumers) ||
|
|
32
|
+
directlyDisposed(observed, context, consumers, catchProven ? node : null) ||
|
|
33
|
+
promiseAllDisposed(observed, context, consumers) ||
|
|
30
34
|
retainedPromise(observed, context, disposed) ||
|
|
31
|
-
mappedPromise(observed,
|
|
32
|
-
retainedCollection(
|
|
33
|
-
value,
|
|
34
|
-
context,
|
|
35
|
-
(aggregate) => isPromiseAllCall(aggregate, context),
|
|
36
|
-
disposed,
|
|
37
|
-
),
|
|
38
|
-
) ||
|
|
35
|
+
mappedPromise(observed, context, consumers, disposed) ||
|
|
39
36
|
collectedPromise(
|
|
40
37
|
observed,
|
|
41
38
|
context,
|
|
@@ -63,7 +60,20 @@ module.exports = rule(
|
|
|
63
60
|
},
|
|
64
61
|
(context) => {
|
|
65
62
|
const matcher = createTargetMatcher(context);
|
|
66
|
-
const
|
|
63
|
+
const sourceFacts = { typed: new WeakMap(), sources: null, targets: matcher.targets };
|
|
64
|
+
const { promiseSlotHandoff, promiseTypedTargetCall } = createPromiseHandoff(
|
|
65
|
+
context,
|
|
66
|
+
sourceFacts,
|
|
67
|
+
);
|
|
68
|
+
const consumers = createAwaitingConsumers(
|
|
69
|
+
context,
|
|
70
|
+
(fn, argument) =>
|
|
71
|
+
mappedArgumentDisposed(fn, argument, context, consumers, (value) =>
|
|
72
|
+
isDisposed(value, context, consumers),
|
|
73
|
+
),
|
|
74
|
+
sourceFacts,
|
|
75
|
+
promiseSlotHandoff,
|
|
76
|
+
);
|
|
67
77
|
const suppressed = ruleSuppression(context, "async-call-disposition");
|
|
68
78
|
if (!matcher.hasTargets) return {};
|
|
69
79
|
|
|
@@ -78,12 +88,10 @@ module.exports = rule(
|
|
|
78
88
|
]),
|
|
79
89
|
),
|
|
80
90
|
CallExpression(node) {
|
|
81
|
-
if (
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
)
|
|
86
|
-
return;
|
|
91
|
+
if (suppressed(node)) return;
|
|
92
|
+
// Disposed calls need no cross-file type lookup, including local/configured JS consumers.
|
|
93
|
+
if (isDisposed(node, context, consumers)) return;
|
|
94
|
+
if (!matcher.isTargetCall(node) && !promiseTypedTargetCall(node)) return;
|
|
87
95
|
context.report({
|
|
88
96
|
node,
|
|
89
97
|
messageId: "disposition",
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { resolveVariable } = require("./async-target-bindings");
|
|
4
|
+
const { propertiesWritten } = require("./async-disposition-mocks");
|
|
5
|
+
|
|
6
|
+
function sameKey(first, key, context) {
|
|
7
|
+
if (key?.type === "Literal")
|
|
8
|
+
return typeof key.value === "string" && first?.type === "Literal" && first.value === key.value;
|
|
9
|
+
return (
|
|
10
|
+
first?.type === "Identifier" &&
|
|
11
|
+
key?.type === "Identifier" &&
|
|
12
|
+
resolveVariable(first, context) === resolveVariable(key, context)
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function stableKey(key, fn, param, context) {
|
|
17
|
+
if (key?.type === "Literal") return typeof key.value === "string";
|
|
18
|
+
if (key?.type !== "Identifier") return false;
|
|
19
|
+
const variable = resolveVariable(key, context);
|
|
20
|
+
if (!variable) return false;
|
|
21
|
+
return (
|
|
22
|
+
fn.params.some((item) => item !== param && resolveVariable(item, context) === variable) &&
|
|
23
|
+
!variable.references.some((ref) => ref.isWrite())
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function builtinMap(node, context) {
|
|
28
|
+
if (resolveVariable(node, context)?.defs.length || propertiesWritten(node, context)) return false;
|
|
29
|
+
// Global-holder writes can replace Map without mentioning its bare identifier.
|
|
30
|
+
const references = context.sourceCode.scopeManager.scopes
|
|
31
|
+
.flatMap((scope) => scope.references)
|
|
32
|
+
.filter((ref) => !(ref.isTypeReference && !ref.isValueReference));
|
|
33
|
+
if (
|
|
34
|
+
references.some(
|
|
35
|
+
(ref) =>
|
|
36
|
+
["eval", "Function"].includes(ref.identifier.name) &&
|
|
37
|
+
!resolveVariable(ref.identifier, context)?.defs.length,
|
|
38
|
+
)
|
|
39
|
+
)
|
|
40
|
+
return false;
|
|
41
|
+
const binding = resolveVariable(node, context);
|
|
42
|
+
// Do not infer built-in semantics after the constructor/prototype escapes to
|
|
43
|
+
// reflective APIs or unknown consumers, whose effects are not modeled here.
|
|
44
|
+
if (
|
|
45
|
+
references.some(
|
|
46
|
+
(ref) =>
|
|
47
|
+
ref.identifier.name === "Map" &&
|
|
48
|
+
resolveVariable(ref.identifier, context) === binding &&
|
|
49
|
+
!(
|
|
50
|
+
ref.identifier.parent.type === "NewExpression" &&
|
|
51
|
+
ref.identifier.parent.callee === ref.identifier
|
|
52
|
+
),
|
|
53
|
+
)
|
|
54
|
+
)
|
|
55
|
+
return false;
|
|
56
|
+
const holders = ["globalThis", "global", "window", "self"];
|
|
57
|
+
return !references.some((ref) => {
|
|
58
|
+
if (!holders.includes(ref.identifier.name)) return false;
|
|
59
|
+
if (resolveVariable(ref.identifier, context)?.defs.length) return false;
|
|
60
|
+
if (propertiesWritten(ref.identifier, context)) return true;
|
|
61
|
+
const member = ref.identifier.parent;
|
|
62
|
+
if (member.type !== "MemberExpression" || member.object !== ref.identifier) return true;
|
|
63
|
+
const name = member.computed ? member.property.value : member.property.name;
|
|
64
|
+
if (["eval", "Function"].includes(name)) return true;
|
|
65
|
+
if (name !== "Map" && typeof name === "string") return false;
|
|
66
|
+
return member.parent.type !== "NewExpression" || member.parent.callee !== member;
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function mapCall(node, method, cache, key, context, count) {
|
|
71
|
+
const member = node?.callee;
|
|
72
|
+
const first = node?.arguments?.[0];
|
|
73
|
+
return (
|
|
74
|
+
node?.type === "CallExpression" &&
|
|
75
|
+
!node.optional &&
|
|
76
|
+
node.arguments.length === count &&
|
|
77
|
+
member?.type === "MemberExpression" &&
|
|
78
|
+
!member.computed &&
|
|
79
|
+
!member.optional &&
|
|
80
|
+
member.property.name === method &&
|
|
81
|
+
member.object.type === "Identifier" &&
|
|
82
|
+
resolveVariable(member.object, context) === cache &&
|
|
83
|
+
sameKey(first, key, context)
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// This is deliberately a complete body grammar: any intervening effect, alias,
|
|
88
|
+
// escape, overwrite, early return, or exception may strand the stored promise.
|
|
89
|
+
function cachedBodyAwaited(fn, param, context) {
|
|
90
|
+
if (!fn.async || fn.generator || fn.params.some((item) => item.type !== "Identifier"))
|
|
91
|
+
return false;
|
|
92
|
+
const body = fn.body?.type === "BlockStatement" ? fn.body.body : [];
|
|
93
|
+
if (body.length !== 3) return false;
|
|
94
|
+
const [declaration, guard, returned] = body;
|
|
95
|
+
const entry = declaration.declarations?.[0];
|
|
96
|
+
const init = entry?.init;
|
|
97
|
+
if (
|
|
98
|
+
declaration.type !== "VariableDeclaration" ||
|
|
99
|
+
declaration.kind !== "const" ||
|
|
100
|
+
declaration.declarations.length !== 1 ||
|
|
101
|
+
entry.id.type !== "Identifier" ||
|
|
102
|
+
init?.type !== "NewExpression" ||
|
|
103
|
+
init.callee.type !== "Identifier" ||
|
|
104
|
+
init.callee.name !== "Map" ||
|
|
105
|
+
init.arguments.length !== 0 ||
|
|
106
|
+
!builtinMap(init.callee, context)
|
|
107
|
+
)
|
|
108
|
+
return false;
|
|
109
|
+
const cache = resolveVariable(entry.id, context);
|
|
110
|
+
const test = guard.test;
|
|
111
|
+
const inserted =
|
|
112
|
+
guard.consequent?.type === "BlockStatement"
|
|
113
|
+
? guard.consequent.body.length === 1 && guard.consequent.body[0]
|
|
114
|
+
: guard.consequent;
|
|
115
|
+
const set = inserted?.expression;
|
|
116
|
+
const key = set?.arguments?.[0];
|
|
117
|
+
if (
|
|
118
|
+
!stableKey(key, fn, param, context) ||
|
|
119
|
+
guard.type !== "IfStatement" ||
|
|
120
|
+
guard.alternate ||
|
|
121
|
+
test?.type !== "UnaryExpression" ||
|
|
122
|
+
test.operator !== "!" ||
|
|
123
|
+
inserted?.type !== "ExpressionStatement" ||
|
|
124
|
+
!mapCall(test.argument, "has", cache, key, context, 1) ||
|
|
125
|
+
!mapCall(set, "set", cache, key, context, 2) ||
|
|
126
|
+
returned.type !== "ReturnStatement" ||
|
|
127
|
+
returned.argument?.type !== "AwaitExpression" ||
|
|
128
|
+
!mapCall(returned.argument.argument, "get", cache, key, context, 1)
|
|
129
|
+
)
|
|
130
|
+
return false;
|
|
131
|
+
const invocation = set.arguments[1];
|
|
132
|
+
const parameter = resolveVariable(param, context);
|
|
133
|
+
const reads = parameter?.references.filter((ref) => !ref.init) || [];
|
|
134
|
+
return (
|
|
135
|
+
invocation?.type === "CallExpression" &&
|
|
136
|
+
!invocation.optional &&
|
|
137
|
+
invocation.arguments.length === 0 &&
|
|
138
|
+
invocation.callee.type === "Identifier" &&
|
|
139
|
+
resolveVariable(invocation.callee, context) === parameter &&
|
|
140
|
+
reads.length === 1 &&
|
|
141
|
+
!reads[0].isWrite() &&
|
|
142
|
+
reads[0].identifier === invocation.callee
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
module.exports = { cachedBodyAwaited };
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { unwrapTransparentParent } = require("./async-ast");
|
|
4
|
+
const { followConst } = require("./async-consumer-property-carrier");
|
|
5
|
+
|
|
6
|
+
const copyOperators = new Set(["??", "||"]);
|
|
7
|
+
|
|
8
|
+
// Only the left operand is the supplied callback. The right-hand default is not.
|
|
9
|
+
function unwrapCopyLogical(node) {
|
|
10
|
+
const selected = unwrapTransparentParent(node);
|
|
11
|
+
const parent = selected.parent;
|
|
12
|
+
if (
|
|
13
|
+
parent?.type === "LogicalExpression" &&
|
|
14
|
+
copyOperators.has(parent.operator) &&
|
|
15
|
+
parent.left === selected
|
|
16
|
+
)
|
|
17
|
+
return unwrapTransparentParent(parent);
|
|
18
|
+
return selected;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function logicalCopyLeft(value) {
|
|
22
|
+
if (value?.type !== "LogicalExpression" || !copyOperators.has(value.operator)) return null;
|
|
23
|
+
return value.left;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function plainProperty(entry) {
|
|
27
|
+
return entry.type === "Property" && !entry.computed && !entry.method && entry.kind === "init";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// A key before the spread is overwritten by the source object. A key after it
|
|
31
|
+
// replaces the copied callback, so that order stays unproven.
|
|
32
|
+
function propertyKey(key) {
|
|
33
|
+
return key.type === "Identifier" ? key.name : key.value;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function patternBinding(value) {
|
|
37
|
+
const node = value?.type === "AssignmentPattern" ? value.left : value;
|
|
38
|
+
return node?.type === "Identifier" ? node : null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function spreadPreserves(object, spread, property) {
|
|
42
|
+
if (object?.type !== "ObjectExpression") return false;
|
|
43
|
+
const index = object.properties.indexOf(spread);
|
|
44
|
+
const keyOf = (entry) => propertyKey(entry.key);
|
|
45
|
+
return (
|
|
46
|
+
object.properties.slice(0, index).every(plainProperty) &&
|
|
47
|
+
object.properties
|
|
48
|
+
.slice(index + 1)
|
|
49
|
+
.every((entry) => plainProperty(entry) && keyOf(entry) !== property)
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Shorthand `const { name }` and `const { name = defaultImpl }` only. A rename,
|
|
54
|
+
// rest, or computed key can hide the callback, so those patterns stay unproven.
|
|
55
|
+
function destructuredBinding(declarator, property) {
|
|
56
|
+
if (declarator.parent.kind !== "const") return false;
|
|
57
|
+
let binding = null;
|
|
58
|
+
for (const entry of declarator.id.properties) {
|
|
59
|
+
if (entry.type !== "Property" || entry.computed || entry.kind !== "init") return false;
|
|
60
|
+
const name = propertyKey(entry.key);
|
|
61
|
+
const id = patternBinding(entry.value);
|
|
62
|
+
if (id?.name !== name) return false;
|
|
63
|
+
if (name === property) binding = id;
|
|
64
|
+
}
|
|
65
|
+
return binding ?? "safe";
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// "safe" is a read of some other field. false is an unproven pattern.
|
|
69
|
+
function destructureMode(value, property, context, shape, api, seen) {
|
|
70
|
+
const declarator = value.parent;
|
|
71
|
+
if (
|
|
72
|
+
declarator?.type !== "VariableDeclarator" ||
|
|
73
|
+
declarator.init !== value ||
|
|
74
|
+
declarator.id.type !== "ObjectPattern"
|
|
75
|
+
)
|
|
76
|
+
return null;
|
|
77
|
+
const binding = destructuredBinding(declarator, property);
|
|
78
|
+
if (binding === "safe" || binding === false) return binding;
|
|
79
|
+
return followConst(binding, null, context, shape, api, seen);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
module.exports = { destructureMode, logicalCopyLeft, spreadPreserves, unwrapCopyLogical };
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
const { createImportedSources } = require("./async-source-store");
|
|
3
|
+
const { unwrapExpression } = require("./async-ast");
|
|
4
|
+
const { resolveVariable } = require("./async-target-bindings");
|
|
5
|
+
const { propertiesWritten } = require("./async-disposition-mocks");
|
|
6
|
+
const { localFunction } = require("./async-consumer-local-proof");
|
|
7
|
+
|
|
8
|
+
function exportedFunction(filename, name, sources, seen = new Set()) {
|
|
9
|
+
if (seen.size >= 32 || seen.has(filename)) return null;
|
|
10
|
+
seen.add(filename);
|
|
11
|
+
const source = sources.load(filename);
|
|
12
|
+
if (!source) return null;
|
|
13
|
+
const matches = [];
|
|
14
|
+
for (const statement of source.ast.body) {
|
|
15
|
+
if (statement.type !== "ExportNamedDeclaration" || statement.exportKind === "type") continue;
|
|
16
|
+
const declaration = statement.declaration;
|
|
17
|
+
const ids =
|
|
18
|
+
declaration?.type === "FunctionDeclaration"
|
|
19
|
+
? [declaration.id]
|
|
20
|
+
: declaration?.type === "VariableDeclaration"
|
|
21
|
+
? declaration.declarations.map((item) => item.id)
|
|
22
|
+
: [];
|
|
23
|
+
for (const id of ids) if (id?.name === name) matches.push({ local: id });
|
|
24
|
+
for (const item of statement.specifiers)
|
|
25
|
+
if (item.exportKind !== "type" && (item.exported.name ?? item.exported.value) === name)
|
|
26
|
+
matches.push({ local: item.local, source: statement.source });
|
|
27
|
+
}
|
|
28
|
+
if (matches.length !== 1) return null;
|
|
29
|
+
const { local, source: reexport } = matches[0];
|
|
30
|
+
if (reexport) {
|
|
31
|
+
const target = sources.resolve(reexport.value, filename);
|
|
32
|
+
return target ? exportedFunction(target, local.name, sources, seen) : null;
|
|
33
|
+
}
|
|
34
|
+
const fn = localFunction(local, source.context);
|
|
35
|
+
const variable = resolveVariable(local, source.context);
|
|
36
|
+
// Exporting preserves live binding identity; any other non-call use may mutate it.
|
|
37
|
+
if (
|
|
38
|
+
!fn ||
|
|
39
|
+
variable.references.some((ref) => {
|
|
40
|
+
if (ref.init) return false;
|
|
41
|
+
const parent = ref.identifier.parent;
|
|
42
|
+
return (
|
|
43
|
+
parent.type !== "ExportSpecifier" &&
|
|
44
|
+
!(parent.type === "CallExpression" && parent.callee === ref.identifier)
|
|
45
|
+
);
|
|
46
|
+
})
|
|
47
|
+
)
|
|
48
|
+
return null;
|
|
49
|
+
return { fn, context: source.context };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function importedAwaited(call, index, context, state, property, proveBody) {
|
|
53
|
+
const program = context.sourceCode.parserServices?.program;
|
|
54
|
+
if (program?.getCompilerOptions().noResolve) return false;
|
|
55
|
+
if (program && property === null)
|
|
56
|
+
return require("./async-consumer-typed-proof").importedAwaited(
|
|
57
|
+
call,
|
|
58
|
+
index,
|
|
59
|
+
context,
|
|
60
|
+
state.typed,
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
const resolved = importedFunction(unwrapExpression(call.callee), context, state);
|
|
64
|
+
if (!resolved) return false;
|
|
65
|
+
const mode = proveBody(resolved.fn, index, resolved.context, property);
|
|
66
|
+
return mode?.returned ? { ...mode, async: Boolean(resolved.fn.async) } : mode;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function importedFunction(callee, context, state) {
|
|
70
|
+
const binding = resolveVariable(callee, context);
|
|
71
|
+
if (
|
|
72
|
+
binding?.defs.length !== 1 ||
|
|
73
|
+
binding.defs[0].type !== "ImportBinding" ||
|
|
74
|
+
binding.references.some((ref) => ref.isWrite()) ||
|
|
75
|
+
propertiesWritten(callee, context, new Set(), null, true)
|
|
76
|
+
)
|
|
77
|
+
return false;
|
|
78
|
+
const def = binding.defs[0];
|
|
79
|
+
if (
|
|
80
|
+
def.node.type !== "ImportSpecifier" ||
|
|
81
|
+
def.parent.importKind === "type" ||
|
|
82
|
+
def.node.importKind === "type"
|
|
83
|
+
)
|
|
84
|
+
return false;
|
|
85
|
+
// Cache unavailable peers too; a new lint context retries without persisting state.
|
|
86
|
+
if (state.sources === null) state.sources = createImportedSources(context) || false;
|
|
87
|
+
const sources = state.sources;
|
|
88
|
+
if (!sources) return false;
|
|
89
|
+
const filename = context.filename;
|
|
90
|
+
const target = sources.resolve(def.parent.source.value, filename);
|
|
91
|
+
const resolved =
|
|
92
|
+
target && exportedFunction(target, def.node.imported.name ?? def.node.imported.value, sources);
|
|
93
|
+
return resolved;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
module.exports = { importedAwaited, importedFunction, createImportedSources };
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
const { unwrapExpression, unwrapTransparentParent } = require("./async-ast");
|
|
3
|
+
const { resolveVariable, stableConstInitializer } = require("./async-target-bindings");
|
|
4
|
+
const { cachedBodyAwaited } = require("./async-consumer-cache-proof");
|
|
5
|
+
const { propertiesWritten } = require("./async-disposition-mocks");
|
|
6
|
+
|
|
7
|
+
function localFunction(id, context, seen = new Set()) {
|
|
8
|
+
if (id?.type !== "Identifier") return null;
|
|
9
|
+
const variable = resolveVariable(id, context);
|
|
10
|
+
if (
|
|
11
|
+
!variable ||
|
|
12
|
+
seen.has(variable) ||
|
|
13
|
+
variable.defs.length !== 1 ||
|
|
14
|
+
variable.references.some((ref) => ref.isWrite() && !ref.init) ||
|
|
15
|
+
propertiesWritten(id, context, new Set(), null, true)
|
|
16
|
+
)
|
|
17
|
+
return null;
|
|
18
|
+
const def = variable.defs[0];
|
|
19
|
+
if (def.type === "FunctionName" && def.node.type === "FunctionDeclaration") return def.node;
|
|
20
|
+
const init = unwrapExpression(stableConstInitializer(id, context, seen));
|
|
21
|
+
return ["ArrowFunctionExpression", "FunctionExpression"].includes(init?.type)
|
|
22
|
+
? init
|
|
23
|
+
: localFunction(init, context, seen);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function localBodySafe(fn, context) {
|
|
27
|
+
let safe = true;
|
|
28
|
+
function visit(node) {
|
|
29
|
+
if (!node?.type || !safe) return;
|
|
30
|
+
if (
|
|
31
|
+
node.type === "Identifier" &&
|
|
32
|
+
(node.name === "arguments" ||
|
|
33
|
+
(node.name === "eval" &&
|
|
34
|
+
node.parent?.type === "CallExpression" &&
|
|
35
|
+
node.parent.callee === node))
|
|
36
|
+
)
|
|
37
|
+
safe = false;
|
|
38
|
+
for (const key of context.sourceCode.visitorKeys[node.type] || []) {
|
|
39
|
+
const child = node[key];
|
|
40
|
+
if (Array.isArray(child)) child.forEach(visit);
|
|
41
|
+
else visit(child);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
fn.params.forEach(visit);
|
|
45
|
+
visit(fn.body);
|
|
46
|
+
return safe;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function bodyAwaited(fn, index, context) {
|
|
50
|
+
const param = fn?.params[index];
|
|
51
|
+
if (
|
|
52
|
+
!param ||
|
|
53
|
+
param.type !== "Identifier" ||
|
|
54
|
+
!localBodySafe(fn, context) ||
|
|
55
|
+
fn.params.some((p) => p.type === "RestElement")
|
|
56
|
+
)
|
|
57
|
+
return false;
|
|
58
|
+
if (cachedBodyAwaited(fn, param, context)) return true;
|
|
59
|
+
const variable = resolveVariable(param, context);
|
|
60
|
+
const reads = variable?.references.filter((ref) => !ref.init) || [];
|
|
61
|
+
return (
|
|
62
|
+
reads.length > 0 &&
|
|
63
|
+
reads.every((ref) => {
|
|
64
|
+
if (ref.isWrite()) return false;
|
|
65
|
+
for (let owner = ref.identifier.parent; owner && owner !== fn; owner = owner.parent)
|
|
66
|
+
if (
|
|
67
|
+
["ArrowFunctionExpression", "FunctionExpression", "FunctionDeclaration"].includes(
|
|
68
|
+
owner.type,
|
|
69
|
+
)
|
|
70
|
+
)
|
|
71
|
+
return false;
|
|
72
|
+
const value = unwrapTransparentParent(ref.identifier);
|
|
73
|
+
const invocation = value.parent;
|
|
74
|
+
if (
|
|
75
|
+
invocation?.type !== "CallExpression" ||
|
|
76
|
+
invocation.callee !== value ||
|
|
77
|
+
invocation.optional
|
|
78
|
+
)
|
|
79
|
+
return false;
|
|
80
|
+
const result = unwrapTransparentParent(invocation);
|
|
81
|
+
return result.parent?.type === "AwaitExpression" && result.parent.argument === result;
|
|
82
|
+
})
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
module.exports = { localFunction, localBodySafe, bodyAwaited };
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { unwrapExpression, unwrapTransparentParent } = require("./async-ast");
|
|
4
|
+
const { resolveVariable } = require("./async-target-bindings");
|
|
5
|
+
const { importedAwaited } = require("./async-consumer-imported-proof");
|
|
6
|
+
const { localFunction, bodyAwaited } = require("./async-consumer-local-proof");
|
|
7
|
+
const { adoptReturned } = require("./async-consumer-property-carrier");
|
|
8
|
+
const {
|
|
9
|
+
forwarded,
|
|
10
|
+
lexicalCallback,
|
|
11
|
+
propertyBodyAwaited,
|
|
12
|
+
propertyWritten,
|
|
13
|
+
} = require("./async-consumer-property-proof");
|
|
14
|
+
const { createReceiverProof } = require("./async-consumer-receivers");
|
|
15
|
+
|
|
16
|
+
function createConsumerProof(context, sourceFacts) {
|
|
17
|
+
const results = new WeakMap();
|
|
18
|
+
const receiverShape = (sourceFacts.receiverShape ||= createReceiverProof(sourceFacts));
|
|
19
|
+
const noParameter = Object.create(null);
|
|
20
|
+
let depth = 0;
|
|
21
|
+
return function provenAwaitingConsumer(argument) {
|
|
22
|
+
let value = unwrapTransparentParent(argument);
|
|
23
|
+
let property = null;
|
|
24
|
+
if (value.parent?.type === "Property") {
|
|
25
|
+
const entry = value.parent;
|
|
26
|
+
const object = entry.parent;
|
|
27
|
+
if (
|
|
28
|
+
entry.value !== value ||
|
|
29
|
+
object.type !== "ObjectExpression" ||
|
|
30
|
+
object.properties.some(
|
|
31
|
+
(item) =>
|
|
32
|
+
item.type !== "Property" || item.computed || item.method || item.kind !== "init",
|
|
33
|
+
)
|
|
34
|
+
)
|
|
35
|
+
return false;
|
|
36
|
+
const names = object.properties.map((item) => item.key.name ?? item.key.value);
|
|
37
|
+
if (new Set(names).size !== names.length) return false;
|
|
38
|
+
property = entry.key.name ?? entry.key.value;
|
|
39
|
+
value = unwrapTransparentParent(object);
|
|
40
|
+
}
|
|
41
|
+
const call = value.parent;
|
|
42
|
+
const mode = proveCall(call, call?.arguments?.indexOf(value), property, context);
|
|
43
|
+
// A returned dependencies object still has to be awaited by this caller.
|
|
44
|
+
if (mode?.returned) {
|
|
45
|
+
return (
|
|
46
|
+
adoptReturned(call, mode, property, context, {
|
|
47
|
+
proveCall,
|
|
48
|
+
receiverShape,
|
|
49
|
+
propertyWritten,
|
|
50
|
+
forward: forwarded,
|
|
51
|
+
lexicalCallback,
|
|
52
|
+
variable: noParameter,
|
|
53
|
+
}) === true
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
return mode === true;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
function withAsync(fn, mode) {
|
|
60
|
+
return mode?.returned ? { ...mode, async: Boolean(fn.async) } : mode;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function proveBody(fn, index, owner, property, shape) {
|
|
64
|
+
return property === null
|
|
65
|
+
? bodyAwaited(fn, index, owner)
|
|
66
|
+
: propertyBodyAwaited(fn, index, owner, property, proveCall, receiverShape, shape);
|
|
67
|
+
}
|
|
68
|
+
function proveCall(call, index, property, owner, shape) {
|
|
69
|
+
if (
|
|
70
|
+
call?.type !== "CallExpression" ||
|
|
71
|
+
call.optional ||
|
|
72
|
+
call.callee.type !== "Identifier" ||
|
|
73
|
+
call.arguments.some((arg) => arg.type === "SpreadElement") ||
|
|
74
|
+
index < 0 ||
|
|
75
|
+
depth >= 32
|
|
76
|
+
)
|
|
77
|
+
return false;
|
|
78
|
+
if (!shape) {
|
|
79
|
+
const argument = unwrapExpression(call.arguments[index]);
|
|
80
|
+
shape = argument.type === "ObjectExpression" ? receiverShape(argument, owner) : new Map();
|
|
81
|
+
}
|
|
82
|
+
// A custom prototype can supply an otherwise absent optional method.
|
|
83
|
+
if (shape.has("__proto__")) return false;
|
|
84
|
+
const binding = resolveVariable(call.callee, owner);
|
|
85
|
+
if (!binding) return false;
|
|
86
|
+
if (!results.has(binding)) results.set(binding, new Map());
|
|
87
|
+
const slots = results.get(binding);
|
|
88
|
+
const key = JSON.stringify([index, property, [...shape].sort()]);
|
|
89
|
+
if (!slots.has(key)) {
|
|
90
|
+
// Mark before recursion: cyclic forwarding cannot establish a contract.
|
|
91
|
+
slots.set(key, false);
|
|
92
|
+
depth++;
|
|
93
|
+
const localFn = localFunction(unwrapExpression(call.callee), owner);
|
|
94
|
+
let mode = false;
|
|
95
|
+
if (localFn) mode = withAsync(localFn, proveBody(localFn, index, owner, property, shape));
|
|
96
|
+
if (!mode) {
|
|
97
|
+
mode = importedAwaited(
|
|
98
|
+
call,
|
|
99
|
+
index,
|
|
100
|
+
owner,
|
|
101
|
+
sourceFacts,
|
|
102
|
+
property,
|
|
103
|
+
(fn, index, owner, property) => proveBody(fn, index, owner, property, shape),
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
slots.set(key, mode || false);
|
|
107
|
+
depth--;
|
|
108
|
+
}
|
|
109
|
+
return slots.get(key);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
module.exports = { createConsumerProof };
|