Skip to content

Commit

Permalink
Strip null used to signify completed iterations in foreach context
Browse files Browse the repository at this point in the history
Even though `Generator::current()` can return `null` once generator is
exhausted, `foreach()` never iterates after iterator ends, so we can
safely remove `null` (unless, of course, generator can yield `null`).
  • Loading branch information
weirdan committed Feb 12, 2024
1 parent 829a670 commit ff66d24
Show file tree
Hide file tree
Showing 2 changed files with 49 additions and 0 deletions.
14 changes: 14 additions & 0 deletions src/Psalm/Internal/Analyzer/Statements/Block/ForeachAnalyzer.php
Original file line number Diff line number Diff line change
Expand Up @@ -929,10 +929,24 @@ public static function handleIterable(
);

if ($iterator_value_type && !$iterator_value_type->isMixed()) {
// remove null coming from current() to signify invalid iterations
// we're in a foreach context, so we know we're not going iterate past the end
if (isset($type_params[1]) && !$type_params[1]->isNullable()) {
$iterator_value_type = $iterator_value_type->getBuilder();
$iterator_value_type->removeType('null');
$iterator_value_type = $iterator_value_type->freeze();
}
$value_type = Type::combineUnionTypes($value_type, $iterator_value_type);
}

if ($iterator_key_type && !$iterator_key_type->isMixed()) {
// remove null coming from key() to signify invalid iterations
// we're in a foreach context, so we know we're not going iterate past the end
if (isset($type_params[0]) && !$type_params[0]->isNullable()) {
$iterator_key_type = $iterator_key_type->getBuilder();
$iterator_key_type->removeType('null');
$iterator_key_type = $iterator_key_type->freeze();
}
$key_type = Type::combineUnionTypes($key_type, $iterator_key_type);
}
} elseif ($codebase->classImplements(
Expand Down
35 changes: 35 additions & 0 deletions tests/Loop/ForeachTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -1191,6 +1191,41 @@ function gen() : Generator {
foreach ($gen as $i) {}
PHP,
],
'nullableGenerator' => [
'code' => <<<'PHP'
<?php
/** @return Generator<int,int|null> */
function gen() : Generator {
yield null;
yield 1;
}
$gen = gen();
$a = "";
foreach ($gen as $i) {
$a = $i;
}
PHP,
'assertions' => [
'$a===' => "''|int|null",
],
],
'nonNullableGenerator' => [
'code' => <<<'PHP'
<?php
/** @return Generator<int,int> */
function gen() : Generator {
yield 1;
}
$gen = gen();
$a = "";
foreach ($gen as $i) {
$a = $i;
}
PHP,
'assertions' => [
'$a===' => "''|int",
],
],
];
}

Expand Down

0 comments on commit ff66d24

Please sign in to comment.