vimeo / psalm

A static analysis tool for finding errors in PHP applications
https://psalm.dev
MIT License
5.57k stars 659 forks source link

Unexpected TypeDoesNotContainType Error after Closure use ref with conditional assignment #10874

Open adamkoppede opened 7 months ago

adamkoppede commented 7 months ago

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.

psalm-github-bot[bot] commented 7 months ago

I found these snippets:

https://psalm.dev/r/a41c3c69f8 ```php $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 $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 $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 ```