eslint-plugin-no-mistakes 0.58.0 → 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 +1 -1
- package/src/index.js +1 -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 +31 -9
- package/src/rules/no-inline-noop-promise-catch.js +1 -1
- package/src/rules/require-options-on-imported-call.js +69 -0
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -37,6 +37,7 @@ const rules = {
|
|
|
37
37
|
"postgres-no-unbounded-query-fanout": require("./rules/postgres-no-unbounded-query-fanout"),
|
|
38
38
|
"react-no-nullish-react-node": require("./rules/react-no-nullish-react-node"),
|
|
39
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"),
|
|
40
41
|
"server-require-nullable-fetch-wrapper": require("./rules/server-require-nullable-fetch-wrapper"),
|
|
41
42
|
"test-no-error-message-matching": require("./rules/test-no-error-message-matching"),
|
|
42
43
|
"test-no-delayed-rejects": require("./rules/test-no-delayed-rejects"),
|
|
@@ -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
|
+
};
|
|
@@ -37,18 +37,40 @@ function isVoidZero(node) {
|
|
|
37
37
|
return argument?.type === "Literal" && argument.value === 0;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
function
|
|
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) {
|
|
41
51
|
const current = unwrapExpression(node);
|
|
42
|
-
if (current.type === "Identifier" && current.name === "undefined")
|
|
52
|
+
if (current.type === "Identifier" && current.name === "undefined") {
|
|
53
|
+
return Boolean(sourceCode) && !isShadowedUndefined(current, sourceCode);
|
|
54
|
+
}
|
|
43
55
|
return isVoidZero(current);
|
|
44
56
|
}
|
|
45
57
|
|
|
46
|
-
function
|
|
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) {
|
|
47
65
|
if (statement.type === "EmptyStatement") return true;
|
|
48
66
|
if (statement.type === "ReturnStatement") {
|
|
49
|
-
return statement.argument == null || isNoopExpression(statement.argument);
|
|
67
|
+
return statement.argument == null || isNoopExpression(statement.argument, sourceCode);
|
|
50
68
|
}
|
|
51
|
-
return
|
|
69
|
+
return (
|
|
70
|
+
statement.type === "ExpressionStatement" &&
|
|
71
|
+
(isNoopExpression(statement.expression, sourceCode) ||
|
|
72
|
+
isNoopLiteralExpression(statement.expression))
|
|
73
|
+
);
|
|
52
74
|
}
|
|
53
75
|
|
|
54
76
|
function isInlineFunction(node) {
|
|
@@ -56,11 +78,11 @@ function isInlineFunction(node) {
|
|
|
56
78
|
return node?.type === "FunctionExpression" && node.id == null;
|
|
57
79
|
}
|
|
58
80
|
|
|
59
|
-
function isInlineNoopFunction(node) {
|
|
81
|
+
function isInlineNoopFunction(node, sourceCode) {
|
|
60
82
|
const current = unwrapExpression(node);
|
|
61
|
-
if (!isInlineFunction(current)) return false;
|
|
62
|
-
if (current.body.type !== "BlockStatement") return isNoopExpression(current.body);
|
|
63
|
-
return current.body.body.every(isNoopStatement);
|
|
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));
|
|
64
86
|
}
|
|
65
87
|
|
|
66
88
|
function originatingCalleeName(call) {
|
|
@@ -37,7 +37,7 @@ module.exports = Object.assign(
|
|
|
37
37
|
return {
|
|
38
38
|
CallExpression(node) {
|
|
39
39
|
const handler = rejectionHandler(node);
|
|
40
|
-
if (!handler || !isInlineNoopFunction(handler)) return;
|
|
40
|
+
if (!handler || !isInlineNoopFunction(handler, context.sourceCode)) return;
|
|
41
41
|
if (isAllowedCallee(node, options)) return;
|
|
42
42
|
context.report({ node: handler, messageId: "noopCatch" });
|
|
43
43
|
},
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { rule } = require("../helpers");
|
|
4
|
+
const { createTargetMatcher } = require("./async-targets");
|
|
5
|
+
const {
|
|
6
|
+
compileImportedCallTargets,
|
|
7
|
+
hasRequiredOptions,
|
|
8
|
+
importedCallOptionsSchema,
|
|
9
|
+
matchingTargets,
|
|
10
|
+
staticPropertyName,
|
|
11
|
+
visiblePropertyNames,
|
|
12
|
+
} = require("./imported-call-options");
|
|
13
|
+
|
|
14
|
+
module.exports = Object.assign(
|
|
15
|
+
rule(
|
|
16
|
+
{
|
|
17
|
+
type: "problem",
|
|
18
|
+
docs: {
|
|
19
|
+
description:
|
|
20
|
+
"require a statically visible options object on calls resolved from configured imports",
|
|
21
|
+
recommended: false,
|
|
22
|
+
},
|
|
23
|
+
schema: importedCallOptionsSchema,
|
|
24
|
+
messages: {
|
|
25
|
+
missingOptions:
|
|
26
|
+
"Call '{{calleeName}}' from '{{source}}' must pass a statically visible options object at argument {{optionsPosition}} containing {{propertyMatch}} of: {{requiredProperties}}.",
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
(context) => {
|
|
30
|
+
const matcher = createTargetMatcher(context);
|
|
31
|
+
const optionTargets = compileImportedCallTargets(context.options?.[0] || {});
|
|
32
|
+
if (!matcher.hasTargets || optionTargets.length === 0) return {};
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
...matcher.visitors,
|
|
36
|
+
CallExpression(node) {
|
|
37
|
+
const resolved = matcher.resolveCallTarget(node);
|
|
38
|
+
if (!resolved) return;
|
|
39
|
+
for (const target of matchingTargets(
|
|
40
|
+
optionTargets,
|
|
41
|
+
resolved.source,
|
|
42
|
+
resolved.calleeName,
|
|
43
|
+
)) {
|
|
44
|
+
if (hasRequiredOptions(node.arguments[target.optionsPosition - 1], target)) continue;
|
|
45
|
+
context.report({
|
|
46
|
+
node,
|
|
47
|
+
messageId: "missingOptions",
|
|
48
|
+
data: {
|
|
49
|
+
source: resolved.source,
|
|
50
|
+
calleeName: resolved.calleeName,
|
|
51
|
+
optionsPosition: String(target.optionsPosition),
|
|
52
|
+
propertyMatch: target.propertyMatch,
|
|
53
|
+
requiredProperties: target.requiredProperties.join(", "),
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
},
|
|
60
|
+
),
|
|
61
|
+
{
|
|
62
|
+
__test: {
|
|
63
|
+
compileImportedCallTargets,
|
|
64
|
+
hasRequiredOptions,
|
|
65
|
+
staticPropertyName,
|
|
66
|
+
visiblePropertyNames,
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
);
|