eslint-plugin-no-mistakes 0.58.0 → 0.60.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-no-mistakes",
3
- "version": "0.58.0",
3
+ "version": "0.60.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": {
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 { propertyName } = require("./module-mock-helpers");
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 requireSource(node) {
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
- return expression?.type === "CallExpression" &&
87
- expression.callee.type === "Identifier" &&
88
- expression.callee.name === "require" &&
89
- typeof expression.arguments[0]?.value === "string"
90
- ? expression.arguments[0].value
91
- : null;
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 node.property?.type === "Literal" ? String(node.property.value) : null;
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 (id?.type !== "Identifier" || !targetMatches(targets, source, calleeName)) return;
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 (id.type !== "Identifier" || !matchesAny(source, sourceSpecifierPatterns)) {
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
- const source = requireSource(node.init);
126
- if (!source) return;
127
- if (node.id.type === "Identifier") {
128
- recordNamespace(node.id, source);
129
- recordDirect(node.id, source, node.id.name);
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
- if (node.id.type === "ObjectPattern") {
133
- for (const property of node.id.properties) {
134
- if (property.type !== "Property") continue;
135
- recordDirect(bindingIdentifier(property.value), source, propertyName(property.key));
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 recordProgramRequires(node) {
141
- for (const statement of node.body) {
142
- const declarations =
143
- statement.type === "VariableDeclaration"
144
- ? statement.declarations
145
- : statement.type === "ExportNamedDeclaration" &&
146
- statement.declaration?.type === "VariableDeclaration"
147
- ? statement.declaration.declarations
148
- : [];
149
- for (const declaration of declarations) recordRequireDeclarator(declaration);
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 isDirectTarget(node) {
154
- if (node.type !== "Identifier") return false;
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 Boolean(variable && directBindings.has(variable));
180
+ return (variable && directBindings.get(variable)) || null;
157
181
  }
158
182
 
159
- function isNamespaceTarget(node) {
160
- if (node.type !== "MemberExpression") return false;
183
+ function resolveNamespaceTarget(node) {
184
+ if (node.type !== "MemberExpression") return null;
161
185
  const name = memberPropertyName(node);
162
- if (!name) return false;
186
+ if (!name) return null;
187
+ const object = unwrapExpression(node.object);
163
188
  const source =
164
- requireSource(node.object) ||
165
- (node.object.type === "Identifier"
166
- ? namespaceBindings.get(resolveVariable(node.object, context))
189
+ requireSource(object, context) ||
190
+ (object.type === "Identifier"
191
+ ? namespaceBindings.get(resolveVariable(object, context))
167
192
  : null);
168
- return Boolean(source && targetMatches(targets, source, name));
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 isDirectTarget(node.callee) || isNamespaceTarget(node.callee);
205
+ return Boolean(resolveCallTarget(node));
175
206
  },
176
207
  visitors: {
177
- Program: recordProgramRequires,
178
- ImportDeclaration(node) {
179
- const source = node.source.value;
180
- for (const specifier of node.specifiers) {
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 isNoopExpression(node) {
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") return true;
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 isNoopStatement(statement) {
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 statement.type === "ExpressionStatement" && isNoopExpression(statement.expression);
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
+ );