create-next-flask
Version:
Create a Next + Flask app
1,358 lines (1,121 loc) • 87.7 kB
JavaScript
/**
* @license React
* 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';
function _unsupportedIterableToArray(o, minLen) {
if (!o) return;
if (typeof o === "string") return _arrayLikeToArray(o, minLen);
var n = Object.prototype.toString.call(o).slice(8, -1);
if (n === "Object" && o.constructor) n = o.constructor.name;
if (n === "Map" || n === "Set") return Array.from(o);
if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen);
}
function _arrayLikeToArray(arr, len) {
if (len == null || len > arr.length) len = arr.length;
for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i];
return arr2;
}
function _createForOfIteratorHelper(o, allowArrayLike) {
var it;
if (typeof Symbol === "undefined" || o[Symbol.iterator] == null) {
if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") {
if (it) o = it;
var i = 0;
var F = function () {};
return {
s: F,
n: function () {
if (i >= o.length) return {
done: true
};
return {
done: false,
value: o[i++]
};
},
e: function (e) {
throw e;
},
f: F
};
}
throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");
}
var normalCompletion = true,
didErr = false,
err;
return {
s: function () {
it = o[Symbol.iterator]();
},
n: function () {
var step = it.next();
normalCompletion = step.done;
return step;
},
e: function (e) {
didErr = true;
err = e;
},
f: function () {
try {
if (!normalCompletion && it.return != null) it.return();
} finally {
if (didErr) throw err;
}
}
};
}
/* global BigInt */
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)) {
var obj = node.object;
var isPascalCaseNameSpace = /^[A-Z].*/;
return obj.type === 'Identifier' && isPascalCaseNameSpace.test(obj.name);
} 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 isReactFunction(node, functionName) {
return node.name === functionName || node.type === 'MemberExpression' && node.object.name === 'React' && node.property.name === functionName;
}
/**
* Checks if the node is a callback argument of forwardRef. This render function
* should follow the rules of hooks.
*/
function isForwardRefCallback(node) {
return !!(node.parent && node.parent.callee && isReactFunction(node.parent.callee, 'forwardRef'));
}
/**
* Checks if the node is a callback argument of React.memo. This anonymous
* functional component should follow the rules of hooks.
*/
function isMemoCallback(node) {
return !!(node.parent && node.parent.callee && isReactFunction(node.parent.callee, 'memo'));
}
function isInsideComponentOrHook(node) {
while (node) {
var functionName = getFunctionName(node);
if (functionName) {
if (isComponentName(functionName) || isHook(functionName)) {
return true;
}
}
if (isForwardRefCallback(node) || isMemoCallback(node)) {
return true;
}
node = node.parent;
}
return false;
}
var RulesOfHooks = {
meta: {
type: 'problem',
docs: {
description: 'enforces the Rules of Hooks',
recommended: true,
url: 'https://reactjs.org/docs/hooks-rules.html'
}
},
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, pathHistory) {
var cache = countPathsFromStart.cache;
var paths = cache.get(segment.id);
var pathList = new Set(pathHistory); // If `pathList` includes the current segment then we've found a cycle!
// We need to fill `cyclic` with all segments inside cycle
if (pathList.has(segment.id)) {
var pathArray = [].concat(pathList);
var cyclicSegments = pathArray.slice(pathArray.indexOf(segment.id) + 1);
var _iterator = _createForOfIteratorHelper(cyclicSegments),
_step;
try {
for (_iterator.s(); !(_step = _iterator.n()).done;) {
var cyclicSegment = _step.value;
cyclic.add(cyclicSegment);
}
} catch (err) {
_iterator.e(err);
} finally {
_iterator.f();
}
return BigInt('0');
} // add the current segment to pathList
pathList.add(segment.id); // We have a cached `paths`. Return it.
if (paths !== undefined) {
return paths;
}
if (codePath.thrownSegments.includes(segment)) {
paths = BigInt('0');
} else if (segment.prevSegments.length === 0) {
paths = BigInt('1');
} else {
paths = BigInt('0');
var _iterator2 = _createForOfIteratorHelper(segment.prevSegments),
_step2;
try {
for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
var prevSegment = _step2.value;
paths += countPathsFromStart(prevSegment, pathList);
}
} catch (err) {
_iterator2.e(err);
} finally {
_iterator2.f();
}
} // 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 === BigInt('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, pathHistory) {
var cache = countPathsToEnd.cache;
var paths = cache.get(segment.id);
var pathList = new Set(pathHistory); // If `pathList` includes the current segment then we've found a cycle!
// We need to fill `cyclic` with all segments inside cycle
if (pathList.has(segment.id)) {
var pathArray = Array.from(pathList);
var cyclicSegments = pathArray.slice(pathArray.indexOf(segment.id) + 1);
var _iterator3 = _createForOfIteratorHelper(cyclicSegments),
_step3;
try {
for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) {
var cyclicSegment = _step3.value;
cyclic.add(cyclicSegment);
}
} catch (err) {
_iterator3.e(err);
} finally {
_iterator3.f();
}
return BigInt('0');
} // add the current segment to pathList
pathList.add(segment.id); // We have a cached `paths`. Return it.
if (paths !== undefined) {
return paths;
}
if (codePath.thrownSegments.includes(segment)) {
paths = BigInt('0');
} else if (segment.nextSegments.length === 0) {
paths = BigInt('1');
} else {
paths = BigInt('0');
var _iterator4 = _createForOfIteratorHelper(segment.nextSegments),
_step4;
try {
for (_iterator4.s(); !(_step4 = _iterator4.n()).done;) {
var nextSegment = _step4.value;
paths += countPathsToEnd(nextSegment, pathList);
}
} catch (err) {
_iterator4.e(err);
} finally {
_iterator4.f();
}
}
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 _iterator5 = _createForOfIteratorHelper(segment.prevSegments),
_step5;
try {
for (_iterator5.s(); !(_step5 = _iterator5.n()).done;) {
var prevSegment = _step5.value;
var prevLength = shortestPathLengthToStart(prevSegment);
if (prevLength < length) {
length = prevLength;
}
}
} catch (err) {
_iterator5.e(err);
} finally {
_iterator5.f();
}
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 - unless it is an anonymous function argument to
// forwardRef or memo.
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) : isForwardRefCallback(codePathNode) || isMemoCallback(codePathNode); // 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 _iterator6 = _createForOfIteratorHelper(codePath.finalSegments),
_step6;
try {
for (_iterator6.s(); !(_step6 = _iterator6.n()).done;) {
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) {
_iterator6.e(err);
} finally {
_iterator6.f();
}
var _iterator7 = _createForOfIteratorHelper(reactHooksMap),
_step7;
try {
for (_iterator7.s(); !(_step7 = _iterator7.n()).done;) {
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 _iterator8 = _createForOfIteratorHelper(reactHooks),
_step8;
try {
for (_iterator8.s(); !(_step8 = _iterator8.n()).done;) {
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) {
// Custom message for hooks inside a class
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.';
context.report({
node: hook,
message: _message
});
} else if (codePathFunctionName) {
// Custom message if we found an invalid function name.
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".';
context.report({
node: hook,
message: _message2
});
} else if (codePathNode.type === 'Program') {
// These are dangerous if you have inline requires enabled.
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.';
context.report({
node: hook,
message: _message3
});
} 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 _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.';
context.report({
node: hook,
message: _message4
});
}
}
}
} catch (err) {
_iterator8.e(err);
} finally {
_iterator8.f();
}
}
} catch (err) {
_iterator7.e(err);
} finally {
_iterator7.f();
}
},
// 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 use case.
*/
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: {
type: 'suggestion',
docs: {
description: 'verifies the list of dependencies for Hooks like useEffect and similar',
recommended: true,
url: 'https://github.com/facebook/react/issues/14920'
},
fixable: 'code',
hasSuggestions: true,
schema: [{
type: 'object',
additionalProperties: false,
enableDangerousAutofixThisMayCauseInfiniteLoops: false,
properties: {
additionalHooks: {
type: 'string'
},
enableDangerousAutofixThisMayCauseInfiniteLoops: {
type: 'boolean'
}
}
}]
},
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 enableDangerousAutofixThisMayCauseInfiniteLoops = context.options && context.options[0] && context.options[0].enableDangerousAutofixThisMayCauseInfiniteLoops || false;
var options = {
additionalHooks: additionalHooks,
enableDangerousAutofixThisMayCauseInfiniteLoops: enableDangerousAutofixThisMayCauseInfiniteLoops
};
function reportProblem(problem) {
if (enableDangerousAutofixThisMayCauseInfiniteLoops) {
// Used to enable legacy behavior. Dangerous.
// Keep this as an option until major IDEs upgrade (including VSCode FB ESLint extension).
if (Array.isArray(problem.suggest) && problem.suggest.length > 0) {
problem.fix = problem.suggest[0].fix;
}
}
context.report(problem);
}
var scopeManager = context.getSourceCode().scopeManager; // Should be shared between visitors.
var setStateCallSites = new WeakMap();
var stateVariables = new WeakSet();
var stableKnownValueCache = 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;
};
}
/**
* Visitor for both function expressions and arrow function expressions.
*/
function visitFunctionWithDependencies(node, declaredDependenciesNode, reactiveHook, reactiveHookName, isEffect) {
if (isEffect && node.async) {
reportProblem({
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://reactjs.org/link/hooks-data-fetching'
});
} // Get the current scope.
var scope = scopeManager.acquire(node); // 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;
}
var isArray = Array.isArray; // 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 stable 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 isStableKnownHookValue(resolved) {
if (!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;
}
while (init.type === 'TSAsExpression') {
init = init.expression;
} // 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 stable
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 stable.
return true;
} else if (name === 'useState' || name === 'useReducer') {
// Only consider second value in initializing tuple stable.
if (id.type === 'ArrayPattern' && id.elements.length === 2 && 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;
var writeCount = 0;
for (var i = 0; i < references.length; i++) {
if (references[i].isWrite()) {
writeCount++;
}
if (writeCount > 1) {
return false;
}
setStateCallSites.set(references[i].identifier, id.elements[0]);
}
} // Setter is stable.
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;
}
}
} else if (name === 'useTransition') {
// Only consider second value in initializing tuple stable.
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]) {
// Setter is stable.
return true;
}
}
} // By default assume it's dynamic.
return false;
} // Some are just functions that don't reference anything dynamic.
function isFunctionWithoutCapturedValues(resolved) {
if (!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;
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) && // Stable values are fine though,
// although we won't check functions deeper.
!memoizedIsStableKnownHookValue(ref.resolved)) {
return false;
}
} // If we got here, this function doesn't capture anything
// from render--or everything it captures is known stable.
return true;
} // Remember such values. Avoid re-running extra checks on them.
var memoizedIsStableKnownHookValue = memoizeWithWeakMap(isStableKnownHookValue, stableKnownValueCache);
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 stable.
var dependencies = new Map();
var optionalChains = new Map();
gatherDependenciesRecursively(scope);
function gatherDependenciesRecursively(currentScope) {
var _iterator = _createForOfIteratorHelper(currentScope.references),
_step;
try {
for (_iterator.s(); !(_step = _iterator.n()).done;) {
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 = analyzePropertyChain(dependencyNode, optionalChains); // 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.type === 'OptionalMemberExpression') && !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
});
}
if (dependencyNode.parent.type === 'TSTypeQuery' || dependencyNode.parent.type === 'TSTypeReference') {
continue;
}
var def = reference.resolved.defs[0];
if (def == null) {
continue;
} // Ignore references to the function itself as it's not defined yet.
if (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 stable.
if (!dependencies.has(dependency)) {
var resolved = reference.resolved;
var isStable = memoizedIsStableKnownHookValue(resolved) || memoizedIsFunctionWithoutCapturedValues(resolved);
dependencies.set(dependency, {
isStable: isStable,
references: [reference]
});
} else {
dependencies.get(dependency).references.push(reference);
}
}
} catch (err) {
_iterator.e(err);
} finally {
_iterator.f();
}
var _iterator2 = _createForOfIteratorHelper(currentScope.childScopes),
_step2;
try {
for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
var childScope = _step2.value;
gatherDependenciesRecursively(childScope);
}
} catch (err) {
_iterator2.e(err);
} finally {
_iterator2.f();
}
} // 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
// Note: no need to handle OptionalMemberExpression because it can't be LHS.
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;
}
reportProblem({
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);
reportProblem({
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 stable and report bad usage first.
var stableDependencies = new Set();
dependencies.forEach(function (_ref2, key) {
var isStable = _ref2.isStable,
references = _ref2.references;
if (isStable) {
stableDependencies.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 isStable = _ref3.isStable,
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: [],
stableDependencies: stableDependencies,
externalDependencies: new Set(),
isEffect: true
}),
_suggestedDependencies = _collectRecommendatio.suggestedDependencies;
reportProblem({
node: reactiveHook,
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."),
suggest: [{
desc: "Add dependencies array: [" + _suggestedDependencies.join(', ') + "]",
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.
reportProblem({
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') {
reportProblem({
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;
try {
declaredDependency = analyzePropertyChain(declaredDependencyNode, null);
} catch (error) {
if (/Unsupported node type/.test(error.message)) {
if (declaredDependencyNode.type === 'Literal') {
if (dependencies.has(declaredDependencyNode.value)) {
reportProblem({
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 {
reportProblem({
node: declaredDependencyNode,
message: "The " + declaredDependencyNode.raw + " literal is not a valid dependency " + 'because it never changes. You can safely remove it.'
});
}
} else {
reportProblem({
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.type === 'OptionalMemberExpression' || maybeID.type === 'ChainExpression') {
maybeID = maybeID.object || maybeID.expression.object;
}
var isDeclaredInComponent = !componentScope.through.some(function (ref) {
return ref.identifier === maybeID;
}); // Add the dependency to our declared dependency map.
declaredDependencies.push({
key: declaredDependency,
node: declaredDependencyNode
});
if (!isDeclaredInComponent) {
externalDependencies.add(declaredDependency);
}
});
}
var _collectRecommendatio2 = collectRecommendations({
d