@@ -468,9 +468,101 @@ export default {
468
468
} ,
469
469
) ;
470
470
471
+ // Warn about assigning to variables in the outer scope.
472
+ // Those are usually bugs.
473
+ let staleAssignments = new Set ( ) ;
474
+ function reportStaleAssignment ( writeExpr , key ) {
475
+ if ( staleAssignments . has ( key ) ) {
476
+ return ;
477
+ }
478
+ staleAssignments . add ( key ) ;
479
+ context . report ( {
480
+ node : writeExpr ,
481
+ message :
482
+ `Assignments to the '${ key } ' variable from inside React Hook ` +
483
+ `${ context . getSource ( reactiveHook ) } will be lost after each ` +
484
+ `render. To preserve the value over time, store it in a useRef ` +
485
+ `Hook and keep the mutable value in the '.current' property. ` +
486
+ `Otherwise, you can move this variable directly inside ` +
487
+ `${ context . getSource ( reactiveHook ) } .` ,
488
+ } ) ;
489
+ }
490
+
491
+ // Remember which deps are optional and report bad usage first.
492
+ const optionalDependencies = new Set ( ) ;
493
+ dependencies . forEach ( ( { isStatic, references} , key ) => {
494
+ if ( isStatic ) {
495
+ optionalDependencies . add ( key ) ;
496
+ }
497
+ references . forEach ( reference => {
498
+ if ( reference . writeExpr ) {
499
+ reportStaleAssignment ( reference . writeExpr , key ) ;
500
+ }
501
+ } ) ;
502
+ } ) ;
503
+
504
+ if ( staleAssignments . size > 0 ) {
505
+ // The intent isn't clear so we'll wait until you fix those first.
506
+ return ;
507
+ }
508
+
471
509
if ( ! declaredDependenciesNode ) {
510
+ // Check if there are any top-level setState() calls.
511
+ // Those tend to lead to infinite loops.
512
+ let setStateInsideEffectWithoutDeps = null ;
513
+ dependencies . forEach ( ( { isStatic, references} , key ) => {
514
+ if ( setStateInsideEffectWithoutDeps ) {
515
+ return ;
516
+ }
517
+ references . forEach ( reference => {
518
+ if ( setStateInsideEffectWithoutDeps ) {
519
+ return ;
520
+ }
521
+
522
+ const id = reference . identifier ;
523
+ const isSetState = setStateCallSites . has ( id ) ;
524
+ if ( ! isSetState ) {
525
+ return ;
526
+ }
527
+
528
+ let fnScope = reference . from ;
529
+ while ( fnScope . type !== 'function' ) {
530
+ fnScope = fnScope . upper ;
531
+ }
532
+ const isDirectlyInsideEffect = fnScope . block === node ;
533
+ if ( isDirectlyInsideEffect ) {
534
+ // TODO: we could potentially ignore early returns.
535
+ setStateInsideEffectWithoutDeps = key ;
536
+ }
537
+ } ) ;
538
+ } ) ;
539
+ if ( setStateInsideEffectWithoutDeps ) {
540
+ let { suggestedDependencies} = collectRecommendations ( {
541
+ dependencies,
542
+ declaredDependencies : [ ] ,
543
+ optionalDependencies,
544
+ externalDependencies : new Set ( ) ,
545
+ isEffect : true ,
546
+ } ) ;
547
+ context . report ( {
548
+ node : node . parent . callee ,
549
+ message :
550
+ `React Hook ${ reactiveHookName } contains a call to '${ setStateInsideEffectWithoutDeps } '. ` +
551
+ `Without a list of dependencies, this can lead to an infinite chain of updates. ` +
552
+ `To fix this, pass [` +
553
+ suggestedDependencies . join ( ', ' ) +
554
+ `] as a second argument to the ${ reactiveHookName } Hook.` ,
555
+ fix ( fixer ) {
556
+ return fixer . insertTextAfter (
557
+ node ,
558
+ `, [${ suggestedDependencies . join ( ', ' ) } ]` ,
559
+ ) ;
560
+ } ,
561
+ } ) ;
562
+ }
472
563
return ;
473
564
}
565
+
474
566
const declaredDependencies = [ ] ;
475
567
const externalDependencies = new Set ( ) ;
476
568
if ( declaredDependenciesNode . type !== 'ArrayExpression' ) {
@@ -569,43 +661,6 @@ export default {
569
661
} ) ;
570
662
}
571
663
572
- // Warn about assigning to variables in the outer scope.
573
- // Those are usually bugs.
574
- let staleAssignments = new Set ( ) ;
575
- function reportStaleAssignment ( writeExpr , key ) {
576
- if ( staleAssignments . has ( key ) ) {
577
- return ;
578
- }
579
- staleAssignments . add ( key ) ;
580
- context . report ( {
581
- node : writeExpr ,
582
- message :
583
- `Assignments to the '${ key } ' variable from inside React Hook ` +
584
- `${ context . getSource ( reactiveHook ) } will be lost after each ` +
585
- `render. To preserve the value over time, store it in a useRef ` +
586
- `Hook and keep the mutable value in the '.current' property. ` +
587
- `Otherwise, you can move this variable directly inside ` +
588
- `${ context . getSource ( reactiveHook ) } .` ,
589
- } ) ;
590
- }
591
-
592
- // Remember which deps are optional and report bad usage first.
593
- const optionalDependencies = new Set ( ) ;
594
- dependencies . forEach ( ( { isStatic, references} , key ) => {
595
- if ( isStatic ) {
596
- optionalDependencies . add ( key ) ;
597
- }
598
- references . forEach ( reference => {
599
- if ( reference . writeExpr ) {
600
- reportStaleAssignment ( reference . writeExpr , key ) ;
601
- }
602
- } ) ;
603
- } ) ;
604
- if ( staleAssignments . size > 0 ) {
605
- // The intent isn't clear so we'll wait until you fix those first.
606
- return ;
607
- }
608
-
609
664
let {
610
665
suggestedDependencies,
611
666
unnecessaryDependencies,
0 commit comments