eslint-plugin-graphile-export 0.0.2-0.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/CHANGELOG.md ADDED
@@ -0,0 +1,9 @@
1
+ # eslint-plugin-graphile-export
2
+
3
+ ## 0.0.2-0.0
4
+
5
+ ### Patch Changes
6
+
7
+ - [#125](https://github.com/benjie/postgraphile-private/pull/125)
8
+ [`91f2256b3`](https://github.com/benjie/postgraphile-private/commit/91f2256b3fd699bec19fc86f1ca79df057e58639)
9
+ Thanks [@benjie](https://github.com/benjie)! - Initial changesets release
package/LICENSE.md ADDED
@@ -0,0 +1,20 @@
1
+ # The MIT License (MIT)
2
+
3
+ Copyright © `2023` Benjie Gillam
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
6
+ this software and associated documentation files (the “Software”), to deal in
7
+ the Software without restriction, including without limitation the rights to
8
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9
+ the Software, and to permit persons to whom the Software is furnished to do so,
10
+ subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,69 @@
1
+ # eslint-plugin-graphile-export
2
+
3
+ [![GitHub Sponsors](https://img.shields.io/github/sponsors/benjie?color=ff69b4&label=github%20sponsors)](https://github.com/sponsors/benjie)
4
+ [![Patreon sponsor button](https://img.shields.io/badge/sponsor-via%20Patreon-orange.svg)](https://patreon.com/benjie)
5
+ [![Discord chat room](https://img.shields.io/discord/489127045289476126.svg)](http://discord.gg/graphile)
6
+ [![Follow](https://img.shields.io/badge/twitter-@GraphileHQ-blue.svg)](https://twitter.com/GraphileHQ)
7
+
8
+ `eslint-plugin-schema-exporter` is an ESLint plugin to help ensure you've listed
9
+ all your scopes for functions.
10
+
11
+ <!-- SPONSORS_BEGIN -->
12
+
13
+ ## Crowd-funded open-source software
14
+
15
+ To help us develop this software sustainably under the MIT license, we ask all
16
+ individuals and businesses that use it to help support its ongoing maintenance
17
+ and development via sponsorship.
18
+
19
+ ### [Click here to find out more about sponsors and sponsorship.](https://www.graphile.org/sponsor/)
20
+
21
+ And please give some love to our featured sponsors 🤩:
22
+
23
+ <table><tr>
24
+ <td align="center"><a href="https://surge.io/"><img src="https://graphile.org/images/sponsors/surge.png" width="90" height="90" alt="Surge" /><br />Surge</a> *</td>
25
+ <td align="center"><a href="https://www.netflix.com/"><img src="https://graphile.org/images/sponsors/Netflix.png" width="90" height="90" alt="Netflix" /><br />Netflix</a> *</td>
26
+ <td align="center"><a href="https://qwick.com/"><img src="https://graphile.org/images/sponsors/qwick.png" width="90" height="90" alt="Qwick" /><br />Qwick</a> *</td>
27
+ <td align="center"><a href="https://www.the-guild.dev/"><img src="https://graphile.org/images/sponsors/theguild.png" width="90" height="90" alt="The Guild" /><br />The Guild</a> *</td>
28
+ </tr><tr>
29
+ <td align="center"><a href="http://chads.website"><img src="https://graphile.org/images/sponsors/chadf.png" width="90" height="90" alt="Chad Furman" /><br />Chad Furman</a> *</td>
30
+ <td align="center"><a href="https://www.fanatics.com/"><img src="https://graphile.org/images/sponsors/fanatics.png" width="90" height="90" alt="Fanatics" /><br />Fanatics</a> *</td>
31
+ <td align="center"><a href="https://dovetailapp.com/"><img src="https://graphile.org/images/sponsors/dovetail.png" width="90" height="90" alt="Dovetail" /><br />Dovetail</a> *</td>
32
+ <td align="center"><a href="https://www.enzuzo.com/"><img src="https://graphile.org/images/sponsors/enzuzo.png" width="90" height="90" alt="Enzuzo" /><br />Enzuzo</a> *</td>
33
+ </tr><tr>
34
+ <td align="center"><a href="https://stellate.co/"><img src="https://graphile.org/images/sponsors/Stellate.png" width="90" height="90" alt="Stellate" /><br />Stellate</a> *</td>
35
+ </tr></table>
36
+
37
+ <em>\* Sponsors the entire Graphile suite</em>
38
+
39
+ <!-- SPONSORS_END -->
40
+
41
+ ## Documentation
42
+
43
+ Add to your eslintrc:
44
+
45
+ ```jsonc
46
+ {
47
+ //...
48
+ "plugins": [
49
+ //...
50
+ "graphile-export"
51
+ //...
52
+ ],
53
+ "extends": [
54
+ // ...
55
+ "plugin:graphile-export/recommended"
56
+ ]
57
+ // ...
58
+ }
59
+ ```
60
+
61
+ NOTE: this plugin will automatically add `EXPORTABLE` callbacks in places where
62
+ it thinks it's suitable, but it currently does not add the `EXPORTABLE` import
63
+ to the file. You'll need to add that yourself:
64
+
65
+ ```ts
66
+ import { EXPORTABLE } from "graphile-export";
67
+ ```
68
+
69
+ TODO: more docs!
@@ -0,0 +1,3 @@
1
+ import type { Rule } from "eslint";
2
+ export declare const ExhaustiveDeps: Rule.RuleModule;
3
+ //# sourceMappingURL=ExhaustiveDeps.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ExhaustiveDeps.d.ts","sourceRoot":"","sources":["../src/ExhaustiveDeps.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAO,IAAI,EAAS,MAAM,QAAQ,CAAC;AAmI/C,eAAO,MAAM,cAAc,EAAE,IAAI,CAAC,UA8XjC,CAAC"}
@@ -0,0 +1,561 @@
1
+ "use strict";
2
+ /*
3
+ * Derived from eslint-plugin-react-hooks which is Copyright (c) Facebook, Inc.
4
+ * and its affiliates and licensed under the MIT license.
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.ExhaustiveDeps = void 0;
8
+ const common_js_1 = require("./common.js");
9
+ function gatherDependenciesRecursively(dependencies, node, currentScope, monitoredScopes, rootNode) {
10
+ for (const reference of currentScope.references) {
11
+ // If this reference is not resolved or it is not declared in a pure
12
+ // scope then we don't care about this reference.
13
+ if (!reference.resolved) {
14
+ continue;
15
+ }
16
+ // If this reference is _defined_ within the function then we don't care about it.
17
+ if (!monitoredScopes.has(reference.resolved.scope) &&
18
+ reference.resolved.defs[0].node !== rootNode) {
19
+ continue;
20
+ }
21
+ // Narrow the scope of a dependency if it is, say, a member expression.
22
+ // Then normalize the narrowed dependency.
23
+ const referenceNode = (0, common_js_1.fastFindReferenceWithParent)(node, reference.identifier);
24
+ if (!referenceNode) {
25
+ console.warn("Could not find referenceNode");
26
+ continue;
27
+ }
28
+ const dependencyNode = getDependency(referenceNode);
29
+ const dependency = analyzePropertyChain(dependencyNode);
30
+ if (dependencyNode.parent.type === "TSTypeQuery" ||
31
+ dependencyNode.parent.type === "TSTypeReference" ||
32
+ dependencyNode.parent.type === "TSClassImplements") {
33
+ continue;
34
+ }
35
+ const def = reference.resolved.defs[0];
36
+ if (def == null) {
37
+ continue;
38
+ }
39
+ // Ignore references to the function itself as it's not defined yet.
40
+ if (def.node != null && def.node.init === node.parent) {
41
+ continue;
42
+ }
43
+ // Ignore Flow type parameters
44
+ if (def.type === "TypeParameter") {
45
+ continue;
46
+ }
47
+ // Add the dependency to a map so we can make sure it is referenced
48
+ // again in our dependencies array.
49
+ const obj = dependencies.get(dependency);
50
+ if (!obj) {
51
+ dependencies.set(dependency, {
52
+ references: [reference],
53
+ });
54
+ }
55
+ else {
56
+ obj.references.push(reference);
57
+ }
58
+ }
59
+ for (const childScope of currentScope.childScopes) {
60
+ gatherDependenciesRecursively(dependencies, node, childScope, monitoredScopes, rootNode);
61
+ }
62
+ }
63
+ function getWarningMessage(deps, singlePrefix, label, fixVerb) {
64
+ if (deps.size === 0) {
65
+ return null;
66
+ }
67
+ return ((deps.size > 1 ? "" : singlePrefix + " ") +
68
+ label +
69
+ " " +
70
+ (deps.size > 1 ? "dependencies" : "dependency") +
71
+ ": " +
72
+ joinEnglish(Array.from(deps)
73
+ .sort()
74
+ .map((name) => "'" + name + "'")) +
75
+ `. You should ${fixVerb} ${deps.size > 1 ? "them" : "it"}.`);
76
+ }
77
+ exports.ExhaustiveDeps = {
78
+ meta: {
79
+ type: "suggestion",
80
+ docs: {
81
+ description: "checks the list of scopes for your function align with the variables used in it",
82
+ recommended: true,
83
+ url: "TODO",
84
+ },
85
+ fixable: "code",
86
+ hasSuggestions: true,
87
+ schema: [
88
+ {
89
+ type: "object",
90
+ additionalProperties: false,
91
+ disableAutofix: false,
92
+ sortExports: false,
93
+ properties: {
94
+ disableAutofix: {
95
+ type: "boolean",
96
+ },
97
+ sortExports: {
98
+ type: "boolean",
99
+ },
100
+ },
101
+ },
102
+ ],
103
+ },
104
+ create(context) {
105
+ const disableAutofix = context.options?.[0]?.disableAutofix ?? false;
106
+ const sortExports = context.options?.[0]?.sortExports ?? false;
107
+ const options = {
108
+ disableAutofix,
109
+ sortExports,
110
+ };
111
+ const scopeManager = context.getSourceCode().scopeManager;
112
+ /**
113
+ * Visitor for both function expressions and arrow function expressions.
114
+ */
115
+ function visitFunctionWithDependencies(node, declaredDependenciesNode, fnCall) {
116
+ // Get the current scope.
117
+ const scope = scopeManager.acquire(node);
118
+ if (!scope) {
119
+ throw new Error("eslint-plugin-graphile-export: could not determine scope");
120
+ }
121
+ const monitoredScopes = new Set();
122
+ {
123
+ let currentScope = scope;
124
+ while ((currentScope = currentScope.upper)) {
125
+ monitoredScopes.add(currentScope);
126
+ }
127
+ }
128
+ const dependencies = new Map();
129
+ gatherDependenciesRecursively(dependencies, node, scope, monitoredScopes, node);
130
+ // Warn about assigning to variables in the outer scope since there's no
131
+ // outer scope when exporting.
132
+ const externalAssigns = new Set();
133
+ function reportStaleAssignment(writeExpr, key) {
134
+ if (externalAssigns.has(key)) {
135
+ return;
136
+ }
137
+ externalAssigns.add(key);
138
+ (0, common_js_1.reportProblem)(context, options, {
139
+ node: writeExpr,
140
+ message: `Assignments to the '${key}' variable from inside ` +
141
+ `${context.getSource(fnCall)} cannot be safely exported.`,
142
+ });
143
+ }
144
+ // Remember which deps are stable and report bad usage first.
145
+ dependencies.forEach(({ references }, key) => {
146
+ references.forEach((reference) => {
147
+ if (reference.writeExpr) {
148
+ reportStaleAssignment(reference.writeExpr, key);
149
+ }
150
+ });
151
+ });
152
+ const argNames = [];
153
+ const invalid = node.params.some((arg) => {
154
+ if (arg.type !== "Identifier") {
155
+ (0, common_js_1.reportProblem)(context, options, {
156
+ node: arg,
157
+ message: `${context.getSource(fnCall)} has an argument which isn't a plain identifier, we don't support this currently.`,
158
+ });
159
+ argNames.push(null);
160
+ return true;
161
+ }
162
+ argNames.push(arg.name);
163
+ return false;
164
+ });
165
+ if (invalid) {
166
+ return;
167
+ }
168
+ const declaredDependencies = [];
169
+ if (declaredDependenciesNode.type !== "ArrayExpression") {
170
+ // If the declared dependencies is not an object expression then we
171
+ // can't verify that the user provided the correct dependencies. Tell
172
+ // the user this in an error.
173
+ (0, common_js_1.reportProblem)(context, options, {
174
+ node: declaredDependenciesNode,
175
+ message: `${context.getSource(fnCall)} was passed a ` +
176
+ "dependency map that is not an array. This means we " +
177
+ "can't statically verify whether you've passed the correct " +
178
+ "dependencies.",
179
+ });
180
+ }
181
+ else {
182
+ declaredDependenciesNode.elements.forEach((declaredDependencyNode, i) => {
183
+ // Skip elided elements.
184
+ if (declaredDependencyNode === null) {
185
+ return;
186
+ }
187
+ // If we see a spread element then add a special warning.
188
+ if (declaredDependencyNode.type === "SpreadElement") {
189
+ (0, common_js_1.reportProblem)(context, options, {
190
+ node: declaredDependencyNode,
191
+ message: `${context.getSource(fnCall)} has a spread element ` +
192
+ "in its dependency map. This means we can't " +
193
+ "statically verify whether you've passed the " +
194
+ "correct dependencies.",
195
+ });
196
+ return;
197
+ }
198
+ const argName = argNames[i] || `_unknownArg${i}`;
199
+ // Add the dependency to our declared dependency map.
200
+ declaredDependencies.push({
201
+ key: argName,
202
+ node: declaredDependencyNode,
203
+ });
204
+ });
205
+ if (node.params.length !== declaredDependenciesNode.elements.length) {
206
+ (0, common_js_1.reportProblem)(context, options, {
207
+ node: declaredDependenciesNode,
208
+ message: `${context.getSource(fnCall)} has different arguments count (${node.params.length}) versus dependencies count (${declaredDependenciesNode.elements.length}); this is invalid.`,
209
+ });
210
+ }
211
+ }
212
+ const { unnecessaryDependencies, missingDependencies, duplicateDependencies, } = collectRecommendations({
213
+ dependencies,
214
+ declaredDependencies,
215
+ });
216
+ const suggestedDeps = [
217
+ ...collectRecommendations({
218
+ dependencies,
219
+ declaredDependencies: [], // Pretend we don't know
220
+ }).suggestedDependencies,
221
+ ].sort();
222
+ const declaredDependencyNames = declaredDependencies.map((d) => d.key);
223
+ const isSorted = suggestedDeps.length === declaredDependencyNames.length &&
224
+ suggestedDeps.every((name, i) => name === declaredDependencyNames[i]);
225
+ if (missingDependencies.size === 0 &&
226
+ unnecessaryDependencies.size === 0 &&
227
+ duplicateDependencies.size === 0 &&
228
+ (isSorted || !options.sortExports)) {
229
+ return;
230
+ }
231
+ (0, common_js_1.reportProblem)(context, options, {
232
+ node: declaredDependenciesNode,
233
+ message: `${context.getSource(fnCall)} has ` +
234
+ // To avoid a long message, show the next actionable item.
235
+ (getWarningMessage(missingDependencies, "a", "missing", "include") ||
236
+ getWarningMessage(unnecessaryDependencies, "an", "unnecessary", "exclude") ||
237
+ getWarningMessage(duplicateDependencies, "a", "duplicate", "omit") ||
238
+ "incorrectly ordered dependencies."),
239
+ suggest: [
240
+ {
241
+ desc: `Update the dependencies array to be: [${suggestedDeps.join(", ")}]`,
242
+ fix(fixer) {
243
+ // TODO: consider preserving the comments or formatting?
244
+ //
245
+ const fixArgs = [];
246
+ const range = node.range != null && node.body.range != null
247
+ ? [node.range[0], node.body.range[0]]
248
+ : node.start != null && node.body.start != null
249
+ ? [node.start, node.body.start]
250
+ : null;
251
+ if (range != null) {
252
+ const preferredArgs = `(${suggestedDeps.join(", ")})`;
253
+ if (node.type === "ArrowFunctionExpression") {
254
+ let prefix = "";
255
+ let suffix = " => ";
256
+ if (node.async) {
257
+ prefix = "async " + prefix;
258
+ }
259
+ if (node.generator) {
260
+ prefix = "generator " + prefix;
261
+ }
262
+ if (node.body.type === "ObjectExpression") {
263
+ suffix = suffix + "(";
264
+ }
265
+ fixArgs.push(fixer.replaceTextRange(range, prefix + preferredArgs + suffix));
266
+ }
267
+ else if (node.type === "FunctionExpression") {
268
+ let prefix = "function ";
269
+ const suffix = " ";
270
+ if (node.generator) {
271
+ prefix = prefix + "*";
272
+ }
273
+ if (node.id) {
274
+ prefix = prefix + `${node.id.name}`;
275
+ }
276
+ if (node.async) {
277
+ prefix = "async " + prefix;
278
+ }
279
+ fixArgs.push(fixer.replaceTextRange(range, prefix + preferredArgs + suffix));
280
+ }
281
+ }
282
+ return [
283
+ ...fixArgs,
284
+ fixer.replaceText(declaredDependenciesNode, `[${suggestedDeps.join(", ")}]`),
285
+ ];
286
+ },
287
+ },
288
+ ],
289
+ });
290
+ }
291
+ return {
292
+ CallExpression(node) {
293
+ const callbackIndex = getScopesCallbackIndex(node.callee);
294
+ if (callbackIndex === -1) {
295
+ // Not a EXPORTABLE call that needs deps.
296
+ return;
297
+ }
298
+ const callback = node.arguments[callbackIndex];
299
+ const fnCall = node.callee;
300
+ const declaredDependenciesNode = node.arguments[callbackIndex + 1];
301
+ // Check whether a callback is supplied.
302
+ if (!callback) {
303
+ (0, common_js_1.reportProblem)(context, options, {
304
+ node: fnCall,
305
+ message: `EXPORTABLE must wrap a function.`,
306
+ });
307
+ return;
308
+ }
309
+ if (!declaredDependenciesNode) {
310
+ // TODO: Can this have a suggestion?
311
+ (0, common_js_1.reportProblem)(context, options, {
312
+ node: fnCall,
313
+ message: `EXPORTABLE does nothing when called with ` +
314
+ `only one argument. Did you forget to pass an array of ` +
315
+ `dependencies?`,
316
+ suggest: [
317
+ {
318
+ desc: "Add dependencies array",
319
+ fix(fixer) {
320
+ // Add `, []` just before the `)` for the call.
321
+ const sourceCode = context.getSourceCode();
322
+ const nextToken = sourceCode.getTokenAfter(callback);
323
+ const hasComma = nextToken &&
324
+ nextToken.value === "," &&
325
+ nextToken.type === "Punctuator";
326
+ return fixer.replaceTextRange([node.range[1] - 1, node.range[1] - 1], `${hasComma ? `` : `, `}[]`);
327
+ },
328
+ },
329
+ ],
330
+ });
331
+ return;
332
+ }
333
+ switch (callback.type) {
334
+ case "FunctionExpression":
335
+ case "ArrowFunctionExpression":
336
+ visitFunctionWithDependencies(callback, declaredDependenciesNode, fnCall);
337
+ return; // Handled
338
+ case "Identifier":
339
+ default:
340
+ (0, common_js_1.reportProblem)(context, options, {
341
+ node: fnCall,
342
+ message: `EXPORTABLE received a function whose dependencies ` +
343
+ `are unknown. Pass an inline function instead.`,
344
+ });
345
+ return; // Handled
346
+ }
347
+ },
348
+ };
349
+ },
350
+ };
351
+ // The meat of the logic.
352
+ function collectRecommendations({ dependencies, declaredDependencies, }) {
353
+ // Our primary data structure.
354
+ // It is a logical representation of property chains:
355
+ // `props` -> `props.foo` -> `props.foo.bar` -> `props.foo.bar.baz`
356
+ // -> `props.lol`
357
+ // -> `props.huh` -> `props.huh.okay`
358
+ // -> `props.wow`
359
+ // We'll use it to mark nodes that are *used* by the programmer,
360
+ // and the nodes that were *declared* as deps. Then we will
361
+ // traverse it to learn which deps are missing or unnecessary.
362
+ const depTree = createDepTree();
363
+ function createDepTree() {
364
+ return {
365
+ isUsed: false,
366
+ isSatisfiedRecursively: false,
367
+ isSubtreeUsed: false,
368
+ children: new Map(), // Nodes for properties
369
+ };
370
+ }
371
+ // Mark all required nodes first.
372
+ // Imagine exclamation marks next to each used deep property.
373
+ dependencies.forEach((_, key) => {
374
+ const node = getOrCreateNodeByPath(depTree, key);
375
+ node.isUsed = true;
376
+ markAllParentsByPath(depTree, key, (parent) => {
377
+ parent.isSubtreeUsed = true;
378
+ });
379
+ });
380
+ // Mark all satisfied nodes.
381
+ // Imagine checkmarks next to each declared dependency.
382
+ declaredDependencies.forEach(({ key }) => {
383
+ const node = getOrCreateNodeByPath(depTree, key);
384
+ node.isSatisfiedRecursively = true;
385
+ });
386
+ // Tree manipulation helpers.
387
+ function getOrCreateNodeByPath(rootNode, path) {
388
+ const keys = path.split(".");
389
+ let node = rootNode;
390
+ for (const key of keys) {
391
+ let child = node.children.get(key);
392
+ if (!child) {
393
+ child = createDepTree();
394
+ node.children.set(key, child);
395
+ }
396
+ node = child;
397
+ }
398
+ return node;
399
+ }
400
+ function markAllParentsByPath(rootNode, path, fn) {
401
+ const keys = path.split(".");
402
+ let node = rootNode;
403
+ for (const key of keys) {
404
+ const child = node.children.get(key);
405
+ if (!child) {
406
+ return;
407
+ }
408
+ fn(child);
409
+ node = child;
410
+ }
411
+ }
412
+ // Now we can learn which dependencies are missing or necessary.
413
+ const missingDependencies = new Set();
414
+ const satisfyingDependencies = new Set();
415
+ scanTreeRecursively(depTree, missingDependencies, satisfyingDependencies, (key) => key);
416
+ function scanTreeRecursively(node, missingPaths, satisfyingPaths, keyToPath) {
417
+ node.children.forEach((child, key) => {
418
+ const path = keyToPath(key);
419
+ if (child.isSatisfiedRecursively) {
420
+ if (child.isSubtreeUsed) {
421
+ // Remember this dep actually satisfied something.
422
+ satisfyingPaths.add(path);
423
+ }
424
+ // It doesn't matter if there's something deeper.
425
+ // It would be transitively satisfied since we assume immutability.
426
+ // `props.foo` is enough if you read `props.foo.id`.
427
+ return;
428
+ }
429
+ if (child.isUsed) {
430
+ // Remember that no declared deps satisfied this node.
431
+ missingPaths.add(path);
432
+ // If we got here, nothing in its subtree was satisfied.
433
+ // No need to search further.
434
+ return;
435
+ }
436
+ scanTreeRecursively(child, missingPaths, satisfyingPaths, (childKey) => path + "." + childKey);
437
+ });
438
+ }
439
+ // Collect suggestions in the order they were originally specified.
440
+ const suggestedDependencies = [];
441
+ const unnecessaryDependencies = new Set();
442
+ const duplicateDependencies = new Set();
443
+ declaredDependencies.forEach(({ key }) => {
444
+ // Does this declared dep satisfy a real need?
445
+ if (satisfyingDependencies.has(key)) {
446
+ if (suggestedDependencies.indexOf(key) === -1) {
447
+ // Good one.
448
+ suggestedDependencies.push(key);
449
+ }
450
+ else {
451
+ // Duplicate.
452
+ duplicateDependencies.add(key);
453
+ }
454
+ }
455
+ else {
456
+ // It's definitely not needed.
457
+ unnecessaryDependencies.add(key);
458
+ }
459
+ });
460
+ // Then add the missing ones at the end.
461
+ missingDependencies.forEach((key) => {
462
+ suggestedDependencies.push(key);
463
+ });
464
+ return {
465
+ suggestedDependencies,
466
+ unnecessaryDependencies,
467
+ duplicateDependencies,
468
+ missingDependencies,
469
+ };
470
+ }
471
+ /**
472
+ * Assuming () means the passed/returned node:
473
+ * `(props) => (props)`
474
+ * `props.(foo) => (props.foo)`
475
+ * `props.foo.(bar) => (props).foo.bar`
476
+ * `props.foo.bar.(baz) => (props).foo.bar.baz`
477
+ */
478
+ function getDependency(node) {
479
+ if ((node.parent.type === "MemberExpression" ||
480
+ node.parent.type === "OptionalMemberExpression") &&
481
+ "object" in node.parent &&
482
+ "property" in node.parent &&
483
+ node.parent.object === node &&
484
+ "name" in node.parent.property &&
485
+ node.parent.property.name !== "current" &&
486
+ !node.parent.computed &&
487
+ !(node.parent.parent != null &&
488
+ (node.parent.parent.type === "CallExpression" ||
489
+ node.parent.parent.type === "OptionalCallExpression") &&
490
+ "callee" in node.parent.parent &&
491
+ node.parent.parent.callee === node.parent)) {
492
+ return getDependency(node.parent);
493
+ }
494
+ else if (
495
+ // Note: we don't check OptionalMemberExpression because it can't be LHS.
496
+ node.type === "MemberExpression" &&
497
+ node.parent &&
498
+ node.parent.type === "AssignmentExpression" &&
499
+ node.parent.left === node) {
500
+ return node.object;
501
+ }
502
+ else {
503
+ return node;
504
+ }
505
+ }
506
+ function analyzePropertyChain(node) {
507
+ if (node.type === "Identifier" || node.type === "JSXIdentifier") {
508
+ const result = node.name;
509
+ return result;
510
+ }
511
+ else if (node.type === "MemberExpression" && !node.computed) {
512
+ const object = analyzePropertyChain(node.object);
513
+ const result = `${object}`;
514
+ return result;
515
+ }
516
+ else if (node.type === "OptionalMemberExpression" && !node.computed) {
517
+ const object = analyzePropertyChain(node.object);
518
+ const result = `${object}`;
519
+ return result;
520
+ }
521
+ else if (node.type === "ChainExpression" && !node.computed) {
522
+ const expression = node.expression;
523
+ if (expression.type === "CallExpression") {
524
+ throw new Error(`Unsupported node type: ${expression.type}`);
525
+ }
526
+ const object = analyzePropertyChain(expression.object);
527
+ const result = `${object}`;
528
+ return result;
529
+ }
530
+ else {
531
+ throw new Error(`Unsupported node type: ${node.type}`);
532
+ }
533
+ }
534
+ /**
535
+ * Returns 0 if this is a EXPORTABLE call, -1 otherwise.
536
+ */
537
+ function getScopesCallbackIndex(node) {
538
+ if (node.type === "Identifier" && node.name === "EXPORTABLE") {
539
+ return 0;
540
+ }
541
+ else {
542
+ return -1;
543
+ }
544
+ }
545
+ function joinEnglish(arr) {
546
+ let s = "";
547
+ for (let i = 0; i < arr.length; i++) {
548
+ s += arr[i];
549
+ if (i === 0 && arr.length === 2) {
550
+ s += " and ";
551
+ }
552
+ else if (i === arr.length - 2 && arr.length > 2) {
553
+ s += ", and ";
554
+ }
555
+ else if (i < arr.length - 1) {
556
+ s += ", ";
557
+ }
558
+ }
559
+ return s;
560
+ }
561
+ //# sourceMappingURL=ExhaustiveDeps.js.map