From 3ebdbda18c1bf684de2005a3cbde3212b9547b27 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 31 Mar 2026 03:53:33 +0000 Subject: [PATCH] Fix phpstan/phpstan#11565: Conditional return type takes wrong branch when variable is reassigned - When processing `$items = iteratorToList($items)` as a statement, the TypeSpecifier evaluated the conditional return type using the post-assignment scope where `$items` was already `list`, causing the condition `$iterable is list` to be true and the return type to resolve to `never` - Added `removeExpr` method to SpecifiedTypes to filter out entries by expression key - In TypeSpecifier, when processing an Assign in null context, remove specifiedTypes that target the assigned variable to prevent the post-assignment type from being incorrectly narrowed by the conditional return type evaluation - New regression test in tests/PHPStan/Analyser/nsrt/bug-11565.php --- src/Analyser/SpecifiedTypes.php | 15 ++++++++++ src/Analyser/TypeSpecifier.php | 3 ++ tests/PHPStan/Analyser/nsrt/bug-11565.php | 36 +++++++++++++++++++++++ 3 files changed, 54 insertions(+) create mode 100644 tests/PHPStan/Analyser/nsrt/bug-11565.php diff --git a/src/Analyser/SpecifiedTypes.php b/src/Analyser/SpecifiedTypes.php index ccb2c5e8a3..5cfa65dc53 100644 --- a/src/Analyser/SpecifiedTypes.php +++ b/src/Analyser/SpecifiedTypes.php @@ -119,6 +119,21 @@ public function getRootExpr(): ?Expr return $this->rootExpr; } + public function removeExpr(string $exprString): self + { + $sureTypes = $this->sureTypes; + $sureNotTypes = $this->sureNotTypes; + unset($sureTypes[$exprString]); + unset($sureNotTypes[$exprString]); + + $self = new self($sureTypes, $sureNotTypes); + $self->overwrite = $this->overwrite; + $self->newConditionalExpressionHolders = $this->newConditionalExpressionHolders; + $self->rootExpr = $this->rootExpr; + + return $self; + } + /** @api */ public function intersectWith(SpecifiedTypes $other): self { diff --git a/src/Analyser/TypeSpecifier.php b/src/Analyser/TypeSpecifier.php index 4f3e7f9c43..fff86a02fa 100644 --- a/src/Analyser/TypeSpecifier.php +++ b/src/Analyser/TypeSpecifier.php @@ -788,6 +788,9 @@ public function specifyTypesInCondition( if ($context->null()) { $specifiedTypes = $this->specifyTypesInCondition($scope->exitFirstLevelStatements(), $expr->expr, $context)->setRootExpr($expr); + if ($expr->var instanceof Expr\Variable && is_string($expr->var->name)) { + $specifiedTypes = $specifiedTypes->removeExpr('$' . $expr->var->name); + } } else { $specifiedTypes = $this->specifyTypesInCondition($scope->exitFirstLevelStatements(), $expr->var, $context)->setRootExpr($expr); } diff --git a/tests/PHPStan/Analyser/nsrt/bug-11565.php b/tests/PHPStan/Analyser/nsrt/bug-11565.php new file mode 100644 index 0000000000..7e43068a80 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-11565.php @@ -0,0 +1,36 @@ + $iterable + * @return ($iterable is list ? never : list) + */ +function iteratorToList(iterable $iterable): array { + $list = []; + foreach ($iterable as $item) { + $list[] = $item; + } + return $list; +} + +/** + * @return iterable + */ +function getItems(): iterable { + yield 'a' => 'foo'; + yield 'b' => 'bar'; +} + +// Bug: when reassigning to the same variable, conditional return type resolves incorrectly +$items = getItems(); +$items = iteratorToList($items); +assertType('list', $items); + +// Works fine when using a different variable +$x = getItems(); +$items2 = iteratorToList($x); +assertType('list', $items2);