psalm
psalm copied to clipboard
Unexpected TypeDoesNotContainType Error after Closure use ref with conditional assignment
In Snippet:
<?php
/**
* @var list<int> $arr
*/
$arr = [];
$hasNumberOne = false;
\usort(
$arr,
static function (mixed $left, mixed $right) use (&$hasNumberOne): int {
if ($left === 1 || $right === 1) {
$hasNumberOne = true;
return 0;
}
return $left <=> $right;
}
);
if ($hasNumberOne) {
echo "hasNumberOne";
}
I expect $hasNumberOne
to be of type bool
after the call to \usort
. Currently an TypeDoesNotContainType error is thrown for the last if statement: https://psalm.dev/r/a41c3c69f8.
The issue can be worked around by adding an explicit type annotation with type bool
to the first assignment of $hasNumberOne
: https://psalm.dev/r/f23829106d
When I tried to look into the issue myself with an extended version of the snippet (https://psalm.dev/r/30c782d34a / github commit for test):
<?php
/**
* @var list<int> $arr
*/
$arr = [0, 1, 2];
$hasSomeNumber = false;
$hasNumberOne = false;
\usort(
$arr,
static function ($left, $right) use (&$hasSomeNumber, &$hasNumberOne): int {
$hasSomeNumber = true;
if ($left === 1 || $right === 1) {
$hasNumberOne = true;
return 0;
}
return $left <=> $right;
}
);
if ($hasSomeNumber) { // has expected type `bool`
echo "hasSomeNumber";
}
if ($hasNumberOne) { // has unexpected type `false`
echo "hasNumberOne";
}
I found that the change from TFalse
to TTrue
inside the conditional is correctly determined in the local variable $if_context
in \Psalm\Internal\Analyzer\Statements\Block\IfElse\IfAnalyzer::analyze
. However, it isn't carried up into $ref_context
in \Psalm\Internal\Analyzer\FunctionLikeAnalyzer::analyze
. There in $ref_context
, $hasSomeNumber
is of expected type TBool
while $hasNumberOne
remains of unexpected type TFalse
.
I found these snippets:
https://psalm.dev/r/a41c3c69f8
<?php
/**
* @var list<int> $arr
*/
$arr = [];
$hasNumberOne = false;
\usort(
$arr,
static function (mixed $left, mixed $right) use (&$hasNumberOne): int {
if ($left === 1 || $right === 1) {
$hasNumberOne = true;
return 0;
}
return $left <=> $right;
}
);
if ($hasNumberOne) {
echo "hasNumberOne";
}
Psalm output (using commit ef3b018):
ERROR: TypeDoesNotContainType - 20:5 - Operand of type false is always falsy
ERROR: TypeDoesNotContainType - 20:5 - Type false for $hasNumberOne is always !falsy
https://psalm.dev/r/f23829106d
<?php
/**
* @var list<int> $arr
*/
$arr = [];
/**
* @var bool $hasNumberOne
*/
$hasNumberOne = false;
\usort(
$arr,
static function (mixed $left, mixed $right) use (&$hasNumberOne): int {
if ($left === 1 || $right === 1) {
$hasNumberOne = true;
return 0;
}
return $left <=> $right;
}
);
if ($hasNumberOne) {
echo "hasNumberOne";
}
Psalm output (using commit ef3b018):
No issues!
https://psalm.dev/r/30c782d34a
<?php
/**
* @var list<int> $arr
*/
$arr = [0, 1, 2];
$hasSomeNumber = false;
$hasNumberOne = false;
\usort(
$arr,
static function ($left, $right) use (&$hasSomeNumber, &$hasNumberOne): int {
$hasSomeNumber = true;
if ($left === 1 || $right === 1) {
$hasNumberOne = true;
return 0;
}
return $left <=> $right;
}
);
if ($hasSomeNumber) { // has expected type `bool`
echo "hasSomeNumber";
}
if ($hasNumberOne) { // has unexpected type `false`
echo "hasNumberOne";
}
Psalm output (using commit ef3b018):
ERROR: TypeDoesNotContainType - 26:5 - Operand of type false is always falsy
ERROR: TypeDoesNotContainType - 26:5 - Type false for $hasNumberOne is always !falsy