diff --git a/src/Analyser/ExprHandler/MatchHandler.php b/src/Analyser/ExprHandler/MatchHandler.php index 28081ef53cc..c5da4944b78 100644 --- a/src/Analyser/ExprHandler/MatchHandler.php +++ b/src/Analyser/ExprHandler/MatchHandler.php @@ -454,11 +454,18 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex } if ($isExhaustive) { + $preMatchConditionalExpressions = $scope->getConditionalExpressions(); $armBodyFinalScope = null; foreach ($armBodyScopes as $armBodyScope) { $armBodyFinalScope = $armBodyScope->mergeWith($armBodyFinalScope); } - $scope = $armBodyFinalScope ?? $scope; + if ($armBodyFinalScope !== null) { + // Prevent conditional expressions created during arm scope merging + // from leaking into the post-match scope. These encode relationships + // between arm-narrowed types (e.g. "if equals(A) is false, then + // equals(B) is true") that should not affect subsequent code. + $scope = $armBodyFinalScope->replaceConditionalExpressions($preMatchConditionalExpressions); + } } else { $armBodyFinalScope = null; foreach ($armBodyScopes as $armBodyScope) { diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 4ce247488f9..daf987c5665 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -3320,6 +3320,39 @@ public function addConditionalExpressions(string $exprString, array $conditional ); } + /** + * @return array + */ + public function getConditionalExpressions(): array + { + return $this->conditionalExpressions; + } + + /** + * @param array $conditionalExpressions + */ + public function replaceConditionalExpressions(array $conditionalExpressions): self + { + return $this->scopeFactory->create( + $this->context, + $this->isDeclareStrictTypes(), + $this->getFunction(), + $this->getNamespace(), + $this->expressionTypes, + $this->nativeExpressionTypes, + $conditionalExpressions, + $this->inClosureBindScopeClasses, + $this->anonymousFunctionReflection, + $this->inFirstLevelStatement, + $this->currentlyAssignedExpressions, + $this->currentlyAllowedUndefinedExpressions, + $this->inFunctionCallsStack, + $this->afterExtractCall, + $this->parentScope, + $this->nativeTypesPromoted, + ); + } + public function exitFirstLevelStatements(): self { if (!$this->inFirstLevelStatement) { diff --git a/tests/PHPStan/Rules/Comparison/MatchExpressionRuleTest.php b/tests/PHPStan/Rules/Comparison/MatchExpressionRuleTest.php index 09330a874be..8b7c9c77e9b 100644 --- a/tests/PHPStan/Rules/Comparison/MatchExpressionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/MatchExpressionRuleTest.php @@ -459,6 +459,12 @@ public function testBug12790(): void $this->analyse([__DIR__ . '/data/bug-12790.php'], []); } + #[RequiresPhp('>= 8.0')] + public function testBug14368(): void + { + $this->analyse([__DIR__ . '/data/bug-14368.php'], []); + } + #[RequiresPhp('>= 8.0')] public function testBug11310(): void { diff --git a/tests/PHPStan/Rules/Comparison/data/bug-14368.php b/tests/PHPStan/Rules/Comparison/data/bug-14368.php new file mode 100644 index 00000000000..e953d40f8cf --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-14368.php @@ -0,0 +1,54 @@ += 8.0 + +namespace Bug14368; + +final class Snowflake +{ + public function __construct( + private readonly int $value, + ) {} + + public static function cast(int $snowflake): self + { + return new self($snowflake); + } + + public function equals(?self $other): bool + { + return $this->value === $other?->value; + } +} + +final class BalanceId +{ + public static function work(): Snowflake + { + /** @var Snowflake */ + static $work = Snowflake::cast(1); + return $work; + } + + public static function holiday(): Snowflake + { + /** @var Snowflake */ + static $holiday = Snowflake::cast(2); + return $holiday; + } +} + +function test(Snowflake $balanceId): void +{ + // First match — no error expected + $a = match (true) { + $balanceId->equals(BalanceId::work()) => -1.0, + $balanceId->equals(BalanceId::holiday()) => 1.0, + default => throw new \InvalidArgumentException(), + }; + + // Second match — should not report match.alwaysTrue + $b = match (true) { + $balanceId->equals(BalanceId::work()) => -2.0, + $balanceId->equals(BalanceId::holiday()) => 2.0, + default => throw new \InvalidArgumentException(), + }; +}