From b92e25cf3edebebcf96c78f38601beea28e94834 Mon Sep 17 00:00:00 2001 From: ondrejmirtes <104888+ondrejmirtes@users.noreply.github.com> Date: Fri, 27 Feb 2026 08:44:20 +0000 Subject: [PATCH] Preserve vacuously true conditional expressions during scope merging Fixes https://github.com/phpstan/phpstan/issues/4173 --- src/Analyser/MutatingScope.php | 47 +++++++++++++++++++ src/Analyser/NodeScopeResolver.php | 2 +- .../InitializerExprTypeResolver.php | 5 +- .../Variables/DefinedVariableRuleTest.php | 27 ++--------- 4 files changed, 54 insertions(+), 27 deletions(-) diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 4036f5ca72..1d5e4f2c9b 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -3956,6 +3956,16 @@ public function mergeWith(?self $otherScope): self $mergedExpressionTypes = $this->mergeVariableHolders($ourExpressionTypes, $theirExpressionTypes); $conditionalExpressions = $this->intersectConditionalExpressions($otherScope->conditionalExpressions); + $conditionalExpressions = $this->preserveVacuousConditionalExpressions( + $conditionalExpressions, + $this->conditionalExpressions, + $theirExpressionTypes, + ); + $conditionalExpressions = $this->preserveVacuousConditionalExpressions( + $conditionalExpressions, + $otherScope->conditionalExpressions, + $ourExpressionTypes, + ); $conditionalExpressions = $this->createConditionalExpressions( $conditionalExpressions, $ourExpressionTypes, @@ -4055,6 +4065,43 @@ private function intersectConditionalExpressions(array $otherConditionalExpressi return $newConditionalExpressions; } + /** + * @param array $currentConditionalExpressions + * @param array $sourceConditionalExpressions + * @param array $otherExpressionTypes + * @return array + */ + private function preserveVacuousConditionalExpressions( + array $currentConditionalExpressions, + array $sourceConditionalExpressions, + array $otherExpressionTypes, + ): array + { + foreach ($sourceConditionalExpressions as $exprString => $holders) { + foreach ($holders as $key => $holder) { + if (isset($currentConditionalExpressions[$exprString][$key])) { + continue; + } + + foreach ($holder->getConditionExpressionTypeHolders() as $guardExprString => $guardTypeHolder) { + if (!array_key_exists($guardExprString, $otherExpressionTypes)) { + continue; + } + + $otherType = $otherExpressionTypes[$guardExprString]->getType(); + $guardType = $guardTypeHolder->getType(); + + if ($otherType->isSuperTypeOf($guardType)->no()) { + $currentConditionalExpressions[$exprString][$key] = $holder; + break; + } + } + } + } + + return $currentConditionalExpressions; + } + /** * @param array $newConditionalExpressions * @param array $existingConditionalExpressions diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 86f616953b..639e131448 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -3217,7 +3217,7 @@ function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $context, $sto $this->callNodeCallback($nodeCallback, new InvalidateExprNode($normalizedExpr->var), $scope, $storage); $scope = $scope->invalidateExpression($normalizedExpr->var, true, $methodReflection->getDeclaringClass()); } - if ($parametersAcceptor !== null && !$methodReflection->isStatic()) { + if (!$methodReflection->isStatic()) { $selfOutType = $methodReflection->getSelfOutType(); if ($selfOutType !== null) { $scope = $scope->assignExpression( diff --git a/src/Reflection/InitializerExprTypeResolver.php b/src/Reflection/InitializerExprTypeResolver.php index 2794147dcc..28add0f92d 100644 --- a/src/Reflection/InitializerExprTypeResolver.php +++ b/src/Reflection/InitializerExprTypeResolver.php @@ -2210,13 +2210,14 @@ private function integerRangeMath(Type $range, BinaryOp $node, Type $operand): T $min = null; } - if ($operand->getMax() === null) { + $operandMax = $operand->getMax(); + if ($operandMax === null) { $min = null; $max = null; } elseif ($rangeMax !== null) { if ($rangeMin !== null && $operand->getMin() === null) { /** @var int|float $min */ - $min = $rangeMin - $operand->getMax(); + $min = $rangeMin - $operandMax; $max = null; } elseif ($operand->getMin() !== null) { /** @var int|float $max */ diff --git a/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php b/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php index 112a57d4cc..ffdbdffffb 100644 --- a/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php +++ b/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php @@ -911,12 +911,7 @@ public function testBug4173(): void $this->polluteScopeWithLoopInitialAssignments = false; $this->checkMaybeUndefinedVariables = true; $this->polluteScopeWithAlwaysIterableForeach = true; - $this->analyse([__DIR__ . '/data/bug-4173.php'], [ - [ - 'Variable $value might not be defined.', // could be fixed - 30, - ], - ]); + $this->analyse([__DIR__ . '/data/bug-4173.php'], []); } public function testBug5805(): void @@ -1119,29 +1114,13 @@ public function testDynamicAccess(): void 18, ], [ - 'Variable $foo might not be defined.', - 36, - ], - [ - 'Variable $foo might not be defined.', - 37, - ], - [ - 'Variable $bar might not be defined.', + 'Undefined variable: $bar', 38, ], [ - 'Variable $bar might not be defined.', - 40, - ], - [ - 'Variable $foo might not be defined.', + 'Undefined variable: $foo', 41, ], - [ - 'Variable $bar might not be defined.', - 42, - ], [ 'Undefined variable: $buz', 44,