github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/node_modules/eslint-plugin-react-hooks/cjs/eslint-plugin-react-hooks.development.js (about) 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 13 if (process.env.NODE_ENV !== "production") { 14 (function() { 15 'use strict'; 16 17 function _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 26 function _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 34 function _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 93 function 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 102 function 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 120 function isComponentName(node) { 121 if (node.type === 'Identifier') { 122 return !/^[a-z]/.test(node.name); 123 } else { 124 return false; 125 } 126 } 127 128 function 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 137 function 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 146 function isMemoCallback(node) { 147 return !!(node.parent && node.parent.callee && isReactFunction(node.parent.callee, 'memo')); 148 } 149 150 function 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 170 var 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 670 function 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 713 function last(array) { 714 return array[array.length - 1]; 715 } 716 717 /* eslint-disable no-for-of-loops/no-for-of-loops */ 718 var 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 1862 function 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 2049 function 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 2113 function 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 2215 function 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 2233 function 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 2256 function 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 2303 function 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 2316 function 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 2370 function 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 2411 function 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 2429 function isNodeLike(val) { 2430 return typeof val === 'object' && val !== null && !Array.isArray(val) && typeof val.type === 'string'; 2431 } 2432 2433 function 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 2437 function isAncestorNodeOf(a, b) { 2438 return a.range[0] <= b.range[0] && a.range[1] >= b.range[1]; 2439 } 2440 2441 var 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 }; 2450 var rules = { 2451 'rules-of-hooks': RulesOfHooks, 2452 'exhaustive-deps': ExhaustiveDeps 2453 }; 2454 2455 exports.configs = configs; 2456 exports.rules = rules; 2457 })(); 2458 }