UNPKG

lightfold

Version:

lightweight scaffolding and archiving utility CLI.

1,275 lines (1,150 loc) 77.7 kB
/** @license React v1.7.0 * eslint-plugin-react-hooks.development.js * * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ 'use strict'; if (process.env.NODE_ENV !== "production") { (function() { 'use strict'; /* eslint-disable no-for-of-loops/no-for-of-loops */ /** * Catch all identifiers that begin with "use" followed by an uppercase Latin * character to exclude identifiers like "user". */ function isHookName(s) { return (/^use[A-Z0-9].*$/.test(s) ); } /** * We consider hooks to be a hook name identifier or a member expression * containing a hook name. */ function isHook(node) { if (node.type === 'Identifier') { return isHookName(node.name); } else if (node.type === 'MemberExpression' && !node.computed && isHook(node.property)) { // Only consider React.useFoo() to be namespace hooks for now to avoid false positives. // We can expand this check later. var obj = node.object; return obj.type === 'Identifier' && obj.name === 'React'; } else { return false; } } /** * Checks if the node is a React component name. React component names must * always start with a non-lowercase letter. So `MyComponent` or `_MyComponent` * are valid component names for instance. */ function isComponentName(node) { if (node.type === 'Identifier') { return !/^[a-z]/.test(node.name); } else { return false; } } function isInsideComponentOrHook(node) { while (node) { var functionName = getFunctionName(node); if (functionName) { if (isComponentName(functionName) || isHook(functionName)) { return true; } } node = node.parent; } return false; } var RuleOfHooks = { create: function (context) { var codePathReactHooksMapStack = []; var codePathSegmentStack = []; return { // Maintain code segment path stack as we traverse. onCodePathSegmentStart: function (segment) { return codePathSegmentStack.push(segment); }, onCodePathSegmentEnd: function () { return codePathSegmentStack.pop(); }, // Maintain code path stack as we traverse. onCodePathStart: function () { return codePathReactHooksMapStack.push(new Map()); }, // Process our code path. // // Everything is ok if all React Hooks are both reachable from the initial // segment and reachable from every final segment. onCodePathEnd: function (codePath, codePathNode) { var reactHooksMap = codePathReactHooksMapStack.pop(); if (reactHooksMap.size === 0) { return; } // All of the segments which are cyclic are recorded in this set. var cyclic = new Set(); /** * Count the number of code paths from the start of the function to this * segment. For example: * * ```js * function MyComponent() { * if (condition) { * // Segment 1 * } else { * // Segment 2 * } * // Segment 3 * } * ``` * * Segments 1 and 2 have one path to the beginning of `MyComponent` and * segment 3 has two paths to the beginning of `MyComponent` since we * could have either taken the path of segment 1 or segment 2. * * Populates `cyclic` with cyclic segments. */ function countPathsFromStart(segment) { var cache = countPathsFromStart.cache; var paths = cache.get(segment.id); // If `paths` is null then we've found a cycle! Add it to `cyclic` and // any other segments which are a part of this cycle. if (paths === null) { if (cyclic.has(segment.id)) { return 0; } else { cyclic.add(segment.id); var _iteratorNormalCompletion = true; var _didIteratorError = false; var _iteratorError = undefined; try { for (var _iterator = segment.prevSegments[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { var prevSegment = _step.value; countPathsFromStart(prevSegment); } } catch (err) { _didIteratorError = true; _iteratorError = err; } finally { try { if (!_iteratorNormalCompletion && _iterator.return) { _iterator.return(); } } finally { if (_didIteratorError) { throw _iteratorError; } } } return 0; } } // We have a cached `paths`. Return it. if (paths !== undefined) { return paths; } // Compute `paths` and cache it. Guarding against cycles. cache.set(segment.id, null); if (codePath.thrownSegments.includes(segment)) { paths = 0; } else if (segment.prevSegments.length === 0) { paths = 1; } else { paths = 0; var _iteratorNormalCompletion2 = true; var _didIteratorError2 = false; var _iteratorError2 = undefined; try { for (var _iterator2 = segment.prevSegments[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) { var _prevSegment = _step2.value; paths += countPathsFromStart(_prevSegment); } } catch (err) { _didIteratorError2 = true; _iteratorError2 = err; } finally { try { if (!_iteratorNormalCompletion2 && _iterator2.return) { _iterator2.return(); } } finally { if (_didIteratorError2) { throw _iteratorError2; } } } } // If our segment is reachable then there should be at least one path // to it from the start of our code path. if (segment.reachable && paths === 0) { cache.delete(segment.id); } else { cache.set(segment.id, paths); } return paths; } /** * Count the number of code paths from this segment to the end of the * function. For example: * * ```js * function MyComponent() { * // Segment 1 * if (condition) { * // Segment 2 * } else { * // Segment 3 * } * } * ``` * * Segments 2 and 3 have one path to the end of `MyComponent` and * segment 1 has two paths to the end of `MyComponent` since we could * either take the path of segment 1 or segment 2. * * Populates `cyclic` with cyclic segments. */ function countPathsToEnd(segment) { var cache = countPathsToEnd.cache; var paths = cache.get(segment.id); // If `paths` is null then we've found a cycle! Add it to `cyclic` and // any other segments which are a part of this cycle. if (paths === null) { if (cyclic.has(segment.id)) { return 0; } else { cyclic.add(segment.id); var _iteratorNormalCompletion3 = true; var _didIteratorError3 = false; var _iteratorError3 = undefined; try { for (var _iterator3 = segment.nextSegments[Symbol.iterator](), _step3; !(_iteratorNormalCompletion3 = (_step3 = _iterator3.next()).done); _iteratorNormalCompletion3 = true) { var nextSegment = _step3.value; countPathsToEnd(nextSegment); } } catch (err) { _didIteratorError3 = true; _iteratorError3 = err; } finally { try { if (!_iteratorNormalCompletion3 && _iterator3.return) { _iterator3.return(); } } finally { if (_didIteratorError3) { throw _iteratorError3; } } } return 0; } } // We have a cached `paths`. Return it. if (paths !== undefined) { return paths; } // Compute `paths` and cache it. Guarding against cycles. cache.set(segment.id, null); if (codePath.thrownSegments.includes(segment)) { paths = 0; } else if (segment.nextSegments.length === 0) { paths = 1; } else { paths = 0; var _iteratorNormalCompletion4 = true; var _didIteratorError4 = false; var _iteratorError4 = undefined; try { for (var _iterator4 = segment.nextSegments[Symbol.iterator](), _step4; !(_iteratorNormalCompletion4 = (_step4 = _iterator4.next()).done); _iteratorNormalCompletion4 = true) { var _nextSegment = _step4.value; paths += countPathsToEnd(_nextSegment); } } catch (err) { _didIteratorError4 = true; _iteratorError4 = err; } finally { try { if (!_iteratorNormalCompletion4 && _iterator4.return) { _iterator4.return(); } } finally { if (_didIteratorError4) { throw _iteratorError4; } } } } cache.set(segment.id, paths); return paths; } /** * Gets the shortest path length to the start of a code path. * For example: * * ```js * function MyComponent() { * if (condition) { * // Segment 1 * } * // Segment 2 * } * ``` * * There is only one path from segment 1 to the code path start. Its * length is one so that is the shortest path. * * There are two paths from segment 2 to the code path start. One * through segment 1 with a length of two and another directly to the * start with a length of one. The shortest path has a length of one * so we would return that. */ function shortestPathLengthToStart(segment) { var cache = shortestPathLengthToStart.cache; var length = cache.get(segment.id); // If `length` is null then we found a cycle! Return infinity since // the shortest path is definitely not the one where we looped. if (length === null) { return Infinity; } // We have a cached `length`. Return it. if (length !== undefined) { return length; } // Compute `length` and cache it. Guarding against cycles. cache.set(segment.id, null); if (segment.prevSegments.length === 0) { length = 1; } else { length = Infinity; var _iteratorNormalCompletion5 = true; var _didIteratorError5 = false; var _iteratorError5 = undefined; try { for (var _iterator5 = segment.prevSegments[Symbol.iterator](), _step5; !(_iteratorNormalCompletion5 = (_step5 = _iterator5.next()).done); _iteratorNormalCompletion5 = true) { var prevSegment = _step5.value; var prevLength = shortestPathLengthToStart(prevSegment); if (prevLength < length) { length = prevLength; } } } catch (err) { _didIteratorError5 = true; _iteratorError5 = err; } finally { try { if (!_iteratorNormalCompletion5 && _iterator5.return) { _iterator5.return(); } } finally { if (_didIteratorError5) { throw _iteratorError5; } } } length += 1; } cache.set(segment.id, length); return length; } countPathsFromStart.cache = new Map(); countPathsToEnd.cache = new Map(); shortestPathLengthToStart.cache = new Map(); // Count all code paths to the end of our component/hook. Also primes // the `countPathsToEnd` cache. var allPathsFromStartToEnd = countPathsToEnd(codePath.initialSegment); // Gets the function name for our code path. If the function name is // `undefined` then we know either that we have an anonymous function // expression or our code path is not in a function. In both cases we // will want to error since neither are React function components or // hook functions. var codePathFunctionName = getFunctionName(codePathNode); // This is a valid code path for React hooks if we are directly in a React // function component or we are in a hook function. var isSomewhereInsideComponentOrHook = isInsideComponentOrHook(codePathNode); var isDirectlyInsideComponentOrHook = codePathFunctionName ? isComponentName(codePathFunctionName) || isHook(codePathFunctionName) : false; // Compute the earliest finalizer level using information from the // cache. We expect all reachable final segments to have a cache entry // after calling `visitSegment()`. var shortestFinalPathLength = Infinity; var _iteratorNormalCompletion6 = true; var _didIteratorError6 = false; var _iteratorError6 = undefined; try { for (var _iterator6 = codePath.finalSegments[Symbol.iterator](), _step6; !(_iteratorNormalCompletion6 = (_step6 = _iterator6.next()).done); _iteratorNormalCompletion6 = true) { var finalSegment = _step6.value; if (!finalSegment.reachable) { continue; } var length = shortestPathLengthToStart(finalSegment); if (length < shortestFinalPathLength) { shortestFinalPathLength = length; } } // Make sure all React Hooks pass our lint invariants. Log warnings // if not. } catch (err) { _didIteratorError6 = true; _iteratorError6 = err; } finally { try { if (!_iteratorNormalCompletion6 && _iterator6.return) { _iterator6.return(); } } finally { if (_didIteratorError6) { throw _iteratorError6; } } } var _iteratorNormalCompletion7 = true; var _didIteratorError7 = false; var _iteratorError7 = undefined; try { for (var _iterator7 = reactHooksMap[Symbol.iterator](), _step7; !(_iteratorNormalCompletion7 = (_step7 = _iterator7.next()).done); _iteratorNormalCompletion7 = true) { var _step7$value = _step7.value, segment = _step7$value[0], reactHooks = _step7$value[1]; // NOTE: We could report here that the hook is not reachable, but // that would be redundant with more general "no unreachable" // lint rules. if (!segment.reachable) { continue; } // If there are any final segments with a shorter path to start then // we possibly have an early return. // // If our segment is a final segment itself then siblings could // possibly be early returns. 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 // our code path that go _through_ this segment. The critical piece // of this is _through_. If we just call `countPathsToEnd(segment)` // then we neglect that we may have gone through multiple paths to get // to this point! Consider: // // ```js // function MyComponent() { // if (a) { // // Segment 1 // } else { // // Segment 2 // } // // Segment 3 // if (b) { // // Segment 4 // } else { // // Segment 5 // } // } // ``` // // In this component we have four code paths: // // 1. `a = true; b = true` // 2. `a = true; b = false` // 3. `a = false; b = true` // 4. `a = false; b = false` // // From segment 3 there are two code paths to the end through segment // 4 and segment 5. However, we took two paths to get here through // segment 1 and segment 2. // // If we multiply the paths from start (two) by the paths to end (two) // for segment 3 we get four. Which is our desired count. var pathsFromStartToEnd = countPathsFromStart(segment) * countPathsToEnd(segment); // Is this hook a part of a cyclic segment? var cycled = cyclic.has(segment.id); var _iteratorNormalCompletion8 = true; var _didIteratorError8 = false; var _iteratorError8 = undefined; try { for (var _iterator8 = reactHooks[Symbol.iterator](), _step8; !(_iteratorNormalCompletion8 = (_step8 = _iterator8.next()).done); _iteratorNormalCompletion8 = true) { var hook = _step8.value; // Report an error if a hook may be called more then once. if (cycled) { context.report({ node: hook, 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.' }); } // If this is not a valid code path for React hooks then we need to // log a warning for every hook in this code path. // // Pick a special message depending on the scope this hook was // called in. if (isDirectlyInsideComponentOrHook) { // Report an error if a hook does not reach all finalizing code // path segments. // // Special case when we think there might be an early return. if (!cycled && pathsFromStartToEnd !== allPathsFromStartToEnd) { 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?' : ''); context.report({ node: hook, message: message }); } } else if (codePathNode.parent && (codePathNode.parent.type === 'MethodDefinition' || codePathNode.parent.type === 'ClassProperty') && codePathNode.parent.value === codePathNode) { // Ignore class methods for now because they produce too many // false positives due to feature flag checks. We're less // sensitive to them in classes because hooks would produce // runtime errors in classes anyway, and because a use*() // call in a class, if it works, is unambiguously *not* a hook. } else if (codePathFunctionName) { // Custom message if we found an invalid function name. var _message = 'React Hook "' + context.getSource(hook) + '" is called in ' + ('function "' + context.getSource(codePathFunctionName) + '" ') + 'which is neither a React function component or a custom ' + 'React Hook function.'; context.report({ node: hook, message: _message }); } else if (codePathNode.type === 'Program') { // For now, ignore if it's in top level scope. // We could warn here but there are false positives related // configuring libraries like `history`. } else { // Assume in all other cases the user called a hook in some // random function callback. This should usually be true for // anonymous function expressions. Hopefully this is clarifying // enough in the common case that the incorrect message in // uncommon cases doesn't matter. if (isSomewhereInsideComponentOrHook) { var _message2 = 'React Hook "' + context.getSource(hook) + '" cannot be called ' + 'inside a callback. React Hooks must be called in a ' + 'React function component or a custom React Hook function.'; context.report({ node: hook, message: _message2 }); } } } } catch (err) { _didIteratorError8 = true; _iteratorError8 = err; } finally { try { if (!_iteratorNormalCompletion8 && _iterator8.return) { _iterator8.return(); } } finally { if (_didIteratorError8) { throw _iteratorError8; } } } } } catch (err) { _didIteratorError7 = true; _iteratorError7 = err; } finally { try { if (!_iteratorNormalCompletion7 && _iterator7.return) { _iterator7.return(); } } finally { if (_didIteratorError7) { throw _iteratorError7; } } } }, // Missed opportunity...We could visit all `Identifier`s instead of all // `CallExpression`s and check that _every use_ of a hook name is valid. // But that gets complicated and enters type-system territory, so we're // only being strict about hook calls for now. CallExpression: function (node) { if (isHook(node.callee)) { // Add the hook node to a map keyed by the code path segment. We will // do full code path analysis at the end of our code path. var reactHooksMap = last(codePathReactHooksMapStack); var codePathSegment = last(codePathSegmentStack); var reactHooks = reactHooksMap.get(codePathSegment); if (!reactHooks) { reactHooks = []; reactHooksMap.set(codePathSegment, reactHooks); } reactHooks.push(node.callee); } } }; } }; /** * Gets the static name of a function AST node. For function declarations it is * easy. For anonymous function expressions it is much harder. If you search for * `IsAnonymousFunctionDefinition()` in the ECMAScript spec you'll find places * where JS gives anonymous function expressions names. We roughly detect the * same AST nodes with some exceptions to better fit our usecase. */ function getFunctionName(node) { if (node.type === 'FunctionDeclaration' || node.type === 'FunctionExpression' && node.id) { // function useHook() {} // const whatever = function useHook() {}; // // Function declaration or function expression names win over any // assignment statements or other renames. return node.id; } else if (node.type === 'FunctionExpression' || node.type === 'ArrowFunctionExpression') { if (node.parent.type === 'VariableDeclarator' && node.parent.init === node) { // const useHook = () => {}; return node.parent.id; } else if (node.parent.type === 'AssignmentExpression' && node.parent.right === node && node.parent.operator === '=') { // useHook = () => {}; return node.parent.left; } else if (node.parent.type === 'Property' && node.parent.value === node && !node.parent.computed) { // {useHook: () => {}} // {useHook() {}} return node.parent.key; // NOTE: We could also support `ClassProperty` and `MethodDefinition` // here to be pedantic. However, hooks in a class are an anti-pattern. So // we don't allow it to error early. // // class {useHook = () => {}} // class {useHook() {}} } else if (node.parent.type === 'AssignmentPattern' && node.parent.right === node && !node.parent.computed) { // const {useHook = () => {}} = {}; // ({useHook = () => {}} = {}); // // Kinda clowny, but we'd said we'd follow spec convention for // `IsAnonymousFunctionDefinition()` usage. return node.parent.left; } else { return undefined; } } else { return undefined; } } /** * Convenience function for peeking the last item in a stack. */ function last(array) { return array[array.length - 1]; } /* eslint-disable no-for-of-loops/no-for-of-loops */ var ExhaustiveDeps = { meta: { fixable: 'code', schema: [{ type: 'object', additionalProperties: false, properties: { additionalHooks: { type: 'string' } } }] }, create: function (context) { // Parse the `additionalHooks` regex. var additionalHooks = context.options && context.options[0] && context.options[0].additionalHooks ? new RegExp(context.options[0].additionalHooks) : undefined; var options = { additionalHooks: additionalHooks }; // Should be shared between visitors. var setStateCallSites = new WeakMap(); var stateVariables = new WeakSet(); var staticKnownValueCache = new WeakMap(); var functionWithoutCapturedValueCache = new WeakMap(); function memoizeWithWeakMap(fn, map) { return function (arg) { if (map.has(arg)) { // to verify cache hits: // console.log(arg.name) return map.get(arg); } var result = fn(arg); map.set(arg, result); return result; }; } return { FunctionExpression: visitFunctionExpression, ArrowFunctionExpression: visitFunctionExpression }; /** * Visitor for both function expressions and arrow function expressions. */ function visitFunctionExpression(node) { // We only want to lint nodes which are reactive hook callbacks. if (node.type !== 'FunctionExpression' && node.type !== 'ArrowFunctionExpression' || node.parent.type !== 'CallExpression') { return; } var callbackIndex = getReactiveHookCallbackIndex(node.parent.callee, options); if (node.parent.arguments[callbackIndex] !== node) { return; } // Get the reactive hook node. var reactiveHook = node.parent.callee; var reactiveHookName = getNodeWithoutReactNamespace(reactiveHook).name; var isEffect = reactiveHookName.endsWith('Effect'); // Get the declared dependencies for this reactive hook. If there is no // second argument then the reactive callback will re-run on every render. // So no need to check for dependency inclusion. var depsIndex = callbackIndex + 1; var declaredDependenciesNode = node.parent.arguments[depsIndex]; if (!declaredDependenciesNode && !isEffect) { // These are only used for optimization. if (reactiveHookName === 'useMemo' || reactiveHookName === 'useCallback') { // TODO: Can this have an autofix? context.report({ node: node.parent.callee, message: 'React Hook ' + reactiveHookName + ' does nothing when called with ' + 'only one argument. Did you forget to pass an array of ' + 'dependencies?' }); } return; } if (isEffect && node.async) { context.report({ node: node, 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://fb.me/react-hooks-data-fetching' }); } // Get the current scope. var scope = context.getScope(); // Find all our "pure scopes". On every re-render of a component these // pure scopes may have changes to the variables declared within. So all // variables used in our reactive hook callback but declared in a pure // scope need to be listed as dependencies of our reactive hook callback. // // According to the rules of React you can't read a mutable value in pure // scope. We can't enforce this in a lint so we trust that all variables // declared outside of pure scope are indeed frozen. var pureScopes = new Set(); var componentScope = null; { var currentScope = scope.upper; while (currentScope) { pureScopes.add(currentScope); if (currentScope.type === 'function') { break; } currentScope = currentScope.upper; } // If there is no parent function scope then there are no pure scopes. // The ones we've collected so far are incorrect. So don't continue with // the lint. if (!currentScope) { return; } componentScope = currentScope; } // Next we'll define a few helpers that helps us // tell if some values don't have to be declared as deps. // Some are known to be static based on Hook calls. // const [state, setState] = useState() / React.useState() // ^^^ true for this reference // const [state, dispatch] = useReducer() / React.useReducer() // ^^^ true for this reference // const ref = useRef() // ^^^ true for this reference // False for everything else. function isStaticKnownHookValue(resolved) { if (!Array.isArray(resolved.defs)) { return false; } var def = resolved.defs[0]; if (def == null) { return false; } // Look for `let stuff = ...` if (def.node.type !== 'VariableDeclarator') { return false; } var init = def.node.init; if (init == null) { return false; } // Detect primitive constants // const foo = 42 var declaration = def.node.parent; if (declaration == null) { // This might happen if variable is declared after the callback. // In that case ESLint won't set up .parent refs. // So we'll set them up manually. fastFindReferenceWithParent(componentScope.block, def.node.id); declaration = def.node.parent; if (declaration == null) { return false; } } if (declaration.kind === 'const' && init.type === 'Literal' && (typeof init.value === 'string' || typeof init.value === 'number' || init.value === null)) { // Definitely static return true; } // Detect known Hook calls // const [_, setState] = useState() if (init.type !== 'CallExpression') { return false; } var callee = init.callee; // Step into `= React.something` initializer. if (callee.type === 'MemberExpression' && callee.object.name === 'React' && callee.property != null && !callee.computed) { callee = callee.property; } if (callee.type !== 'Identifier') { return false; } var id = def.node.id; var _callee = callee, name = _callee.name; if (name === 'useRef' && id.type === 'Identifier') { // useRef() return value is static. return true; } else if (name === 'useState' || name === 'useReducer') { // Only consider second value in initializing tuple static. if (id.type === 'ArrayPattern' && id.elements.length === 2 && Array.isArray(resolved.identifiers)) { // Is second tuple value the same reference we're checking? if (id.elements[1] === resolved.identifiers[0]) { if (name === 'useState') { var references = resolved.references; for (var i = 0; i < references.length; i++) { setStateCallSites.set(references[i].identifier, id.elements[0]); } } // Setter is static. return true; } else if (id.elements[0] === resolved.identifiers[0]) { if (name === 'useState') { var _references = resolved.references; for (var _i = 0; _i < _references.length; _i++) { stateVariables.add(_references[_i].identifier); } } // State variable itself is dynamic. return false; } } } // By default assume it's dynamic. return false; } // Some are just functions that don't reference anything dynamic. function isFunctionWithoutCapturedValues(resolved) { if (!Array.isArray(resolved.defs)) { return false; } var def = resolved.defs[0]; if (def == null) { return false; } if (def.node == null || def.node.id == null) { return false; } // Search the direct component subscopes for // top-level function definitions matching this reference. var fnNode = def.node; var childScopes = componentScope.childScopes; var fnScope = null; var i = void 0; for (i = 0; i < childScopes.length; i++) { var childScope = childScopes[i]; var childScopeBlock = childScope.block; if ( // function handleChange() {} fnNode.type === 'FunctionDeclaration' && childScopeBlock === fnNode || // const handleChange = () => {} // const handleChange = function() {} fnNode.type === 'VariableDeclarator' && childScopeBlock.parent === fnNode) { // Found it! fnScope = childScope; break; } } if (fnScope == null) { return false; } // Does this function capture any values // that are in pure scopes (aka render)? for (i = 0; i < fnScope.through.length; i++) { var ref = fnScope.through[i]; if (ref.resolved == null) { continue; } if (pureScopes.has(ref.resolved.scope) && // Static values are fine though, // although we won't check functions deeper. !memoizedIsStaticKnownHookValue(ref.resolved)) { return false; } } // If we got here, this function doesn't capture anything // from render--or everything it captures is known static. return true; } // Remember such values. Avoid re-running extra checks on them. var memoizedIsStaticKnownHookValue = memoizeWithWeakMap(isStaticKnownHookValue, staticKnownValueCache); var memoizedIsFunctionWithoutCapturedValues = memoizeWithWeakMap(isFunctionWithoutCapturedValues, functionWithoutCapturedValueCache); // These are usually mistaken. Collect them. var currentRefsInEffectCleanup = new Map(); // Is this reference inside a cleanup function for this effect node? // We can check by traversing scopes upwards from the reference, and checking // if the last "return () => " we encounter is located directly inside the effect. function isInsideEffectCleanup(reference) { var curScope = reference.from; var isInReturnedFunction = false; while (curScope.block !== node) { if (curScope.type === 'function') { isInReturnedFunction = curScope.block.parent != null && curScope.block.parent.type === 'ReturnStatement'; } curScope = curScope.upper; } return isInReturnedFunction; } // Get dependencies from all our resolved references in pure scopes. // Key is dependency string, value is whether it's static. var dependencies = new Map(); gatherDependenciesRecursively(scope); function gatherDependenciesRecursively(currentScope) { var _iteratorNormalCompletion = true; var _didIteratorError = false; var _iteratorError = undefined; try { for (var _iterator = currentScope.references[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { var reference = _step.value; // If this reference is not resolved or it is not declared in a pure // scope then we don't care about this reference. if (!reference.resolved) { continue; } if (!pureScopes.has(reference.resolved.scope)) { continue; } // Narrow the scope of a dependency if it is, say, a member expression. // Then normalize the narrowed dependency. var referenceNode = fastFindReferenceWithParent(node, reference.identifier); var dependencyNode = getDependency(referenceNode); var dependency = toPropertyAccessString(dependencyNode); // Accessing ref.current inside effect cleanup is bad. if ( // We're in an effect... isEffect && // ... and this look like accessing .current... dependencyNode.type === 'Identifier' && dependencyNode.parent.type === 'MemberExpression' && !dependencyNode.parent.computed && dependencyNode.parent.property.type === 'Identifier' && dependencyNode.parent.property.name === 'current' && // ...in a cleanup function or below... isInsideEffectCleanup(reference)) { currentRefsInEffectCleanup.set(dependency, { reference: reference, dependencyNode: dependencyNode }); } // Ignore references to the function itself as it's not defined yet. var def = reference.resolved.defs[0]; if (def != null && def.node != null && def.node.init === node.parent) { continue; } // Ignore Flow type parameters if (def.type === 'TypeParameter') { continue; } // Add the dependency to a map so we can make sure it is referenced // again in our dependencies array. Remember whether it's static. if (!dependencies.has(dependency)) { var resolved = reference.resolved; var isStatic = memoizedIsStaticKnownHookValue(resolved) || memoizedIsFunctionWithoutCapturedValues(resolved); dependencies.set(dependency, { isStatic: isStatic, references: [reference] }); } else { dependencies.get(dependency).references.push(reference); } } } catch (err) { _didIteratorError = true; _iteratorError = err; } finally { try { if (!_iteratorNormalCompletion && _iterator.return) { _iterator.return(); } } finally { if (_didIteratorError) { throw _iteratorError; } } } var _iteratorNormalCompletion2 = true; var _didIteratorError2 = false; var _iteratorError2 = undefined; try { for (var _iterator2 = currentScope.childScopes[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) { var childScope = _step2.value; gatherDependenciesRecursively(childScope); } } catch (err) { _didIteratorError2 = true; _iteratorError2 = err; } finally { try { if (!_iteratorNormalCompletion2 && _iterator2.return) { _iterator2.return(); } } finally { if (_didIteratorError2) { throw _iteratorError2; } } } } // Warn about accessing .current in cleanup effects. currentRefsInEffectCleanup.forEach(function (_ref, dependency) { var reference = _ref.reference, dependencyNode = _ref.dependencyNode; var references = reference.resolved.references; // Is React managing this ref or us? // Let's see if we can find a .current assignment. var foundCurrentAssignment = false; for (var i = 0; i < references.length; i++) { var identifier = references[i].identifier; var parent = identifier.parent; if (parent != null && // ref.current parent.type === 'MemberExpression' && !parent.computed && parent.property.type === 'Identifier' && parent.property.name === 'current' && // ref.current = <something> parent.parent.type === 'AssignmentExpression' && parent.parent.left === parent) { foundCurrentAssignment = true; break; } } // We only want to warn about React-managed refs. if (foundCurrentAssignment) { return; } context.report({ node: dependencyNode.parent.property, 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.' }); }); // Warn about assigning to variables in the outer scope. // Those are usually bugs. var staleAssignments = new Set(); function reportStaleAssignment(writeExpr, key) { if (staleAssignments.has(key)) { return; } staleAssignments.add(key); context.report({ node: writeExpr, 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) + '.') }); } // Remember which deps are optional and report bad usage first. var optionalDependencies = new Set(); dependencies.forEach(function (_ref2, key) { var isStatic = _ref2.isStatic, references = _ref2.references; if (isStatic) { optionalDependencies.add(key); } references.forEach(function (reference) { if (reference.writeExpr) { reportStaleAssignment(reference.writeExpr, key); } }); }); if (staleAssignments.size > 0) { // The intent isn't clear so we'll wait until you fix those first. return; } if (!declaredDependenciesNode) { // Check if there are any top-level setState() calls. // Those tend to lead to infinite loops. var setStateInsideEffectWithoutDeps = null; dependencies.forEach(function (_ref3, key) { var isStatic = _ref3.isStatic, references = _ref3.references; if (setStateInsideEffectWithoutDeps) { return; } references.forEach(function (reference) { if (setStateInsideEffectWithoutDeps) { return; } var id = reference.identifier; var isSetState = setStateCallSites.has(id); if (!isSetState) { return; } var fnScope = reference.from; while (fnScope.type !== 'function') { fnScope = fnScope.upper; } var isDirectlyInsideEffect = fnScope.block === node; if (isDirectlyInsideEffect) { // TODO: we could potentially ignore early returns. setStateInsideEffectWithoutDeps = key; } }); }); if (setStateInsideEffectWithoutDeps) { var _collectRecommendatio = collectRecommendations({ dependencies: dependencies, declaredDependencies: [], optionalDependencies: optionalDependencies, externalDependencies: new Set(), isEffect: true }), _suggestedDependencies = _collectRecommendatio.suggestedDependencies; context.report({ node: node.parent.callee, 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.'), fix: function (fixer) { return fixer.insertTextAfter(node, ', [' + _suggestedDependencies.join(', ') + ']'); } }); } return; } var declaredDependencies = []; var externalDependencies = new Set(); if (declaredDependenciesNode.type !== 'ArrayExpression') { // If the declared dependencies are not an array expression then we // can't verify that the user provided the correct dependencies. Tell // the user this in an error. context.report({ node: declaredDependenciesNode, 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.' }); } else { declaredDependenciesNode.elements.forEach(function (declaredDependencyNode) { // Skip elided elements. if (declaredDependencyNode === null) { return; } // If we see a spread element then add a special warning. if (declaredDependencyNode.type === 'SpreadElement') { context.report({ node: declaredDependencyNode, 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.' }); return; } // Try to normalize the declared dependency. If we can't then an error // will be thrown. We will catch that error and report an error. var declaredDependency = void 0; try { declaredDependency = toPropertyAccessString(declaredDependencyNode); } catch (error) { if (/Unsupported node type/.test(error.message)) { if (declaredDependencyNode.type === 'Literal') { if (dependencies.has(declaredDependencyNode.value)) { context.report({ node: declaredDependencyNode, 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?') }); } else { context.report({ node: declaredDependencyNode, message: 'The ' + declaredDependencyNode.raw + ' literal is not a valid dependency ' + 'because it never changes. You can safely remove it.' }); } } else { context.report({ node: declaredDependencyNode, 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.' }); } return; } else { throw error; } } var maybeID = declaredDependencyNode; while (maybeID.type === 'MemberExpression') { maybeID = maybeID.object; } var isDeclaredInComponent = !componentScope.through.some(function (ref) { return ref.identifier === maybeID; }); // Add the dependency to our