vimeo / psalm

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

Psalm's inference of array keys as an `int<min, max>` range clashes with invariance of templated collection types #10985

Open Ocramius opened 4 months ago

Ocramius commented 4 months ago

Psalm seems to behave incorrectly with Collection<TKey, ...> types when TKey is inferred to be int<0, n>, rather than int, 1|2|3, and a Collection<int> is requested. I lack the proper terminology/keywords to explain and search this better, so here's an example:

<?php

/** @template TKey */
final class Collection
{
    /** @param array<TKey, mixed> $items */
    function __construct(array $items) {}
}

/** @param Collection<int> $c */
function usesIntCollection(Collection $c): void {
    echo var_export($c, true); // irrelevant
}

// ok
$empty = new Collection([]);
/** @psalm-trace $empty */
usesIntCollection($empty);

// ok
$keysInferredAsUnionType = new Collection([1 => 'a', 2 => 'b']);
/** @psalm-trace $keysInferredAsUnionType */
usesIntCollection($keysInferredAsUnionType);

$keysInferredAsRange = new Collection(['a', 'b']);
/** @psalm-trace $keysInferredAsRange */
usesIntCollection($keysInferredAsRange);
Psalm output (using commit [16b24bd](https://github.com/vimeo/psalm/commit/16b24bd)): 

INFO: [Trace](https://psalm.dev/224) - 18:1 - $empty: Collection<int>

INFO: [Trace](https://psalm.dev/224) - 24:1 - $keysInferredAsUnionType: Collection<int>

ERROR: [InvalidArgument](https://psalm.dev/004) - 29:19 - Argument 1 of usesIntCollection expects Collection<int>, but Collection<int<0, 1>> provided

INFO: [Trace](https://psalm.dev/224) - 29:1 - $keysInferredAsRange: Collection<int<0, 1>>

https://psalm.dev/r/8a5261da42

In the above example, all 3 examples should probably pass the @param Collection<int> $c parameter type required by usesIntCollection().

Refs

Related: https://github.com/doctrine/orm/issues/11451 Related: https://github.com/doctrine/orm/pull/11454 Longer explanation @ https://github.com/doctrine/orm/pull/11454#issuecomment-2120717927

/cc @greg0ire @MatteoFeltrin

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

I found these snippets:

https://psalm.dev/r/8a5261da42 ```php $items */ function __construct(array $items) {} } /** @param Collection $c */ function usesIntCollection(Collection $c): void { echo var_export($c, true); // irrelevant } // ok $empty = new Collection([]); /** @psalm-trace $empty */ usesIntCollection($empty); // ok $keysInferredAsUnionType = new Collection([1 => 'a', 2 => 'b']); /** @psalm-trace $keysInferredAsUnionType */ usesIntCollection($keysInferredAsUnionType); $keysInferredAsRange = new Collection(['a', 'b']); /** @psalm-trace $keysInferredAsRange */ usesIntCollection($keysInferredAsRange); ``` ``` Psalm output (using commit 16b24bd): INFO: Trace - 18:1 - $empty: Collection INFO: Trace - 24:1 - $keysInferredAsUnionType: Collection ERROR: InvalidArgument - 29:19 - Argument 1 of usesIntCollection expects Collection, but Collection> provided INFO: Trace - 29:1 - $keysInferredAsRange: Collection> ```