@@ -787,8 +787,9 @@ public function getMaybeDefinedVariables(): array
787787 public function findPossiblyImpureCallDescriptions (Expr $ expr ): array
788788 {
789789 $ nodeFinder = new NodeFinder ();
790- $ descriptions = [];
790+ $ callExprDescriptions = [];
791791 $ foundCallExprMatch = false ;
792+ $ matchedCallExprKeys = [];
792793 foreach ($ this ->expressionTypes as $ holder ) {
793794 $ holderExpr = $ holder ->getExpr ();
794795 if (!$ holderExpr instanceof PossiblyImpureCallExpr) {
@@ -810,6 +811,7 @@ public function findPossiblyImpureCallDescriptions(Expr $expr): array
810811 }
811812
812813 $ foundCallExprMatch = true ;
814+ $ matchedCallExprKeys [$ callExprKey ] = true ;
813815
814816 // Only show the tip when the scope's type for the call expression
815817 // differs from the declared return type, meaning control flow
@@ -821,58 +823,70 @@ public function findPossiblyImpureCallDescriptions(Expr $expr): array
821823 continue ;
822824 }
823825
824- $ descriptions [] = $ holderExpr ->getCallDescription ();
825- }
826-
827- if (count ($ descriptions ) > 0 ) {
828- return array_values (array_unique ($ descriptions ));
826+ $ callExprDescriptions [] = $ holderExpr ->getCallDescription ();
829827 }
830828
831829 // If the first pass found a callExpr in the error expression but
832830 // filtered it out (return type wasn't narrowed), the error is
833831 // explained by the return type alone - skip the fallback.
834- if ($ foundCallExprMatch ) {
832+ if ($ foundCallExprMatch && count ( $ callExprDescriptions ) === 0 ) {
835833 return [];
836834 }
837835
838- // Fallback : match by impactedExpr for cases where a maybe-impure method
836+ // Second pass : match by impactedExpr for cases where a maybe-impure method
839837 // on an object didn't invalidate it, but a different method's return
840838 // value was narrowed on that object.
841839 // Skip when the expression itself is a direct method/static call -
842840 // those are passed by ImpossibleCheckType rules where the error is
843841 // about the call's arguments, not about object state.
844- if ($ expr instanceof Expr \MethodCall || $ expr instanceof Expr \StaticCall) {
845- return [];
846- }
847- foreach ($ this ->expressionTypes as $ holder ) {
848- $ holderExpr = $ holder ->getExpr ();
849- if (!$ holderExpr instanceof PossiblyImpureCallExpr) {
850- continue ;
851- }
842+ if (!($ expr instanceof Expr \MethodCall || $ expr instanceof Expr \StaticCall)) {
843+ $ impactedExprDescriptions = [];
844+ foreach ($ this ->expressionTypes as $ holder ) {
845+ $ holderExpr = $ holder ->getExpr ();
846+ if (!$ holderExpr instanceof PossiblyImpureCallExpr) {
847+ continue ;
848+ }
852849
853- $ impactedExprKey = $ this ->getNodeKey ($ holderExpr ->impactedExpr );
850+ $ impactedExprKey = $ this ->getNodeKey ($ holderExpr ->impactedExpr );
854851
855- // Skip if impactedExpr is the same as callExpr (function calls)
856- if ($ impactedExprKey === $ this ->getNodeKey ($ holderExpr ->callExpr )) {
857- continue ;
858- }
852+ // Skip if impactedExpr is the same as callExpr (function calls)
853+ if ($ impactedExprKey === $ this ->getNodeKey ($ holderExpr ->callExpr )) {
854+ continue ;
855+ }
859856
860- $ found = $ nodeFinder ->findFirst ([$ expr ], function (Node $ node ) use ($ impactedExprKey ): bool {
861- if (!$ node instanceof Expr) {
862- return false ;
857+ // Skip if this entry's callExpr was already matched in the first pass
858+ $ callExprKey = $ this ->getNodeKey ($ holderExpr ->callExpr );
859+ if (isset ($ matchedCallExprKeys [$ callExprKey ])) {
860+ continue ;
863861 }
864862
865- return $ this ->getNodeKey ($ node ) === $ impactedExprKey ;
866- });
863+ $ found = $ nodeFinder ->findFirst ([$ expr ], function (Node $ node ) use ($ impactedExprKey ): bool {
864+ if (!$ node instanceof Expr) {
865+ return false ;
866+ }
867867
868- if ($ found === null ) {
869- continue ;
868+ return $ this ->getNodeKey ($ node ) === $ impactedExprKey ;
869+ });
870+
871+ if ($ found === null ) {
872+ continue ;
873+ }
874+
875+ $ impactedExprDescriptions [] = $ holderExpr ->getCallDescription ();
870876 }
871877
872- $ descriptions [] = $ holderExpr ->getCallDescription ();
878+ // Prefer impactedExpr matches (intermediate calls that could have
879+ // invalidated the object) over callExpr matches
880+ if (count ($ impactedExprDescriptions ) > 0 ) {
881+ return array_values (array_unique ($ impactedExprDescriptions ));
882+ }
883+ }
884+
885+ if (count ($ callExprDescriptions ) > 0 ) {
886+ return array_values (array_unique ($ callExprDescriptions ));
873887 }
874888
875- return array_values ( array_unique ( $ descriptions )) ;
889+ return [] ;
876890 }
877891
878892 private function isGlobalVariable (string $ variableName ): bool
0 commit comments