@@ -468,9 +468,101 @@ export default {
468468 } ,
469469 ) ;
470470
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+
471509 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+ }
472563 return ;
473564 }
565+
474566 const declaredDependencies = [ ] ;
475567 const externalDependencies = new Set ( ) ;
476568 if ( declaredDependenciesNode . type !== 'ArrayExpression' ) {
@@ -569,43 +661,6 @@ export default {
569661 } ) ;
570662 }
571663
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-
609664 let {
610665 suggestedDependencies,
611666 unnecessaryDependencies,
0 commit comments