[d565449] | 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 | }
|
---|