66use PhpParser \Node \Expr ;
77use PhpParser \Node \Expr \FuncCall ;
88use PhpParser \Node \Expr \MethodCall ;
9+ use PhpParser \Node \Expr \NullsafePropertyFetch ;
910use PhpParser \Node \Expr \PropertyFetch ;
1011use PhpParser \Node \Expr \Variable ;
1112use PhpParser \Node \Name ;
@@ -640,6 +641,7 @@ public static function invalidateExpressionEntries(
640641 array $ expressionTypes ,
641642 array $ nativeExpressionTypes ,
642643 array $ conditionalExpressions ,
644+ bool $ keepPropertyFetches = false ,
643645 ): ?array
644646 {
645647 $ invalidated = false ;
@@ -661,7 +663,7 @@ public static function invalidateExpressionEntries(
661663 ) {
662664 continue ;
663665 }
664- if (!self ::shouldInvalidateExpression ($ scope , $ exprPrinter , $ exprStringToInvalidate , $ expressionToInvalidate , $ exprTypeHolder ->getExpr (), $ exprString , $ requireMoreCharacters , $ invalidatingClass )) {
666+ if (!self ::shouldInvalidateExpression ($ scope , $ exprPrinter , $ exprStringToInvalidate , $ expressionToInvalidate , $ exprTypeHolder ->getExpr (), $ exprString , $ requireMoreCharacters , $ invalidatingClass, $ keepPropertyFetches )) {
665667 continue ;
666668 }
667669
@@ -682,7 +684,7 @@ public static function invalidateExpressionEntries(
682684 || self ::keyMayHideSubExpressions ($ conditionalExprString )
683685 ) {
684686 $ firstHolder = $ holders [array_key_first ($ holders )]->getTypeHolder ();
685- if (self ::shouldInvalidateExpression ($ scope , $ exprPrinter , $ exprStringToInvalidate , $ expressionToInvalidate , $ firstHolder ->getExpr (), self ::nodeKey ($ firstHolder ->getExpr (), $ exprPrinter ), $ requireMoreCharacters , $ invalidatingClass )) {
687+ if (self ::shouldInvalidateExpression ($ scope , $ exprPrinter , $ exprStringToInvalidate , $ expressionToInvalidate , $ firstHolder ->getExpr (), self ::nodeKey ($ firstHolder ->getExpr (), $ exprPrinter ), $ requireMoreCharacters , $ invalidatingClass, $ keepPropertyFetches )) {
686688 $ invalidated = true ;
687689 continue ;
688690 }
@@ -712,7 +714,7 @@ public static function invalidateExpressionEntries(
712714 $ shouldKeep = true ;
713715 $ conditionalTypeHolders = $ holder ->getConditionExpressionTypeHolders ();
714716 foreach ($ conditionalTypeHolders as $ conditionalTypeHolderExprString => $ conditionalTypeHolder ) {
715- if (self ::shouldInvalidateExpression ($ scope , $ exprPrinter , $ exprStringToInvalidate , $ expressionToInvalidate , $ conditionalTypeHolder ->getExpr (), $ conditionalTypeHolderExprString , invalidatingClass: $ invalidatingClass )) {
717+ if (self ::shouldInvalidateExpression ($ scope , $ exprPrinter , $ exprStringToInvalidate , $ expressionToInvalidate , $ conditionalTypeHolder ->getExpr (), $ conditionalTypeHolderExprString , invalidatingClass: $ invalidatingClass, keepPropertyFetches: $ keepPropertyFetches )) {
716718 $ invalidated = true ;
717719 $ shouldKeep = false ;
718720 break ;
@@ -799,7 +801,7 @@ private static function containsExpressionToInvalidate(Scope $scope, ExprPrinter
799801 /**
800802 * Mirrors the former MutatingScope::shouldInvalidateExpression().
801803 */
802- public static function shouldInvalidateExpression (MutatingScope $ scope , ExprPrinter $ exprPrinter , string $ exprStringToInvalidate , Expr $ exprToInvalidate , Expr $ expr , string $ exprString , bool $ requireMoreCharacters = false , ?ClassReflection $ invalidatingClass = null ): bool
804+ public static function shouldInvalidateExpression (MutatingScope $ scope , ExprPrinter $ exprPrinter , string $ exprStringToInvalidate , Expr $ exprToInvalidate , Expr $ expr , string $ exprString , bool $ requireMoreCharacters = false , ?ClassReflection $ invalidatingClass = null , bool $ keepPropertyFetches = false ): bool
803805 {
804806 if (
805807 $ expr instanceof IntertwinedVariableByReferenceWithExpr
@@ -831,6 +833,10 @@ public static function shouldInvalidateExpression(MutatingScope $scope, ExprPrin
831833 return $ exprStringToInvalidate === $ exprString ;
832834 }
833835
836+ if ($ keepPropertyFetches && self ::isPropertyFetchChainOn ($ expr , $ exprStringToInvalidate , $ exprPrinter )) {
837+ return false ;
838+ }
839+
834840 // nodeKey() is the pretty-printed expression, and the standard printer is
835841 // compositional: the key of any sub-expression appears verbatim as a substring of
836842 // the key of the expression containing it. So if the invalidated expression's key
@@ -874,6 +880,42 @@ public static function shouldInvalidateExpression(MutatingScope $scope, ExprPrin
874880 return true ;
875881 }
876882
883+ /**
884+ * Whether $expr is a chain of property fetches rooted at the invalidated
885+ * expression, like '$this->foo', '$this->foo?->bar' or '$this->$name' for '$this'.
886+ *
887+ * Such an expression only reads state reachable through the object itself, so a
888+ * callee that never receives the object - a static method, a static closure -
889+ * cannot change it and it survives the invalidation. Anything else rooted at the
890+ * object (a method call, an offset access) can observe static state and keeps
891+ * being invalidated. A property name computed from anything but a plain variable
892+ * could do the same, so it is not accepted either.
893+ *
894+ * Callers must still invalidate the object when they hand it to the callee as an
895+ * argument. Reaching it through static state ('self::$instance = $this;' and then
896+ * a static method writing through 'self::$instance') is not tracked - the same
897+ * limitation every receiver other than '$this' has always had.
898+ */
899+ private static function isPropertyFetchChainOn (Expr $ expr , string $ exprStringToInvalidate , ExprPrinter $ exprPrinter ): bool
900+ {
901+ if (!$ expr instanceof PropertyFetch && !$ expr instanceof NullsafePropertyFetch) {
902+ return false ;
903+ }
904+
905+ while ($ expr instanceof PropertyFetch || $ expr instanceof NullsafePropertyFetch) {
906+ if (
907+ !$ expr ->name instanceof Node \Identifier
908+ && !($ expr ->name instanceof Variable && is_string ($ expr ->name ->name ))
909+ ) {
910+ return false ;
911+ }
912+
913+ $ expr = $ expr ->var ;
914+ }
915+
916+ return self ::nodeKey ($ expr , $ exprPrinter ) === $ exprStringToInvalidate ;
917+ }
918+
877919 /**
878920 * The conditional-expressions fixed-point matching of
879921 * MutatingScope::applySpecifiedTypes().
0 commit comments