source: imaps-frontend/node_modules/eslint-plugin-react-hooks/cjs/eslint-plugin-react-hooks.development.js

main
Last change on this file was d565449, checked in by stefan toskovski <stefantoska84@…>, 4 weeks ago

Update repo after prototype presentation

  • Property mode set to 100644
File size: 85.6 KB
Line 
1/**
2 * @license React
3 * eslint-plugin-react-hooks.development.js
4 *
5 * Copyright (c) Facebook, Inc. and its affiliates.
6 *
7 * This source code is licensed under the MIT license found in the
8 * LICENSE file in the root directory of this source tree.
9 */
10
11'use strict';
12
13if (process.env.NODE_ENV !== "production") {
14 (function() {
15'use strict';
16
17function _unsupportedIterableToArray(o, minLen) {
18 if (!o) return;
19 if (typeof o === "string") return _arrayLikeToArray(o, minLen);
20 var n = Object.prototype.toString.call(o).slice(8, -1);
21 if (n === "Object" && o.constructor) n = o.constructor.name;
22 if (n === "Map" || n === "Set") return Array.from(o);
23 if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen);
24}
25
26function _arrayLikeToArray(arr, len) {
27 if (len == null || len > arr.length) len = arr.length;
28
29 for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i];
30
31 return arr2;
32}
33
34function _createForOfIteratorHelper(o, allowArrayLike) {
35 var it;
36
37 if (typeof Symbol === "undefined" || o[Symbol.iterator] == null) {
38 if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") {
39 if (it) o = it;
40 var i = 0;
41
42 var F = function () {};
43
44 return {
45 s: F,
46 n: function () {
47 if (i >= o.length) return {
48 done: true
49 };
50 return {
51 done: false,
52 value: o[i++]
53 };
54 },
55 e: function (e) {
56 throw e;
57 },
58 f: F
59 };
60 }
61
62 throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");
63 }
64
65 var normalCompletion = true,
66 didErr = false,
67 err;
68 return {
69 s: function () {
70 it = o[Symbol.iterator]();
71 },
72 n: function () {
73 var step = it.next();
74 normalCompletion = step.done;
75 return step;
76 },
77 e: function (e) {
78 didErr = true;
79 err = e;
80 },
81 f: function () {
82 try {
83 if (!normalCompletion && it.return != null) it.return();
84 } finally {
85 if (didErr) throw err;
86 }
87 }
88 };
89}
90
91/* global BigInt */
92
93function isHookName(s) {
94 return /^use[A-Z0-9].*$/.test(s);
95}
96/**
97 * We consider hooks to be a hook name identifier or a member expression
98 * containing a hook name.
99 */
100
101
102function isHook(node) {
103 if (node.type === 'Identifier') {
104 return isHookName(node.name);
105 } else if (node.type === 'MemberExpression' && !node.computed && isHook(node.property)) {
106 var obj = node.object;
107 var isPascalCaseNameSpace = /^[A-Z].*/;
108 return obj.type === 'Identifier' && isPascalCaseNameSpace.test(obj.name);
109 } else {
110 return false;
111 }
112}
113/**
114 * Checks if the node is a React component name. React component names must
115 * always start with a non-lowercase letter. So `MyComponent` or `_MyComponent`
116 * are valid component names for instance.
117 */
118
119
120function isComponentName(node) {
121 if (node.type === 'Identifier') {
122 return !/^[a-z]/.test(node.name);
123 } else {
124 return false;
125 }
126}
127
128function isReactFunction(node, functionName) {
129 return node.name === functionName || node.type === 'MemberExpression' && node.object.name === 'React' && node.property.name === functionName;
130}
131/**
132 * Checks if the node is a callback argument of forwardRef. This render function
133 * should follow the rules of hooks.
134 */
135
136
137function isForwardRefCallback(node) {
138 return !!(node.parent && node.parent.callee && isReactFunction(node.parent.callee, 'forwardRef'));
139}
140/**
141 * Checks if the node is a callback argument of React.memo. This anonymous
142 * functional component should follow the rules of hooks.
143 */
144
145
146function isMemoCallback(node) {
147 return !!(node.parent && node.parent.callee && isReactFunction(node.parent.callee, 'memo'));
148}
149
150function isInsideComponentOrHook(node) {
151 while (node) {
152 var functionName = getFunctionName(node);
153
154 if (functionName) {
155 if (isComponentName(functionName) || isHook(functionName)) {
156 return true;
157 }
158 }
159
160 if (isForwardRefCallback(node) || isMemoCallback(node)) {
161 return true;
162 }
163
164 node = node.parent;
165 }
166
167 return false;
168}
169
170var RulesOfHooks = {
171 meta: {
172 type: 'problem',
173 docs: {
174 description: 'enforces the Rules of Hooks',
175 recommended: true,
176 url: 'https://reactjs.org/docs/hooks-rules.html'
177 }
178 },
179 create: function (context) {
180 var codePathReactHooksMapStack = [];
181 var codePathSegmentStack = [];
182 return {
183 // Maintain code segment path stack as we traverse.
184 onCodePathSegmentStart: function (segment) {
185 return codePathSegmentStack.push(segment);
186 },
187 onCodePathSegmentEnd: function () {
188 return codePathSegmentStack.pop();
189 },
190 // Maintain code path stack as we traverse.
191 onCodePathStart: function () {
192 return codePathReactHooksMapStack.push(new Map());
193 },
194 // Process our code path.
195 //
196 // Everything is ok if all React Hooks are both reachable from the initial
197 // segment and reachable from every final segment.
198 onCodePathEnd: function (codePath, codePathNode) {
199 var reactHooksMap = codePathReactHooksMapStack.pop();
200
201 if (reactHooksMap.size === 0) {
202 return;
203 } // All of the segments which are cyclic are recorded in this set.
204
205
206 var cyclic = new Set();
207 /**
208 * Count the number of code paths from the start of the function to this
209 * segment. For example:
210 *
211 * ```js
212 * function MyComponent() {
213 * if (condition) {
214 * // Segment 1
215 * } else {
216 * // Segment 2
217 * }
218 * // Segment 3
219 * }
220 * ```
221 *
222 * Segments 1 and 2 have one path to the beginning of `MyComponent` and
223 * segment 3 has two paths to the beginning of `MyComponent` since we
224 * could have either taken the path of segment 1 or segment 2.
225 *
226 * Populates `cyclic` with cyclic segments.
227 */
228
229 function countPathsFromStart(segment, pathHistory) {
230 var cache = countPathsFromStart.cache;
231 var paths = cache.get(segment.id);
232 var pathList = new Set(pathHistory); // If `pathList` includes the current segment then we've found a cycle!
233 // We need to fill `cyclic` with all segments inside cycle
234
235 if (pathList.has(segment.id)) {
236 var pathArray = [].concat(pathList);
237 var cyclicSegments = pathArray.slice(pathArray.indexOf(segment.id) + 1);
238
239 var _iterator = _createForOfIteratorHelper(cyclicSegments),
240 _step;
241
242 try {
243 for (_iterator.s(); !(_step = _iterator.n()).done;) {
244 var cyclicSegment = _step.value;
245 cyclic.add(cyclicSegment);
246 }
247 } catch (err) {
248 _iterator.e(err);
249 } finally {
250 _iterator.f();
251 }
252
253 return BigInt('0');
254 } // add the current segment to pathList
255
256
257 pathList.add(segment.id); // We have a cached `paths`. Return it.
258
259 if (paths !== undefined) {
260 return paths;
261 }
262
263 if (codePath.thrownSegments.includes(segment)) {
264 paths = BigInt('0');
265 } else if (segment.prevSegments.length === 0) {
266 paths = BigInt('1');
267 } else {
268 paths = BigInt('0');
269
270 var _iterator2 = _createForOfIteratorHelper(segment.prevSegments),
271 _step2;
272
273 try {
274 for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
275 var prevSegment = _step2.value;
276 paths += countPathsFromStart(prevSegment, pathList);
277 }
278 } catch (err) {
279 _iterator2.e(err);
280 } finally {
281 _iterator2.f();
282 }
283 } // If our segment is reachable then there should be at least one path
284 // to it from the start of our code path.
285
286
287 if (segment.reachable && paths === BigInt('0')) {
288 cache.delete(segment.id);
289 } else {
290 cache.set(segment.id, paths);
291 }
292
293 return paths;
294 }
295 /**
296 * Count the number of code paths from this segment to the end of the
297 * function. For example:
298 *
299 * ```js
300 * function MyComponent() {
301 * // Segment 1
302 * if (condition) {
303 * // Segment 2
304 * } else {
305 * // Segment 3
306 * }
307 * }
308 * ```
309 *
310 * Segments 2 and 3 have one path to the end of `MyComponent` and
311 * segment 1 has two paths to the end of `MyComponent` since we could
312 * either take the path of segment 1 or segment 2.
313 *
314 * Populates `cyclic` with cyclic segments.
315 */
316
317
318 function countPathsToEnd(segment, pathHistory) {
319 var cache = countPathsToEnd.cache;
320 var paths = cache.get(segment.id);
321 var pathList = new Set(pathHistory); // If `pathList` includes the current segment then we've found a cycle!
322 // We need to fill `cyclic` with all segments inside cycle
323
324 if (pathList.has(segment.id)) {
325 var pathArray = Array.from(pathList);
326 var cyclicSegments = pathArray.slice(pathArray.indexOf(segment.id) + 1);
327
328 var _iterator3 = _createForOfIteratorHelper(cyclicSegments),
329 _step3;
330
331 try {
332 for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) {
333 var cyclicSegment = _step3.value;
334 cyclic.add(cyclicSegment);
335 }
336 } catch (err) {
337 _iterator3.e(err);
338 } finally {
339 _iterator3.f();
340 }
341
342 return BigInt('0');
343 } // add the current segment to pathList
344
345
346 pathList.add(segment.id); // We have a cached `paths`. Return it.
347
348 if (paths !== undefined) {
349 return paths;
350 }
351
352 if (codePath.thrownSegments.includes(segment)) {
353 paths = BigInt('0');
354 } else if (segment.nextSegments.length === 0) {
355 paths = BigInt('1');
356 } else {
357 paths = BigInt('0');
358
359 var _iterator4 = _createForOfIteratorHelper(segment.nextSegments),
360 _step4;
361
362 try {
363 for (_iterator4.s(); !(_step4 = _iterator4.n()).done;) {
364 var nextSegment = _step4.value;
365 paths += countPathsToEnd(nextSegment, pathList);
366 }
367 } catch (err) {
368 _iterator4.e(err);
369 } finally {
370 _iterator4.f();
371 }
372 }
373
374 cache.set(segment.id, paths);
375 return paths;
376 }
377 /**
378 * Gets the shortest path length to the start of a code path.
379 * For example:
380 *
381 * ```js
382 * function MyComponent() {
383 * if (condition) {
384 * // Segment 1
385 * }
386 * // Segment 2
387 * }
388 * ```
389 *
390 * There is only one path from segment 1 to the code path start. Its
391 * length is one so that is the shortest path.
392 *
393 * There are two paths from segment 2 to the code path start. One
394 * through segment 1 with a length of two and another directly to the
395 * start with a length of one. The shortest path has a length of one
396 * so we would return that.
397 */
398
399
400 function shortestPathLengthToStart(segment) {
401 var cache = shortestPathLengthToStart.cache;
402 var length = cache.get(segment.id); // If `length` is null then we found a cycle! Return infinity since
403 // the shortest path is definitely not the one where we looped.
404
405 if (length === null) {
406 return Infinity;
407 } // We have a cached `length`. Return it.
408
409
410 if (length !== undefined) {
411 return length;
412 } // Compute `length` and cache it. Guarding against cycles.
413
414
415 cache.set(segment.id, null);
416
417 if (segment.prevSegments.length === 0) {
418 length = 1;
419 } else {
420 length = Infinity;
421
422 var _iterator5 = _createForOfIteratorHelper(segment.prevSegments),
423 _step5;
424
425 try {
426 for (_iterator5.s(); !(_step5 = _iterator5.n()).done;) {
427 var prevSegment = _step5.value;
428 var prevLength = shortestPathLengthToStart(prevSegment);
429
430 if (prevLength < length) {
431 length = prevLength;
432 }
433 }
434 } catch (err) {
435 _iterator5.e(err);
436 } finally {
437 _iterator5.f();
438 }
439
440 length += 1;
441 }
442
443 cache.set(segment.id, length);
444 return length;
445 }
446
447 countPathsFromStart.cache = new Map();
448 countPathsToEnd.cache = new Map();
449 shortestPathLengthToStart.cache = new Map(); // Count all code paths to the end of our component/hook. Also primes
450 // the `countPathsToEnd` cache.
451
452 var allPathsFromStartToEnd = countPathsToEnd(codePath.initialSegment); // Gets the function name for our code path. If the function name is
453 // `undefined` then we know either that we have an anonymous function
454 // expression or our code path is not in a function. In both cases we
455 // will want to error since neither are React function components or
456 // hook functions - unless it is an anonymous function argument to
457 // forwardRef or memo.
458
459 var codePathFunctionName = getFunctionName(codePathNode); // This is a valid code path for React hooks if we are directly in a React
460 // function component or we are in a hook function.
461
462 var isSomewhereInsideComponentOrHook = isInsideComponentOrHook(codePathNode);
463 var isDirectlyInsideComponentOrHook = codePathFunctionName ? isComponentName(codePathFunctionName) || isHook(codePathFunctionName) : isForwardRefCallback(codePathNode) || isMemoCallback(codePathNode); // Compute the earliest finalizer level using information from the
464 // cache. We expect all reachable final segments to have a cache entry
465 // after calling `visitSegment()`.
466
467 var shortestFinalPathLength = Infinity;
468
469 var _iterator6 = _createForOfIteratorHelper(codePath.finalSegments),
470 _step6;
471
472 try {
473 for (_iterator6.s(); !(_step6 = _iterator6.n()).done;) {
474 var finalSegment = _step6.value;
475
476 if (!finalSegment.reachable) {
477 continue;
478 }
479
480 var length = shortestPathLengthToStart(finalSegment);
481
482 if (length < shortestFinalPathLength) {
483 shortestFinalPathLength = length;
484 }
485 } // Make sure all React Hooks pass our lint invariants. Log warnings
486 // if not.
487
488 } catch (err) {
489 _iterator6.e(err);
490 } finally {
491 _iterator6.f();
492 }
493
494 var _iterator7 = _createForOfIteratorHelper(reactHooksMap),
495 _step7;
496
497 try {
498 for (_iterator7.s(); !(_step7 = _iterator7.n()).done;) {
499 var _step7$value = _step7.value,
500 segment = _step7$value[0],
501 reactHooks = _step7$value[1];
502
503 // NOTE: We could report here that the hook is not reachable, but
504 // that would be redundant with more general "no unreachable"
505 // lint rules.
506 if (!segment.reachable) {
507 continue;
508 } // If there are any final segments with a shorter path to start then
509 // we possibly have an early return.
510 //
511 // If our segment is a final segment itself then siblings could
512 // possibly be early returns.
513
514
515 var possiblyHasEarlyReturn = segment.nextSegments.length === 0 ? shortestFinalPathLength <= shortestPathLengthToStart(segment) : shortestFinalPathLength < shortestPathLengthToStart(segment); // Count all the paths from the start of our code path to the end of
516 // our code path that go _through_ this segment. The critical piece
517 // of this is _through_. If we just call `countPathsToEnd(segment)`
518 // then we neglect that we may have gone through multiple paths to get
519 // to this point! Consider:
520 //
521 // ```js
522 // function MyComponent() {
523 // if (a) {
524 // // Segment 1
525 // } else {
526 // // Segment 2
527 // }
528 // // Segment 3
529 // if (b) {
530 // // Segment 4
531 // } else {
532 // // Segment 5
533 // }
534 // }
535 // ```
536 //
537 // In this component we have four code paths:
538 //
539 // 1. `a = true; b = true`
540 // 2. `a = true; b = false`
541 // 3. `a = false; b = true`
542 // 4. `a = false; b = false`
543 //
544 // From segment 3 there are two code paths to the end through segment
545 // 4 and segment 5. However, we took two paths to get here through
546 // segment 1 and segment 2.
547 //
548 // If we multiply the paths from start (two) by the paths to end (two)
549 // for segment 3 we get four. Which is our desired count.
550
551 var pathsFromStartToEnd = countPathsFromStart(segment) * countPathsToEnd(segment); // Is this hook a part of a cyclic segment?
552
553 var cycled = cyclic.has(segment.id);
554
555 var _iterator8 = _createForOfIteratorHelper(reactHooks),
556 _step8;
557
558 try {
559 for (_iterator8.s(); !(_step8 = _iterator8.n()).done;) {
560 var hook = _step8.value;
561
562 // Report an error if a hook may be called more then once.
563 if (cycled) {
564 context.report({
565 node: hook,
566 message: "React Hook \"" + context.getSource(hook) + "\" may be executed " + 'more than once. Possibly because it is called in a loop. ' + 'React Hooks must be called in the exact same order in ' + 'every component render.'
567 });
568 } // If this is not a valid code path for React hooks then we need to
569 // log a warning for every hook in this code path.
570 //
571 // Pick a special message depending on the scope this hook was
572 // called in.
573
574
575 if (isDirectlyInsideComponentOrHook) {
576 // Report an error if a hook does not reach all finalizing code
577 // path segments.
578 //
579 // Special case when we think there might be an early return.
580 if (!cycled && pathsFromStartToEnd !== allPathsFromStartToEnd) {
581 var message = "React Hook \"" + context.getSource(hook) + "\" is called " + 'conditionally. React Hooks must be called in the exact ' + 'same order in every component render.' + (possiblyHasEarlyReturn ? ' Did you accidentally call a React Hook after an' + ' early return?' : '');
582 context.report({
583 node: hook,
584 message: message
585 });
586 }
587 } else if (codePathNode.parent && (codePathNode.parent.type === 'MethodDefinition' || codePathNode.parent.type === 'ClassProperty') && codePathNode.parent.value === codePathNode) {
588 // Custom message for hooks inside a class
589 var _message = "React Hook \"" + context.getSource(hook) + "\" cannot be called " + 'in a class component. React Hooks must be called in a ' + 'React function component or a custom React Hook function.';
590
591 context.report({
592 node: hook,
593 message: _message
594 });
595 } else if (codePathFunctionName) {
596 // Custom message if we found an invalid function name.
597 var _message2 = "React Hook \"" + context.getSource(hook) + "\" is called in " + ("function \"" + context.getSource(codePathFunctionName) + "\" ") + 'that is neither a React function component nor a custom ' + 'React Hook function.' + ' React component names must start with an uppercase letter.' + ' React Hook names must start with the word "use".';
598
599 context.report({
600 node: hook,
601 message: _message2
602 });
603 } else if (codePathNode.type === 'Program') {
604 // These are dangerous if you have inline requires enabled.
605 var _message3 = "React Hook \"" + context.getSource(hook) + "\" cannot be called " + 'at the top level. React Hooks must be called in a ' + 'React function component or a custom React Hook function.';
606
607 context.report({
608 node: hook,
609 message: _message3
610 });
611 } else {
612 // Assume in all other cases the user called a hook in some
613 // random function callback. This should usually be true for
614 // anonymous function expressions. Hopefully this is clarifying
615 // enough in the common case that the incorrect message in
616 // uncommon cases doesn't matter.
617 if (isSomewhereInsideComponentOrHook) {
618 var _message4 = "React Hook \"" + context.getSource(hook) + "\" cannot be called " + 'inside a callback. React Hooks must be called in a ' + 'React function component or a custom React Hook function.';
619
620 context.report({
621 node: hook,
622 message: _message4
623 });
624 }
625 }
626 }
627 } catch (err) {
628 _iterator8.e(err);
629 } finally {
630 _iterator8.f();
631 }
632 }
633 } catch (err) {
634 _iterator7.e(err);
635 } finally {
636 _iterator7.f();
637 }
638 },
639 // Missed opportunity...We could visit all `Identifier`s instead of all
640 // `CallExpression`s and check that _every use_ of a hook name is valid.
641 // But that gets complicated and enters type-system territory, so we're
642 // only being strict about hook calls for now.
643 CallExpression: function (node) {
644 if (isHook(node.callee)) {
645 // Add the hook node to a map keyed by the code path segment. We will
646 // do full code path analysis at the end of our code path.
647 var reactHooksMap = last(codePathReactHooksMapStack);
648 var codePathSegment = last(codePathSegmentStack);
649 var reactHooks = reactHooksMap.get(codePathSegment);
650
651 if (!reactHooks) {
652 reactHooks = [];
653 reactHooksMap.set(codePathSegment, reactHooks);
654 }
655
656 reactHooks.push(node.callee);
657 }
658 }
659 };
660 }
661};
662/**
663 * Gets the static name of a function AST node. For function declarations it is
664 * easy. For anonymous function expressions it is much harder. If you search for
665 * `IsAnonymousFunctionDefinition()` in the ECMAScript spec you'll find places
666 * where JS gives anonymous function expressions names. We roughly detect the
667 * same AST nodes with some exceptions to better fit our use case.
668 */
669
670function getFunctionName(node) {
671 if (node.type === 'FunctionDeclaration' || node.type === 'FunctionExpression' && node.id) {
672 // function useHook() {}
673 // const whatever = function useHook() {};
674 //
675 // Function declaration or function expression names win over any
676 // assignment statements or other renames.
677 return node.id;
678 } else if (node.type === 'FunctionExpression' || node.type === 'ArrowFunctionExpression') {
679 if (node.parent.type === 'VariableDeclarator' && node.parent.init === node) {
680 // const useHook = () => {};
681 return node.parent.id;
682 } else if (node.parent.type === 'AssignmentExpression' && node.parent.right === node && node.parent.operator === '=') {
683 // useHook = () => {};
684 return node.parent.left;
685 } else if (node.parent.type === 'Property' && node.parent.value === node && !node.parent.computed) {
686 // {useHook: () => {}}
687 // {useHook() {}}
688 return node.parent.key; // NOTE: We could also support `ClassProperty` and `MethodDefinition`
689 // here to be pedantic. However, hooks in a class are an anti-pattern. So
690 // we don't allow it to error early.
691 //
692 // class {useHook = () => {}}
693 // class {useHook() {}}
694 } else if (node.parent.type === 'AssignmentPattern' && node.parent.right === node && !node.parent.computed) {
695 // const {useHook = () => {}} = {};
696 // ({useHook = () => {}} = {});
697 //
698 // Kinda clowny, but we'd said we'd follow spec convention for
699 // `IsAnonymousFunctionDefinition()` usage.
700 return node.parent.left;
701 } else {
702 return undefined;
703 }
704 } else {
705 return undefined;
706 }
707}
708/**
709 * Convenience function for peeking the last item in a stack.
710 */
711
712
713function last(array) {
714 return array[array.length - 1];
715}
716
717/* eslint-disable no-for-of-loops/no-for-of-loops */
718var ExhaustiveDeps = {
719 meta: {
720 type: 'suggestion',
721 docs: {
722 description: 'verifies the list of dependencies for Hooks like useEffect and similar',
723 recommended: true,
724 url: 'https://github.com/facebook/react/issues/14920'
725 },
726 fixable: 'code',
727 hasSuggestions: true,
728 schema: [{
729 type: 'object',
730 additionalProperties: false,
731 enableDangerousAutofixThisMayCauseInfiniteLoops: false,
732 properties: {
733 additionalHooks: {
734 type: 'string'
735 },
736 enableDangerousAutofixThisMayCauseInfiniteLoops: {
737 type: 'boolean'
738 }
739 }
740 }]
741 },
742 create: function (context) {
743 // Parse the `additionalHooks` regex.
744 var additionalHooks = context.options && context.options[0] && context.options[0].additionalHooks ? new RegExp(context.options[0].additionalHooks) : undefined;
745 var enableDangerousAutofixThisMayCauseInfiniteLoops = context.options && context.options[0] && context.options[0].enableDangerousAutofixThisMayCauseInfiniteLoops || false;
746 var options = {
747 additionalHooks: additionalHooks,
748 enableDangerousAutofixThisMayCauseInfiniteLoops: enableDangerousAutofixThisMayCauseInfiniteLoops
749 };
750
751 function reportProblem(problem) {
752 if (enableDangerousAutofixThisMayCauseInfiniteLoops) {
753 // Used to enable legacy behavior. Dangerous.
754 // Keep this as an option until major IDEs upgrade (including VSCode FB ESLint extension).
755 if (Array.isArray(problem.suggest) && problem.suggest.length > 0) {
756 problem.fix = problem.suggest[0].fix;
757 }
758 }
759
760 context.report(problem);
761 }
762
763 var scopeManager = context.getSourceCode().scopeManager; // Should be shared between visitors.
764
765 var setStateCallSites = new WeakMap();
766 var stateVariables = new WeakSet();
767 var stableKnownValueCache = new WeakMap();
768 var functionWithoutCapturedValueCache = new WeakMap();
769
770 function memoizeWithWeakMap(fn, map) {
771 return function (arg) {
772 if (map.has(arg)) {
773 // to verify cache hits:
774 // console.log(arg.name)
775 return map.get(arg);
776 }
777
778 var result = fn(arg);
779 map.set(arg, result);
780 return result;
781 };
782 }
783 /**
784 * Visitor for both function expressions and arrow function expressions.
785 */
786
787
788 function visitFunctionWithDependencies(node, declaredDependenciesNode, reactiveHook, reactiveHookName, isEffect) {
789 if (isEffect && node.async) {
790 reportProblem({
791 node: node,
792 message: "Effect callbacks are synchronous to prevent race conditions. " + "Put the async function inside:\n\n" + 'useEffect(() => {\n' + ' async function fetchData() {\n' + ' // You can await here\n' + ' const response = await MyAPI.getData(someId);\n' + ' // ...\n' + ' }\n' + ' fetchData();\n' + "}, [someId]); // Or [] if effect doesn't need props or state\n\n" + 'Learn more about data fetching with Hooks: https://reactjs.org/link/hooks-data-fetching'
793 });
794 } // Get the current scope.
795
796
797 var scope = scopeManager.acquire(node); // Find all our "pure scopes". On every re-render of a component these
798 // pure scopes may have changes to the variables declared within. So all
799 // variables used in our reactive hook callback but declared in a pure
800 // scope need to be listed as dependencies of our reactive hook callback.
801 //
802 // According to the rules of React you can't read a mutable value in pure
803 // scope. We can't enforce this in a lint so we trust that all variables
804 // declared outside of pure scope are indeed frozen.
805
806 var pureScopes = new Set();
807 var componentScope = null;
808 {
809 var currentScope = scope.upper;
810
811 while (currentScope) {
812 pureScopes.add(currentScope);
813
814 if (currentScope.type === 'function') {
815 break;
816 }
817
818 currentScope = currentScope.upper;
819 } // If there is no parent function scope then there are no pure scopes.
820 // The ones we've collected so far are incorrect. So don't continue with
821 // the lint.
822
823
824 if (!currentScope) {
825 return;
826 }
827
828 componentScope = currentScope;
829 }
830 var isArray = Array.isArray; // Next we'll define a few helpers that helps us
831 // tell if some values don't have to be declared as deps.
832 // Some are known to be stable based on Hook calls.
833 // const [state, setState] = useState() / React.useState()
834 // ^^^ true for this reference
835 // const [state, dispatch] = useReducer() / React.useReducer()
836 // ^^^ true for this reference
837 // const ref = useRef()
838 // ^^^ true for this reference
839 // False for everything else.
840
841 function isStableKnownHookValue(resolved) {
842 if (!isArray(resolved.defs)) {
843 return false;
844 }
845
846 var def = resolved.defs[0];
847
848 if (def == null) {
849 return false;
850 } // Look for `let stuff = ...`
851
852
853 if (def.node.type !== 'VariableDeclarator') {
854 return false;
855 }
856
857 var init = def.node.init;
858
859 if (init == null) {
860 return false;
861 }
862
863 while (init.type === 'TSAsExpression') {
864 init = init.expression;
865 } // Detect primitive constants
866 // const foo = 42
867
868
869 var declaration = def.node.parent;
870
871 if (declaration == null) {
872 // This might happen if variable is declared after the callback.
873 // In that case ESLint won't set up .parent refs.
874 // So we'll set them up manually.
875 fastFindReferenceWithParent(componentScope.block, def.node.id);
876 declaration = def.node.parent;
877
878 if (declaration == null) {
879 return false;
880 }
881 }
882
883 if (declaration.kind === 'const' && init.type === 'Literal' && (typeof init.value === 'string' || typeof init.value === 'number' || init.value === null)) {
884 // Definitely stable
885 return true;
886 } // Detect known Hook calls
887 // const [_, setState] = useState()
888
889
890 if (init.type !== 'CallExpression') {
891 return false;
892 }
893
894 var callee = init.callee; // Step into `= React.something` initializer.
895
896 if (callee.type === 'MemberExpression' && callee.object.name === 'React' && callee.property != null && !callee.computed) {
897 callee = callee.property;
898 }
899
900 if (callee.type !== 'Identifier') {
901 return false;
902 }
903
904 var id = def.node.id;
905 var _callee = callee,
906 name = _callee.name;
907
908 if (name === 'useRef' && id.type === 'Identifier') {
909 // useRef() return value is stable.
910 return true;
911 } else if (name === 'useState' || name === 'useReducer') {
912 // Only consider second value in initializing tuple stable.
913 if (id.type === 'ArrayPattern' && id.elements.length === 2 && isArray(resolved.identifiers)) {
914 // Is second tuple value the same reference we're checking?
915 if (id.elements[1] === resolved.identifiers[0]) {
916 if (name === 'useState') {
917 var references = resolved.references;
918 var writeCount = 0;
919
920 for (var i = 0; i < references.length; i++) {
921 if (references[i].isWrite()) {
922 writeCount++;
923 }
924
925 if (writeCount > 1) {
926 return false;
927 }
928
929 setStateCallSites.set(references[i].identifier, id.elements[0]);
930 }
931 } // Setter is stable.
932
933
934 return true;
935 } else if (id.elements[0] === resolved.identifiers[0]) {
936 if (name === 'useState') {
937 var _references = resolved.references;
938
939 for (var _i = 0; _i < _references.length; _i++) {
940 stateVariables.add(_references[_i].identifier);
941 }
942 } // State variable itself is dynamic.
943
944
945 return false;
946 }
947 }
948 } else if (name === 'useTransition') {
949 // Only consider second value in initializing tuple stable.
950 if (id.type === 'ArrayPattern' && id.elements.length === 2 && Array.isArray(resolved.identifiers)) {
951 // Is second tuple value the same reference we're checking?
952 if (id.elements[1] === resolved.identifiers[0]) {
953 // Setter is stable.
954 return true;
955 }
956 }
957 } // By default assume it's dynamic.
958
959
960 return false;
961 } // Some are just functions that don't reference anything dynamic.
962
963
964 function isFunctionWithoutCapturedValues(resolved) {
965 if (!isArray(resolved.defs)) {
966 return false;
967 }
968
969 var def = resolved.defs[0];
970
971 if (def == null) {
972 return false;
973 }
974
975 if (def.node == null || def.node.id == null) {
976 return false;
977 } // Search the direct component subscopes for
978 // top-level function definitions matching this reference.
979
980
981 var fnNode = def.node;
982 var childScopes = componentScope.childScopes;
983 var fnScope = null;
984 var i;
985
986 for (i = 0; i < childScopes.length; i++) {
987 var childScope = childScopes[i];
988 var childScopeBlock = childScope.block;
989
990 if ( // function handleChange() {}
991 fnNode.type === 'FunctionDeclaration' && childScopeBlock === fnNode || // const handleChange = () => {}
992 // const handleChange = function() {}
993 fnNode.type === 'VariableDeclarator' && childScopeBlock.parent === fnNode) {
994 // Found it!
995 fnScope = childScope;
996 break;
997 }
998 }
999
1000 if (fnScope == null) {
1001 return false;
1002 } // Does this function capture any values
1003 // that are in pure scopes (aka render)?
1004
1005
1006 for (i = 0; i < fnScope.through.length; i++) {
1007 var ref = fnScope.through[i];
1008
1009 if (ref.resolved == null) {
1010 continue;
1011 }
1012
1013 if (pureScopes.has(ref.resolved.scope) && // Stable values are fine though,
1014 // although we won't check functions deeper.
1015 !memoizedIsStableKnownHookValue(ref.resolved)) {
1016 return false;
1017 }
1018 } // If we got here, this function doesn't capture anything
1019 // from render--or everything it captures is known stable.
1020
1021
1022 return true;
1023 } // Remember such values. Avoid re-running extra checks on them.
1024
1025
1026 var memoizedIsStableKnownHookValue = memoizeWithWeakMap(isStableKnownHookValue, stableKnownValueCache);
1027 var memoizedIsFunctionWithoutCapturedValues = memoizeWithWeakMap(isFunctionWithoutCapturedValues, functionWithoutCapturedValueCache); // These are usually mistaken. Collect them.
1028
1029 var currentRefsInEffectCleanup = new Map(); // Is this reference inside a cleanup function for this effect node?
1030 // We can check by traversing scopes upwards from the reference, and checking
1031 // if the last "return () => " we encounter is located directly inside the effect.
1032
1033 function isInsideEffectCleanup(reference) {
1034 var curScope = reference.from;
1035 var isInReturnedFunction = false;
1036
1037 while (curScope.block !== node) {
1038 if (curScope.type === 'function') {
1039 isInReturnedFunction = curScope.block.parent != null && curScope.block.parent.type === 'ReturnStatement';
1040 }
1041
1042 curScope = curScope.upper;
1043 }
1044
1045 return isInReturnedFunction;
1046 } // Get dependencies from all our resolved references in pure scopes.
1047 // Key is dependency string, value is whether it's stable.
1048
1049
1050 var dependencies = new Map();
1051 var optionalChains = new Map();
1052 gatherDependenciesRecursively(scope);
1053
1054 function gatherDependenciesRecursively(currentScope) {
1055 var _iterator = _createForOfIteratorHelper(currentScope.references),
1056 _step;
1057
1058 try {
1059 for (_iterator.s(); !(_step = _iterator.n()).done;) {
1060 var reference = _step.value;
1061
1062 // If this reference is not resolved or it is not declared in a pure
1063 // scope then we don't care about this reference.
1064 if (!reference.resolved) {
1065 continue;
1066 }
1067
1068 if (!pureScopes.has(reference.resolved.scope)) {
1069 continue;
1070 } // Narrow the scope of a dependency if it is, say, a member expression.
1071 // Then normalize the narrowed dependency.
1072
1073
1074 var referenceNode = fastFindReferenceWithParent(node, reference.identifier);
1075 var dependencyNode = getDependency(referenceNode);
1076 var dependency = analyzePropertyChain(dependencyNode, optionalChains); // Accessing ref.current inside effect cleanup is bad.
1077
1078 if ( // We're in an effect...
1079 isEffect && // ... and this look like accessing .current...
1080 dependencyNode.type === 'Identifier' && (dependencyNode.parent.type === 'MemberExpression' || dependencyNode.parent.type === 'OptionalMemberExpression') && !dependencyNode.parent.computed && dependencyNode.parent.property.type === 'Identifier' && dependencyNode.parent.property.name === 'current' && // ...in a cleanup function or below...
1081 isInsideEffectCleanup(reference)) {
1082 currentRefsInEffectCleanup.set(dependency, {
1083 reference: reference,
1084 dependencyNode: dependencyNode
1085 });
1086 }
1087
1088 if (dependencyNode.parent.type === 'TSTypeQuery' || dependencyNode.parent.type === 'TSTypeReference') {
1089 continue;
1090 }
1091
1092 var def = reference.resolved.defs[0];
1093
1094 if (def == null) {
1095 continue;
1096 } // Ignore references to the function itself as it's not defined yet.
1097
1098
1099 if (def.node != null && def.node.init === node.parent) {
1100 continue;
1101 } // Ignore Flow type parameters
1102
1103
1104 if (def.type === 'TypeParameter') {
1105 continue;
1106 } // Add the dependency to a map so we can make sure it is referenced
1107 // again in our dependencies array. Remember whether it's stable.
1108
1109
1110 if (!dependencies.has(dependency)) {
1111 var resolved = reference.resolved;
1112 var isStable = memoizedIsStableKnownHookValue(resolved) || memoizedIsFunctionWithoutCapturedValues(resolved);
1113 dependencies.set(dependency, {
1114 isStable: isStable,
1115 references: [reference]
1116 });
1117 } else {
1118 dependencies.get(dependency).references.push(reference);
1119 }
1120 }
1121 } catch (err) {
1122 _iterator.e(err);
1123 } finally {
1124 _iterator.f();
1125 }
1126
1127 var _iterator2 = _createForOfIteratorHelper(currentScope.childScopes),
1128 _step2;
1129
1130 try {
1131 for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
1132 var childScope = _step2.value;
1133 gatherDependenciesRecursively(childScope);
1134 }
1135 } catch (err) {
1136 _iterator2.e(err);
1137 } finally {
1138 _iterator2.f();
1139 }
1140 } // Warn about accessing .current in cleanup effects.
1141
1142
1143 currentRefsInEffectCleanup.forEach(function (_ref, dependency) {
1144 var reference = _ref.reference,
1145 dependencyNode = _ref.dependencyNode;
1146 var references = reference.resolved.references; // Is React managing this ref or us?
1147 // Let's see if we can find a .current assignment.
1148
1149 var foundCurrentAssignment = false;
1150
1151 for (var i = 0; i < references.length; i++) {
1152 var identifier = references[i].identifier;
1153 var parent = identifier.parent;
1154
1155 if (parent != null && // ref.current
1156 // Note: no need to handle OptionalMemberExpression because it can't be LHS.
1157 parent.type === 'MemberExpression' && !parent.computed && parent.property.type === 'Identifier' && parent.property.name === 'current' && // ref.current = <something>
1158 parent.parent.type === 'AssignmentExpression' && parent.parent.left === parent) {
1159 foundCurrentAssignment = true;
1160 break;
1161 }
1162 } // We only want to warn about React-managed refs.
1163
1164
1165 if (foundCurrentAssignment) {
1166 return;
1167 }
1168
1169 reportProblem({
1170 node: dependencyNode.parent.property,
1171 message: "The ref value '" + dependency + ".current' will likely have " + "changed by the time this effect cleanup function runs. If " + "this ref points to a node rendered by React, copy " + ("'" + dependency + ".current' to a variable inside the effect, and ") + "use that variable in the cleanup function."
1172 });
1173 }); // Warn about assigning to variables in the outer scope.
1174 // Those are usually bugs.
1175
1176 var staleAssignments = new Set();
1177
1178 function reportStaleAssignment(writeExpr, key) {
1179 if (staleAssignments.has(key)) {
1180 return;
1181 }
1182
1183 staleAssignments.add(key);
1184 reportProblem({
1185 node: writeExpr,
1186 message: "Assignments to the '" + key + "' variable from inside React Hook " + (context.getSource(reactiveHook) + " will be lost after each ") + "render. To preserve the value over time, store it in a useRef " + "Hook and keep the mutable value in the '.current' property. " + "Otherwise, you can move this variable directly inside " + (context.getSource(reactiveHook) + ".")
1187 });
1188 } // Remember which deps are stable and report bad usage first.
1189
1190
1191 var stableDependencies = new Set();
1192 dependencies.forEach(function (_ref2, key) {
1193 var isStable = _ref2.isStable,
1194 references = _ref2.references;
1195
1196 if (isStable) {
1197 stableDependencies.add(key);
1198 }
1199
1200 references.forEach(function (reference) {
1201 if (reference.writeExpr) {
1202 reportStaleAssignment(reference.writeExpr, key);
1203 }
1204 });
1205 });
1206
1207 if (staleAssignments.size > 0) {
1208 // The intent isn't clear so we'll wait until you fix those first.
1209 return;
1210 }
1211
1212 if (!declaredDependenciesNode) {
1213 // Check if there are any top-level setState() calls.
1214 // Those tend to lead to infinite loops.
1215 var setStateInsideEffectWithoutDeps = null;
1216 dependencies.forEach(function (_ref3, key) {
1217 var isStable = _ref3.isStable,
1218 references = _ref3.references;
1219
1220 if (setStateInsideEffectWithoutDeps) {
1221 return;
1222 }
1223
1224 references.forEach(function (reference) {
1225 if (setStateInsideEffectWithoutDeps) {
1226 return;
1227 }
1228
1229 var id = reference.identifier;
1230 var isSetState = setStateCallSites.has(id);
1231
1232 if (!isSetState) {
1233 return;
1234 }
1235
1236 var fnScope = reference.from;
1237
1238 while (fnScope.type !== 'function') {
1239 fnScope = fnScope.upper;
1240 }
1241
1242 var isDirectlyInsideEffect = fnScope.block === node;
1243
1244 if (isDirectlyInsideEffect) {
1245 // TODO: we could potentially ignore early returns.
1246 setStateInsideEffectWithoutDeps = key;
1247 }
1248 });
1249 });
1250
1251 if (setStateInsideEffectWithoutDeps) {
1252 var _collectRecommendatio = collectRecommendations({
1253 dependencies: dependencies,
1254 declaredDependencies: [],
1255 stableDependencies: stableDependencies,
1256 externalDependencies: new Set(),
1257 isEffect: true
1258 }),
1259 _suggestedDependencies = _collectRecommendatio.suggestedDependencies;
1260
1261 reportProblem({
1262 node: reactiveHook,
1263 message: "React Hook " + reactiveHookName + " contains a call to '" + setStateInsideEffectWithoutDeps + "'. " + "Without a list of dependencies, this can lead to an infinite chain of updates. " + "To fix this, pass [" + _suggestedDependencies.join(', ') + ("] as a second argument to the " + reactiveHookName + " Hook."),
1264 suggest: [{
1265 desc: "Add dependencies array: [" + _suggestedDependencies.join(', ') + "]",
1266 fix: function (fixer) {
1267 return fixer.insertTextAfter(node, ", [" + _suggestedDependencies.join(', ') + "]");
1268 }
1269 }]
1270 });
1271 }
1272
1273 return;
1274 }
1275
1276 var declaredDependencies = [];
1277 var externalDependencies = new Set();
1278
1279 if (declaredDependenciesNode.type !== 'ArrayExpression') {
1280 // If the declared dependencies are not an array expression then we
1281 // can't verify that the user provided the correct dependencies. Tell
1282 // the user this in an error.
1283 reportProblem({
1284 node: declaredDependenciesNode,
1285 message: "React Hook " + context.getSource(reactiveHook) + " was passed a " + 'dependency list that is not an array literal. This means we ' + "can't statically verify whether you've passed the correct " + 'dependencies.'
1286 });
1287 } else {
1288 declaredDependenciesNode.elements.forEach(function (declaredDependencyNode) {
1289 // Skip elided elements.
1290 if (declaredDependencyNode === null) {
1291 return;
1292 } // If we see a spread element then add a special warning.
1293
1294
1295 if (declaredDependencyNode.type === 'SpreadElement') {
1296 reportProblem({
1297 node: declaredDependencyNode,
1298 message: "React Hook " + context.getSource(reactiveHook) + " has a spread " + "element in its dependency array. This means we can't " + "statically verify whether you've passed the " + 'correct dependencies.'
1299 });
1300 return;
1301 } // Try to normalize the declared dependency. If we can't then an error
1302 // will be thrown. We will catch that error and report an error.
1303
1304
1305 var declaredDependency;
1306
1307 try {
1308 declaredDependency = analyzePropertyChain(declaredDependencyNode, null);
1309 } catch (error) {
1310 if (/Unsupported node type/.test(error.message)) {
1311 if (declaredDependencyNode.type === 'Literal') {
1312 if (dependencies.has(declaredDependencyNode.value)) {
1313 reportProblem({
1314 node: declaredDependencyNode,
1315 message: "The " + declaredDependencyNode.raw + " literal is not a valid dependency " + "because it never changes. " + ("Did you mean to include " + declaredDependencyNode.value + " in the array instead?")
1316 });
1317 } else {
1318 reportProblem({
1319 node: declaredDependencyNode,
1320 message: "The " + declaredDependencyNode.raw + " literal is not a valid dependency " + 'because it never changes. You can safely remove it.'
1321 });
1322 }
1323 } else {
1324 reportProblem({
1325 node: declaredDependencyNode,
1326 message: "React Hook " + context.getSource(reactiveHook) + " has a " + "complex expression in the dependency array. " + 'Extract it to a separate variable so it can be statically checked.'
1327 });
1328 }
1329
1330 return;
1331 } else {
1332 throw error;
1333 }
1334 }
1335
1336 var maybeID = declaredDependencyNode;
1337
1338 while (maybeID.type === 'MemberExpression' || maybeID.type === 'OptionalMemberExpression' || maybeID.type === 'ChainExpression') {
1339 maybeID = maybeID.object || maybeID.expression.object;
1340 }
1341
1342 var isDeclaredInComponent = !componentScope.through.some(function (ref) {
1343 return ref.identifier === maybeID;
1344 }); // Add the dependency to our declared dependency map.
1345
1346 declaredDependencies.push({
1347 key: declaredDependency,
1348 node: declaredDependencyNode
1349 });
1350
1351 if (!isDeclaredInComponent) {
1352 externalDependencies.add(declaredDependency);
1353 }
1354 });
1355 }
1356
1357 var _collectRecommendatio2 = collectRecommendations({
1358 dependencies: dependencies,
1359 declaredDependencies: declaredDependencies,
1360 stableDependencies: stableDependencies,
1361 externalDependencies: externalDependencies,
1362 isEffect: isEffect
1363 }),
1364 suggestedDependencies = _collectRecommendatio2.suggestedDependencies,
1365 unnecessaryDependencies = _collectRecommendatio2.unnecessaryDependencies,
1366 missingDependencies = _collectRecommendatio2.missingDependencies,
1367 duplicateDependencies = _collectRecommendatio2.duplicateDependencies;
1368
1369 var suggestedDeps = suggestedDependencies;
1370 var problemCount = duplicateDependencies.size + missingDependencies.size + unnecessaryDependencies.size;
1371
1372 if (problemCount === 0) {
1373 // If nothing else to report, check if some dependencies would
1374 // invalidate on every render.
1375 var constructions = scanForConstructions({
1376 declaredDependencies: declaredDependencies,
1377 declaredDependenciesNode: declaredDependenciesNode,
1378 componentScope: componentScope,
1379 scope: scope
1380 });
1381 constructions.forEach(function (_ref4) {
1382 var construction = _ref4.construction,
1383 isUsedOutsideOfHook = _ref4.isUsedOutsideOfHook,
1384 depType = _ref4.depType;
1385 var wrapperHook = depType === 'function' ? 'useCallback' : 'useMemo';
1386 var constructionType = depType === 'function' ? 'definition' : 'initialization';
1387 var defaultAdvice = "wrap the " + constructionType + " of '" + construction.name.name + "' in its own " + wrapperHook + "() Hook.";
1388 var advice = isUsedOutsideOfHook ? "To fix this, " + defaultAdvice : "Move it inside the " + reactiveHookName + " callback. Alternatively, " + defaultAdvice;
1389 var causation = depType === 'conditional' || depType === 'logical expression' ? 'could make' : 'makes';
1390 var message = "The '" + construction.name.name + "' " + depType + " " + causation + " the dependencies of " + (reactiveHookName + " Hook (at line " + declaredDependenciesNode.loc.start.line + ") ") + ("change on every render. " + advice);
1391 var suggest; // Only handle the simple case of variable assignments.
1392 // Wrapping function declarations can mess up hoisting.
1393
1394 if (isUsedOutsideOfHook && construction.type === 'Variable' && // Objects may be mutated after construction, which would make this
1395 // fix unsafe. Functions _probably_ won't be mutated, so we'll
1396 // allow this fix for them.
1397 depType === 'function') {
1398 suggest = [{
1399 desc: "Wrap the " + constructionType + " of '" + construction.name.name + "' in its own " + wrapperHook + "() Hook.",
1400 fix: function (fixer) {
1401 var _ref5 = wrapperHook === 'useMemo' ? ["useMemo(() => { return ", '; })'] : ['useCallback(', ')'],
1402 before = _ref5[0],
1403 after = _ref5[1];
1404
1405 return [// TODO: also add an import?
1406 fixer.insertTextBefore(construction.node.init, before), // TODO: ideally we'd gather deps here but it would require
1407 // restructuring the rule code. This will cause a new lint
1408 // error to appear immediately for useCallback. Note we're
1409 // not adding [] because would that changes semantics.
1410 fixer.insertTextAfter(construction.node.init, after)];
1411 }
1412 }];
1413 } // TODO: What if the function needs to change on every render anyway?
1414 // Should we suggest removing effect deps as an appropriate fix too?
1415
1416
1417 reportProblem({
1418 // TODO: Why not report this at the dependency site?
1419 node: construction.node,
1420 message: message,
1421 suggest: suggest
1422 });
1423 });
1424 return;
1425 } // If we're going to report a missing dependency,
1426 // we might as well recalculate the list ignoring
1427 // the currently specified deps. This can result
1428 // in some extra deduplication. We can't do this
1429 // for effects though because those have legit
1430 // use cases for over-specifying deps.
1431
1432
1433 if (!isEffect && missingDependencies.size > 0) {
1434 suggestedDeps = collectRecommendations({
1435 dependencies: dependencies,
1436 declaredDependencies: [],
1437 // Pretend we don't know
1438 stableDependencies: stableDependencies,
1439 externalDependencies: externalDependencies,
1440 isEffect: isEffect
1441 }).suggestedDependencies;
1442 } // Alphabetize the suggestions, but only if deps were already alphabetized.
1443
1444
1445 function areDeclaredDepsAlphabetized() {
1446 if (declaredDependencies.length === 0) {
1447 return true;
1448 }
1449
1450 var declaredDepKeys = declaredDependencies.map(function (dep) {
1451 return dep.key;
1452 });
1453 var sortedDeclaredDepKeys = declaredDepKeys.slice().sort();
1454 return declaredDepKeys.join(',') === sortedDeclaredDepKeys.join(',');
1455 }
1456
1457 if (areDeclaredDepsAlphabetized()) {
1458 suggestedDeps.sort();
1459 } // Most of our algorithm deals with dependency paths with optional chaining stripped.
1460 // This function is the last step before printing a dependency, so now is a good time to
1461 // check whether any members in our path are always used as optional-only. In that case,
1462 // we will use ?. instead of . to concatenate those parts of the path.
1463
1464
1465 function formatDependency(path) {
1466 var members = path.split('.');
1467 var finalPath = '';
1468
1469 for (var i = 0; i < members.length; i++) {
1470 if (i !== 0) {
1471 var pathSoFar = members.slice(0, i + 1).join('.');
1472 var isOptional = optionalChains.get(pathSoFar) === true;
1473 finalPath += isOptional ? '?.' : '.';
1474 }
1475
1476 finalPath += members[i];
1477 }
1478
1479 return finalPath;
1480 }
1481
1482 function getWarningMessage(deps, singlePrefix, label, fixVerb) {
1483 if (deps.size === 0) {
1484 return null;
1485 }
1486
1487 return (deps.size > 1 ? '' : singlePrefix + ' ') + label + ' ' + (deps.size > 1 ? 'dependencies' : 'dependency') + ': ' + joinEnglish(Array.from(deps).sort().map(function (name) {
1488 return "'" + formatDependency(name) + "'";
1489 })) + (". Either " + fixVerb + " " + (deps.size > 1 ? 'them' : 'it') + " or remove the dependency array.");
1490 }
1491
1492 var extraWarning = '';
1493
1494 if (unnecessaryDependencies.size > 0) {
1495 var badRef = null;
1496 Array.from(unnecessaryDependencies.keys()).forEach(function (key) {
1497 if (badRef !== null) {
1498 return;
1499 }
1500
1501 if (key.endsWith('.current')) {
1502 badRef = key;
1503 }
1504 });
1505
1506 if (badRef !== null) {
1507 extraWarning = " Mutable values like '" + badRef + "' aren't valid dependencies " + "because mutating them doesn't re-render the component.";
1508 } else if (externalDependencies.size > 0) {
1509 var dep = Array.from(externalDependencies)[0]; // Don't show this warning for things that likely just got moved *inside* the callback
1510 // because in that case they're clearly not referring to globals.
1511
1512 if (!scope.set.has(dep)) {
1513 extraWarning = " Outer scope values like '" + dep + "' aren't valid dependencies " + "because mutating them doesn't re-render the component.";
1514 }
1515 }
1516 } // `props.foo()` marks `props` as a dependency because it has
1517 // a `this` value. This warning can be confusing.
1518 // So if we're going to show it, append a clarification.
1519
1520
1521 if (!extraWarning && missingDependencies.has('props')) {
1522 var propDep = dependencies.get('props');
1523
1524 if (propDep == null) {
1525 return;
1526 }
1527
1528 var refs = propDep.references;
1529
1530 if (!Array.isArray(refs)) {
1531 return;
1532 }
1533
1534 var isPropsOnlyUsedInMembers = true;
1535
1536 for (var i = 0; i < refs.length; i++) {
1537 var ref = refs[i];
1538 var id = fastFindReferenceWithParent(componentScope.block, ref.identifier);
1539
1540 if (!id) {
1541 isPropsOnlyUsedInMembers = false;
1542 break;
1543 }
1544
1545 var parent = id.parent;
1546
1547 if (parent == null) {
1548 isPropsOnlyUsedInMembers = false;
1549 break;
1550 }
1551
1552 if (parent.type !== 'MemberExpression' && parent.type !== 'OptionalMemberExpression') {
1553 isPropsOnlyUsedInMembers = false;
1554 break;
1555 }
1556 }
1557
1558 if (isPropsOnlyUsedInMembers) {
1559 extraWarning = " However, 'props' will change when *any* prop changes, so the " + "preferred fix is to destructure the 'props' object outside of " + ("the " + reactiveHookName + " call and refer to those specific props ") + ("inside " + context.getSource(reactiveHook) + ".");
1560 }
1561 }
1562
1563 if (!extraWarning && missingDependencies.size > 0) {
1564 // See if the user is trying to avoid specifying a callable prop.
1565 // This usually means they're unaware of useCallback.
1566 var missingCallbackDep = null;
1567 missingDependencies.forEach(function (missingDep) {
1568 if (missingCallbackDep) {
1569 return;
1570 } // Is this a variable from top scope?
1571
1572
1573 var topScopeRef = componentScope.set.get(missingDep);
1574 var usedDep = dependencies.get(missingDep);
1575
1576 if (usedDep.references[0].resolved !== topScopeRef) {
1577 return;
1578 } // Is this a destructured prop?
1579
1580
1581 var def = topScopeRef.defs[0];
1582
1583 if (def == null || def.name == null || def.type !== 'Parameter') {
1584 return;
1585 } // Was it called in at least one case? Then it's a function.
1586
1587
1588 var isFunctionCall = false;
1589 var id;
1590
1591 for (var _i2 = 0; _i2 < usedDep.references.length; _i2++) {
1592 id = usedDep.references[_i2].identifier;
1593
1594 if (id != null && id.parent != null && (id.parent.type === 'CallExpression' || id.parent.type === 'OptionalCallExpression') && id.parent.callee === id) {
1595 isFunctionCall = true;
1596 break;
1597 }
1598 }
1599
1600 if (!isFunctionCall) {
1601 return;
1602 } // If it's missing (i.e. in component scope) *and* it's a parameter
1603 // then it is definitely coming from props destructuring.
1604 // (It could also be props itself but we wouldn't be calling it then.)
1605
1606
1607 missingCallbackDep = missingDep;
1608 });
1609
1610 if (missingCallbackDep !== null) {
1611 extraWarning = " If '" + missingCallbackDep + "' changes too often, " + "find the parent component that defines it " + "and wrap that definition in useCallback.";
1612 }
1613 }
1614
1615 if (!extraWarning && missingDependencies.size > 0) {
1616 var setStateRecommendation = null;
1617 missingDependencies.forEach(function (missingDep) {
1618 if (setStateRecommendation !== null) {
1619 return;
1620 }
1621
1622 var usedDep = dependencies.get(missingDep);
1623 var references = usedDep.references;
1624 var id;
1625 var maybeCall;
1626
1627 for (var _i3 = 0; _i3 < references.length; _i3++) {
1628 id = references[_i3].identifier;
1629 maybeCall = id.parent; // Try to see if we have setState(someExpr(missingDep)).
1630
1631 while (maybeCall != null && maybeCall !== componentScope.block) {
1632 if (maybeCall.type === 'CallExpression') {
1633 var correspondingStateVariable = setStateCallSites.get(maybeCall.callee);
1634
1635 if (correspondingStateVariable != null) {
1636 if (correspondingStateVariable.name === missingDep) {
1637 // setCount(count + 1)
1638 setStateRecommendation = {
1639 missingDep: missingDep,
1640 setter: maybeCall.callee.name,
1641 form: 'updater'
1642 };
1643 } else if (stateVariables.has(id)) {
1644 // setCount(count + increment)
1645 setStateRecommendation = {
1646 missingDep: missingDep,
1647 setter: maybeCall.callee.name,
1648 form: 'reducer'
1649 };
1650 } else {
1651 var resolved = references[_i3].resolved;
1652
1653 if (resolved != null) {
1654 // If it's a parameter *and* a missing dep,
1655 // it must be a prop or something inside a prop.
1656 // Therefore, recommend an inline reducer.
1657 var def = resolved.defs[0];
1658
1659 if (def != null && def.type === 'Parameter') {
1660 setStateRecommendation = {
1661 missingDep: missingDep,
1662 setter: maybeCall.callee.name,
1663 form: 'inlineReducer'
1664 };
1665 }
1666 }
1667 }
1668
1669 break;
1670 }
1671 }
1672
1673 maybeCall = maybeCall.parent;
1674 }
1675
1676 if (setStateRecommendation !== null) {
1677 break;
1678 }
1679 }
1680 });
1681
1682 if (setStateRecommendation !== null) {
1683 switch (setStateRecommendation.form) {
1684 case 'reducer':
1685 extraWarning = " You can also replace multiple useState variables with useReducer " + ("if '" + setStateRecommendation.setter + "' needs the ") + ("current value of '" + setStateRecommendation.missingDep + "'.");
1686 break;
1687
1688 case 'inlineReducer':
1689 extraWarning = " If '" + setStateRecommendation.setter + "' needs the " + ("current value of '" + setStateRecommendation.missingDep + "', ") + "you can also switch to useReducer instead of useState and " + ("read '" + setStateRecommendation.missingDep + "' in the reducer.");
1690 break;
1691
1692 case 'updater':
1693 extraWarning = " You can also do a functional update '" + setStateRecommendation.setter + "(" + setStateRecommendation.missingDep.substring(0, 1) + " => ...)' if you only need '" + setStateRecommendation.missingDep + "'" + (" in the '" + setStateRecommendation.setter + "' call.");
1694 break;
1695
1696 default:
1697 throw new Error('Unknown case.');
1698 }
1699 }
1700 }
1701
1702 reportProblem({
1703 node: declaredDependenciesNode,
1704 message: "React Hook " + context.getSource(reactiveHook) + " has " + ( // To avoid a long message, show the next actionable item.
1705 getWarningMessage(missingDependencies, 'a', 'missing', 'include') || getWarningMessage(unnecessaryDependencies, 'an', 'unnecessary', 'exclude') || getWarningMessage(duplicateDependencies, 'a', 'duplicate', 'omit')) + extraWarning,
1706 suggest: [{
1707 desc: "Update the dependencies array to be: [" + suggestedDeps.map(formatDependency).join(', ') + "]",
1708 fix: function (fixer) {
1709 // TODO: consider preserving the comments or formatting?
1710 return fixer.replaceText(declaredDependenciesNode, "[" + suggestedDeps.map(formatDependency).join(', ') + "]");
1711 }
1712 }]
1713 });
1714 }
1715
1716 function visitCallExpression(node) {
1717 var callbackIndex = getReactiveHookCallbackIndex(node.callee, options);
1718
1719 if (callbackIndex === -1) {
1720 // Not a React Hook call that needs deps.
1721 return;
1722 }
1723
1724 var callback = node.arguments[callbackIndex];
1725 var reactiveHook = node.callee;
1726 var reactiveHookName = getNodeWithoutReactNamespace(reactiveHook).name;
1727 var declaredDependenciesNode = node.arguments[callbackIndex + 1];
1728 var isEffect = /Effect($|[^a-z])/g.test(reactiveHookName); // Check whether a callback is supplied. If there is no callback supplied
1729 // then the hook will not work and React will throw a TypeError.
1730 // So no need to check for dependency inclusion.
1731
1732 if (!callback) {
1733 reportProblem({
1734 node: reactiveHook,
1735 message: "React Hook " + reactiveHookName + " requires an effect callback. " + "Did you forget to pass a callback to the hook?"
1736 });
1737 return;
1738 } // Check the declared dependencies for this reactive hook. If there is no
1739 // second argument then the reactive callback will re-run on every render.
1740 // So no need to check for dependency inclusion.
1741
1742
1743 if (!declaredDependenciesNode && !isEffect) {
1744 // These are only used for optimization.
1745 if (reactiveHookName === 'useMemo' || reactiveHookName === 'useCallback') {
1746 // TODO: Can this have a suggestion?
1747 reportProblem({
1748 node: reactiveHook,
1749 message: "React Hook " + reactiveHookName + " does nothing when called with " + "only one argument. Did you forget to pass an array of " + "dependencies?"
1750 });
1751 }
1752
1753 return;
1754 }
1755
1756 switch (callback.type) {
1757 case 'FunctionExpression':
1758 case 'ArrowFunctionExpression':
1759 visitFunctionWithDependencies(callback, declaredDependenciesNode, reactiveHook, reactiveHookName, isEffect);
1760 return;
1761 // Handled
1762
1763 case 'Identifier':
1764 if (!declaredDependenciesNode) {
1765 // No deps, no problems.
1766 return; // Handled
1767 } // The function passed as a callback is not written inline.
1768 // But perhaps it's in the dependencies array?
1769
1770
1771 if (declaredDependenciesNode.elements && declaredDependenciesNode.elements.some(function (el) {
1772 return el && el.type === 'Identifier' && el.name === callback.name;
1773 })) {
1774 // If it's already in the list of deps, we don't care because
1775 // this is valid regardless.
1776 return; // Handled
1777 } // We'll do our best effort to find it, complain otherwise.
1778
1779
1780 var variable = context.getScope().set.get(callback.name);
1781
1782 if (variable == null || variable.defs == null) {
1783 // If it's not in scope, we don't care.
1784 return; // Handled
1785 } // The function passed as a callback is not written inline.
1786 // But it's defined somewhere in the render scope.
1787 // We'll do our best effort to find and check it, complain otherwise.
1788
1789
1790 var def = variable.defs[0];
1791
1792 if (!def || !def.node) {
1793 break; // Unhandled
1794 }
1795
1796 if (def.type !== 'Variable' && def.type !== 'FunctionName') {
1797 // Parameter or an unusual pattern. Bail out.
1798 break; // Unhandled
1799 }
1800
1801 switch (def.node.type) {
1802 case 'FunctionDeclaration':
1803 // useEffect(() => { ... }, []);
1804 visitFunctionWithDependencies(def.node, declaredDependenciesNode, reactiveHook, reactiveHookName, isEffect);
1805 return;
1806 // Handled
1807
1808 case 'VariableDeclarator':
1809 var init = def.node.init;
1810
1811 if (!init) {
1812 break; // Unhandled
1813 }
1814
1815 switch (init.type) {
1816 // const effectBody = () => {...};
1817 // useEffect(effectBody, []);
1818 case 'ArrowFunctionExpression':
1819 case 'FunctionExpression':
1820 // We can inspect this function as if it were inline.
1821 visitFunctionWithDependencies(init, declaredDependenciesNode, reactiveHook, reactiveHookName, isEffect);
1822 return;
1823 // Handled
1824 }
1825
1826 break;
1827 // Unhandled
1828 }
1829
1830 break;
1831 // Unhandled
1832
1833 default:
1834 // useEffect(generateEffectBody(), []);
1835 reportProblem({
1836 node: reactiveHook,
1837 message: "React Hook " + reactiveHookName + " received a function whose dependencies " + "are unknown. Pass an inline function instead."
1838 });
1839 return;
1840 // Handled
1841 } // Something unusual. Fall back to suggesting to add the body itself as a dep.
1842
1843
1844 reportProblem({
1845 node: reactiveHook,
1846 message: "React Hook " + reactiveHookName + " has a missing dependency: '" + callback.name + "'. " + "Either include it or remove the dependency array.",
1847 suggest: [{
1848 desc: "Update the dependencies array to be: [" + callback.name + "]",
1849 fix: function (fixer) {
1850 return fixer.replaceText(declaredDependenciesNode, "[" + callback.name + "]");
1851 }
1852 }]
1853 });
1854 }
1855
1856 return {
1857 CallExpression: visitCallExpression
1858 };
1859 }
1860}; // The meat of the logic.
1861
1862function collectRecommendations(_ref6) {
1863 var dependencies = _ref6.dependencies,
1864 declaredDependencies = _ref6.declaredDependencies,
1865 stableDependencies = _ref6.stableDependencies,
1866 externalDependencies = _ref6.externalDependencies,
1867 isEffect = _ref6.isEffect;
1868 // Our primary data structure.
1869 // It is a logical representation of property chains:
1870 // `props` -> `props.foo` -> `props.foo.bar` -> `props.foo.bar.baz`
1871 // -> `props.lol`
1872 // -> `props.huh` -> `props.huh.okay`
1873 // -> `props.wow`
1874 // We'll use it to mark nodes that are *used* by the programmer,
1875 // and the nodes that were *declared* as deps. Then we will
1876 // traverse it to learn which deps are missing or unnecessary.
1877 var depTree = createDepTree();
1878
1879 function createDepTree() {
1880 return {
1881 isUsed: false,
1882 // True if used in code
1883 isSatisfiedRecursively: false,
1884 // True if specified in deps
1885 isSubtreeUsed: false,
1886 // True if something deeper is used by code
1887 children: new Map() // Nodes for properties
1888
1889 };
1890 } // Mark all required nodes first.
1891 // Imagine exclamation marks next to each used deep property.
1892
1893
1894 dependencies.forEach(function (_, key) {
1895 var node = getOrCreateNodeByPath(depTree, key);
1896 node.isUsed = true;
1897 markAllParentsByPath(depTree, key, function (parent) {
1898 parent.isSubtreeUsed = true;
1899 });
1900 }); // Mark all satisfied nodes.
1901 // Imagine checkmarks next to each declared dependency.
1902
1903 declaredDependencies.forEach(function (_ref7) {
1904 var key = _ref7.key;
1905 var node = getOrCreateNodeByPath(depTree, key);
1906 node.isSatisfiedRecursively = true;
1907 });
1908 stableDependencies.forEach(function (key) {
1909 var node = getOrCreateNodeByPath(depTree, key);
1910 node.isSatisfiedRecursively = true;
1911 }); // Tree manipulation helpers.
1912
1913 function getOrCreateNodeByPath(rootNode, path) {
1914 var keys = path.split('.');
1915 var node = rootNode;
1916
1917 var _iterator3 = _createForOfIteratorHelper(keys),
1918 _step3;
1919
1920 try {
1921 for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) {
1922 var key = _step3.value;
1923 var child = node.children.get(key);
1924
1925 if (!child) {
1926 child = createDepTree();
1927 node.children.set(key, child);
1928 }
1929
1930 node = child;
1931 }
1932 } catch (err) {
1933 _iterator3.e(err);
1934 } finally {
1935 _iterator3.f();
1936 }
1937
1938 return node;
1939 }
1940
1941 function markAllParentsByPath(rootNode, path, fn) {
1942 var keys = path.split('.');
1943 var node = rootNode;
1944
1945 var _iterator4 = _createForOfIteratorHelper(keys),
1946 _step4;
1947
1948 try {
1949 for (_iterator4.s(); !(_step4 = _iterator4.n()).done;) {
1950 var key = _step4.value;
1951 var child = node.children.get(key);
1952
1953 if (!child) {
1954 return;
1955 }
1956
1957 fn(child);
1958 node = child;
1959 }
1960 } catch (err) {
1961 _iterator4.e(err);
1962 } finally {
1963 _iterator4.f();
1964 }
1965 } // Now we can learn which dependencies are missing or necessary.
1966
1967
1968 var missingDependencies = new Set();
1969 var satisfyingDependencies = new Set();
1970 scanTreeRecursively(depTree, missingDependencies, satisfyingDependencies, function (key) {
1971 return key;
1972 });
1973
1974 function scanTreeRecursively(node, missingPaths, satisfyingPaths, keyToPath) {
1975 node.children.forEach(function (child, key) {
1976 var path = keyToPath(key);
1977
1978 if (child.isSatisfiedRecursively) {
1979 if (child.isSubtreeUsed) {
1980 // Remember this dep actually satisfied something.
1981 satisfyingPaths.add(path);
1982 } // It doesn't matter if there's something deeper.
1983 // It would be transitively satisfied since we assume immutability.
1984 // `props.foo` is enough if you read `props.foo.id`.
1985
1986
1987 return;
1988 }
1989
1990 if (child.isUsed) {
1991 // Remember that no declared deps satisfied this node.
1992 missingPaths.add(path); // If we got here, nothing in its subtree was satisfied.
1993 // No need to search further.
1994
1995 return;
1996 }
1997
1998 scanTreeRecursively(child, missingPaths, satisfyingPaths, function (childKey) {
1999 return path + '.' + childKey;
2000 });
2001 });
2002 } // Collect suggestions in the order they were originally specified.
2003
2004
2005 var suggestedDependencies = [];
2006 var unnecessaryDependencies = new Set();
2007 var duplicateDependencies = new Set();
2008 declaredDependencies.forEach(function (_ref8) {
2009 var key = _ref8.key;
2010
2011 // Does this declared dep satisfy a real need?
2012 if (satisfyingDependencies.has(key)) {
2013 if (suggestedDependencies.indexOf(key) === -1) {
2014 // Good one.
2015 suggestedDependencies.push(key);
2016 } else {
2017 // Duplicate.
2018 duplicateDependencies.add(key);
2019 }
2020 } else {
2021 if (isEffect && !key.endsWith('.current') && !externalDependencies.has(key)) {
2022 // Effects are allowed extra "unnecessary" deps.
2023 // Such as resetting scroll when ID changes.
2024 // Consider them legit.
2025 // The exception is ref.current which is always wrong.
2026 if (suggestedDependencies.indexOf(key) === -1) {
2027 suggestedDependencies.push(key);
2028 }
2029 } else {
2030 // It's definitely not needed.
2031 unnecessaryDependencies.add(key);
2032 }
2033 }
2034 }); // Then add the missing ones at the end.
2035
2036 missingDependencies.forEach(function (key) {
2037 suggestedDependencies.push(key);
2038 });
2039 return {
2040 suggestedDependencies: suggestedDependencies,
2041 unnecessaryDependencies: unnecessaryDependencies,
2042 duplicateDependencies: duplicateDependencies,
2043 missingDependencies: missingDependencies
2044 };
2045} // If the node will result in constructing a referentially unique value, return
2046// its human readable type name, else return null.
2047
2048
2049function getConstructionExpressionType(node) {
2050 switch (node.type) {
2051 case 'ObjectExpression':
2052 return 'object';
2053
2054 case 'ArrayExpression':
2055 return 'array';
2056
2057 case 'ArrowFunctionExpression':
2058 case 'FunctionExpression':
2059 return 'function';
2060
2061 case 'ClassExpression':
2062 return 'class';
2063
2064 case 'ConditionalExpression':
2065 if (getConstructionExpressionType(node.consequent) != null || getConstructionExpressionType(node.alternate) != null) {
2066 return 'conditional';
2067 }
2068
2069 return null;
2070
2071 case 'LogicalExpression':
2072 if (getConstructionExpressionType(node.left) != null || getConstructionExpressionType(node.right) != null) {
2073 return 'logical expression';
2074 }
2075
2076 return null;
2077
2078 case 'JSXFragment':
2079 return 'JSX fragment';
2080
2081 case 'JSXElement':
2082 return 'JSX element';
2083
2084 case 'AssignmentExpression':
2085 if (getConstructionExpressionType(node.right) != null) {
2086 return 'assignment expression';
2087 }
2088
2089 return null;
2090
2091 case 'NewExpression':
2092 return 'object construction';
2093
2094 case 'Literal':
2095 if (node.value instanceof RegExp) {
2096 return 'regular expression';
2097 }
2098
2099 return null;
2100
2101 case 'TypeCastExpression':
2102 return getConstructionExpressionType(node.expression);
2103
2104 case 'TSAsExpression':
2105 return getConstructionExpressionType(node.expression);
2106 }
2107
2108 return null;
2109} // Finds variables declared as dependencies
2110// that would invalidate on every render.
2111
2112
2113function scanForConstructions(_ref9) {
2114 var declaredDependencies = _ref9.declaredDependencies,
2115 declaredDependenciesNode = _ref9.declaredDependenciesNode,
2116 componentScope = _ref9.componentScope,
2117 scope = _ref9.scope;
2118 var constructions = declaredDependencies.map(function (_ref10) {
2119 var key = _ref10.key;
2120 var ref = componentScope.variables.find(function (v) {
2121 return v.name === key;
2122 });
2123
2124 if (ref == null) {
2125 return null;
2126 }
2127
2128 var node = ref.defs[0];
2129
2130 if (node == null) {
2131 return null;
2132 } // const handleChange = function () {}
2133 // const handleChange = () => {}
2134 // const foo = {}
2135 // const foo = []
2136 // etc.
2137
2138
2139 if (node.type === 'Variable' && node.node.type === 'VariableDeclarator' && node.node.id.type === 'Identifier' && // Ensure this is not destructed assignment
2140 node.node.init != null) {
2141 var constantExpressionType = getConstructionExpressionType(node.node.init);
2142
2143 if (constantExpressionType != null) {
2144 return [ref, constantExpressionType];
2145 }
2146 } // function handleChange() {}
2147
2148
2149 if (node.type === 'FunctionName' && node.node.type === 'FunctionDeclaration') {
2150 return [ref, 'function'];
2151 } // class Foo {}
2152
2153
2154 if (node.type === 'ClassName' && node.node.type === 'ClassDeclaration') {
2155 return [ref, 'class'];
2156 }
2157
2158 return null;
2159 }).filter(Boolean);
2160
2161 function isUsedOutsideOfHook(ref) {
2162 var foundWriteExpr = false;
2163
2164 for (var i = 0; i < ref.references.length; i++) {
2165 var reference = ref.references[i];
2166
2167 if (reference.writeExpr) {
2168 if (foundWriteExpr) {
2169 // Two writes to the same function.
2170 return true;
2171 } else {
2172 // Ignore first write as it's not usage.
2173 foundWriteExpr = true;
2174 continue;
2175 }
2176 }
2177
2178 var currentScope = reference.from;
2179
2180 while (currentScope !== scope && currentScope != null) {
2181 currentScope = currentScope.upper;
2182 }
2183
2184 if (currentScope !== scope) {
2185 // This reference is outside the Hook callback.
2186 // It can only be legit if it's the deps array.
2187 if (!isAncestorNodeOf(declaredDependenciesNode, reference.identifier)) {
2188 return true;
2189 }
2190 }
2191 }
2192
2193 return false;
2194 }
2195
2196 return constructions.map(function (_ref11) {
2197 var ref = _ref11[0],
2198 depType = _ref11[1];
2199 return {
2200 construction: ref.defs[0],
2201 depType: depType,
2202 isUsedOutsideOfHook: isUsedOutsideOfHook(ref)
2203 };
2204 });
2205}
2206/**
2207 * Assuming () means the passed/returned node:
2208 * (props) => (props)
2209 * props.(foo) => (props.foo)
2210 * props.foo.(bar) => (props).foo.bar
2211 * props.foo.bar.(baz) => (props).foo.bar.baz
2212 */
2213
2214
2215function getDependency(node) {
2216 if ((node.parent.type === 'MemberExpression' || node.parent.type === 'OptionalMemberExpression') && node.parent.object === node && node.parent.property.name !== 'current' && !node.parent.computed && !(node.parent.parent != null && (node.parent.parent.type === 'CallExpression' || node.parent.parent.type === 'OptionalCallExpression') && node.parent.parent.callee === node.parent)) {
2217 return getDependency(node.parent);
2218 } else if ( // Note: we don't check OptionalMemberExpression because it can't be LHS.
2219 node.type === 'MemberExpression' && node.parent && node.parent.type === 'AssignmentExpression' && node.parent.left === node) {
2220 return node.object;
2221 } else {
2222 return node;
2223 }
2224}
2225/**
2226 * Mark a node as either optional or required.
2227 * Note: If the node argument is an OptionalMemberExpression, it doesn't necessarily mean it is optional.
2228 * It just means there is an optional member somewhere inside.
2229 * This particular node might still represent a required member, so check .optional field.
2230 */
2231
2232
2233function markNode(node, optionalChains, result) {
2234 if (optionalChains) {
2235 if (node.optional) {
2236 // We only want to consider it optional if *all* usages were optional.
2237 if (!optionalChains.has(result)) {
2238 // Mark as (maybe) optional. If there's a required usage, this will be overridden.
2239 optionalChains.set(result, true);
2240 }
2241 } else {
2242 // Mark as required.
2243 optionalChains.set(result, false);
2244 }
2245 }
2246}
2247/**
2248 * Assuming () means the passed node.
2249 * (foo) -> 'foo'
2250 * foo(.)bar -> 'foo.bar'
2251 * foo.bar(.)baz -> 'foo.bar.baz'
2252 * Otherwise throw.
2253 */
2254
2255
2256function analyzePropertyChain(node, optionalChains) {
2257 if (node.type === 'Identifier' || node.type === 'JSXIdentifier') {
2258 var result = node.name;
2259
2260 if (optionalChains) {
2261 // Mark as required.
2262 optionalChains.set(result, false);
2263 }
2264
2265 return result;
2266 } else if (node.type === 'MemberExpression' && !node.computed) {
2267 var object = analyzePropertyChain(node.object, optionalChains);
2268 var property = analyzePropertyChain(node.property, null);
2269
2270 var _result = object + "." + property;
2271
2272 markNode(node, optionalChains, _result);
2273 return _result;
2274 } else if (node.type === 'OptionalMemberExpression' && !node.computed) {
2275 var _object = analyzePropertyChain(node.object, optionalChains);
2276
2277 var _property = analyzePropertyChain(node.property, null);
2278
2279 var _result2 = _object + "." + _property;
2280
2281 markNode(node, optionalChains, _result2);
2282 return _result2;
2283 } else if (node.type === 'ChainExpression' && !node.computed) {
2284 var expression = node.expression;
2285
2286 if (expression.type === 'CallExpression') {
2287 throw new Error("Unsupported node type: " + expression.type);
2288 }
2289
2290 var _object2 = analyzePropertyChain(expression.object, optionalChains);
2291
2292 var _property2 = analyzePropertyChain(expression.property, null);
2293
2294 var _result3 = _object2 + "." + _property2;
2295
2296 markNode(expression, optionalChains, _result3);
2297 return _result3;
2298 } else {
2299 throw new Error("Unsupported node type: " + node.type);
2300 }
2301}
2302
2303function getNodeWithoutReactNamespace(node, options) {
2304 if (node.type === 'MemberExpression' && node.object.type === 'Identifier' && node.object.name === 'React' && node.property.type === 'Identifier' && !node.computed) {
2305 return node.property;
2306 }
2307
2308 return node;
2309} // What's the index of callback that needs to be analyzed for a given Hook?
2310// -1 if it's not a Hook we care about (e.g. useState).
2311// 0 for useEffect/useMemo/useCallback(fn).
2312// 1 for useImperativeHandle(ref, fn).
2313// For additionally configured Hooks, assume that they're like useEffect (0).
2314
2315
2316function getReactiveHookCallbackIndex(calleeNode, options) {
2317 var node = getNodeWithoutReactNamespace(calleeNode);
2318
2319 if (node.type !== 'Identifier') {
2320 return -1;
2321 }
2322
2323 switch (node.name) {
2324 case 'useEffect':
2325 case 'useLayoutEffect':
2326 case 'useCallback':
2327 case 'useMemo':
2328 // useEffect(fn)
2329 return 0;
2330
2331 case 'useImperativeHandle':
2332 // useImperativeHandle(ref, fn)
2333 return 1;
2334
2335 default:
2336 if (node === calleeNode && options && options.additionalHooks) {
2337 // Allow the user to provide a regular expression which enables the lint to
2338 // target custom reactive hooks.
2339 var name;
2340
2341 try {
2342 name = analyzePropertyChain(node, null);
2343 } catch (error) {
2344 if (/Unsupported node type/.test(error.message)) {
2345 return 0;
2346 } else {
2347 throw error;
2348 }
2349 }
2350
2351 return options.additionalHooks.test(name) ? 0 : -1;
2352 } else {
2353 return -1;
2354 }
2355
2356 }
2357}
2358/**
2359 * ESLint won't assign node.parent to references from context.getScope()
2360 *
2361 * So instead we search for the node from an ancestor assigning node.parent
2362 * as we go. This mutates the AST.
2363 *
2364 * This traversal is:
2365 * - optimized by only searching nodes with a range surrounding our target node
2366 * - agnostic to AST node types, it looks for `{ type: string, ... }`
2367 */
2368
2369
2370function fastFindReferenceWithParent(start, target) {
2371 var queue = [start];
2372 var item = null;
2373
2374 while (queue.length) {
2375 item = queue.shift();
2376
2377 if (isSameIdentifier(item, target)) {
2378 return item;
2379 }
2380
2381 if (!isAncestorNodeOf(item, target)) {
2382 continue;
2383 }
2384
2385 for (var _i4 = 0, _Object$entries = Object.entries(item); _i4 < _Object$entries.length; _i4++) {
2386 var _Object$entries$_i = _Object$entries[_i4],
2387 key = _Object$entries$_i[0],
2388 value = _Object$entries$_i[1];
2389
2390 if (key === 'parent') {
2391 continue;
2392 }
2393
2394 if (isNodeLike(value)) {
2395 value.parent = item;
2396 queue.push(value);
2397 } else if (Array.isArray(value)) {
2398 value.forEach(function (val) {
2399 if (isNodeLike(val)) {
2400 val.parent = item;
2401 queue.push(val);
2402 }
2403 });
2404 }
2405 }
2406 }
2407
2408 return null;
2409}
2410
2411function joinEnglish(arr) {
2412 var s = '';
2413
2414 for (var i = 0; i < arr.length; i++) {
2415 s += arr[i];
2416
2417 if (i === 0 && arr.length === 2) {
2418 s += ' and ';
2419 } else if (i === arr.length - 2 && arr.length > 2) {
2420 s += ', and ';
2421 } else if (i < arr.length - 1) {
2422 s += ', ';
2423 }
2424 }
2425
2426 return s;
2427}
2428
2429function isNodeLike(val) {
2430 return typeof val === 'object' && val !== null && !Array.isArray(val) && typeof val.type === 'string';
2431}
2432
2433function isSameIdentifier(a, b) {
2434 return (a.type === 'Identifier' || a.type === 'JSXIdentifier') && a.type === b.type && a.name === b.name && a.range[0] === b.range[0] && a.range[1] === b.range[1];
2435}
2436
2437function isAncestorNodeOf(a, b) {
2438 return a.range[0] <= b.range[0] && a.range[1] >= b.range[1];
2439}
2440
2441var configs = {
2442 recommended: {
2443 plugins: ['react-hooks'],
2444 rules: {
2445 'react-hooks/rules-of-hooks': 'error',
2446 'react-hooks/exhaustive-deps': 'warn'
2447 }
2448 }
2449};
2450var rules = {
2451 'rules-of-hooks': RulesOfHooks,
2452 'exhaustive-deps': ExhaustiveDeps
2453};
2454
2455exports.configs = configs;
2456exports.rules = rules;
2457 })();
2458}
Note: See TracBrowser for help on using the repository browser.