From ca881d712626e59eca788442aa402372c61139cd Mon Sep 17 00:00:00 2001 From: phpstan-bot <79867460+phpstan-bot@users.noreply.github.com> Date: Tue, 17 Feb 2026 01:02:25 +0000 Subject: [PATCH 1/2] Fix match expression with post/pre-increment condition using wrong type - MutatingScope::enterMatch() re-evaluated the condition type after the scope had already been updated by processing the increment expression, causing $i++ to be seen as int<1, max> instead of int<0, max> - Pass the pre-computed condition type and native type from NodeScopeResolver to enterMatch() so it uses the correct types - New regression test in tests/PHPStan/Rules/Comparison/data/bug-11310.php Closes https://github.com/phpstan/phpstan/issues/11310 --- src/Analyser/MutatingScope.php | 6 ++-- src/Analyser/NodeScopeResolver.php | 3 +- .../Comparison/MatchExpressionRuleTest.php | 11 ++++++++ .../Rules/Comparison/data/bug-11310.php | 28 +++++++++++++++++++ 4 files changed, 44 insertions(+), 4 deletions(-) create mode 100644 tests/PHPStan/Rules/Comparison/data/bug-11310.php diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index db31ce7669..9c02106f92 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -2987,7 +2987,7 @@ private static function intersectButNotNever(Type $nativeType, Type $inferredTyp return $result; } - public function enterMatch(Expr\Match_ $expr): self + public function enterMatch(Expr\Match_ $expr, ?Type $condType, ?Type $condNativeType): self { if ($expr->cond instanceof Variable) { return $this; @@ -3001,8 +3001,8 @@ public function enterMatch(Expr\Match_ $expr): self return $this; } - $type = $this->getType($cond); - $nativeType = $this->getNativeType($cond); + $type = $condType ?? $this->getType($cond); + $nativeType = $condNativeType ?? $this->getNativeType($cond); $condExpr = new AlwaysRememberedExpr($cond, $type, $nativeType); $expr->cond = $condExpr; diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 8a807ea745..6aaf991c5d 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -4176,13 +4176,14 @@ function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $context, $sto } elseif ($expr instanceof Expr\Match_) { $deepContext = $context->enterDeep(); $condType = $scope->getType($expr->cond); + $condNativeType = $scope->getNativeType($expr->cond); $condResult = $this->processExprNode($stmt, $expr->cond, $scope, $storage, $nodeCallback, $deepContext); $scope = $condResult->getScope(); $hasYield = $condResult->hasYield(); $throwPoints = $condResult->getThrowPoints(); $impurePoints = $condResult->getImpurePoints(); $isAlwaysTerminating = $condResult->isAlwaysTerminating(); - $matchScope = $scope->enterMatch($expr); + $matchScope = $scope->enterMatch($expr, $condType, $condNativeType); $armNodes = []; $hasDefaultCond = false; $hasAlwaysTrueCond = false; diff --git a/tests/PHPStan/Rules/Comparison/MatchExpressionRuleTest.php b/tests/PHPStan/Rules/Comparison/MatchExpressionRuleTest.php index dc7e8c05a7..55f69dee4d 100644 --- a/tests/PHPStan/Rules/Comparison/MatchExpressionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/MatchExpressionRuleTest.php @@ -446,4 +446,15 @@ public function testBug9534(): void ]); } + #[RequiresPhp('>= 8.0')] + public function testBug11310(): void + { + $this->analyse([__DIR__ . '/data/bug-11310.php'], [ + [ + 'Match arm comparison between int<1, max> and 0 is always false.', + 24, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Comparison/data/bug-11310.php b/tests/PHPStan/Rules/Comparison/data/bug-11310.php new file mode 100644 index 0000000000..257d985548 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-11310.php @@ -0,0 +1,28 @@ += 8.0 + +namespace Bug11310; + +/** @param int<0, max> $i */ +function foo(int $i): void { + echo match ($i++) { + 0 => 'zero', + default => 'default', + }; +} + +/** @param int<0, max> $i */ +function bar(int $i): void { + echo match ($i--) { + 0 => 'zero', + default => 'default', + }; +} + +/** @param int<0, max> $i */ +function baz(int $i): void { + echo match (++$i) { + 0 => 'zero', + 1 => 'one', + default => 'default', + }; +} From 5514dd0a03de0a3e079a6e9458887848a200829f Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Tue, 17 Feb 2026 01:36:06 +0000 Subject: [PATCH 2/2] Fix CI failures [claude-ci-fix] Automated fix attempt 1 for CI failures. --- src/Analyser/NodeScopeResolver.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 6aaf991c5d..19f8c7f9a3 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -4175,6 +4175,10 @@ function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $context, $sto $hasYield = true; } elseif ($expr instanceof Expr\Match_) { $deepContext = $context->enterDeep(); + $condIsIncDec = $expr->cond instanceof Expr\PreInc + || $expr->cond instanceof Expr\PostInc + || $expr->cond instanceof Expr\PreDec + || $expr->cond instanceof Expr\PostDec; $condType = $scope->getType($expr->cond); $condNativeType = $scope->getNativeType($expr->cond); $condResult = $this->processExprNode($stmt, $expr->cond, $scope, $storage, $nodeCallback, $deepContext); @@ -4183,7 +4187,7 @@ function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $context, $sto $throwPoints = $condResult->getThrowPoints(); $impurePoints = $condResult->getImpurePoints(); $isAlwaysTerminating = $condResult->isAlwaysTerminating(); - $matchScope = $scope->enterMatch($expr, $condType, $condNativeType); + $matchScope = $scope->enterMatch($expr, $condIsIncDec ? $condType : null, $condIsIncDec ? $condNativeType : null); $armNodes = []; $hasDefaultCond = false; $hasAlwaysTrueCond = false;