Skip to content

Commit bf13d3e

Browse files
M-IzadmehrLuke Kang
andauthored
[eslint-plugin-react-hooks] Fix cyclic caching for loops containing a… (#16853)
* [eslint-plugin-react-hooks] Fix cyclic caching for loops containing a condition * [eslint-plugin-react-hooks] prettier write * [eslint-plugin-react-hooks] Fix set for tests * Update packages/eslint-plugin-react-hooks/src/RulesOfHooks.js Co-Authored-By: Luke Kang <[email protected]> Co-authored-by: Luke Kang <[email protected]>
1 parent 0e49074 commit bf13d3e

File tree

2 files changed

+50
-41
lines changed

2 files changed

+50
-41
lines changed

packages/eslint-plugin-react-hooks/__tests__/ESLintRulesOfHooks-test.js

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -405,6 +405,18 @@ const tests = {
405405
useHook();
406406
}
407407
`,
408+
`
409+
// Valid because the neither the condition nor the loop affect the hook call.
410+
function App(props) {
411+
const someObject = {propA: true};
412+
for (const propName in someObject) {
413+
if (propName === true) {
414+
} else {
415+
}
416+
}
417+
const [myState, setMyState] = useState(null);
418+
}
419+
`,
408420
],
409421
invalid: [
410422
{
@@ -640,14 +652,7 @@ const tests = {
640652
}
641653
}
642654
`,
643-
errors: [
644-
loopError('useHook1'),
645-
646-
// NOTE: Small imprecision in error reporting due to caching means we
647-
// have a conditional error here instead of a loop error. However,
648-
// we will always get an error so this is acceptable.
649-
conditionalError('useHook2', true),
650-
],
655+
errors: [loopError('useHook1'), loopError('useHook2', true)],
651656
},
652657
{
653658
code: `

packages/eslint-plugin-react-hooks/src/RulesOfHooks.js

Lines changed: 37 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -152,39 +152,41 @@ export default {
152152
* Populates `cyclic` with cyclic segments.
153153
*/
154154

155-
function countPathsFromStart(segment) {
155+
function countPathsFromStart(segment, pathHistory) {
156156
const {cache} = countPathsFromStart;
157157
let paths = cache.get(segment.id);
158-
159-
// If `paths` is null then we've found a cycle! Add it to `cyclic` and
160-
// any other segments which are a part of this cycle.
161-
if (paths === null) {
162-
if (cyclic.has(segment.id)) {
163-
return 0;
164-
} else {
165-
cyclic.add(segment.id);
166-
for (const prevSegment of segment.prevSegments) {
167-
countPathsFromStart(prevSegment);
168-
}
169-
return 0;
158+
const pathList = new Set(pathHistory);
159+
160+
// If `pathList` includes the current segment then we've found a cycle!
161+
// We need to fill `cyclic` with all segments inside cycle
162+
if (pathList.has(segment.id)) {
163+
const pathArray = [...pathList];
164+
const cyclicSegments = pathArray.slice(
165+
pathArray.indexOf(segment.id) + 1,
166+
);
167+
for (const cyclicSegment of cyclicSegments) {
168+
cyclic.add(cyclicSegment);
170169
}
170+
171+
return 0;
171172
}
172173

174+
// add the current segment to pathList
175+
pathList.add(segment.id);
176+
173177
// We have a cached `paths`. Return it.
174178
if (paths !== undefined) {
175179
return paths;
176180
}
177181

178-
// Compute `paths` and cache it. Guarding against cycles.
179-
cache.set(segment.id, null);
180182
if (codePath.thrownSegments.includes(segment)) {
181183
paths = 0;
182184
} else if (segment.prevSegments.length === 0) {
183185
paths = 1;
184186
} else {
185187
paths = 0;
186188
for (const prevSegment of segment.prevSegments) {
187-
paths += countPathsFromStart(prevSegment);
189+
paths += countPathsFromStart(prevSegment, pathList);
188190
}
189191
}
190192

@@ -221,43 +223,45 @@ export default {
221223
* Populates `cyclic` with cyclic segments.
222224
*/
223225

224-
function countPathsToEnd(segment) {
226+
function countPathsToEnd(segment, pathHistory) {
225227
const {cache} = countPathsToEnd;
226228
let paths = cache.get(segment.id);
227-
228-
// If `paths` is null then we've found a cycle! Add it to `cyclic` and
229-
// any other segments which are a part of this cycle.
230-
if (paths === null) {
231-
if (cyclic.has(segment.id)) {
232-
return 0;
233-
} else {
234-
cyclic.add(segment.id);
235-
for (const nextSegment of segment.nextSegments) {
236-
countPathsToEnd(nextSegment);
237-
}
238-
return 0;
229+
let pathList = new Set(pathHistory);
230+
231+
// If `pathList` includes the current segment then we've found a cycle!
232+
// We need to fill `cyclic` with all segments inside cycle
233+
if (pathList.has(segment.id)) {
234+
const pathArray = Array.from(pathList);
235+
const cyclicSegments = pathArray.slice(
236+
pathArray.indexOf(segment.id) + 1,
237+
);
238+
for (const cyclicSegment of cyclicSegments) {
239+
cyclic.add(cyclicSegment);
239240
}
241+
242+
return 0;
240243
}
241244

245+
// add the current segment to pathList
246+
pathList.add(segment.id);
247+
242248
// We have a cached `paths`. Return it.
243249
if (paths !== undefined) {
244250
return paths;
245251
}
246252

247-
// Compute `paths` and cache it. Guarding against cycles.
248-
cache.set(segment.id, null);
249253
if (codePath.thrownSegments.includes(segment)) {
250254
paths = 0;
251255
} else if (segment.nextSegments.length === 0) {
252256
paths = 1;
253257
} else {
254258
paths = 0;
255259
for (const nextSegment of segment.nextSegments) {
256-
paths += countPathsToEnd(nextSegment);
260+
paths += countPathsToEnd(nextSegment, pathList);
257261
}
258262
}
259-
cache.set(segment.id, paths);
260263

264+
cache.set(segment.id, paths);
261265
return paths;
262266
}
263267

0 commit comments

Comments
 (0)