Skip to content

Commit 3ebdbda

Browse files
github-actions[bot]phpstan-bot
authored andcommitted
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<string>`, 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
1 parent c36922b commit 3ebdbda

File tree

3 files changed

+54
-0
lines changed

3 files changed

+54
-0
lines changed

src/Analyser/SpecifiedTypes.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,21 @@ public function getRootExpr(): ?Expr
119119
return $this->rootExpr;
120120
}
121121

122+
public function removeExpr(string $exprString): self
123+
{
124+
$sureTypes = $this->sureTypes;
125+
$sureNotTypes = $this->sureNotTypes;
126+
unset($sureTypes[$exprString]);
127+
unset($sureNotTypes[$exprString]);
128+
129+
$self = new self($sureTypes, $sureNotTypes);
130+
$self->overwrite = $this->overwrite;
131+
$self->newConditionalExpressionHolders = $this->newConditionalExpressionHolders;
132+
$self->rootExpr = $this->rootExpr;
133+
134+
return $self;
135+
}
136+
122137
/** @api */
123138
public function intersectWith(SpecifiedTypes $other): self
124139
{

src/Analyser/TypeSpecifier.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -788,6 +788,9 @@ public function specifyTypesInCondition(
788788

789789
if ($context->null()) {
790790
$specifiedTypes = $this->specifyTypesInCondition($scope->exitFirstLevelStatements(), $expr->expr, $context)->setRootExpr($expr);
791+
if ($expr->var instanceof Expr\Variable && is_string($expr->var->name)) {
792+
$specifiedTypes = $specifiedTypes->removeExpr('$' . $expr->var->name);
793+
}
791794
} else {
792795
$specifiedTypes = $this->specifyTypesInCondition($scope->exitFirstLevelStatements(), $expr->var, $context)->setRootExpr($expr);
793796
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug11565;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
/**
8+
* @template T
9+
* @param iterable<mixed, T> $iterable
10+
* @return ($iterable is list ? never : list<T>)
11+
*/
12+
function iteratorToList(iterable $iterable): array {
13+
$list = [];
14+
foreach ($iterable as $item) {
15+
$list[] = $item;
16+
}
17+
return $list;
18+
}
19+
20+
/**
21+
* @return iterable<string, string>
22+
*/
23+
function getItems(): iterable {
24+
yield 'a' => 'foo';
25+
yield 'b' => 'bar';
26+
}
27+
28+
// Bug: when reassigning to the same variable, conditional return type resolves incorrectly
29+
$items = getItems();
30+
$items = iteratorToList($items);
31+
assertType('list<string>', $items);
32+
33+
// Works fine when using a different variable
34+
$x = getItems();
35+
$items2 = iteratorToList($x);
36+
assertType('list<string>', $items2);

0 commit comments

Comments
 (0)