Skip to content

Commit 3530c90

Browse files
committed
Make Iterator::current() and ::key() nullable
1 parent d825a7b commit 3530c90

File tree

4 files changed

+83
-8
lines changed

4 files changed

+83
-8
lines changed

src/Analyser/NodeScopeResolver.php

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use ArrayAccess;
66
use Closure;
77
use DivisionByZeroError;
8+
use Iterator;
89
use PhpParser\Comment\Doc;
910
use PhpParser\Modifiers;
1011
use PhpParser\Node;
@@ -1184,6 +1185,14 @@ private function processStmtNode(
11841185
$stmt->expr,
11851186
new Array_([]),
11861187
);
1188+
$exprType = $scope->getType($stmt->expr);
1189+
$iteratorValidExpr = null;
1190+
if ((new ObjectType(Iterator::class))->isSuperTypeOf($exprType)->yes()) {
1191+
$iteratorValidExpr = new BinaryOp\Identical(
1192+
new MethodCall($stmt->expr, 'valid'),
1193+
new ConstFetch(new Name\FullyQualified('true')),
1194+
);
1195+
}
11871196
if ($stmt->expr instanceof Variable && is_string($stmt->expr->name)) {
11881197
$scope = $this->processVarAnnotation($scope, [$stmt->expr->name], $stmt);
11891198
}
@@ -1193,11 +1202,17 @@ private function processStmtNode(
11931202

11941203
if ($context->isTopLevel()) {
11951204
$originalScope = $this->polluteScopeWithAlwaysIterableForeach ? $scope->filterByTruthyValue($arrayComparisonExpr) : $scope;
1205+
if ($iteratorValidExpr !== null) {
1206+
$originalScope = $originalScope->filterByTruthyValue($iteratorValidExpr);
1207+
}
11961208
$bodyScope = $this->enterForeach($originalScope, $originalScope, $stmt);
11971209
$count = 0;
11981210
do {
11991211
$prevScope = $bodyScope;
12001212
$bodyScope = $bodyScope->mergeWith($this->polluteScopeWithAlwaysIterableForeach ? $scope->filterByTruthyValue($arrayComparisonExpr) : $scope);
1213+
if ($iteratorValidExpr !== null) {
1214+
$bodyScope = $bodyScope->filterByTruthyValue($iteratorValidExpr);
1215+
}
12011216
$bodyScope = $this->enterForeach($bodyScope, $originalScope, $stmt);
12021217
$bodyScopeResult = $this->processStmtNodes($stmt, $stmt->stmts, $bodyScope, static function (): void {
12031218
}, $context->enterDeep())->filterOutLoopExitPoints();
@@ -1217,6 +1232,9 @@ private function processStmtNode(
12171232
}
12181233

12191234
$bodyScope = $bodyScope->mergeWith($this->polluteScopeWithAlwaysIterableForeach ? $scope->filterByTruthyValue($arrayComparisonExpr) : $scope);
1235+
if ($iteratorValidExpr !== null) {
1236+
$bodyScope = $bodyScope->filterByTruthyValue($iteratorValidExpr);
1237+
}
12201238
$bodyScope = $this->enterForeach($bodyScope, $originalScope, $stmt);
12211239
$finalScopeResult = $this->processStmtNodes($stmt, $stmt->stmts, $bodyScope, $nodeCallback, $context)->filterOutLoopExitPoints();
12221240
$finalScope = $finalScopeResult->getScope();
@@ -1227,7 +1245,6 @@ private function processStmtNode(
12271245
$finalScope = $breakExitPoint->getScope()->mergeWith($finalScope);
12281246
}
12291247

1230-
$exprType = $scope->getType($stmt->expr);
12311248
$isIterableAtLeastOnce = $exprType->isIterableAtLeastOnce();
12321249
if ($exprType->isIterable()->no() || $isIterableAtLeastOnce->maybe()) {
12331250
$finalScope = $finalScope->mergeWith($scope->filterByTruthyValue(new BooleanOr(
@@ -1250,10 +1267,14 @@ private function processStmtNode(
12501267
$throwPoints = array_merge($throwPoints, $finalScopeResult->getThrowPoints());
12511268
$impurePoints = array_merge($impurePoints, $finalScopeResult->getImpurePoints());
12521269
}
1253-
if (!(new ObjectType(Traversable::class))->isSuperTypeOf($scope->getType($stmt->expr))->no()) {
1270+
if (!(new ObjectType(Traversable::class))->isSuperTypeOf($exprType)->no()) {
12541271
$throwPoints[] = ThrowPoint::createImplicit($scope, $stmt->expr);
12551272
}
12561273

1274+
if ($iteratorValidExpr !== null) {
1275+
$finalScope = $finalScope->filterByFalseyValue($iteratorValidExpr);
1276+
}
1277+
12571278
return new StatementResult(
12581279
$finalScope,
12591280
$finalScopeResult->hasYield() || $condResult->hasYield(),

stubs/iterable.stub

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,21 @@ interface Iterator extends Traversable
3434
{
3535

3636
/**
37-
* @return TValue
37+
* @phpstan-assert-if-true =TValue $this->current()
38+
* @phpstan-assert-if-false =null $this->current()
39+
*
40+
* @phpstan-assert-if-true =TKey $this->key()
41+
* @phpstan-assert-if-false =null $this->key()
42+
*/
43+
public function valid(): bool;
44+
45+
/**
46+
* @return TValue|null
3847
*/
3948
public function current();
4049

4150
/**
42-
* @return TKey
51+
* @return TKey|null
4352
*/
4453
public function key();
4554

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug3674;
4+
5+
use Iterator;
6+
use function PHPStan\Testing\assertType;
7+
8+
/**
9+
* @param Iterator<int> $it
10+
*/
11+
function foo(Iterator $it): void {
12+
assertType('int|null', $it->current());
13+
14+
if ($it->valid()) {
15+
assertType('int', $it->current());
16+
17+
$it->rewind();
18+
19+
assertType('int|null', $it->current());
20+
21+
if ($it->valid()) {
22+
assertType('int', $it->current());
23+
} else {
24+
assertType('null', $it->current());
25+
}
26+
} else {
27+
assertType('null', $it->current());
28+
}
29+
}
30+
31+
/**
32+
* @param Iterator<int> $it
33+
*/
34+
function bar(Iterator $it): void {
35+
assertType('bool', $it->valid());
36+
assertType('int|null', $it->current());
37+
38+
foreach ($it as $v) {
39+
assertType('true', $it->valid());
40+
assertType('int', $it->current());
41+
}
42+
43+
assertType('false', $it->valid());
44+
assertType('null', $it->current());
45+
}

tests/PHPStan/Analyser/nsrt/bug-7519.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,8 @@ function doFoo() {
4141

4242
$iterator = new FooFilterIterator($generator());
4343

44-
assertType('array{}|bool|stdClass', $iterator->key());
45-
assertType('array{}|bool|stdClass', $iterator->current());
44+
assertType('array{}|bool|stdClass|null', $iterator->key());
45+
assertType('array{}|bool|stdClass|null', $iterator->current());
4646

4747
$generator = static function (): Generator {
4848
yield true => true;
@@ -51,6 +51,6 @@ function doFoo() {
5151

5252
$iterator = new FooFilterIterator($generator());
5353

54-
assertType('bool', $iterator->key());
55-
assertType('bool', $iterator->current());
54+
assertType('bool|null', $iterator->key());
55+
assertType('bool|null', $iterator->current());
5656
}

0 commit comments

Comments
 (0)