phpstatic-analysislintpsalm-php

How to solve a problem with a DocblockTypeContradiction in Psalm


I have the simplified code example from my library, to which psalm, for reasons I don't understand, outputs warnings.

class Example {
    public const C_1 = 'val1';
    public const C_2 = 'val2';
    public const C_3 = 'val3';

    /**
    * @param self::C_1 | self::C_2 $c
    */
    public function __construct(string $c)
    {
       if (!in_array($c, [self::C_1, self::C_2], true)) {
            throw new InvalidArgumentException('Unsupported value');
        }
    }
}

On if (!in_array($c, [self::C_1, self::C_2], true)) line displays a warning with the type DocblockTypeContradiction and the message Docblock-defined type "val1" for $c is always string(val1).

As I understand it, psalm requires remove the check if (!in_array($c, [self::C_1, self::C_2], true)), since constants are already checked by psalm. But what if a user who will use my library will not use psalm in his project? How then to check the correctness of the passed argument to the constructor?

Sorry for the bad English. I hope I was able to explain the problem.


Solution

  • If you want Psalm to help statically validate that only valid arguments are passed in, but at the same time you still want the code to do a runtime validation in case users are not pre-validating with Psalm, your best option is to leave everything as it is, and then just suppress the issue inline using /** @psalm-suppress DocblockTypeContradiction */.

    class Example {
        public const C_1 = 'val1';
        public const C_2 = 'val2';
        public const C_3 = 'val3';
    
        /**
         * @param self::C_1|self::C_2 $c
         */
        public function __construct(string $c) {
           /** @psalm-suppress DocblockTypeContradiction */
           if (!in_array($c, [self::C_1, self::C_2], true)) {
                throw new InvalidArgumentException('Unsupported value');
           }
        }
    }
    

    https://psalm.dev/r/2e8e443bbd

    It makes sense for Psalm to show this issue because that is the point of Psalm, validate the things that can be validated at static analysis time, so they don't have to be validated at runtime. But your use case makes sense as well, and I think suppressing the issue is the best option.

    Alternatively, you could use a string backed enum. If you specify your enum as the type for the constructor parameter, you won't need runtime validation or Psalm annotations, as php will validated it at runtime for you, and the actual php type hint will be enough information for Psalm to go off of. Then Psalm can still statically validate only the correct types are passed for anyone that wants to use Psalm.