-
-
Notifications
You must be signed in to change notification settings - Fork 933
Description
Feature request
Currently an assignment of the form$this->attribute = $value
narrows the type of $this->attribute
to the same type as $value
for the remainder of the method. In most cases, this makes sense.
For example, the following code is statically sound. Iterables don't always have a forAll
method, but there's no possible way $this->stuff
can be of a type other than ArrayCollection
when we attempt to call the forAll
method.
/** @var iterable<int, Thing> */
private iterable $stuff;
public function __construct() {
$this->stuff = new ArrayCollection();
$this->stuff->forAll($doAThing);
}
However, a problem arises if the type of $value
is in some way wider than the type of $this->attribute
. This is only possible when generic types are used: $this->attribute
might have narrower type parameters than $value
does, although $value
might have a more specific class type. Since the assignment only affects the inferred type of $this->attribute
, it actually widens those type parameters.
I demonstrated an example of this problem a few minutes ago, which I'll reproduce here with dumpType
information included:
/** @var Collection<int, int> $ints */
public iterable $ints;
/** @var Collection<int, string> $strings */
public iterable $strings;
public function __construct() {
dumpType($this->ints); // Doctrine\Common\Collections\Collection<int, int>
dumpType($this->strings); // Doctrine\Common\Collections\Collection<int, string>
$array = new ArrayCollection();
dumpType($array); // Doctrine\Common\Collections\ArrayCollection<*NEVER*, *NEVER*>
$this->ints = $array;
dumpType($this->ints); // Doctrine\Common\Collections\ArrayCollection<*NEVER*, *NEVER*>
$this->strings = $array;
dumpType($this->strings); // Doctrine\Common\Collections\ArrayCollection<*NEVER*, *NEVER*>
}
As you can see, PHPStan "forgets" that $this->ints
and $this->strings
have different type parameters. This leads to an admittedly-minor hole in the type system, since this mechanism may be used to statically cast from any type to any other, without a corresponding runtime conversion occurring.
In a situation like this, I believe that PHPStan should effectively unify the types of $this->attribute
and $value
, in the logic programming sense. The inferred types of both variables ought to be narrowed by the assignment expression, preserving type compatibility for both the outer class type and its type parameters. In other words, this ought to happen:
dumpType($this->ints); // Doctrine\Common\Collections\Collection<int, int>
$array = new ArrayCollection();
dumpType($array); // Doctrine\Common\Collections\ArrayCollection<*NEVER*, *NEVER*>
$this->ints = $array;
dumpType($this->ints); // Doctrine\Common\Collections\ArrayCollection<int, int>
dumpType($array); // Doctrine\Common\Collections\ArrayCollection<int, int>
This narrowing step would cause the subsequent assignment, $this->strings = $array
, to fail type-checking: a Collection<int, int>
cannot be assigned to a variable of type Collection<int, string>
. The hole in the type system is sealed.
I do think this is an unlikely situation, since one would typically write $this->ints = new ArrayCollection()
in the first place rather than using a temporary variable. Additionally, Psalm has the exact same type system hole at time of writing. Still, this is an issue worth being aware of. Also, narrowing bidirectionally would allow the constructor to go ahead and populate $this->ints
in a properly typesafe way, which currently doesn't occur.
Did PHPStan help you today? Did it make you happy in any way?
Yes! I've been experimenting with API Platform at work, since we're hoping to adopt it for newly planned projects, and API Platform's very structured design has turned out to mesh wonderfully with PHPStan's type checking. As a Haskell fan I generally love powerful type systems, and PHPStan has been giving me those warm fuzzy TypeScript feels despite working in PHP. 🐱