Page MenuHomePhabricator

Phan gets confused around nullable types
Open, Needs TriagePublicBUG REPORT

Description

Steps to replicate the issue (include links if applicable):

Create a PHP file with the following content:

<?php

class Weird {
        /** @var ?array */
        public $stuff;
}

class TestPhan {

        public function doStuff( Weird $w ) {
                if ( $w->stuff !== null ) {
                        $this->doSubStuff( $w->stuff );
                }
        }

        private function doSubStuff( array $arg ) {
                //nop
        }
}

Run phan on it.

What happens?:

Phan returns TestPhan.php:12 PhanTypeMismatchArgumentNullable Argument 1 ($arg) is $w->stuff of type ?array but \TestPhan::doSubStuff() takes array defined at TestPhan.php:16 (expected type to be non-nullable) despite the check on the line above that it's not null.

What should have happened instead?:

$w->stuff should not be detected as being possibly null.

Event Timeline

Daimona subscribed.

This is reproducible in phan demo, and it's a known limitation. Phan does not track context-based restrictions on the union type of a property, except for properties of $this. So, in your example, when the doSubStuff call is encountered, phan reads the type of the argument ($w->stuff) from the property declaration, which is nullable, and does not remember about the conditional right above. That would only be done if $stuff were a property of the current class, like in this demo. I think there's a task upstream but I can't find it; I only found a mention in https://github.com/phan/phan/issues/4611#issuecomment-968352849.

This can be avoided by using a local variable.

<?php

class Weird {
        /** @var ?array */
        public $stuff;
}

class TestPhan {

        public function doStuff( Weird $w ) {
                $stuff = $w->stuff;
                if ( $stuff !== null ) {
                        $this->doSubStuff( $stuff );
                }
        }

        private function doSubStuff( array $arg ) {
                //nop
        }
}

Actually, the current implementation is safer (even if, based on the GitHub comment, this is probably not intentional) – you cannot really be sure the property doesn’t change, since a __get() magic method or a property hook could return different values each time.