Psalm: [Bug] Incorrect TypeDoesNotContainType detection when variable is defined within loop expression

Created on 10 May 2020  路  6Comments  路  Source: vimeo/psalm

Given the following function:

    public function getRenderersForClass(string $nodeClass): iterable
    {
       // If renderers are defined for this specific class, return them immediately
        if (isset($this->renderersByClass[$nodeClass])) {
            return $this->renderersByClass[$nodeClass];
        }

        // Check if a renderer is registered for any parent class
        while ($parent = \get_parent_class($parent ?? $nodeClass)) {
            if (! isset($this->renderersByClass[$parent])) {
                continue;
            }

            // "Cache" this result to avoid future loops
            return $this->renderersByClass[$nodeClass] = $this->renderersByClass[$parent];
        }

        return [];
    }

Psalm incorrectly raises the following TypeDoesNotContainType error:

image

Immediately before the first iteration of that loop, $parent is null, but Psalm thinks it's set to a class-string which it isn't until after that expression is evaluated.

bug

All 6 comments

Hey @colinodell, can you reproduce the issue on https://psalm.dev ?

Yes, I can reproduce it: https://psalm.dev/r/c10550b943

I found these snippets:


https://psalm.dev/r/c10550b943

<?php

final class Foo
{
    /** @var array<string, iterable> */
    private $renderersByClass = [];

    public function getRenderersForClass(string $nodeClass): iterable
    {
       // If renderers are defined for this specific class, return them immediately
        if (isset($this->renderersByClass[$nodeClass])) {
            return $this->renderersByClass[$nodeClass];
        }

        // Check if a renderer is registered for any parent class
        while ($parent = \get_parent_class($parent ?? $nodeClass)) {
            if (! isset($this->renderersByClass[$parent])) {
                continue;
            }

            // "Cache" this result to avoid future loops
            return $this->renderersByClass[$nodeClass] = $this->renderersByClass[$parent];
        }

        return [];
    }
}
Psalm output (using commit 3e16aec):

ERROR: TypeDoesNotContainType - 16:44 - class-string is always defined and non-null

I found these snippets:


https://psalm.dev/r/5b885422e8

<?php

final class Foo
{
    /** @var array<string, iterable> */
    private $renderersByClass = [];

    public function getRenderersForClass(string $nodeClass): iterable
    {
       // If renderers are defined for this specific class, return them immediately
        if (isset($this->renderersByClass[$nodeClass])) {
            return $this->renderersByClass[$nodeClass];
        }

        $parent = $nodeClass;
        // Check if a renderer is registered for any parent class
        while ($parent = \get_parent_class($parent)) {
            if (! isset($this->renderersByClass[$parent])) {
                continue;
            }

            // "Cache" this result to avoid future loops
            return $this->renderersByClass[$nodeClass] = $this->renderersByClass[$parent];
        }

        return [];
    }
}
Psalm output (using commit 3e16aec):

No issues!

Reduced to

function foo(string $a): void {
    while ($b = getString($b ?? $a)) {
        $c = "hello";
    }
}

function getString(string $s) : ?string {
    return rand(0, 1) ? $s : null;
}
Was this page helpful?
0 / 5 - 0 ratings