Skip to content

Commit d7e37ec

Browse files
phpstan-botgithub-actions[bot]claudestaabm
authored
Fix phpstan/phpstan#11565: False positive: Conditional return type takes the wrong branch (#5362)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Markus Staab <markus.staab@redaxo.de>
1 parent bb22ec9 commit d7e37ec

3 files changed

Lines changed: 79 additions & 0 deletions

File tree

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: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -814,6 +814,7 @@ public function specifyTypesInCondition(
814814

815815
if ($context->null()) {
816816
$specifiedTypes = $this->specifyTypesInCondition($scope->exitFirstLevelStatements(), $expr->expr, $context)->setRootExpr($expr);
817+
$specifiedTypes = $specifiedTypes->removeExpr($this->exprPrinter->printExpr($expr->var));
817818
} else {
818819
$specifiedTypes = $this->specifyTypesInCondition($scope->exitFirstLevelStatements(), $expr->var, $context)->setRootExpr($expr);
819820
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
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);
37+
38+
// Same variable reassignment inside if condition (truthy context)
39+
// Non-null context recurses into $expr->var, not $expr->expr, so not affected
40+
$items3 = getItems();
41+
if ($items3 = iteratorToList($items3)) {
42+
assertType('non-empty-list<string>', $items3);
43+
}
44+
45+
// Property fetch as LHS - exercises removeExpr for non-Variable expressions
46+
class Holder {
47+
/** @var iterable<string, string> */
48+
public iterable $items;
49+
}
50+
51+
function testPropertyFetch(Holder $holder): void {
52+
$holder->items = iteratorToList($holder->items);
53+
assertType('list<string>', $holder->items);
54+
}
55+
56+
// Array dim fetch as LHS
57+
/**
58+
* @param array{items: iterable<string, string>} $data
59+
*/
60+
function testArrayDimFetch(array $data): void {
61+
$data['items'] = iteratorToList($data['items']);
62+
assertType('list<string>', $data['items']);
63+
}

0 commit comments

Comments
 (0)