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 <kidkkr@icloud.com> Co-authored-by: Luke Kang <kidkkr@icloud.com>
1 parent 0e49074 commit bf13d3e

2 files changed

Lines changed: 50 additions & 41 deletions

File tree

‎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-
functioncountPathsFromStart(segment){
155+
functioncountPathsFromStart(segment,pathHistory){
156156
const{cache}=countPathsFromStart;
157157
letpaths=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-
return0;
164-
}else{
165-
cyclic.add(segment.id);
166-
for(constprevSegmentofsegment.prevSegments){
167-
countPathsFromStart(prevSegment);
168-
}
169-
return0;
158+
constpathList=newSet(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+
constpathArray=[...pathList];
164+
constcyclicSegments=pathArray.slice(
165+
pathArray.indexOf(segment.id)+1,
166+
);
167+
for(constcyclicSegmentofcyclicSegments){
168+
cyclic.add(cyclicSegment);
170169
}
170+
171+
return0;
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
returnpaths;
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
}elseif(segment.prevSegments.length===0){
183185
paths=1;
184186
}else{
185187
paths=0;
186188
for(constprevSegmentofsegment.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-
functioncountPathsToEnd(segment){
226+
functioncountPathsToEnd(segment,pathHistory){
225227
const{cache}=countPathsToEnd;
226228
letpaths=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-
return0;
233-
}else{
234-
cyclic.add(segment.id);
235-
for(constnextSegmentofsegment.nextSegments){
236-
countPathsToEnd(nextSegment);
237-
}
238-
return0;
229+
letpathList=newSet(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+
constpathArray=Array.from(pathList);
235+
constcyclicSegments=pathArray.slice(
236+
pathArray.indexOf(segment.id)+1,
237+
);
238+
for(constcyclicSegmentofcyclicSegments){
239+
cyclic.add(cyclicSegment);
239240
}
241+
242+
return0;
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
returnpaths;
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
}elseif(segment.nextSegments.length===0){
252256
paths=1;
253257
}else{
254258
paths=0;
255259
for(constnextSegmentofsegment.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
returnpaths;
262266
}
263267

0 commit comments

Comments
 (0)