eslint-plugin-no-mistakes 0.57.1 → 0.59.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 +3 -3
- package/src/index.js +4 -0
- package/src/rules/async-patterns.js +73 -0
- package/src/rules/async-targets.js +136 -117
- package/src/rules/imported-call-options.js +95 -0
- package/src/rules/no-inline-noop-promise-catch-helpers.js +114 -0
- package/src/rules/no-inline-noop-promise-catch.js +48 -0
- package/src/rules/require-options-on-imported-call.js +69 -0
- package/src/rules/test-no-delayed-rejects-abrupt.js +180 -0
- package/src/rules/test-no-delayed-rejects-chains.js +83 -0
- package/src/rules/test-no-delayed-rejects-finalizers.js +29 -0
- package/src/rules/test-no-delayed-rejects-flow.js +201 -0
- package/src/rules/test-no-delayed-rejects-handlers.js +44 -0
- package/src/rules/test-no-delayed-rejects-loop-jumps.js +167 -0
- package/src/rules/test-no-delayed-rejects-observers.js +60 -0
- package/src/rules/test-no-delayed-rejects-order.js +24 -0
- package/src/rules/test-no-delayed-rejects-suspensions.js +112 -0
- package/src/rules/test-no-delayed-rejects-transfers.js +106 -0
- package/src/rules/test-no-delayed-rejects.js +209 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eslint-plugin-no-mistakes",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.59.0",
|
|
4
4
|
"description": "ESLint and Oxlint rules that keep code static enough for no-mistakes analyzers and coding agents",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -21,9 +21,9 @@
|
|
|
21
21
|
"test": "vitest run --coverage"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
|
-
"@typescript-eslint/parser": "^8.
|
|
24
|
+
"@typescript-eslint/parser": "^8.68.0",
|
|
25
25
|
"@vitest/coverage-v8": "^4.1.11",
|
|
26
|
-
"eslint": "^10.9.
|
|
26
|
+
"eslint": "^10.9.1",
|
|
27
27
|
"oxlint": "^1.80.0",
|
|
28
28
|
"vitest": "^4.1.11"
|
|
29
29
|
},
|
package/src/index.js
CHANGED
|
@@ -14,6 +14,7 @@ const rules = {
|
|
|
14
14
|
"no-global-fetch-outside-helper": require("./rules/no-global-fetch-outside-helper"),
|
|
15
15
|
"no-delete-property": require("./rules/no-delete-property"),
|
|
16
16
|
"no-import-only-test-files": require("./rules/no-import-only-test-files"),
|
|
17
|
+
"no-inline-noop-promise-catch": require("./rules/no-inline-noop-promise-catch"),
|
|
17
18
|
"no-placeholder-never-type-exports": require("./rules/no-placeholder-never-type-exports"),
|
|
18
19
|
"no-vitest-sequential": require("./rules/no-vitest-sequential"),
|
|
19
20
|
"playwright-consistent-attribute": require("./rules/playwright-consistent-attribute"),
|
|
@@ -36,8 +37,10 @@ const rules = {
|
|
|
36
37
|
"postgres-no-unbounded-query-fanout": require("./rules/postgres-no-unbounded-query-fanout"),
|
|
37
38
|
"react-no-nullish-react-node": require("./rules/react-no-nullish-react-node"),
|
|
38
39
|
"react-no-use-promise-resolve": require("./rules/react-no-use-promise-resolve"),
|
|
40
|
+
"require-options-on-imported-call": require("./rules/require-options-on-imported-call"),
|
|
39
41
|
"server-require-nullable-fetch-wrapper": require("./rules/server-require-nullable-fetch-wrapper"),
|
|
40
42
|
"test-no-error-message-matching": require("./rules/test-no-error-message-matching"),
|
|
43
|
+
"test-no-delayed-rejects": require("./rules/test-no-delayed-rejects"),
|
|
41
44
|
"test-no-shared-state": require("./rules/test-no-shared-state"),
|
|
42
45
|
"ts-no-const-aliases": require("./rules/ts-no-const-aliases"),
|
|
43
46
|
"ts-no-export-renaming": require("./rules/ts-no-export-renaming"),
|
|
@@ -96,6 +99,7 @@ plugin.configs.strict = {
|
|
|
96
99
|
"no-mistakes/react-no-iife-in-jsx": "error",
|
|
97
100
|
"no-mistakes/react-no-use-promise-resolve": "error",
|
|
98
101
|
"no-mistakes/test-no-error-message-matching": "error",
|
|
102
|
+
"no-mistakes/test-no-delayed-rejects": "error",
|
|
99
103
|
"no-mistakes/test-no-shared-state": "error",
|
|
100
104
|
"no-mistakes/vitest-mock-test-file-naming": "error",
|
|
101
105
|
},
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
function safeRegExp(source) {
|
|
4
|
+
try {
|
|
5
|
+
return new RegExp(source);
|
|
6
|
+
} catch {
|
|
7
|
+
return null;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function patternToRegExp(pattern) {
|
|
12
|
+
if (pattern.startsWith("/") && pattern.endsWith("/") && pattern.length > 2) {
|
|
13
|
+
return safeRegExp(pattern.slice(1, -1));
|
|
14
|
+
}
|
|
15
|
+
let source = "^";
|
|
16
|
+
for (let index = 0; index < pattern.length; index += 1) {
|
|
17
|
+
const ch = pattern[index];
|
|
18
|
+
const next = pattern[index + 1];
|
|
19
|
+
if (ch === "*" && next === "*") {
|
|
20
|
+
if (pattern[index + 2] === "/") {
|
|
21
|
+
source += "(?:.*/)?";
|
|
22
|
+
index += 2;
|
|
23
|
+
} else {
|
|
24
|
+
source += ".*";
|
|
25
|
+
index += 1;
|
|
26
|
+
}
|
|
27
|
+
} else if (ch === "*") {
|
|
28
|
+
source += "[^/]*";
|
|
29
|
+
} else if (ch === "?") {
|
|
30
|
+
source += "[^/]";
|
|
31
|
+
} else {
|
|
32
|
+
source += ch.replace(/[\\^$+?.()|[\]{}]/g, "\\$&");
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return safeRegExp(`${source}$`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function compileTargets(options, optionKey) {
|
|
39
|
+
return (options[optionKey] || [])
|
|
40
|
+
.map((target) => ({
|
|
41
|
+
sourceSpecifierPatterns: (target.sourceSpecifierPatterns || [])
|
|
42
|
+
.map(patternToRegExp)
|
|
43
|
+
.filter(Boolean),
|
|
44
|
+
calleeNamePatterns: (target.calleeNamePatterns || []).map(patternToRegExp).filter(Boolean),
|
|
45
|
+
}))
|
|
46
|
+
.filter(
|
|
47
|
+
(target) => target.sourceSpecifierPatterns.length > 0 && target.calleeNamePatterns.length > 0,
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function matchesAny(value, patterns) {
|
|
52
|
+
return typeof value === "string" && patterns.some((pattern) => pattern.test(value));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function matchingTargets(targets, source, calleeName) {
|
|
56
|
+
return targets.filter(
|
|
57
|
+
(target) =>
|
|
58
|
+
matchesAny(source, target.sourceSpecifierPatterns) &&
|
|
59
|
+
matchesAny(calleeName, target.calleeNamePatterns),
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function targetMatches(targets, source, calleeName) {
|
|
64
|
+
return matchingTargets(targets, source, calleeName).length > 0;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
module.exports = {
|
|
68
|
+
compileTargets,
|
|
69
|
+
matchingTargets,
|
|
70
|
+
matchesAny,
|
|
71
|
+
patternToRegExp,
|
|
72
|
+
targetMatches,
|
|
73
|
+
};
|
|
@@ -1,67 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
const { unwrapExpression } = require("./async-ast");
|
|
4
|
-
const {
|
|
5
|
-
|
|
6
|
-
function safeRegExp(source) {
|
|
7
|
-
try {
|
|
8
|
-
return new RegExp(source);
|
|
9
|
-
} catch {
|
|
10
|
-
return null;
|
|
11
|
-
}
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
function patternToRegExp(pattern) {
|
|
15
|
-
if (pattern.startsWith("/") && pattern.endsWith("/") && pattern.length > 2) {
|
|
16
|
-
return safeRegExp(pattern.slice(1, -1));
|
|
17
|
-
}
|
|
18
|
-
let source = "^";
|
|
19
|
-
for (let index = 0; index < pattern.length; index += 1) {
|
|
20
|
-
const ch = pattern[index];
|
|
21
|
-
const next = pattern[index + 1];
|
|
22
|
-
if (ch === "*" && next === "*") {
|
|
23
|
-
if (pattern[index + 2] === "/") {
|
|
24
|
-
source += "(?:.*/)?";
|
|
25
|
-
index += 2;
|
|
26
|
-
} else {
|
|
27
|
-
source += ".*";
|
|
28
|
-
index += 1;
|
|
29
|
-
}
|
|
30
|
-
} else if (ch === "*") {
|
|
31
|
-
source += "[^/]*";
|
|
32
|
-
} else if (ch === "?") {
|
|
33
|
-
source += "[^/]";
|
|
34
|
-
} else {
|
|
35
|
-
source += ch.replace(/[\\^$+?.()|[\]{}]/g, "\\$&");
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
return safeRegExp(`${source}$`);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function compileTargets(options, optionKey) {
|
|
42
|
-
return (options[optionKey] || [])
|
|
43
|
-
.map((target) => ({
|
|
44
|
-
sourceSpecifierPatterns: (target.sourceSpecifierPatterns || [])
|
|
45
|
-
.map(patternToRegExp)
|
|
46
|
-
.filter(Boolean),
|
|
47
|
-
calleeNamePatterns: (target.calleeNamePatterns || []).map(patternToRegExp).filter(Boolean),
|
|
48
|
-
}))
|
|
49
|
-
.filter(
|
|
50
|
-
(target) => target.sourceSpecifierPatterns.length > 0 && target.calleeNamePatterns.length > 0,
|
|
51
|
-
);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function matchesAny(value, patterns) {
|
|
55
|
-
return typeof value === "string" && patterns.some((pattern) => pattern.test(value));
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function targetMatches(targets, source, calleeName) {
|
|
59
|
-
return targets.some(
|
|
60
|
-
(target) =>
|
|
61
|
-
matchesAny(source, target.sourceSpecifierPatterns) &&
|
|
62
|
-
matchesAny(calleeName, target.calleeNamePatterns),
|
|
63
|
-
);
|
|
64
|
-
}
|
|
4
|
+
const { compileTargets, matchesAny, targetMatches } = require("./async-patterns");
|
|
5
|
+
const { propertyName, literalString } = require("./module-mock-helpers");
|
|
65
6
|
|
|
66
7
|
function findVariable(scope, name) {
|
|
67
8
|
while (scope) {
|
|
@@ -81,14 +22,24 @@ function importSpecifierName(specifier) {
|
|
|
81
22
|
return imported.type === "Literal" ? String(imported.value) : imported.name;
|
|
82
23
|
}
|
|
83
24
|
|
|
84
|
-
function
|
|
25
|
+
function isLocalRequire(id, context) {
|
|
26
|
+
const variable = resolveVariable(id, context);
|
|
27
|
+
return Boolean(variable?.defs.some((def) => def.type && def.type !== "ImplicitGlobalVariable"));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function requireSource(node, context) {
|
|
85
31
|
const expression = unwrapExpression(node);
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
expression
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
32
|
+
const source = literalString(unwrapExpression(expression?.arguments?.[0]));
|
|
33
|
+
if (
|
|
34
|
+
expression?.type !== "CallExpression" ||
|
|
35
|
+
expression.callee.type !== "Identifier" ||
|
|
36
|
+
expression.callee.name !== "require" ||
|
|
37
|
+
source === null ||
|
|
38
|
+
isLocalRequire(expression.callee, context)
|
|
39
|
+
) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
return source;
|
|
92
43
|
}
|
|
93
44
|
|
|
94
45
|
function bindingIdentifier(node) {
|
|
@@ -98,7 +49,11 @@ function bindingIdentifier(node) {
|
|
|
98
49
|
|
|
99
50
|
function memberPropertyName(node) {
|
|
100
51
|
if (!node.computed) return propertyName(node.property);
|
|
101
|
-
return
|
|
52
|
+
return staticComputedPropertyName(node.property);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function staticComputedPropertyName(node) {
|
|
56
|
+
return literalString(unwrapExpression(node));
|
|
102
57
|
}
|
|
103
58
|
|
|
104
59
|
function createTargetMatcher(context, optionKey = "targets") {
|
|
@@ -107,14 +62,36 @@ function createTargetMatcher(context, optionKey = "targets") {
|
|
|
107
62
|
const directBindings = new Map();
|
|
108
63
|
const namespaceBindings = new Map();
|
|
109
64
|
|
|
65
|
+
function isReassigned(id) {
|
|
66
|
+
const variable = resolveVariable(id, context);
|
|
67
|
+
const writes = variable?.references.filter((reference) => reference.isWrite()) || [];
|
|
68
|
+
const initializationSites = new Set(
|
|
69
|
+
writes.filter((reference) => reference.init).map((reference) => reference.identifier),
|
|
70
|
+
);
|
|
71
|
+
const isVar = variable?.defs.some(
|
|
72
|
+
(definition) => definition.type === "Variable" && definition.parent?.kind === "var",
|
|
73
|
+
);
|
|
74
|
+
return writes.some((reference) => !reference.init) || (isVar && initializationSites.size > 1);
|
|
75
|
+
}
|
|
76
|
+
|
|
110
77
|
function recordDirect(id, source, calleeName) {
|
|
111
|
-
if (
|
|
78
|
+
if (
|
|
79
|
+
id?.type !== "Identifier" ||
|
|
80
|
+
isReassigned(id) ||
|
|
81
|
+
!targetMatches(targets, source, calleeName)
|
|
82
|
+
) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
112
85
|
const variable = resolveVariable(id, context);
|
|
113
86
|
if (variable) directBindings.set(variable, { source, calleeName });
|
|
114
87
|
}
|
|
115
88
|
|
|
116
89
|
function recordNamespace(id, source) {
|
|
117
|
-
if (
|
|
90
|
+
if (
|
|
91
|
+
id.type !== "Identifier" ||
|
|
92
|
+
isReassigned(id) ||
|
|
93
|
+
!matchesAny(source, sourceSpecifierPatterns)
|
|
94
|
+
) {
|
|
118
95
|
return;
|
|
119
96
|
}
|
|
120
97
|
const variable = resolveVariable(id, context);
|
|
@@ -122,74 +99,116 @@ function createTargetMatcher(context, optionKey = "targets") {
|
|
|
122
99
|
}
|
|
123
100
|
|
|
124
101
|
function recordRequireDeclarator(node) {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
if (
|
|
128
|
-
|
|
129
|
-
|
|
102
|
+
if (node.parent?.type !== "VariableDeclaration") return;
|
|
103
|
+
const source = requireSource(node.init, context);
|
|
104
|
+
if (source) {
|
|
105
|
+
if (node.id.type === "Identifier") {
|
|
106
|
+
recordNamespace(node.id, source);
|
|
107
|
+
recordDirect(node.id, source, node.id.name);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
if (node.id.type === "ObjectPattern") {
|
|
111
|
+
for (const property of node.id.properties) {
|
|
112
|
+
if (property.type !== "Property") continue;
|
|
113
|
+
const name = property.computed
|
|
114
|
+
? staticComputedPropertyName(property.key)
|
|
115
|
+
: propertyName(property.key);
|
|
116
|
+
if (name) recordDirect(bindingIdentifier(property.value), source, name);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
130
119
|
return;
|
|
131
120
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
121
|
+
const member = unwrapExpression(node.init);
|
|
122
|
+
if (member?.type !== "MemberExpression" || node.id.type !== "Identifier") return;
|
|
123
|
+
const memberSource = requireSource(member.object, context);
|
|
124
|
+
const name = memberPropertyName(member);
|
|
125
|
+
if (memberSource && name) recordDirect(node.id, memberSource, name);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function recordImportEquals(node) {
|
|
129
|
+
if (node.importKind === "type" || node.id?.type !== "Identifier") return;
|
|
130
|
+
const ref = node.moduleReference;
|
|
131
|
+
if (ref?.type !== "TSExternalModuleReference") return;
|
|
132
|
+
const source = ref.expression?.type === "Literal" ? String(ref.expression.value) : null;
|
|
133
|
+
if (!source) return;
|
|
134
|
+
recordNamespace(node.id, source);
|
|
135
|
+
recordDirect(node.id, source, node.id.name);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function recordImportDeclaration(node) {
|
|
139
|
+
const source = node.source.value;
|
|
140
|
+
for (const specifier of node.specifiers) {
|
|
141
|
+
if (specifier.type === "ImportNamespaceSpecifier") {
|
|
142
|
+
recordNamespace(specifier.local, source);
|
|
143
|
+
} else if (specifier.type === "ImportDefaultSpecifier") {
|
|
144
|
+
recordDirect(specifier.local, source, specifier.local.name);
|
|
145
|
+
} else if (specifier.type === "ImportSpecifier") {
|
|
146
|
+
const imported = importSpecifierName(specifier);
|
|
147
|
+
recordDirect(
|
|
148
|
+
specifier.local,
|
|
149
|
+
source,
|
|
150
|
+
imported === "default" ? specifier.local.name : imported,
|
|
151
|
+
);
|
|
136
152
|
}
|
|
137
153
|
}
|
|
138
154
|
}
|
|
139
155
|
|
|
140
|
-
function
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
156
|
+
function walk(node, visit) {
|
|
157
|
+
if (!node?.type) return;
|
|
158
|
+
visit(node);
|
|
159
|
+
for (const key of context.sourceCode.visitorKeys[node.type] || []) {
|
|
160
|
+
const value = node[key];
|
|
161
|
+
if (Array.isArray(value)) {
|
|
162
|
+
for (const child of value) walk(child, visit);
|
|
163
|
+
} else {
|
|
164
|
+
walk(value, visit);
|
|
165
|
+
}
|
|
150
166
|
}
|
|
151
167
|
}
|
|
152
168
|
|
|
153
|
-
function
|
|
154
|
-
|
|
169
|
+
function recordProgram(node) {
|
|
170
|
+
walk(node, (child) => {
|
|
171
|
+
if (child.type === "ImportDeclaration") recordImportDeclaration(child);
|
|
172
|
+
else if (child.type === "TSImportEqualsDeclaration") recordImportEquals(child);
|
|
173
|
+
else if (child.type === "VariableDeclarator") recordRequireDeclarator(child);
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function resolveDirectTarget(node) {
|
|
178
|
+
if (node.type !== "Identifier") return null;
|
|
155
179
|
const variable = resolveVariable(node, context);
|
|
156
|
-
return
|
|
180
|
+
return (variable && directBindings.get(variable)) || null;
|
|
157
181
|
}
|
|
158
182
|
|
|
159
|
-
function
|
|
160
|
-
if (node.type !== "MemberExpression") return
|
|
183
|
+
function resolveNamespaceTarget(node) {
|
|
184
|
+
if (node.type !== "MemberExpression") return null;
|
|
161
185
|
const name = memberPropertyName(node);
|
|
162
|
-
if (!name) return
|
|
186
|
+
if (!name) return null;
|
|
187
|
+
const object = unwrapExpression(node.object);
|
|
163
188
|
const source =
|
|
164
|
-
requireSource(
|
|
165
|
-
(
|
|
166
|
-
? namespaceBindings.get(resolveVariable(
|
|
189
|
+
requireSource(object, context) ||
|
|
190
|
+
(object.type === "Identifier"
|
|
191
|
+
? namespaceBindings.get(resolveVariable(object, context))
|
|
167
192
|
: null);
|
|
168
|
-
return
|
|
193
|
+
return source && targetMatches(targets, source, name) ? { source, calleeName: name } : null;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function resolveCallTarget(node) {
|
|
197
|
+
const callee = unwrapExpression(node.callee);
|
|
198
|
+
return resolveDirectTarget(callee) || resolveNamespaceTarget(callee);
|
|
169
199
|
}
|
|
170
200
|
|
|
171
201
|
return {
|
|
172
202
|
hasTargets: targets.length > 0,
|
|
203
|
+
resolveCallTarget,
|
|
173
204
|
isTargetCall(node) {
|
|
174
|
-
return
|
|
205
|
+
return Boolean(resolveCallTarget(node));
|
|
175
206
|
},
|
|
176
207
|
visitors: {
|
|
177
|
-
Program:
|
|
178
|
-
ImportDeclaration
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
if (specifier.type === "ImportNamespaceSpecifier") {
|
|
182
|
-
recordNamespace(specifier.local, source);
|
|
183
|
-
} else if (specifier.type === "ImportDefaultSpecifier") {
|
|
184
|
-
recordDirect(specifier.local, source, specifier.local.name);
|
|
185
|
-
} else if (specifier.type === "ImportSpecifier") {
|
|
186
|
-
recordDirect(specifier.local, source, importSpecifierName(specifier));
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
},
|
|
190
|
-
VariableDeclarator(node) {
|
|
191
|
-
recordRequireDeclarator(node);
|
|
192
|
-
},
|
|
208
|
+
Program: recordProgram,
|
|
209
|
+
ImportDeclaration: recordImportDeclaration,
|
|
210
|
+
TSImportEqualsDeclaration: recordImportEquals,
|
|
211
|
+
VariableDeclarator: recordRequireDeclarator,
|
|
193
212
|
},
|
|
194
213
|
};
|
|
195
214
|
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { unwrapExpression } = require("./async-ast");
|
|
4
|
+
const { compileTargets, matchingTargets } = require("./async-patterns");
|
|
5
|
+
const { propertyName, literalString } = require("./module-mock-helpers");
|
|
6
|
+
|
|
7
|
+
const importedCallOptionsSchema = [
|
|
8
|
+
{
|
|
9
|
+
type: "object",
|
|
10
|
+
properties: {
|
|
11
|
+
targets: {
|
|
12
|
+
type: "array",
|
|
13
|
+
items: {
|
|
14
|
+
type: "object",
|
|
15
|
+
properties: {
|
|
16
|
+
sourceSpecifierPatterns: { type: "array", items: { type: "string" } },
|
|
17
|
+
calleeNamePatterns: { type: "array", items: { type: "string" } },
|
|
18
|
+
optionsPosition: { type: "integer", minimum: 1 },
|
|
19
|
+
requiredProperties: { type: "array", items: { type: "string" }, minItems: 1 },
|
|
20
|
+
propertyMatch: { type: "string", enum: ["any", "all"] },
|
|
21
|
+
},
|
|
22
|
+
required: [
|
|
23
|
+
"sourceSpecifierPatterns",
|
|
24
|
+
"calleeNamePatterns",
|
|
25
|
+
"optionsPosition",
|
|
26
|
+
"requiredProperties",
|
|
27
|
+
],
|
|
28
|
+
additionalProperties: false,
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
additionalProperties: false,
|
|
33
|
+
},
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
function compileImportedCallTargets(options) {
|
|
37
|
+
const compiled = [];
|
|
38
|
+
for (const target of options.targets || []) {
|
|
39
|
+
const [matched] = compileTargets({ targets: [target] }, "targets");
|
|
40
|
+
const optionsPosition = target.optionsPosition;
|
|
41
|
+
const requiredProperties = Array.isArray(target.requiredProperties)
|
|
42
|
+
? [...new Set(target.requiredProperties.filter((name) => typeof name === "string" && name))]
|
|
43
|
+
: [];
|
|
44
|
+
if (
|
|
45
|
+
!matched ||
|
|
46
|
+
!Number.isInteger(optionsPosition) ||
|
|
47
|
+
optionsPosition < 1 ||
|
|
48
|
+
requiredProperties.length === 0
|
|
49
|
+
) {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
compiled.push({
|
|
53
|
+
...matched,
|
|
54
|
+
optionsPosition,
|
|
55
|
+
requiredProperties,
|
|
56
|
+
propertyMatch: target.propertyMatch === "all" ? "all" : "any",
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
return compiled;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function staticPropertyName(property) {
|
|
63
|
+
if (property.type !== "Property") return null;
|
|
64
|
+
if (property.computed) return literalString(unwrapExpression(property.key));
|
|
65
|
+
return propertyName(property.key);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function visiblePropertyNames(node) {
|
|
69
|
+
const names = new Set();
|
|
70
|
+
if (node?.type !== "ObjectExpression") return names;
|
|
71
|
+
for (const property of node.properties) {
|
|
72
|
+
const name = staticPropertyName(property);
|
|
73
|
+
if (name) names.add(name);
|
|
74
|
+
}
|
|
75
|
+
return names;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function hasRequiredOptions(argument, target) {
|
|
79
|
+
const object = unwrapExpression(argument);
|
|
80
|
+
if (object?.type !== "ObjectExpression") return false;
|
|
81
|
+
const names = visiblePropertyNames(object);
|
|
82
|
+
if (target.propertyMatch === "all") {
|
|
83
|
+
return target.requiredProperties.every((name) => names.has(name));
|
|
84
|
+
}
|
|
85
|
+
return target.requiredProperties.some((name) => names.has(name));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
module.exports = {
|
|
89
|
+
compileImportedCallTargets,
|
|
90
|
+
hasRequiredOptions,
|
|
91
|
+
importedCallOptionsSchema,
|
|
92
|
+
matchingTargets,
|
|
93
|
+
staticPropertyName,
|
|
94
|
+
visiblePropertyNames,
|
|
95
|
+
};
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { unwrapExpression } = require("./async-ast");
|
|
4
|
+
const {
|
|
5
|
+
memberPropertyName,
|
|
6
|
+
repoRelativeFilename,
|
|
7
|
+
stringMatches,
|
|
8
|
+
} = require("./module-mock-helpers");
|
|
9
|
+
|
|
10
|
+
const CHAIN_METHODS = new Set(["catch", "finally", "then"]);
|
|
11
|
+
|
|
12
|
+
function shouldCheckFile(filename, options) {
|
|
13
|
+
const file = repoRelativeFilename(filename);
|
|
14
|
+
const checked = options.checkedPathPatterns ?? [];
|
|
15
|
+
const allowed = options.allowedPathPatterns ?? [];
|
|
16
|
+
if (checked.length > 0 && !stringMatches(file, checked)) return false;
|
|
17
|
+
return !stringMatches(file, allowed);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function promiseMethodName(call) {
|
|
21
|
+
const callee = unwrapExpression(call.callee);
|
|
22
|
+
if (callee?.type !== "MemberExpression" || callee.computed) return null;
|
|
23
|
+
return callee.property.name;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function rejectionHandler(call) {
|
|
27
|
+
const method = promiseMethodName(call);
|
|
28
|
+
if (method === "catch") return call.arguments[0] ?? null;
|
|
29
|
+
if (method === "then") return call.arguments[1] ?? null;
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function isVoidZero(node) {
|
|
34
|
+
const current = unwrapExpression(node);
|
|
35
|
+
if (current?.type !== "UnaryExpression" || current.operator !== "void") return false;
|
|
36
|
+
const argument = unwrapExpression(current.argument);
|
|
37
|
+
return argument?.type === "Literal" && argument.value === 0;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function isShadowedUndefined(node, sourceCode) {
|
|
41
|
+
let scope = sourceCode?.getScope?.(node) ?? null;
|
|
42
|
+
while (scope) {
|
|
43
|
+
const variable = scope.set?.get("undefined");
|
|
44
|
+
if (variable?.defs?.length > 0) return true;
|
|
45
|
+
scope = scope.upper;
|
|
46
|
+
}
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function isNoopExpression(node, sourceCode) {
|
|
51
|
+
const current = unwrapExpression(node);
|
|
52
|
+
if (current.type === "Identifier" && current.name === "undefined") {
|
|
53
|
+
return Boolean(sourceCode) && !isShadowedUndefined(current, sourceCode);
|
|
54
|
+
}
|
|
55
|
+
return isVoidZero(current);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function isNoopLiteralExpression(node) {
|
|
59
|
+
const current = unwrapExpression(node);
|
|
60
|
+
if (current?.type === "Literal") return true;
|
|
61
|
+
return current?.type === "TemplateLiteral" && current.expressions.length === 0;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function isNoopStatement(statement, sourceCode) {
|
|
65
|
+
if (statement.type === "EmptyStatement") return true;
|
|
66
|
+
if (statement.type === "ReturnStatement") {
|
|
67
|
+
return statement.argument == null || isNoopExpression(statement.argument, sourceCode);
|
|
68
|
+
}
|
|
69
|
+
return (
|
|
70
|
+
statement.type === "ExpressionStatement" &&
|
|
71
|
+
(isNoopExpression(statement.expression, sourceCode) ||
|
|
72
|
+
isNoopLiteralExpression(statement.expression))
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function isInlineFunction(node) {
|
|
77
|
+
if (node?.type === "ArrowFunctionExpression") return true;
|
|
78
|
+
return node?.type === "FunctionExpression" && node.id == null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function isInlineNoopFunction(node, sourceCode) {
|
|
82
|
+
const current = unwrapExpression(node);
|
|
83
|
+
if (!isInlineFunction(current) || current.generator) return false;
|
|
84
|
+
if (current.body.type !== "BlockStatement") return isNoopExpression(current.body, sourceCode);
|
|
85
|
+
return current.body.body.every((statement) => isNoopStatement(statement, sourceCode));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function originatingCalleeName(call) {
|
|
89
|
+
let current = unwrapExpression(call);
|
|
90
|
+
while (current?.type === "CallExpression") {
|
|
91
|
+
const method = promiseMethodName(current);
|
|
92
|
+
if (!CHAIN_METHODS.has(method)) break;
|
|
93
|
+
const callee = unwrapExpression(current.callee);
|
|
94
|
+
current = unwrapExpression(callee?.object);
|
|
95
|
+
}
|
|
96
|
+
if (current?.type !== "CallExpression") return null;
|
|
97
|
+
const callee = unwrapExpression(current.callee);
|
|
98
|
+
if (callee?.type === "Identifier") return callee.name;
|
|
99
|
+
if (callee?.type === "MemberExpression") return memberPropertyName(callee);
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function isAllowedCallee(call, options) {
|
|
104
|
+
const name = originatingCalleeName(call);
|
|
105
|
+
return Boolean(name && stringMatches(name, options.allowedCalleeNamePatterns ?? []));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
module.exports = {
|
|
109
|
+
isAllowedCallee,
|
|
110
|
+
isInlineNoopFunction,
|
|
111
|
+
originatingCalleeName,
|
|
112
|
+
rejectionHandler,
|
|
113
|
+
shouldCheckFile,
|
|
114
|
+
};
|