From 11c9f071b2602d3d90f97561f77c182107ec6638 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sun, 26 Jul 2026 16:27:24 +0200 Subject: [PATCH 1/3] Iterative NullsafeOperatorHelper --- src/Analyser/NullsafeOperatorHelper.php | 95 ++++++++++++------------- 1 file changed, 47 insertions(+), 48 deletions(-) diff --git a/src/Analyser/NullsafeOperatorHelper.php b/src/Analyser/NullsafeOperatorHelper.php index 0b439191c7..15cf545c78 100644 --- a/src/Analyser/NullsafeOperatorHelper.php +++ b/src/Analyser/NullsafeOperatorHelper.php @@ -24,60 +24,59 @@ public static function getNullsafeShortcircuitedExprRespectingScope(Scope $scope */ public static function getNullsafeShortcircuitedExpr(Expr $expr): Expr { - if ($expr instanceof Expr\NullsafeMethodCall) { - return new Expr\MethodCall(self::getNullsafeShortcircuitedExpr($expr->var), $expr->name, $expr->args); - } - - if ($expr instanceof Expr\MethodCall) { - $var = self::getNullsafeShortcircuitedExpr($expr->var); - if ($expr->var === $var) { - return $expr; - } - - return new Expr\MethodCall($var, $expr->name, $expr->getArgs()); - } - - if ($expr instanceof Expr\StaticCall && $expr->class instanceof Expr) { - $class = self::getNullsafeShortcircuitedExpr($expr->class); - if ($expr->class === $class) { - return $expr; - } - - return new Expr\StaticCall($class, $expr->name, $expr->getArgs()); - } - - if ($expr instanceof Expr\ArrayDimFetch) { - $var = self::getNullsafeShortcircuitedExpr($expr->var); - if ($expr->var === $var) { - return $expr; + // Collect the chain of chained-access wrappers (outermost first) walking inward. + $chain = []; + $current = $expr; + + while (true) { + if ( + $current instanceof Expr\NullsafeMethodCall + || $current instanceof Expr\MethodCall + || $current instanceof Expr\ArrayDimFetch + || $current instanceof Expr\NullsafePropertyFetch + || $current instanceof Expr\PropertyFetch + ) { + $chain[] = $current; + $current = $current->var; + } elseif ( + ($current instanceof Expr\StaticCall || $current instanceof Expr\StaticPropertyFetch) + && $current->class instanceof Expr + ) { + $chain[] = $current; + $current = $current->class; + } else { + break; } - - return new Expr\ArrayDimFetch($var, $expr->dim); } - if ($expr instanceof Expr\NullsafePropertyFetch) { - return new Expr\PropertyFetch(self::getNullsafeShortcircuitedExpr($expr->var), $expr->name); - } - - if ($expr instanceof Expr\PropertyFetch) { - $var = self::getNullsafeShortcircuitedExpr($expr->var); - if ($expr->var === $var) { - return $expr; + // Rebuild from innermost outward, replacing Nullsafe* nodes as we go. + $result = $current; + $changed = false; + + foreach (array_reverse($chain) as $node) { + if ($node instanceof Expr\NullsafeMethodCall) { + $result = new Expr\MethodCall($result, $node->name, $node->args); + $changed = true; + } elseif ($node instanceof Expr\NullsafePropertyFetch) { + $result = new Expr\PropertyFetch($result, $node->name); + $changed = true; + } elseif (!$changed) { + // Nothing has changed yet — keep the original node to preserve identity. + $result = $node; + } elseif ($node instanceof Expr\MethodCall) { + $result = new Expr\MethodCall($result, $node->name, $node->getArgs()); + } elseif ($node instanceof Expr\ArrayDimFetch) { + $result = new Expr\ArrayDimFetch($result, $node->dim); + } elseif ($node instanceof Expr\PropertyFetch) { + $result = new Expr\PropertyFetch($result, $node->name); + } elseif ($node instanceof Expr\StaticCall) { + $result = new Expr\StaticCall($result, $node->name, $node->getArgs()); + } elseif ($node instanceof Expr\StaticPropertyFetch) { + $result = new Expr\StaticPropertyFetch($result, $node->name); } - - return new Expr\PropertyFetch($var, $expr->name); - } - - if ($expr instanceof Expr\StaticPropertyFetch && $expr->class instanceof Expr) { - $class = self::getNullsafeShortcircuitedExpr($expr->class); - if ($expr->class === $class) { - return $expr; - } - - return new Expr\StaticPropertyFetch($class, $expr->name); } - return $expr; + return $result; } } From 0643b624ea40ef299d15d51e6edf032e101568c1 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sun, 26 Jul 2026 16:29:58 +0200 Subject: [PATCH 2/3] Update NullsafeOperatorHelper.php --- src/Analyser/NullsafeOperatorHelper.php | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/Analyser/NullsafeOperatorHelper.php b/src/Analyser/NullsafeOperatorHelper.php index 15cf545c78..9311c1ebb3 100644 --- a/src/Analyser/NullsafeOperatorHelper.php +++ b/src/Analyser/NullsafeOperatorHelper.php @@ -10,13 +10,20 @@ final class NullsafeOperatorHelper public static function getNullsafeShortcircuitedExprRespectingScope(Scope $scope, Expr $expr): Expr { + $shortcircuitedExpr = self::getNullsafeShortcircuitedExpr($expr); + if ($shortcircuitedExpr === $expr) { + // No nullsafe operator anywhere in the expression - the result is $expr + // either way, so skip asking the scope for the expression's type. + return $expr; + } + if (!TypeCombinator::containsNull($scope->getType($expr))) { // We're in most likely in context of a null-safe operator ($scope->moreSpecificType is defined for $expr) // Modifying the expression would not bring any value or worse ruin the context information return $expr; } - return self::getNullsafeShortcircuitedExpr($expr); + return $shortcircuitedExpr; } /** From 5882871622ab05bd7388b7e13085f7e85e64eaba Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sun, 26 Jul 2026 17:14:17 +0200 Subject: [PATCH 3/3] Update NullsafeOperatorHelper.php --- src/Analyser/NullsafeOperatorHelper.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Analyser/NullsafeOperatorHelper.php b/src/Analyser/NullsafeOperatorHelper.php index 9311c1ebb3..ab131c3b29 100644 --- a/src/Analyser/NullsafeOperatorHelper.php +++ b/src/Analyser/NullsafeOperatorHelper.php @@ -4,6 +4,7 @@ use PhpParser\Node\Expr; use PHPStan\Type\TypeCombinator; +use function array_reverse; final class NullsafeOperatorHelper {