psalm icon indicating copy to clipboard operation
psalm copied to clipboard

Unexpected TypeDoesNotContainType Error after Closure use ref with conditional assignment

Open adamkoppede opened this issue 10 months ago • 1 comments

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.

adamkoppede avatar Apr 02 '24 12:04 adamkoppede

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

psalm-github-bot[bot] avatar Apr 02 '24 12:04 psalm-github-bot[bot]