[ESLint] Add more hints to lint messages (#15046)

* A clearer message for props destructuring where applicable

* Add line number to the "move function" message

* Add a hint for how to fix callbacks from props

* Simplify code and harden tests

* Collect all dependency references for better warnings

* Suggest updater or reducer where appropriate
This commit is contained in:
Dan Abramov 2019-03-07 12:39:15 +00:00 committed by GitHub
parent 6d2666bab1
commit 197703ecc7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 744 additions and 44 deletions

View File

@ -849,6 +849,106 @@ const tests = {
}
`,
},
{
code: `
function withFetch(fetchPodcasts) {
return function Podcasts({ id }) {
let [podcasts, setPodcasts] = useState(null);
useEffect(() => {
fetchPodcasts(id).then(setPodcasts);
}, [id]);
}
}
`,
},
{
code: `
function Podcasts({ id }) {
let [podcasts, setPodcasts] = useState(null);
useEffect(() => {
function doFetch({ fetchPodcasts }) {
fetchPodcasts(id).then(setPodcasts);
}
doFetch({ fetchPodcasts: API.fetchPodcasts });
}, [id]);
}
`,
},
{
code: `
function Counter() {
let [count, setCount] = useState(0);
function increment(x) {
return x + 1;
}
useEffect(() => {
let id = setInterval(() => {
setCount(increment);
}, 1000);
return () => clearInterval(id);
}, []);
return <h1>{count}</h1>;
}
`,
},
{
code: `
function Counter() {
let [count, setCount] = useState(0);
function increment(x) {
return x + 1;
}
useEffect(() => {
let id = setInterval(() => {
setCount(count => increment(count));
}, 1000);
return () => clearInterval(id);
}, []);
return <h1>{count}</h1>;
}
`,
},
{
code: `
import increment from './increment';
function Counter() {
let [count, setCount] = useState(0);
useEffect(() => {
let id = setInterval(() => {
setCount(count => count + increment);
}, 1000);
return () => clearInterval(id);
}, []);
return <h1>{count}</h1>;
}
`,
},
{
code: `
function withStuff(increment) {
return function Counter() {
let [count, setCount] = useState(0);
useEffect(() => {
let id = setInterval(() => {
setCount(count => count + increment);
}, 1000);
return () => clearInterval(id);
}, []);
return <h1>{count}</h1>;
}
}
`,
},
],
invalid: [
{
@ -2203,7 +2303,10 @@ const tests = {
`,
errors: [
"React Hook useEffect has a missing dependency: 'state'. " +
'Either include it or remove the dependency array.',
'Either include it or remove the dependency array. ' +
`If 'state' is only necessary for calculating the next state, ` +
`consider refactoring to the setState(state => ...) form which ` +
`doesn't need to depend on the state from outside.`,
],
},
{
@ -2232,7 +2335,10 @@ const tests = {
`,
errors: [
"React Hook useEffect has a missing dependency: 'state'. " +
'Either include it or remove the dependency array.',
'Either include it or remove the dependency array. ' +
`If 'state' is only necessary for calculating the next state, ` +
`consider refactoring to the setState(state => ...) form which ` +
`doesn't need to depend on the state from outside.`,
],
},
{
@ -2447,7 +2553,9 @@ const tests = {
errors: [
"React Hook useEffect has a missing dependency: 'props'. " +
'Either include it or remove the dependency array. ' +
'Alternatively, destructure the necessary props outside the callback.',
`However, the preferred fix is to destructure the 'props' ` +
`object outside of the useEffect call and refer to specific ` +
`props directly by their names.`,
],
},
{
@ -2478,7 +2586,9 @@ const tests = {
errors: [
"React Hook useEffect has a missing dependency: 'props'. " +
'Either include it or remove the dependency array. ' +
'Alternatively, destructure the necessary props outside the callback.',
`However, the preferred fix is to destructure the 'props' ` +
`object outside of the useEffect call and refer to specific ` +
`props directly by their names.`,
],
},
{
@ -2529,7 +2639,9 @@ const tests = {
errors: [
"React Hook useEffect has a missing dependency: 'props'. " +
'Either include it or remove the dependency array. ' +
'Alternatively, destructure the necessary props outside the callback.',
`However, the preferred fix is to destructure the 'props' ` +
`object outside of the useEffect call and refer to specific ` +
`props directly by their names.`,
],
},
{
@ -2556,7 +2668,9 @@ const tests = {
errors: [
"React Hook useEffect has a missing dependency: 'props'. " +
'Either include it or remove the dependency array. ' +
'Alternatively, destructure the necessary props outside the callback.',
`However, the preferred fix is to destructure the 'props' ` +
`object outside of the useEffect call and refer to specific ` +
`props directly by their names.`,
],
},
{
@ -2583,7 +2697,9 @@ const tests = {
errors: [
"React Hook useEffect has missing dependencies: 'props' and 'skillsCount'. " +
'Either include them or remove the dependency array. ' +
'Alternatively, destructure the necessary props outside the callback.',
`However, the preferred fix is to destructure the 'props' ` +
`object outside of the useEffect call and refer to specific ` +
`props directly by their names.`,
],
},
{
@ -2638,11 +2754,15 @@ const tests = {
let value;
let value2;
let value3;
let value4;
let asyncValue;
useEffect(() => {
value = {};
if (value4) {
value = {};
}
value2 = 100;
value = 43;
value4 = true;
console.log(value2);
console.log(value3);
setTimeout(() => {
@ -2659,11 +2779,15 @@ const tests = {
let value;
let value2;
let value3;
let value4;
let asyncValue;
useEffect(() => {
value = {};
if (value4) {
value = {};
}
value2 = 100;
value = 43;
value4 = true;
console.log(value2);
console.log(value3);
setTimeout(() => {
@ -2673,14 +2797,20 @@ const tests = {
}
`,
errors: [
// value2
`Assignments to the 'value2' variable from inside a React useEffect Hook ` +
`will not persist between re-renders. ` +
`If it's only needed by this Hook, move the variable inside it. ` +
`Alternatively, declare a ref with the useRef Hook, ` +
`and keep the mutable value in its 'current' property.`,
// value
`Assignments to the 'value' variable from inside a React useEffect Hook ` +
`will not persist between re-renders. ` +
`If it's only needed by this Hook, move the variable inside it. ` +
`Alternatively, declare a ref with the useRef Hook, ` +
`and keep the mutable value in its 'current' property.`,
// value2
`Assignments to the 'value2' variable from inside a React useEffect Hook ` +
// value4
`Assignments to the 'value4' variable from inside a React useEffect Hook ` +
`will not persist between re-renders. ` +
`If it's only needed by this Hook, move the variable inside it. ` +
`Alternatively, declare a ref with the useRef Hook, ` +
@ -3339,7 +3469,7 @@ const tests = {
`The 'handleNext' function makes the dependencies of ` +
`useEffect Hook (at line 11) change on every render. ` +
`To fix this, move the 'handleNext' function ` +
`inside the useEffect callback. Alternatively, ` +
`inside the useEffect callback (at line 9). Alternatively, ` +
`wrap the 'handleNext' definition into its own useCallback() Hook.`,
],
},
@ -3378,7 +3508,7 @@ const tests = {
`The 'handleNext' function makes the dependencies of ` +
`useEffect Hook (at line 11) change on every render. ` +
`To fix this, move the 'handleNext' function ` +
`inside the useEffect callback. Alternatively, ` +
`inside the useEffect callback (at line 9). Alternatively, ` +
`wrap the 'handleNext' definition into its own useCallback() Hook.`,
],
},
@ -3476,15 +3606,15 @@ const tests = {
errors: [
"The 'handleNext1' function makes the dependencies of useEffect Hook " +
"(at line 14) change on every render. To fix this, move the 'handleNext1' " +
'function inside the useEffect callback. Alternatively, wrap the ' +
'function inside the useEffect callback (at line 12). Alternatively, wrap the ' +
"'handleNext1' definition into its own useCallback() Hook.",
"The 'handleNext2' function makes the dependencies of useLayoutEffect Hook " +
"(at line 17) change on every render. To fix this, move the 'handleNext2' " +
'function inside the useLayoutEffect callback. Alternatively, wrap the ' +
'function inside the useLayoutEffect callback (at line 15). Alternatively, wrap the ' +
"'handleNext2' definition into its own useCallback() Hook.",
"The 'handleNext3' function makes the dependencies of useMemo Hook " +
"(at line 20) change on every render. To fix this, move the 'handleNext3' " +
'function inside the useMemo callback. Alternatively, wrap the ' +
'function inside the useMemo callback (at line 18). Alternatively, wrap the ' +
"'handleNext3' definition into its own useCallback() Hook.",
],
},
@ -3544,15 +3674,15 @@ const tests = {
errors: [
"The 'handleNext1' function makes the dependencies of useEffect Hook " +
"(at line 15) change on every render. To fix this, move the 'handleNext1' " +
'function inside the useEffect callback. Alternatively, wrap the ' +
'function inside the useEffect callback (at line 12). Alternatively, wrap the ' +
"'handleNext1' definition into its own useCallback() Hook.",
"The 'handleNext2' function makes the dependencies of useLayoutEffect Hook " +
"(at line 19) change on every render. To fix this, move the 'handleNext2' " +
'function inside the useLayoutEffect callback. Alternatively, wrap the ' +
'function inside the useLayoutEffect callback (at line 16). Alternatively, wrap the ' +
"'handleNext2' definition into its own useCallback() Hook.",
"The 'handleNext3' function makes the dependencies of useMemo Hook " +
"(at line 23) change on every render. To fix this, move the 'handleNext3' " +
'function inside the useMemo callback. Alternatively, wrap the ' +
'function inside the useMemo callback (at line 20). Alternatively, wrap the ' +
"'handleNext3' definition into its own useCallback() Hook.",
],
},
@ -3700,6 +3830,47 @@ const tests = {
"'handleNext2' definition into its own useCallback() Hook.",
],
},
{
code: `
function MyComponent(props) {
let handleNext = () => {
console.log('hello');
};
if (props.foo) {
handleNext = () => {
console.log('hello');
};
}
useEffect(() => {
return Store.subscribe(handleNext);
}, [handleNext]);
}
`,
// Normally we'd suggest moving handleNext inside an
// effect. But it's used more than once.
// TODO: our autofix here isn't quite sufficient because
// it only wraps the first definition. But seems ok.
output: `
function MyComponent(props) {
let handleNext = useCallback(() => {
console.log('hello');
});
if (props.foo) {
handleNext = () => {
console.log('hello');
};
}
useEffect(() => {
return Store.subscribe(handleNext);
}, [handleNext]);
}
`,
errors: [
"The 'handleNext' function makes the dependencies of useEffect Hook " +
'(at line 13) change on every render. To fix this, wrap the ' +
"'handleNext' definition into its own useCallback() Hook.",
],
},
{
code: `
function MyComponent(props) {
@ -3737,7 +3908,7 @@ const tests = {
`The 'handleNext' function makes the dependencies of ` +
`useEffect Hook (at line 14) change on every render. ` +
`To fix this, move the 'handleNext' function inside ` +
`the useEffect callback. Alternatively, wrap the ` +
`the useEffect callback (at line 12). Alternatively, wrap the ` +
`'handleNext' definition into its own useCallback() Hook.`,
],
},
@ -3770,13 +3941,260 @@ const tests = {
return <h1>{count}</h1>;
}
`,
// TODO: ideally this should suggest useState updater form
// since this code doesn't actually work.
errors: [
"React Hook useEffect has a missing dependency: 'count'. " +
'Either include it or remove the dependency array. ' +
`If 'count' is only necessary for calculating the next state, ` +
`consider refactoring to the setCount(count => ...) form which ` +
`doesn't need to depend on the state from outside.`,
],
},
{
code: `
function Counter() {
let [count, setCount] = useState(0);
let [increment, setIncrement] = useState(0);
useEffect(() => {
let id = setInterval(() => {
setCount(count + increment);
}, 1000);
return () => clearInterval(id);
}, []);
return <h1>{count}</h1>;
}
`,
output: `
function Counter() {
let [count, setCount] = useState(0);
let [increment, setIncrement] = useState(0);
useEffect(() => {
let id = setInterval(() => {
setCount(count + increment);
}, 1000);
return () => clearInterval(id);
}, [count, increment]);
return <h1>{count}</h1>;
}
`,
errors: [
"React Hook useEffect has missing dependencies: 'count' and 'increment'. " +
'Either include them or remove the dependency array. ' +
`If 'count' is only necessary for calculating the next state, ` +
`consider refactoring to the setCount(count => ...) form which ` +
`doesn't need to depend on the state from outside.`,
],
},
{
code: `
function Counter() {
let [count, setCount] = useState(0);
let [increment, setIncrement] = useState(0);
useEffect(() => {
let id = setInterval(() => {
setCount(count => count + increment);
}, 1000);
return () => clearInterval(id);
}, []);
return <h1>{count}</h1>;
}
`,
output: `
function Counter() {
let [count, setCount] = useState(0);
let [increment, setIncrement] = useState(0);
useEffect(() => {
let id = setInterval(() => {
setCount(count => count + increment);
}, 1000);
return () => clearInterval(id);
}, [increment]);
return <h1>{count}</h1>;
}
`,
errors: [
"React Hook useEffect has a missing dependency: 'increment'. " +
'Either include it or remove the dependency array. ' +
`If 'increment' is only necessary for calculating the next state, ` +
`consider refactoring to the useReducer Hook. This ` +
`lets you move the calculation of next state outside the effect.`,
],
},
{
code: `
function Counter() {
let [count, setCount] = useState(0);
let increment = useCustomHook();
useEffect(() => {
let id = setInterval(() => {
setCount(count => count + increment);
}, 1000);
return () => clearInterval(id);
}, []);
return <h1>{count}</h1>;
}
`,
output: `
function Counter() {
let [count, setCount] = useState(0);
let increment = useCustomHook();
useEffect(() => {
let id = setInterval(() => {
setCount(count => count + increment);
}, 1000);
return () => clearInterval(id);
}, [increment]);
return <h1>{count}</h1>;
}
`,
// This intentionally doesn't show the reducer message
// because we don't know if it's safe for it to close over a value.
// We only show it for state variables (and possibly props).
errors: [
"React Hook useEffect has a missing dependency: 'increment'. " +
'Either include it or remove the dependency array.',
],
},
{
code: `
function Counter({ step }) {
let [count, setCount] = useState(0);
function increment(x) {
return x + step;
}
useEffect(() => {
let id = setInterval(() => {
setCount(count => increment(count));
}, 1000);
return () => clearInterval(id);
}, []);
return <h1>{count}</h1>;
}
`,
output: `
function Counter({ step }) {
let [count, setCount] = useState(0);
function increment(x) {
return x + step;
}
useEffect(() => {
let id = setInterval(() => {
setCount(count => increment(count));
}, 1000);
return () => clearInterval(id);
}, [increment]);
return <h1>{count}</h1>;
}
`,
// This intentionally doesn't show the reducer message
// because we don't know if it's safe for it to close over a value.
// We only show it for state variables (and possibly props).
errors: [
"React Hook useEffect has a missing dependency: 'increment'. " +
'Either include it or remove the dependency array.',
],
},
{
code: `
function Counter({ step }) {
let [count, setCount] = useState(0);
function increment(x) {
return x + step;
}
useEffect(() => {
let id = setInterval(() => {
setCount(count => increment(count));
}, 1000);
return () => clearInterval(id);
}, [increment]);
return <h1>{count}</h1>;
}
`,
output: `
function Counter({ step }) {
let [count, setCount] = useState(0);
function increment(x) {
return x + step;
}
useEffect(() => {
let id = setInterval(() => {
setCount(count => increment(count));
}, 1000);
return () => clearInterval(id);
}, [increment]);
return <h1>{count}</h1>;
}
`,
errors: [
`The 'increment' function makes the dependencies of useEffect Hook ` +
`(at line 14) change on every render. To fix this, move the ` +
`'increment' function inside the useEffect callback (at line 9). ` +
`Alternatively, wrap the \'increment\' definition into its own ` +
`useCallback() Hook.`,
],
},
{
code: `
function Counter({ increment }) {
let [count, setCount] = useState(0);
useEffect(() => {
let id = setInterval(() => {
setCount(count => count + increment);
}, 1000);
return () => clearInterval(id);
}, []);
return <h1>{count}</h1>;
}
`,
output: `
function Counter({ increment }) {
let [count, setCount] = useState(0);
useEffect(() => {
let id = setInterval(() => {
setCount(count => count + increment);
}, 1000);
return () => clearInterval(id);
}, [increment]);
return <h1>{count}</h1>;
}
`,
errors: [
"React Hook useEffect has a missing dependency: 'increment'. " +
'Either include it or remove the dependency array. ' +
`If 'increment' is only necessary for calculating the next state, ` +
`consider refactoring to the useReducer Hook. This lets you move ` +
`the calculation of next state outside the effect. ` +
`You can then read 'increment' from the reducer ` +
`by putting it directly in your component.`,
],
},
{
code: `
function Counter() {
@ -3849,6 +4267,112 @@ const tests = {
`Either include it or remove the dependency array.`,
],
},
{
code: `
function Podcasts({ fetchPodcasts, id }) {
let [podcasts, setPodcasts] = useState(null);
useEffect(() => {
fetchPodcasts(id).then(setPodcasts);
}, [id]);
}
`,
output: `
function Podcasts({ fetchPodcasts, id }) {
let [podcasts, setPodcasts] = useState(null);
useEffect(() => {
fetchPodcasts(id).then(setPodcasts);
}, [fetchPodcasts, id]);
}
`,
errors: [
`React Hook useEffect has a missing dependency: 'fetchPodcasts'. ` +
`Either include it or remove the dependency array. ` +
`If specifying 'fetchPodcasts' makes the dependencies change too often, ` +
`find the parent component that defines it and wrap that definition in useCallback.`,
],
},
{
code: `
function Podcasts({ api: { fetchPodcasts }, id }) {
let [podcasts, setPodcasts] = useState(null);
useEffect(() => {
fetchPodcasts(id).then(setPodcasts);
}, [id]);
}
`,
output: `
function Podcasts({ api: { fetchPodcasts }, id }) {
let [podcasts, setPodcasts] = useState(null);
useEffect(() => {
fetchPodcasts(id).then(setPodcasts);
}, [fetchPodcasts, id]);
}
`,
errors: [
`React Hook useEffect has a missing dependency: 'fetchPodcasts'. ` +
`Either include it or remove the dependency array. ` +
`If specifying 'fetchPodcasts' makes the dependencies change too often, ` +
`find the parent component that defines it and wrap that definition in useCallback.`,
],
},
{
code: `
function Podcasts({ fetchPodcasts, fetchPodcasts2, id }) {
let [podcasts, setPodcasts] = useState(null);
useEffect(() => {
setTimeout(() => {
console.log(id);
fetchPodcasts(id).then(setPodcasts);
fetchPodcasts2(id).then(setPodcasts);
});
}, [id]);
}
`,
output: `
function Podcasts({ fetchPodcasts, fetchPodcasts2, id }) {
let [podcasts, setPodcasts] = useState(null);
useEffect(() => {
setTimeout(() => {
console.log(id);
fetchPodcasts(id).then(setPodcasts);
fetchPodcasts2(id).then(setPodcasts);
});
}, [fetchPodcasts, fetchPodcasts2, id]);
}
`,
errors: [
`React Hook useEffect has missing dependencies: 'fetchPodcasts' and 'fetchPodcasts2'. ` +
`Either include them or remove the dependency array. ` +
`If specifying 'fetchPodcasts' makes the dependencies change too often, ` +
`find the parent component that defines it and wrap that definition in useCallback.`,
],
},
{
code: `
function Podcasts({ fetchPodcasts, id }) {
let [podcasts, setPodcasts] = useState(null);
useEffect(() => {
console.log(fetchPodcasts);
fetchPodcasts(id).then(setPodcasts);
}, [id]);
}
`,
output: `
function Podcasts({ fetchPodcasts, id }) {
let [podcasts, setPodcasts] = useState(null);
useEffect(() => {
console.log(fetchPodcasts);
fetchPodcasts(id).then(setPodcasts);
}, [fetchPodcasts, id]);
}
`,
errors: [
`React Hook useEffect has a missing dependency: 'fetchPodcasts'. ` +
`Either include it or remove the dependency array. ` +
`If specifying 'fetchPodcasts' makes the dependencies change too often, ` +
`find the parent component that defines it and wrap that definition in useCallback.`,
],
},
],
};

View File

@ -35,6 +35,8 @@ export default {
const options = {additionalHooks};
// Should be shared between visitors.
let setStateCallSites = new WeakMap();
let stateVariables = new WeakSet();
let staticKnownValueCache = new WeakMap();
let functionWithoutCapturedValueCache = new WeakMap();
function memoizeWithWeakMap(fn, map) {
@ -204,19 +206,40 @@ export default {
return false;
}
const id = def.node.id;
if (callee.name === 'useRef' && id.type === 'Identifier') {
const {name} = callee;
if (name === 'useRef' && id.type === 'Identifier') {
// useRef() return value is static.
return true;
} else if (callee.name === 'useState' || callee.name === 'useReducer') {
} 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?
id.elements[1] === resolved.identifiers[0]
Array.isArray(resolved.identifiers)
) {
return true;
// Is second tuple value the same reference we're checking?
if (id.elements[1] === resolved.identifiers[0]) {
if (name === 'useState') {
const references = resolved.references;
for (let 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') {
const references = resolved.references;
for (let i = 0; i < references.length; i++) {
stateVariables.add(references[i].identifier);
}
}
// State variable itself is dynamic.
return false;
}
}
}
// By default assume it's dynamic.
@ -365,8 +388,10 @@ export default {
memoizedIsFunctionWithoutCapturedValues(resolved);
dependencies.set(dependency, {
isStatic,
reference,
references: [reference],
});
} else {
dependencies.get(dependency).references.push(reference);
}
}
for (const childScope of currentScope.childScopes) {
@ -514,9 +539,12 @@ export default {
// Warn about assigning to variables in the outer scope.
// Those are usually bugs.
let foundStaleAssignments = false;
let staleAssignments = new Set();
function reportStaleAssignment(writeExpr, key) {
foundStaleAssignments = true;
if (staleAssignments.has(key)) {
return;
}
staleAssignments.add(key);
context.report({
node: writeExpr,
message:
@ -532,15 +560,17 @@ export default {
// Remember which deps are optional and report bad usage first.
const optionalDependencies = new Set();
dependencies.forEach(({isStatic, reference}, key) => {
dependencies.forEach(({isStatic, references}, key) => {
if (isStatic) {
optionalDependencies.add(key);
}
if (reference.writeExpr) {
reportStaleAssignment(reference.writeExpr, key);
}
references.forEach(reference => {
if (reference.writeExpr) {
reportStaleAssignment(reference.writeExpr, key);
}
});
});
if (foundStaleAssignments) {
if (staleAssignments.size > 0) {
// The intent isn't clear so we'll wait until you fix those first.
return;
}
@ -588,8 +618,10 @@ export default {
} else {
message +=
` To fix this, move the '${fn.name.name}' function ` +
`inside the ${reactiveHookName} callback. Alternatively, ` +
`wrap the '${
`inside the ${reactiveHookName} callback (at line ${
node.loc.start.line
}). ` +
`Alternatively, wrap the '${
fn.name.name
}' definition into its own useCallback() Hook.`;
}
@ -697,7 +729,7 @@ export default {
if (propDep == null) {
return;
}
const refs = propDep.reference.resolved.references;
const refs = propDep.references;
if (!Array.isArray(refs)) {
return;
}
@ -724,8 +756,154 @@ export default {
}
if (isPropsOnlyUsedInMembers) {
extraWarning =
' Alternatively, destructure the necessary props ' +
'outside the callback.';
` However, the preferred fix is to destructure the 'props' ` +
`object outside of the ${reactiveHookName} call and ` +
`refer to specific props directly by their names.`;
}
}
if (!extraWarning && missingDependencies.size > 0) {
// See if the user is trying to avoid specifying a callable prop.
// This usually means they're unaware of useCallback.
let missingCallbackDep = null;
missingDependencies.forEach(missingDep => {
if (missingCallbackDep) {
return;
}
// Is this a variable from top scope?
const topScopeRef = componentScope.set.get(missingDep);
const usedDep = dependencies.get(missingDep);
if (usedDep.references[0].resolved !== topScopeRef) {
return;
}
// Is this a destructured prop?
const def = topScopeRef.defs[0];
if (def == null || def.name == null || def.type !== 'Parameter') {
return;
}
// Was it called in at least one case? Then it's a function.
let isFunctionCall = false;
let id;
for (let i = 0; i < usedDep.references.length; i++) {
id = usedDep.references[i].identifier;
if (
id != null &&
id.parent != null &&
id.parent.type === 'CallExpression' &&
id.parent.callee === id
) {
isFunctionCall = true;
break;
}
}
if (!isFunctionCall) {
return;
}
// If it's missing (i.e. in component scope) *and* it's a parameter
// then it is definitely coming from props destructuring.
// (It could also be props itself but we wouldn't be calling it then.)
missingCallbackDep = missingDep;
});
if (missingCallbackDep !== null) {
extraWarning =
` If specifying '${missingCallbackDep}'` +
` makes the dependencies change too often, ` +
`find the parent component that defines it ` +
`and wrap that definition in useCallback.`;
}
}
if (!extraWarning && missingDependencies.size > 0) {
let setStateRecommendation = null;
missingDependencies.forEach(missingDep => {
if (setStateRecommendation !== null) {
return;
}
const usedDep = dependencies.get(missingDep);
const references = usedDep.references;
let id;
let maybeCall;
for (let i = 0; i < references.length; i++) {
id = references[i].identifier;
maybeCall = id.parent;
// Try to see if we have setState(someExpr(missingDep)).
while (maybeCall != null && maybeCall !== componentScope.block) {
if (maybeCall.type === 'CallExpression') {
const correspondingStateVariable = setStateCallSites.get(
maybeCall.callee,
);
if (correspondingStateVariable != null) {
if (correspondingStateVariable.name === missingDep) {
// setCount(count + 1)
setStateRecommendation = {
missingDep,
setter: maybeCall.callee.name,
form: 'updater',
};
} else if (stateVariables.has(id)) {
// setCount(count + increment)
setStateRecommendation = {
missingDep,
setter: maybeCall.callee.name,
form: 'reducer',
};
} else {
const resolved = references[i].resolved;
if (resolved != null) {
// If it's a parameter *and* a missing dep,
// it must be a prop or something inside a prop.
// Therefore, recommend an inline reducer.
const def = resolved.defs[0];
if (def != null && def.type === 'Parameter') {
setStateRecommendation = {
missingDep,
setter: maybeCall.callee.name,
form: 'inlineReducer',
};
}
}
}
break;
}
}
maybeCall = maybeCall.parent;
}
if (setStateRecommendation !== null) {
break;
}
}
});
if (setStateRecommendation !== null) {
let suggestion;
switch (setStateRecommendation.form) {
case 'reducer':
suggestion =
'useReducer Hook. This lets you move the calculation ' +
'of next state outside the effect.';
break;
case 'inlineReducer':
suggestion =
'useReducer Hook. This lets you move the calculation ' +
'of next state outside the effect. You can then ' +
`read '${
setStateRecommendation.missingDep
}' from the reducer ` +
`by putting it directly in your component.`;
break;
case 'updater':
suggestion =
`${setStateRecommendation.setter}(${
setStateRecommendation.missingDep
} => ...) form ` +
`which doesn't need to depend on the state from outside.`;
break;
default:
throw new Error('Unknown case.');
}
extraWarning =
` If '${setStateRecommendation.missingDep}'` +
` is only necessary for calculating the next state, ` +
`consider refactoring to the ${suggestion}`;
}
}
@ -981,9 +1159,7 @@ function scanForDeclaredBareFunctions({
if (currentScope !== scope) {
// This reference is outside the Hook callback.
// It can only be legit if it's the deps array.
if (isAncestorNodeOf(declaredDependenciesNode, reference.identifier)) {
continue;
} else {
if (!isAncestorNodeOf(declaredDependenciesNode, reference.identifier)) {
return true;
}
}