UNPKG

59.1 kBJavaScriptView Raw
1/** @license React v16.6.1
2 * eslint-plugin-react-hooks.development.js
3 *
4 * Copyright (c) Facebook, Inc. and its affiliates.
5 *
6 * This source code is licensed under the MIT license found in the
7 * LICENSE file in the root directory of this source tree.
8 */
9
10'use strict';
11
12
13
14if (process.env.NODE_ENV !== "production") {
15 (function() {
16'use strict';
17
18/* eslint-disable no-for-of-loops/no-for-of-loops */
19
20
21
22/**
23 * Catch all identifiers that begin with "use" followed by an uppercase Latin
24 * character to exclude identifiers like "user".
25 */
26
27function isHookName(s) {
28 return (/^use[A-Z0-9].*$/.test(s)
29 );
30}
31
32/**
33 * We consider hooks to be a hook name identifier or a member expression
34 * containing a hook name.
35 */
36
37function isHook(node) {
38 if (node.type === 'Identifier') {
39 return isHookName(node.name);
40 } else if (node.type === 'MemberExpression' && !node.computed && isHook(node.property)) {
41 // Only consider React.useFoo() to be namespace hooks for now to avoid false positives.
42 // We can expand this check later.
43 var obj = node.object;
44 return obj.type === 'Identifier' && obj.name === 'React';
45 } else {
46 return false;
47 }
48}
49
50/**
51 * Checks if the node is a React component name. React component names must
52 * always start with a non-lowercase letter. So `MyComponent` or `_MyComponent`
53 * are valid component names for instance.
54 */
55
56function isComponentName(node) {
57 if (node.type === 'Identifier') {
58 return !/^[a-z]/.test(node.name);
59 } else {
60 return false;
61 }
62}
63
64function isInsideComponentOrHook(node) {
65 while (node) {
66 var functionName = getFunctionName(node);
67 if (functionName) {
68 if (isComponentName(functionName) || isHook(functionName)) {
69 return true;
70 }
71 }
72 node = node.parent;
73 }
74 return false;
75}
76
77var RuleOfHooks = {
78 create: function (context) {
79 var codePathReactHooksMapStack = [];
80 var codePathSegmentStack = [];
81 return {
82 // Maintain code segment path stack as we traverse.
83 onCodePathSegmentStart: function (segment) {
84 return codePathSegmentStack.push(segment);
85 },
86 onCodePathSegmentEnd: function () {
87 return codePathSegmentStack.pop();
88 },
89
90 // Maintain code path stack as we traverse.
91 onCodePathStart: function () {
92 return codePathReactHooksMapStack.push(new Map());
93 },
94
95 // Process our code path.
96 //
97 // Everything is ok if all React Hooks are both reachable from the initial
98 // segment and reachable from every final segment.
99 onCodePathEnd: function (codePath, codePathNode) {
100 var reactHooksMap = codePathReactHooksMapStack.pop();
101 if (reactHooksMap.size === 0) {
102 return;
103 }
104
105 // All of the segments which are cyclic are recorded in this set.
106 var cyclic = new Set();
107
108 /**
109 * Count the number of code paths from the start of the function to this
110 * segment. For example:
111 *
112 * ```js
113 * function MyComponent() {
114 * if (condition) {
115 * // Segment 1
116 * } else {
117 * // Segment 2
118 * }
119 * // Segment 3
120 * }
121 * ```
122 *
123 * Segments 1 and 2 have one path to the beginning of `MyComponent` and
124 * segment 3 has two paths to the beginning of `MyComponent` since we
125 * could have either taken the path of segment 1 or segment 2.
126 *
127 * Populates `cyclic` with cyclic segments.
128 */
129
130 function countPathsFromStart(segment) {
131 var cache = countPathsFromStart.cache;
132
133 var paths = cache.get(segment.id);
134
135 // If `paths` is null then we've found a cycle! Add it to `cyclic` and
136 // any other segments which are a part of this cycle.
137 if (paths === null) {
138 if (cyclic.has(segment.id)) {
139 return 0;
140 } else {
141 cyclic.add(segment.id);
142 var _iteratorNormalCompletion = true;
143 var _didIteratorError = false;
144 var _iteratorError = undefined;
145
146 try {
147 for (var _iterator = segment.prevSegments[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {
148 var prevSegment = _step.value;
149
150 countPathsFromStart(prevSegment);
151 }
152 } catch (err) {
153 _didIteratorError = true;
154 _iteratorError = err;
155 } finally {
156 try {
157 if (!_iteratorNormalCompletion && _iterator.return) {
158 _iterator.return();
159 }
160 } finally {
161 if (_didIteratorError) {
162 throw _iteratorError;
163 }
164 }
165 }
166
167 return 0;
168 }
169 }
170
171 // We have a cached `paths`. Return it.
172 if (paths !== undefined) {
173 return paths;
174 }
175
176 // Compute `paths` and cache it. Guarding against cycles.
177 cache.set(segment.id, null);
178 if (codePath.thrownSegments.includes(segment)) {
179 paths = 0;
180 } else if (segment.prevSegments.length === 0) {
181 paths = 1;
182 } else {
183 paths = 0;
184 var _iteratorNormalCompletion2 = true;
185 var _didIteratorError2 = false;
186 var _iteratorError2 = undefined;
187
188 try {
189 for (var _iterator2 = segment.prevSegments[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) {
190 var _prevSegment = _step2.value;
191
192 paths += countPathsFromStart(_prevSegment);
193 }
194 } catch (err) {
195 _didIteratorError2 = true;
196 _iteratorError2 = err;
197 } finally {
198 try {
199 if (!_iteratorNormalCompletion2 && _iterator2.return) {
200 _iterator2.return();
201 }
202 } finally {
203 if (_didIteratorError2) {
204 throw _iteratorError2;
205 }
206 }
207 }
208 }
209
210 // If our segment is reachable then there should be at least one path
211 // to it from the start of our code path.
212 if (segment.reachable && paths === 0) {
213 cache.delete(segment.id);
214 } else {
215 cache.set(segment.id, paths);
216 }
217
218 return paths;
219 }
220
221 /**
222 * Count the number of code paths from this segment to the end of the
223 * function. For example:
224 *
225 * ```js
226 * function MyComponent() {
227 * // Segment 1
228 * if (condition) {
229 * // Segment 2
230 * } else {
231 * // Segment 3
232 * }
233 * }
234 * ```
235 *
236 * Segments 2 and 3 have one path to the end of `MyComponent` and
237 * segment 1 has two paths to the end of `MyComponent` since we could
238 * either take the path of segment 1 or segment 2.
239 *
240 * Populates `cyclic` with cyclic segments.
241 */
242
243 function countPathsToEnd(segment) {
244 var cache = countPathsToEnd.cache;
245
246 var paths = cache.get(segment.id);
247
248 // If `paths` is null then we've found a cycle! Add it to `cyclic` and
249 // any other segments which are a part of this cycle.
250 if (paths === null) {
251 if (cyclic.has(segment.id)) {
252 return 0;
253 } else {
254 cyclic.add(segment.id);
255 var _iteratorNormalCompletion3 = true;
256 var _didIteratorError3 = false;
257 var _iteratorError3 = undefined;
258
259 try {
260 for (var _iterator3 = segment.nextSegments[Symbol.iterator](), _step3; !(_iteratorNormalCompletion3 = (_step3 = _iterator3.next()).done); _iteratorNormalCompletion3 = true) {
261 var nextSegment = _step3.value;
262
263 countPathsToEnd(nextSegment);
264 }
265 } catch (err) {
266 _didIteratorError3 = true;
267 _iteratorError3 = err;
268 } finally {
269 try {
270 if (!_iteratorNormalCompletion3 && _iterator3.return) {
271 _iterator3.return();
272 }
273 } finally {
274 if (_didIteratorError3) {
275 throw _iteratorError3;
276 }
277 }
278 }
279
280 return 0;
281 }
282 }
283
284 // We have a cached `paths`. Return it.
285 if (paths !== undefined) {
286 return paths;
287 }
288
289 // Compute `paths` and cache it. Guarding against cycles.
290 cache.set(segment.id, null);
291 if (codePath.thrownSegments.includes(segment)) {
292 paths = 0;
293 } else if (segment.nextSegments.length === 0) {
294 paths = 1;
295 } else {
296 paths = 0;
297 var _iteratorNormalCompletion4 = true;
298 var _didIteratorError4 = false;
299 var _iteratorError4 = undefined;
300
301 try {
302 for (var _iterator4 = segment.nextSegments[Symbol.iterator](), _step4; !(_iteratorNormalCompletion4 = (_step4 = _iterator4.next()).done); _iteratorNormalCompletion4 = true) {
303 var _nextSegment = _step4.value;
304
305 paths += countPathsToEnd(_nextSegment);
306 }
307 } catch (err) {
308 _didIteratorError4 = true;
309 _iteratorError4 = err;
310 } finally {
311 try {
312 if (!_iteratorNormalCompletion4 && _iterator4.return) {
313 _iterator4.return();
314 }
315 } finally {
316 if (_didIteratorError4) {
317 throw _iteratorError4;
318 }
319 }
320 }
321 }
322 cache.set(segment.id, paths);
323
324 return paths;
325 }
326
327 /**
328 * Gets the shortest path length to the start of a code path.
329 * For example:
330 *
331 * ```js
332 * function MyComponent() {
333 * if (condition) {
334 * // Segment 1
335 * }
336 * // Segment 2
337 * }
338 * ```
339 *
340 * There is only one path from segment 1 to the code path start. Its
341 * length is one so that is the shortest path.
342 *
343 * There are two paths from segment 2 to the code path start. One
344 * through segment 1 with a length of two and another directly to the
345 * start with a length of one. The shortest path has a length of one
346 * so we would return that.
347 */
348
349 function shortestPathLengthToStart(segment) {
350 var cache = shortestPathLengthToStart.cache;
351
352 var length = cache.get(segment.id);
353
354 // If `length` is null then we found a cycle! Return infinity since
355 // the shortest path is definitely not the one where we looped.
356 if (length === null) {
357 return Infinity;
358 }
359
360 // We have a cached `length`. Return it.
361 if (length !== undefined) {
362 return length;
363 }
364
365 // Compute `length` and cache it. Guarding against cycles.
366 cache.set(segment.id, null);
367 if (segment.prevSegments.length === 0) {
368 length = 1;
369 } else {
370 length = Infinity;
371 var _iteratorNormalCompletion5 = true;
372 var _didIteratorError5 = false;
373 var _iteratorError5 = undefined;
374
375 try {
376 for (var _iterator5 = segment.prevSegments[Symbol.iterator](), _step5; !(_iteratorNormalCompletion5 = (_step5 = _iterator5.next()).done); _iteratorNormalCompletion5 = true) {
377 var prevSegment = _step5.value;
378
379 var prevLength = shortestPathLengthToStart(prevSegment);
380 if (prevLength < length) {
381 length = prevLength;
382 }
383 }
384 } catch (err) {
385 _didIteratorError5 = true;
386 _iteratorError5 = err;
387 } finally {
388 try {
389 if (!_iteratorNormalCompletion5 && _iterator5.return) {
390 _iterator5.return();
391 }
392 } finally {
393 if (_didIteratorError5) {
394 throw _iteratorError5;
395 }
396 }
397 }
398
399 length += 1;
400 }
401 cache.set(segment.id, length);
402 return length;
403 }
404
405 countPathsFromStart.cache = new Map();
406 countPathsToEnd.cache = new Map();
407 shortestPathLengthToStart.cache = new Map();
408
409 // Count all code paths to the end of our component/hook. Also primes
410 // the `countPathsToEnd` cache.
411 var allPathsFromStartToEnd = countPathsToEnd(codePath.initialSegment);
412
413 // Gets the function name for our code path. If the function name is
414 // `undefined` then we know either that we have an anonymous function
415 // expression or our code path is not in a function. In both cases we
416 // will want to error since neither are React function components or
417 // hook functions.
418 var codePathFunctionName = getFunctionName(codePathNode);
419
420 // This is a valid code path for React hooks if we are directly in a React
421 // function component or we are in a hook function.
422 var isSomewhereInsideComponentOrHook = isInsideComponentOrHook(codePathNode);
423 var isDirectlyInsideComponentOrHook = codePathFunctionName ? isComponentName(codePathFunctionName) || isHook(codePathFunctionName) : false;
424
425 // Compute the earliest finalizer level using information from the
426 // cache. We expect all reachable final segments to have a cache entry
427 // after calling `visitSegment()`.
428 var shortestFinalPathLength = Infinity;
429 var _iteratorNormalCompletion6 = true;
430 var _didIteratorError6 = false;
431 var _iteratorError6 = undefined;
432
433 try {
434 for (var _iterator6 = codePath.finalSegments[Symbol.iterator](), _step6; !(_iteratorNormalCompletion6 = (_step6 = _iterator6.next()).done); _iteratorNormalCompletion6 = true) {
435 var finalSegment = _step6.value;
436
437 if (!finalSegment.reachable) {
438 continue;
439 }
440 var length = shortestPathLengthToStart(finalSegment);
441 if (length < shortestFinalPathLength) {
442 shortestFinalPathLength = length;
443 }
444 }
445
446 // Make sure all React Hooks pass our lint invariants. Log warnings
447 // if not.
448 } catch (err) {
449 _didIteratorError6 = true;
450 _iteratorError6 = err;
451 } finally {
452 try {
453 if (!_iteratorNormalCompletion6 && _iterator6.return) {
454 _iterator6.return();
455 }
456 } finally {
457 if (_didIteratorError6) {
458 throw _iteratorError6;
459 }
460 }
461 }
462
463 var _iteratorNormalCompletion7 = true;
464 var _didIteratorError7 = false;
465 var _iteratorError7 = undefined;
466
467 try {
468 for (var _iterator7 = reactHooksMap[Symbol.iterator](), _step7; !(_iteratorNormalCompletion7 = (_step7 = _iterator7.next()).done); _iteratorNormalCompletion7 = true) {
469 var _step7$value = _step7.value,
470 segment = _step7$value[0],
471 reactHooks = _step7$value[1];
472
473 // NOTE: We could report here that the hook is not reachable, but
474 // that would be redundant with more general "no unreachable"
475 // lint rules.
476 if (!segment.reachable) {
477 continue;
478 }
479
480 // If there are any final segments with a shorter path to start then
481 // we possibly have an early return.
482 //
483 // If our segment is a final segment itself then siblings could
484 // possibly be early returns.
485 var possiblyHasEarlyReturn = segment.nextSegments.length === 0 ? shortestFinalPathLength <= shortestPathLengthToStart(segment) : shortestFinalPathLength < shortestPathLengthToStart(segment);
486
487 // Count all the paths from the start of our code path to the end of
488 // our code path that go _through_ this segment. The critical piece
489 // of this is _through_. If we just call `countPathsToEnd(segment)`
490 // then we neglect that we may have gone through multiple paths to get
491 // to this point! Consider:
492 //
493 // ```js
494 // function MyComponent() {
495 // if (a) {
496 // // Segment 1
497 // } else {
498 // // Segment 2
499 // }
500 // // Segment 3
501 // if (b) {
502 // // Segment 4
503 // } else {
504 // // Segment 5
505 // }
506 // }
507 // ```
508 //
509 // In this component we have four code paths:
510 //
511 // 1. `a = true; b = true`
512 // 2. `a = true; b = false`
513 // 3. `a = false; b = true`
514 // 4. `a = false; b = false`
515 //
516 // From segment 3 there are two code paths to the end through segment
517 // 4 and segment 5. However, we took two paths to get here through
518 // segment 1 and segment 2.
519 //
520 // If we multiply the paths from start (two) by the paths to end (two)
521 // for segment 3 we get four. Which is our desired count.
522 var pathsFromStartToEnd = countPathsFromStart(segment) * countPathsToEnd(segment);
523
524 // Is this hook a part of a cyclic segment?
525 var cycled = cyclic.has(segment.id);
526
527 var _iteratorNormalCompletion8 = true;
528 var _didIteratorError8 = false;
529 var _iteratorError8 = undefined;
530
531 try {
532 for (var _iterator8 = reactHooks[Symbol.iterator](), _step8; !(_iteratorNormalCompletion8 = (_step8 = _iterator8.next()).done); _iteratorNormalCompletion8 = true) {
533 var hook = _step8.value;
534
535 // Report an error if a hook may be called more then once.
536 if (cycled) {
537 context.report({
538 node: hook,
539 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.'
540 });
541 }
542
543 // If this is not a valid code path for React hooks then we need to
544 // log a warning for every hook in this code path.
545 //
546 // Pick a special message depending on the scope this hook was
547 // called in.
548 if (isDirectlyInsideComponentOrHook) {
549 // Report an error if a hook does not reach all finalizing code
550 // path segments.
551 //
552 // Special case when we think there might be an early return.
553 if (!cycled && pathsFromStartToEnd !== allPathsFromStartToEnd) {
554 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?' : '');
555 context.report({ node: hook, message: message });
556 }
557 } else if (codePathNode.parent && (codePathNode.parent.type === 'MethodDefinition' || codePathNode.parent.type === 'ClassProperty') && codePathNode.parent.value === codePathNode) {
558 // Ignore class methods for now because they produce too many
559 // false positives due to feature flag checks. We're less
560 // sensitive to them in classes because hooks would produce
561 // runtime errors in classes anyway, and because a use*()
562 // call in a class, if it works, is unambiguously *not* a hook.
563 } else if (codePathFunctionName) {
564 // Custom message if we found an invalid function name.
565 var _message = 'React Hook "' + context.getSource(hook) + '" is called in ' + ('function "' + context.getSource(codePathFunctionName) + '" ') + 'which is neither a React function component or a custom ' + 'React Hook function.';
566 context.report({ node: hook, message: _message });
567 } else if (codePathNode.type === 'Program') {
568 // For now, ignore if it's in top level scope.
569 // We could warn here but there are false positives related
570 // configuring libraries like `history`.
571 } else {
572 // Assume in all other cases the user called a hook in some
573 // random function callback. This should usually be true for
574 // anonymous function expressions. Hopefully this is clarifying
575 // enough in the common case that the incorrect message in
576 // uncommon cases doesn't matter.
577 if (isSomewhereInsideComponentOrHook) {
578 var _message2 = '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.';
579 context.report({ node: hook, message: _message2 });
580 }
581 }
582 }
583 } catch (err) {
584 _didIteratorError8 = true;
585 _iteratorError8 = err;
586 } finally {
587 try {
588 if (!_iteratorNormalCompletion8 && _iterator8.return) {
589 _iterator8.return();
590 }
591 } finally {
592 if (_didIteratorError8) {
593 throw _iteratorError8;
594 }
595 }
596 }
597 }
598 } catch (err) {
599 _didIteratorError7 = true;
600 _iteratorError7 = err;
601 } finally {
602 try {
603 if (!_iteratorNormalCompletion7 && _iterator7.return) {
604 _iterator7.return();
605 }
606 } finally {
607 if (_didIteratorError7) {
608 throw _iteratorError7;
609 }
610 }
611 }
612 },
613
614
615 // Missed opportunity...We could visit all `Identifier`s instead of all
616 // `CallExpression`s and check that _every use_ of a hook name is valid.
617 // But that gets complicated and enters type-system territory, so we're
618 // only being strict about hook calls for now.
619 CallExpression: function (node) {
620 if (isHook(node.callee)) {
621 // Add the hook node to a map keyed by the code path segment. We will
622 // do full code path analysis at the end of our code path.
623 var reactHooksMap = last(codePathReactHooksMapStack);
624 var codePathSegment = last(codePathSegmentStack);
625 var reactHooks = reactHooksMap.get(codePathSegment);
626 if (!reactHooks) {
627 reactHooks = [];
628 reactHooksMap.set(codePathSegment, reactHooks);
629 }
630 reactHooks.push(node.callee);
631 }
632 }
633 };
634 }
635};
636
637/**
638 * Gets the static name of a function AST node. For function declarations it is
639 * easy. For anonymous function expressions it is much harder. If you search for
640 * `IsAnonymousFunctionDefinition()` in the ECMAScript spec you'll find places
641 * where JS gives anonymous function expressions names. We roughly detect the
642 * same AST nodes with some exceptions to better fit our usecase.
643 */
644
645function getFunctionName(node) {
646 if (node.type === 'FunctionDeclaration' || node.type === 'FunctionExpression' && node.id) {
647 // function useHook() {}
648 // const whatever = function useHook() {};
649 //
650 // Function declaration or function expression names win over any
651 // assignment statements or other renames.
652 return node.id;
653 } else if (node.type === 'FunctionExpression' || node.type === 'ArrowFunctionExpression') {
654 if (node.parent.type === 'VariableDeclarator' && node.parent.init === node) {
655 // const useHook = () => {};
656 return node.parent.id;
657 } else if (node.parent.type === 'AssignmentExpression' && node.parent.right === node && node.parent.operator === '=') {
658 // useHook = () => {};
659 return node.parent.left;
660 } else if (node.parent.type === 'Property' && node.parent.value === node && !node.parent.computed) {
661 // {useHook: () => {}}
662 // {useHook() {}}
663 return node.parent.key;
664
665 // NOTE: We could also support `ClassProperty` and `MethodDefinition`
666 // here to be pedantic. However, hooks in a class are an anti-pattern. So
667 // we don't allow it to error early.
668 //
669 // class {useHook = () => {}}
670 // class {useHook() {}}
671 } else if (node.parent.type === 'AssignmentPattern' && node.parent.right === node && !node.parent.computed) {
672 // const {useHook = () => {}} = {};
673 // ({useHook = () => {}} = {});
674 //
675 // Kinda clowny, but we'd said we'd follow spec convention for
676 // `IsAnonymousFunctionDefinition()` usage.
677 return node.parent.left;
678 } else {
679 return undefined;
680 }
681 } else {
682 return undefined;
683 }
684}
685
686/**
687 * Convenience function for peeking the last item in a stack.
688 */
689
690function last(array) {
691 return array[array.length - 1];
692}
693
694/* eslint-disable no-for-of-loops/no-for-of-loops */
695
696
697
698var ExhaustiveDeps = {
699 meta: {
700 fixable: 'code',
701 schema: [{
702 type: 'object',
703 additionalProperties: false,
704 properties: {
705 additionalHooks: {
706 type: 'string'
707 }
708 }
709 }]
710 },
711 create: function (context) {
712 // Parse the `additionalHooks` regex.
713 var additionalHooks = context.options && context.options[0] && context.options[0].additionalHooks ? new RegExp(context.options[0].additionalHooks) : undefined;
714 var options = { additionalHooks: additionalHooks };
715
716 return {
717 FunctionExpression: visitFunctionExpression,
718 ArrowFunctionExpression: visitFunctionExpression
719 };
720
721 /**
722 * Visitor for both function expressions and arrow function expressions.
723 */
724 function visitFunctionExpression(node) {
725 // We only want to lint nodes which are reactive hook callbacks.
726 if (node.type !== 'FunctionExpression' && node.type !== 'ArrowFunctionExpression' || node.parent.type !== 'CallExpression') {
727 return;
728 }
729
730 var callbackIndex = getReactiveHookCallbackIndex(node.parent.callee, options);
731 if (node.parent.arguments[callbackIndex] !== node) {
732 return;
733 }
734
735 // Get the reactive hook node.
736 var reactiveHook = node.parent.callee;
737 var reactiveHookName = getNodeWithoutReactNamespace(reactiveHook).name;
738 var isEffect = reactiveHookName.endsWith('Effect');
739
740 // Get the declared dependencies for this reactive hook. If there is no
741 // second argument then the reactive callback will re-run on every render.
742 // So no need to check for dependency inclusion.
743 var depsIndex = callbackIndex + 1;
744 var declaredDependenciesNode = node.parent.arguments[depsIndex];
745 if (!declaredDependenciesNode) {
746 return;
747 }
748
749 // Get the current scope.
750 var scope = context.getScope();
751
752 // Find all our "pure scopes". On every re-render of a component these
753 // pure scopes may have changes to the variables declared within. So all
754 // variables used in our reactive hook callback but declared in a pure
755 // scope need to be listed as dependencies of our reactive hook callback.
756 //
757 // According to the rules of React you can't read a mutable value in pure
758 // scope. We can't enforce this in a lint so we trust that all variables
759 // declared outside of pure scope are indeed frozen.
760 var pureScopes = new Set();
761 var componentScope = null;
762 {
763 var currentScope = scope.upper;
764 while (currentScope) {
765 pureScopes.add(currentScope);
766 if (currentScope.type === 'function') {
767 break;
768 }
769 currentScope = currentScope.upper;
770 }
771 // If there is no parent function scope then there are no pure scopes.
772 // The ones we've collected so far are incorrect. So don't continue with
773 // the lint.
774 if (!currentScope) {
775 return;
776 }
777 componentScope = currentScope;
778 }
779
780 // These are usually mistaken. Collect them.
781 var currentRefsInEffectCleanup = new Map();
782
783 // Is this reference inside a cleanup function for this effect node?
784 // We can check by traversing scopes upwards from the reference, and checking
785 // if the last "return () => " we encounter is located directly inside the effect.
786 function isInsideEffectCleanup(reference) {
787 var curScope = reference.from;
788 var isInReturnedFunction = false;
789 while (curScope.block !== node) {
790 if (curScope.type === 'function') {
791 isInReturnedFunction = curScope.block.parent != null && curScope.block.parent.type === 'ReturnStatement';
792 }
793 curScope = curScope.upper;
794 }
795 return isInReturnedFunction;
796 }
797
798 // Get dependencies from all our resolved references in pure scopes.
799 // Key is dependency string, value is whether it's static.
800 var dependencies = new Map();
801 gatherDependenciesRecursively(scope);
802
803 function gatherDependenciesRecursively(currentScope) {
804 var _iteratorNormalCompletion = true;
805 var _didIteratorError = false;
806 var _iteratorError = undefined;
807
808 try {
809 for (var _iterator = currentScope.references[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {
810 var reference = _step.value;
811
812 // If this reference is not resolved or it is not declared in a pure
813 // scope then we don't care about this reference.
814 if (!reference.resolved) {
815 continue;
816 }
817 if (!pureScopes.has(reference.resolved.scope)) {
818 continue;
819 }
820
821 // Narrow the scope of a dependency if it is, say, a member expression.
822 // Then normalize the narrowed dependency.
823 var referenceNode = fastFindReferenceWithParent(node, reference.identifier);
824 var dependencyNode = getDependency(referenceNode);
825 var dependency = toPropertyAccessString(dependencyNode);
826
827 // Accessing ref.current inside effect cleanup is bad.
828 if (
829 // We're in an effect...
830 isEffect &&
831 // ... and this look like accessing .current...
832 dependencyNode.type === 'Identifier' && dependencyNode.parent.type === 'MemberExpression' && !dependencyNode.parent.computed && dependencyNode.parent.property.type === 'Identifier' && dependencyNode.parent.property.name === 'current' &&
833 // ...in a cleanup function or below...
834 isInsideEffectCleanup(reference)) {
835 currentRefsInEffectCleanup.set(dependency, {
836 reference: reference,
837 dependencyNode: dependencyNode
838 });
839 }
840
841 // Add the dependency to a map so we can make sure it is referenced
842 // again in our dependencies array. Remember whether it's static.
843 if (!dependencies.has(dependency)) {
844 var isStatic = isDefinitelyStaticDependency(reference);
845 dependencies.set(dependency, {
846 isStatic: isStatic,
847 reference: reference
848 });
849 }
850 }
851 } catch (err) {
852 _didIteratorError = true;
853 _iteratorError = err;
854 } finally {
855 try {
856 if (!_iteratorNormalCompletion && _iterator.return) {
857 _iterator.return();
858 }
859 } finally {
860 if (_didIteratorError) {
861 throw _iteratorError;
862 }
863 }
864 }
865
866 var _iteratorNormalCompletion2 = true;
867 var _didIteratorError2 = false;
868 var _iteratorError2 = undefined;
869
870 try {
871 for (var _iterator2 = currentScope.childScopes[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) {
872 var childScope = _step2.value;
873
874 gatherDependenciesRecursively(childScope);
875 }
876 } catch (err) {
877 _didIteratorError2 = true;
878 _iteratorError2 = err;
879 } finally {
880 try {
881 if (!_iteratorNormalCompletion2 && _iterator2.return) {
882 _iterator2.return();
883 }
884 } finally {
885 if (_didIteratorError2) {
886 throw _iteratorError2;
887 }
888 }
889 }
890 }
891
892 // Warn about accessing .current in cleanup effects.
893 currentRefsInEffectCleanup.forEach(function (_ref, dependency) {
894 var reference = _ref.reference,
895 dependencyNode = _ref.dependencyNode;
896
897 var references = reference.resolved.references;
898 // Is React managing this ref or us?
899 // Let's see if we can find a .current assignment.
900 var foundCurrentAssignment = false;
901 for (var i = 0; i < references.length; i++) {
902 var identifier = references[i].identifier;
903 var parent = identifier.parent;
904
905 if (parent != null &&
906 // ref.current
907 parent.type === 'MemberExpression' && !parent.computed && parent.property.type === 'Identifier' && parent.property.name === 'current' &&
908 // ref.current = <something>
909 parent.parent.type === 'AssignmentExpression' && parent.parent.left === parent) {
910 foundCurrentAssignment = true;
911 break;
912 }
913 }
914 // We only want to warn about React-managed refs.
915 if (foundCurrentAssignment) {
916 return;
917 }
918 context.report({
919 node: dependencyNode.parent.property,
920 message: 'Accessing \'' + dependency + '.current\' during the effect cleanup ' + 'will likely read a different ref value because by this time React ' + 'has already updated the ref. If this ref is managed by React, store ' + ('\'' + dependency + '.current\' in a variable inside ') + 'the effect itself and refer to that variable from the cleanup function.'
921 });
922 });
923
924 var declaredDependencies = [];
925 var externalDependencies = new Set();
926 if (declaredDependenciesNode.type !== 'ArrayExpression') {
927 // If the declared dependencies are not an array expression then we
928 // can't verify that the user provided the correct dependencies. Tell
929 // the user this in an error.
930 context.report({
931 node: declaredDependenciesNode,
932 message: 'React Hook ' + context.getSource(reactiveHook) + ' has a second ' + "argument which is not an array literal. This means we can't " + "statically verify whether you've passed the correct dependencies."
933 });
934 } else {
935 declaredDependenciesNode.elements.forEach(function (declaredDependencyNode) {
936 // Skip elided elements.
937 if (declaredDependencyNode === null) {
938 return;
939 }
940 // If we see a spread element then add a special warning.
941 if (declaredDependencyNode.type === 'SpreadElement') {
942 context.report({
943 node: declaredDependencyNode,
944 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.'
945 });
946 return;
947 }
948 // Try to normalize the declared dependency. If we can't then an error
949 // will be thrown. We will catch that error and report an error.
950 var declaredDependency = void 0;
951 try {
952 declaredDependency = toPropertyAccessString(declaredDependencyNode);
953 } catch (error) {
954 if (/Unsupported node type/.test(error.message)) {
955 if (declaredDependencyNode.type === 'Literal') {
956 if (typeof declaredDependencyNode.value === 'string') {
957 context.report({
958 node: declaredDependencyNode,
959 message: 'The ' + declaredDependencyNode.raw + ' string literal is not a valid dependency ' + 'because it never changes. Did you mean to ' + ('include ' + declaredDependencyNode.value + ' in the array instead?')
960 });
961 } else {
962 context.report({
963 node: declaredDependencyNode,
964 message: 'The \'' + declaredDependencyNode.raw + '\' literal is not a valid dependency ' + 'because it never changes. You can safely remove it.'
965 });
966 }
967 } else {
968 context.report({
969 node: declaredDependencyNode,
970 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.'
971 });
972 }
973
974 return;
975 } else {
976 throw error;
977 }
978 }
979
980 var maybeID = declaredDependencyNode;
981 while (maybeID.type === 'MemberExpression') {
982 maybeID = maybeID.object;
983 }
984 var isDeclaredInComponent = !componentScope.through.some(function (ref) {
985 return ref.identifier === maybeID;
986 });
987
988 // Add the dependency to our declared dependency map.
989 declaredDependencies.push({
990 key: declaredDependency,
991 node: declaredDependencyNode
992 });
993
994 if (!isDeclaredInComponent) {
995 externalDependencies.add(declaredDependency);
996 }
997 });
998 }
999
1000 // Warn about assigning to variables in the outer scope.
1001 // Those are usually bugs.
1002 var foundStaleAssignments = false;
1003 function reportStaleAssignment(writeExpr, key) {
1004 foundStaleAssignments = true;
1005 context.report({
1006 node: writeExpr,
1007 message: 'Assignments to the \'' + key + '\' variable from inside a React ' + context.getSource(reactiveHook) + ' Hook ' + 'will not persist between re-renders. ' + 'If it\'s only needed by this Hook, move the variable inside it. ' + 'Alternatively, declare a ref with the useRef Hook, ' + 'and keep the mutable value in its \'current\' property.'
1008 });
1009 }
1010
1011 // Remember which deps are optional and report bad usage first.
1012 var optionalDependencies = new Set();
1013 dependencies.forEach(function (_ref2, key) {
1014 var isStatic = _ref2.isStatic,
1015 reference = _ref2.reference;
1016
1017 if (isStatic) {
1018 optionalDependencies.add(key);
1019 }
1020 if (reference.writeExpr) {
1021 reportStaleAssignment(reference.writeExpr, key);
1022 }
1023 });
1024 if (foundStaleAssignments) {
1025 // The intent isn't clear so we'll wait until you fix those first.
1026 return;
1027 }
1028
1029 var _collectRecommendatio = collectRecommendations({
1030 dependencies: dependencies,
1031 declaredDependencies: declaredDependencies,
1032 optionalDependencies: optionalDependencies,
1033 externalDependencies: externalDependencies,
1034 isEffect: isEffect
1035 }),
1036 suggestedDependencies = _collectRecommendatio.suggestedDependencies,
1037 unnecessaryDependencies = _collectRecommendatio.unnecessaryDependencies,
1038 missingDependencies = _collectRecommendatio.missingDependencies,
1039 duplicateDependencies = _collectRecommendatio.duplicateDependencies;
1040
1041 var problemCount = duplicateDependencies.size + missingDependencies.size + unnecessaryDependencies.size;
1042 if (problemCount === 0) {
1043 return;
1044 }
1045
1046 // If we're going to report a missing dependency,
1047 // we might as well recalculate the list ignoring
1048 // the currently specified deps. This can result
1049 // in some extra deduplication. We can't do this
1050 // for effects though because those have legit
1051 // use cases for over-specifying deps.
1052 if (!isEffect && missingDependencies.size > 0) {
1053 suggestedDependencies = collectRecommendations({
1054 dependencies: dependencies,
1055 declaredDependencies: [], // Pretend we don't know
1056 optionalDependencies: optionalDependencies,
1057 externalDependencies: externalDependencies,
1058 isEffect: isEffect
1059 }).suggestedDependencies;
1060 }
1061
1062 // Alphabetize the suggestions, but only if deps were already alphabetized.
1063 function areDeclaredDepsAlphabetized() {
1064 if (declaredDependencies.length === 0) {
1065 return true;
1066 }
1067 var declaredDepKeys = declaredDependencies.map(function (dep) {
1068 return dep.key;
1069 });
1070 var sortedDeclaredDepKeys = declaredDepKeys.slice().sort();
1071 return declaredDepKeys.join(',') === sortedDeclaredDepKeys.join(',');
1072 }
1073 if (areDeclaredDepsAlphabetized()) {
1074 suggestedDependencies.sort();
1075 }
1076
1077 function getWarningMessage(deps, singlePrefix, label, fixVerb) {
1078 if (deps.size === 0) {
1079 return null;
1080 }
1081 return (deps.size > 1 ? '' : singlePrefix + ' ') + label + ' ' + (deps.size > 1 ? 'dependencies' : 'dependency') + ': ' + joinEnglish(Array.from(deps).sort().map(function (name) {
1082 return "'" + name + "'";
1083 })) + ('. Either ' + fixVerb + ' ' + (deps.size > 1 ? 'them' : 'it') + ' or remove the dependency array.');
1084 }
1085
1086 var extraWarning = '';
1087 if (unnecessaryDependencies.size > 0) {
1088 var badRef = null;
1089 Array.from(unnecessaryDependencies.keys()).forEach(function (key) {
1090 if (badRef !== null) {
1091 return;
1092 }
1093 if (key.endsWith('.current')) {
1094 badRef = key;
1095 }
1096 });
1097 if (badRef !== null) {
1098 extraWarning = ' Mutable values like \'' + badRef + '\' aren\'t valid dependencies ' + "because their mutation doesn't re-render the component.";
1099 } else if (externalDependencies.size > 0) {
1100 var dep = Array.from(externalDependencies)[0];
1101 extraWarning = ' Values like \'' + dep + '\' aren\'t valid dependencies ' + 'because their mutation doesn\'t re-render the component.';
1102 }
1103 }
1104
1105 // `props.foo()` marks `props` as a dependency because it has
1106 // a `this` value. This warning can be confusing.
1107 // So if we're going to show it, append a clarification.
1108 if (!extraWarning && missingDependencies.has('props')) {
1109 var propDep = dependencies.get('props');
1110 if (propDep == null) {
1111 return;
1112 }
1113 var refs = propDep.reference.resolved.references;
1114 if (!Array.isArray(refs)) {
1115 return;
1116 }
1117 var isPropsOnlyUsedInMembers = true;
1118 for (var i = 0; i < refs.length; i++) {
1119 var ref = refs[i];
1120 var id = fastFindReferenceWithParent(componentScope.block, ref.identifier);
1121 if (!id) {
1122 isPropsOnlyUsedInMembers = false;
1123 break;
1124 }
1125 var parent = id.parent;
1126 if (parent == null) {
1127 isPropsOnlyUsedInMembers = false;
1128 break;
1129 }
1130 if (parent.type !== 'MemberExpression') {
1131 isPropsOnlyUsedInMembers = false;
1132 break;
1133 }
1134 }
1135 if (isPropsOnlyUsedInMembers) {
1136 extraWarning = ' Alternatively, destructure the necessary props ' + 'outside the callback.';
1137 }
1138 }
1139
1140 context.report({
1141 node: declaredDependenciesNode,
1142 message: 'React Hook ' + context.getSource(reactiveHook) + ' has ' + (
1143 // To avoid a long message, show the next actionable item.
1144 getWarningMessage(missingDependencies, 'a', 'missing', 'include') || getWarningMessage(unnecessaryDependencies, 'an', 'unnecessary', 'exclude') || getWarningMessage(duplicateDependencies, 'a', 'duplicate', 'omit')) + extraWarning,
1145 fix: function (fixer) {
1146 // TODO: consider preserving the comments or formatting?
1147 return fixer.replaceText(declaredDependenciesNode, '[' + suggestedDependencies.join(', ') + ']');
1148 }
1149 });
1150 }
1151 }
1152};
1153
1154// The meat of the logic.
1155function collectRecommendations(_ref3) {
1156 var dependencies = _ref3.dependencies,
1157 declaredDependencies = _ref3.declaredDependencies,
1158 optionalDependencies = _ref3.optionalDependencies,
1159 externalDependencies = _ref3.externalDependencies,
1160 isEffect = _ref3.isEffect;
1161
1162 // Our primary data structure.
1163 // It is a logical representation of property chains:
1164 // `props` -> `props.foo` -> `props.foo.bar` -> `props.foo.bar.baz`
1165 // -> `props.lol`
1166 // -> `props.huh` -> `props.huh.okay`
1167 // -> `props.wow`
1168 // We'll use it to mark nodes that are *used* by the programmer,
1169 // and the nodes that were *declared* as deps. Then we will
1170 // traverse it to learn which deps are missing or unnecessary.
1171 var depTree = createDepTree();
1172 function createDepTree() {
1173 return {
1174 isRequired: false, // True if used in code
1175 isSatisfiedRecursively: false, // True if specified in deps
1176 hasRequiredNodesBelow: false, // True if something deeper is used by code
1177 children: new Map() // Nodes for properties
1178 };
1179 }
1180
1181 // Mark all required nodes first.
1182 // Imagine exclamation marks next to each used deep property.
1183 dependencies.forEach(function (_, key) {
1184 var node = getOrCreateNodeByPath(depTree, key);
1185 node.isRequired = true;
1186 markAllParentsByPath(depTree, key, function (parent) {
1187 parent.hasRequiredNodesBelow = true;
1188 });
1189 });
1190
1191 // Mark all satisfied nodes.
1192 // Imagine checkmarks next to each declared dependency.
1193 declaredDependencies.forEach(function (_ref4) {
1194 var key = _ref4.key;
1195
1196 var node = getOrCreateNodeByPath(depTree, key);
1197 node.isSatisfiedRecursively = true;
1198 });
1199 optionalDependencies.forEach(function (key) {
1200 var node = getOrCreateNodeByPath(depTree, key);
1201 node.isSatisfiedRecursively = true;
1202 });
1203
1204 // Tree manipulation helpers.
1205 function getOrCreateNodeByPath(rootNode, path) {
1206 var keys = path.split('.');
1207 var node = rootNode;
1208 var _iteratorNormalCompletion3 = true;
1209 var _didIteratorError3 = false;
1210 var _iteratorError3 = undefined;
1211
1212 try {
1213 for (var _iterator3 = keys[Symbol.iterator](), _step3; !(_iteratorNormalCompletion3 = (_step3 = _iterator3.next()).done); _iteratorNormalCompletion3 = true) {
1214 var key = _step3.value;
1215
1216 var child = node.children.get(key);
1217 if (!child) {
1218 child = createDepTree();
1219 node.children.set(key, child);
1220 }
1221 node = child;
1222 }
1223 } catch (err) {
1224 _didIteratorError3 = true;
1225 _iteratorError3 = err;
1226 } finally {
1227 try {
1228 if (!_iteratorNormalCompletion3 && _iterator3.return) {
1229 _iterator3.return();
1230 }
1231 } finally {
1232 if (_didIteratorError3) {
1233 throw _iteratorError3;
1234 }
1235 }
1236 }
1237
1238 return node;
1239 }
1240 function markAllParentsByPath(rootNode, path, fn) {
1241 var keys = path.split('.');
1242 var node = rootNode;
1243 var _iteratorNormalCompletion4 = true;
1244 var _didIteratorError4 = false;
1245 var _iteratorError4 = undefined;
1246
1247 try {
1248 for (var _iterator4 = keys[Symbol.iterator](), _step4; !(_iteratorNormalCompletion4 = (_step4 = _iterator4.next()).done); _iteratorNormalCompletion4 = true) {
1249 var key = _step4.value;
1250
1251 var child = node.children.get(key);
1252 if (!child) {
1253 return;
1254 }
1255 fn(child);
1256 node = child;
1257 }
1258 } catch (err) {
1259 _didIteratorError4 = true;
1260 _iteratorError4 = err;
1261 } finally {
1262 try {
1263 if (!_iteratorNormalCompletion4 && _iterator4.return) {
1264 _iterator4.return();
1265 }
1266 } finally {
1267 if (_didIteratorError4) {
1268 throw _iteratorError4;
1269 }
1270 }
1271 }
1272 }
1273
1274 // Now we can learn which dependencies are missing or necessary.
1275 var missingDependencies = new Set();
1276 var satisfyingDependencies = new Set();
1277 scanTreeRecursively(depTree, missingDependencies, satisfyingDependencies, function (key) {
1278 return key;
1279 });
1280 function scanTreeRecursively(node, missingPaths, satisfyingPaths, keyToPath) {
1281 node.children.forEach(function (child, key) {
1282 var path = keyToPath(key);
1283 if (child.isSatisfiedRecursively) {
1284 if (child.hasRequiredNodesBelow) {
1285 // Remember this dep actually satisfied something.
1286 satisfyingPaths.add(path);
1287 }
1288 // It doesn't matter if there's something deeper.
1289 // It would be transitively satisfied since we assume immutability.
1290 // `props.foo` is enough if you read `props.foo.id`.
1291 return;
1292 }
1293 if (child.isRequired) {
1294 // Remember that no declared deps satisfied this node.
1295 missingPaths.add(path);
1296 // If we got here, nothing in its subtree was satisfied.
1297 // No need to search further.
1298 return;
1299 }
1300 scanTreeRecursively(child, missingPaths, satisfyingPaths, function (childKey) {
1301 return path + '.' + childKey;
1302 });
1303 });
1304 }
1305
1306 // Collect suggestions in the order they were originally specified.
1307 var suggestedDependencies = [];
1308 var unnecessaryDependencies = new Set();
1309 var duplicateDependencies = new Set();
1310 declaredDependencies.forEach(function (_ref5) {
1311 var key = _ref5.key;
1312
1313 // Does this declared dep satsify a real need?
1314 if (satisfyingDependencies.has(key)) {
1315 if (suggestedDependencies.indexOf(key) === -1) {
1316 // Good one.
1317 suggestedDependencies.push(key);
1318 } else {
1319 // Duplicate.
1320 duplicateDependencies.add(key);
1321 }
1322 } else {
1323 if (isEffect && !key.endsWith('.current') && !externalDependencies.has(key)) {
1324 // Effects are allowed extra "unnecessary" deps.
1325 // Such as resetting scroll when ID changes.
1326 // Consider them legit.
1327 // The exception is ref.current which is always wrong.
1328 if (suggestedDependencies.indexOf(key) === -1) {
1329 suggestedDependencies.push(key);
1330 }
1331 } else {
1332 // It's definitely not needed.
1333 unnecessaryDependencies.add(key);
1334 }
1335 }
1336 });
1337
1338 // Then add the missing ones at the end.
1339 missingDependencies.forEach(function (key) {
1340 suggestedDependencies.push(key);
1341 });
1342
1343 return {
1344 suggestedDependencies: suggestedDependencies,
1345 unnecessaryDependencies: unnecessaryDependencies,
1346 duplicateDependencies: duplicateDependencies,
1347 missingDependencies: missingDependencies
1348 };
1349}
1350
1351/**
1352 * Assuming () means the passed/returned node:
1353 * (props) => (props)
1354 * props.(foo) => (props.foo)
1355 * props.foo.(bar) => (props).foo.bar
1356 * props.foo.bar.(baz) => (props).foo.bar.baz
1357 */
1358function getDependency(node) {
1359 if (node.parent.type === 'MemberExpression' && node.parent.object === node && node.parent.property.name !== 'current' && !node.parent.computed && !(node.parent.parent != null && node.parent.parent.type === 'CallExpression' && node.parent.parent.callee === node.parent)) {
1360 return getDependency(node.parent);
1361 } else {
1362 return node;
1363 }
1364}
1365
1366/**
1367 * Assuming () means the passed node.
1368 * (foo) -> 'foo'
1369 * foo.(bar) -> 'foo.bar'
1370 * foo.bar.(baz) -> 'foo.bar.baz'
1371 * Otherwise throw.
1372 */
1373function toPropertyAccessString(node) {
1374 if (node.type === 'Identifier') {
1375 return node.name;
1376 } else if (node.type === 'MemberExpression' && !node.computed) {
1377 var object = toPropertyAccessString(node.object);
1378 var property = toPropertyAccessString(node.property);
1379 return object + '.' + property;
1380 } else {
1381 throw new Error('Unsupported node type: ' + node.type);
1382 }
1383}
1384
1385function getNodeWithoutReactNamespace(node, options) {
1386 if (node.type === 'MemberExpression' && node.object.type === 'Identifier' && node.object.name === 'React' && node.property.type === 'Identifier' && !node.computed) {
1387 return node.property;
1388 }
1389 return node;
1390}
1391
1392// What's the index of callback that needs to be analyzed for a given Hook?
1393// -1 if it's not a Hook we care about (e.g. useState).
1394// 0 for useEffect/useMemo/useCallback(fn).
1395// 1 for useImperativeHandle(ref, fn).
1396// For additionally configured Hooks, assume that they're like useEffect (0).
1397function getReactiveHookCallbackIndex(calleeNode, options) {
1398 var node = getNodeWithoutReactNamespace(calleeNode);
1399 if (node.type !== 'Identifier') {
1400 return null;
1401 }
1402 switch (node.name) {
1403 case 'useEffect':
1404 case 'useLayoutEffect':
1405 case 'useCallback':
1406 case 'useMemo':
1407 // useEffect(fn)
1408 return 0;
1409 case 'useImperativeHandle':
1410 // useImperativeHandle(ref, fn)
1411 return 1;
1412 default:
1413 if (node === calleeNode && options && options.additionalHooks) {
1414 // Allow the user to provide a regular expression which enables the lint to
1415 // target custom reactive hooks.
1416 var name = void 0;
1417 try {
1418 name = toPropertyAccessString(node);
1419 } catch (error) {
1420 if (/Unsupported node type/.test(error.message)) {
1421 return 0;
1422 } else {
1423 throw error;
1424 }
1425 }
1426 return options.additionalHooks.test(name) ? 0 : -1;
1427 } else {
1428 return -1;
1429 }
1430 }
1431}
1432
1433// const [state, setState] = useState() / React.useState()
1434// ^^^ true for this reference
1435// const [state, dispatch] = useReducer() / React.useReducer()
1436// ^^^ true for this reference
1437// const ref = useRef()
1438// ^^^ true for this reference
1439// False for everything else.
1440function isDefinitelyStaticDependency(reference) {
1441 // This function is written defensively because I'm not sure about corner cases.
1442 // TODO: we can strengthen this if we're sure about the types.
1443 var resolved = reference.resolved;
1444 if (resolved == null || !Array.isArray(resolved.defs)) {
1445 return false;
1446 }
1447 var def = resolved.defs[0];
1448 if (def == null) {
1449 return false;
1450 }
1451 // Look for `let stuff = ...`
1452 if (def.node.type !== 'VariableDeclarator') {
1453 return false;
1454 }
1455 var init = def.node.init;
1456 if (init == null) {
1457 return false;
1458 }
1459 // Detect primitive constants
1460 // const foo = 42
1461 var declaration = def.node.parent;
1462 if (declaration.kind === 'const' && init.type === 'Literal' && (typeof init.value === 'string' || typeof init.value === 'number' || init.value === null)) {
1463 // Definitely static
1464 return true;
1465 }
1466 // Detect known Hook calls
1467 // const [_, setState] = useState()
1468 if (init.type !== 'CallExpression') {
1469 return false;
1470 }
1471 var callee = init.callee;
1472 // Step into `= React.something` initializer.
1473 if (callee.type === 'MemberExpression' && callee.object.name === 'React' && callee.property != null && !callee.computed) {
1474 callee = callee.property;
1475 }
1476 if (callee.type !== 'Identifier') {
1477 return false;
1478 }
1479 var id = def.node.id;
1480 if (callee.name === 'useRef' && id.type === 'Identifier') {
1481 // useRef() return value is static.
1482 return true;
1483 } else if (callee.name === 'useState' || callee.name === 'useReducer') {
1484 // Only consider second value in initializing tuple static.
1485 if (id.type === 'ArrayPattern' && id.elements.length === 2 && Array.isArray(reference.resolved.identifiers) &&
1486 // Is second tuple value the same reference we're checking?
1487 id.elements[1] === reference.resolved.identifiers[0]) {
1488 return true;
1489 }
1490 }
1491 // By default assume it's dynamic.
1492 return false;
1493}
1494
1495/**
1496 * ESLint won't assign node.parent to references from context.getScope()
1497 *
1498 * So instead we search for the node from an ancestor assigning node.parent
1499 * as we go. This mutates the AST.
1500 *
1501 * This traversal is:
1502 * - optimized by only searching nodes with a range surrounding our target node
1503 * - agnostic to AST node types, it looks for `{ type: string, ... }`
1504 */
1505function fastFindReferenceWithParent(start, target) {
1506 var queue = [start];
1507 var item = null;
1508
1509 while (queue.length) {
1510 item = queue.shift();
1511
1512 if (isSameIdentifier(item, target)) {
1513 return item;
1514 }
1515
1516 if (!isAncestorNodeOf(item, target)) {
1517 continue;
1518 }
1519
1520 var _iteratorNormalCompletion5 = true;
1521 var _didIteratorError5 = false;
1522 var _iteratorError5 = undefined;
1523
1524 try {
1525 for (var _iterator5 = Object.entries(item)[Symbol.iterator](), _step5; !(_iteratorNormalCompletion5 = (_step5 = _iterator5.next()).done); _iteratorNormalCompletion5 = true) {
1526 var _step5$value = _step5.value,
1527 key = _step5$value[0],
1528 value = _step5$value[1];
1529
1530 if (key === 'parent') {
1531 continue;
1532 }
1533 if (isNodeLike(value)) {
1534 value.parent = item;
1535 queue.push(value);
1536 } else if (Array.isArray(value)) {
1537 value.forEach(function (val) {
1538 if (isNodeLike(val)) {
1539 val.parent = item;
1540 queue.push(val);
1541 }
1542 });
1543 }
1544 }
1545 } catch (err) {
1546 _didIteratorError5 = true;
1547 _iteratorError5 = err;
1548 } finally {
1549 try {
1550 if (!_iteratorNormalCompletion5 && _iterator5.return) {
1551 _iterator5.return();
1552 }
1553 } finally {
1554 if (_didIteratorError5) {
1555 throw _iteratorError5;
1556 }
1557 }
1558 }
1559 }
1560
1561 return null;
1562}
1563
1564function joinEnglish(arr) {
1565 var s = '';
1566 for (var i = 0; i < arr.length; i++) {
1567 s += arr[i];
1568 if (i === 0 && arr.length === 2) {
1569 s += ' and ';
1570 } else if (i === arr.length - 2 && arr.length > 2) {
1571 s += ', and ';
1572 } else if (i < arr.length - 1) {
1573 s += ', ';
1574 }
1575 }
1576 return s;
1577}
1578
1579function isNodeLike(val) {
1580 return typeof val === 'object' && val !== null && !Array.isArray(val) && typeof val.type === 'string';
1581}
1582
1583function isSameIdentifier(a, b) {
1584 return a.type === 'Identifier' && a.name === b.name && a.range[0] === b.range[0] && a.range[1] === b.range[1];
1585}
1586
1587function isAncestorNodeOf(a, b) {
1588 return a.range[0] <= b.range[0] && a.range[1] >= b.range[1];
1589}
1590
1591var rules = {
1592 'rules-of-hooks': RuleOfHooks,
1593 'exhaustive-deps': ExhaustiveDeps
1594};
1595
1596var src = Object.freeze({
1597 rules: rules
1598});
1599
1600var eslintPluginReactHooks = src;
1601
1602module.exports = eslintPluginReactHooks;
1603 })();
1604}