Skip to content

Commit

Permalink
Fix template type subtraction and union
Browse files Browse the repository at this point in the history
  • Loading branch information
ondrejmirtes committed Mar 8, 2022
1 parent ec1aa02 commit 51e2df3
Show file tree
Hide file tree
Showing 5 changed files with 154 additions and 35 deletions.
73 changes: 59 additions & 14 deletions src/Type/Generic/TemplateTypeTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use PHPStan\Type\GeneralizePrecision;
use PHPStan\Type\IntersectionType;
use PHPStan\Type\MixedType;
use PHPStan\Type\SubtractableType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use PHPStan\Type\UnionType;
Expand Down Expand Up @@ -89,19 +90,70 @@ public function isValidVariance(Type $a, Type $b): TrinaryLogic
return $this->variance->isValidVariance($a, $b);
}

public function subtract(Type $type): Type
public function subtract(Type $typeToRemove): Type
{
return $this;
$removedBound = TypeCombinator::remove($this->getBound(), $typeToRemove);
$type = TemplateTypeFactory::create(
$this->getScope(),
$this->getName(),
$removedBound,
$this->getVariance(),
);
if ($this->isArgument()) {
return TemplateTypeHelper::toArgument($type);
}

return $type;
}

public function getTypeWithoutSubtractedType(): Type
{
return $this;
$bound = $this->getBound();
if (!$bound instanceof SubtractableType) { // @phpstan-ignore-line
return $this;
}

$type = TemplateTypeFactory::create(
$this->getScope(),
$this->getName(),
$bound->getTypeWithoutSubtractedType(),
$this->getVariance(),
);
if ($this->isArgument()) {
return TemplateTypeHelper::toArgument($type);
}

return $type;
}

public function changeSubtractedType(?Type $subtractedType): Type
{
return $this;
$bound = $this->getBound();
if (!$bound instanceof SubtractableType) { // @phpstan-ignore-line
return $this;
}

$type = TemplateTypeFactory::create(
$this->getScope(),
$this->getName(),
$bound->changeSubtractedType($subtractedType),
$this->getVariance(),
);
if ($this->isArgument()) {
return TemplateTypeHelper::toArgument($type);
}

return $type;
}

public function getSubtractedType(): ?Type
{
$bound = $this->getBound();
if (!$bound instanceof SubtractableType) { // @phpstan-ignore-line
return null;
}

return $bound->getSubtractedType();
}

public function equals(Type $type): bool
Expand Down Expand Up @@ -220,18 +272,11 @@ protected function shouldGeneralizeInferredType(): bool

public function tryRemove(Type $typeToRemove): ?Type
{
$removedBound = TypeCombinator::remove($this->getBound(), $typeToRemove);
$type = TemplateTypeFactory::create(
$this->getScope(),
$this->getName(),
$removedBound,
$this->getVariance(),
);
if ($this->isArgument()) {
return TemplateTypeHelper::toArgument($type);
if ($this->getBound()->isSuperTypeOf($typeToRemove)->yes()) {
return $this->subtract($typeToRemove);
}

return $type;
return null;
}

/**
Expand Down
39 changes: 19 additions & 20 deletions src/Type/TypeCombinator.php
Original file line number Diff line number Diff line change
Expand Up @@ -360,12 +360,7 @@ private static function compareTypesInUnion(Type $a, Type $b): ?array
$isSuperType = $typeWithoutSubtractedTypeA->isSuperTypeOf($b);
}
if ($isSuperType->yes()) {
if ($b instanceof SubtractableType) {
$subtractedType = $b->getSubtractedType();
} else {
$subtractedType = new MixedType(false, $b);
}
$a = self::intersectWithSubtractedType($a, $subtractedType);
$a = self::intersectWithSubtractedType($a, $b);
return [$a, null];
}
}
Expand All @@ -378,12 +373,7 @@ private static function compareTypesInUnion(Type $a, Type $b): ?array
$isSuperType = $typeWithoutSubtractedTypeB->isSuperTypeOf($a);
}
if ($isSuperType->yes()) {
if ($a instanceof SubtractableType) {
$subtractedType = $a->getSubtractedType();
} else {
$subtractedType = new MixedType(false, $a);
}
$b = self::intersectWithSubtractedType($b, $subtractedType);
$b = self::intersectWithSubtractedType($b, $a);
return [null, $b];
}
}
Expand Down Expand Up @@ -454,27 +444,36 @@ private static function unionWithSubtractedType(
}

private static function intersectWithSubtractedType(
SubtractableType $subtractableType,
?Type $subtractedType,
SubtractableType $a,
Type $b,
): Type
{
if ($subtractableType->getSubtractedType() === null) {
return $subtractableType;
if ($a->getSubtractedType() === null) {
return $a;
}

if ($subtractedType === null) {
return $subtractableType->getTypeWithoutSubtractedType();
if ($b instanceof SubtractableType) {
$subtractedType = $b->getSubtractedType();
if ($subtractedType === null) {
return $a->getTypeWithoutSubtractedType();
}
} else {
$subtractedTypeTmp = self::intersect($a->getTypeWithoutSubtractedType(), $a->getSubtractedType());
if ($b->isSuperTypeOf($subtractedTypeTmp)->yes()) {
return $a->getTypeWithoutSubtractedType();
}
$subtractedType = new MixedType(false, $b);
}

$subtractedType = self::intersect(
$subtractableType->getSubtractedType(),
$a->getSubtractedType(),
$subtractedType,
);
if ($subtractedType instanceof NeverType) {
$subtractedType = null;
}

return $subtractableType->changeSubtractedType($subtractedType);
return $a->changeSubtractedType($subtractedType);
}

/**
Expand Down
1 change: 1 addition & 0 deletions tests/PHPStan/Analyser/NodeScopeResolverTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -775,6 +775,7 @@ public function dataFileAsserts(): iterable
yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Methods/data/bug-6635.php');
}
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6584.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6591.php');
}

/**
Expand Down
74 changes: 74 additions & 0 deletions tests/PHPStan/Analyser/data/bug-6591.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
<?php declare(strict_types=1);

namespace Bug6591;

use function PHPStan\Testing\assertType;

interface HydratorInterface {
/**
* @return array<string, mixed>
*/
public function extract(object $object): array;
}

interface EntityInterface {
public const IDENTITY = 'identity';
public const CREATED = 'created';
public function getIdentity(): string;
public function getCreated(): \DateTimeImmutable;
}
interface UpdatableInterface extends EntityInterface {
public const UPDATED = 'updated';
public function getUpdated(): \DateTimeImmutable;
public function setUpdated(\DateTimeImmutable $updated): void;
}
interface EnableableInterface extends UpdatableInterface {
public const ENABLED = 'enabled';
public function isEnabled(): bool;
public function setEnabled(bool $enabled): void;
}


/**
* @template T of EntityInterface
*/
class DoctrineEntityHydrator implements HydratorInterface
{
/** @param T $object */
public function extract(object $object): array
{
$data = [
EntityInterface::IDENTITY => $object->getIdentity(),
EntityInterface::CREATED => $object->getCreated()->format('c'),
];
assertType('T of Bug6591\EntityInterface (class Bug6591\DoctrineEntityHydrator, argument)', $object);
if ($object instanceof UpdatableInterface) {
assertType('Bug6591\UpdatableInterface&T of Bug6591\EntityInterface (class Bug6591\DoctrineEntityHydrator, argument)', $object);
$data[UpdatableInterface::UPDATED] = $object->getUpdated()->format('c');
} else {
assertType('T of Bug6591\EntityInterface~Bug6591\UpdatableInterface (class Bug6591\DoctrineEntityHydrator, argument)', $object);
}

assertType('T of Bug6591\EntityInterface (class Bug6591\DoctrineEntityHydrator, argument)', $object);

if ($object instanceof EnableableInterface) {
assertType('Bug6591\EnableableInterface&T of Bug6591\EntityInterface (class Bug6591\DoctrineEntityHydrator, argument)', $object);
$data[EnableableInterface::ENABLED] = $object->isEnabled();
} else {
assertType('T of Bug6591\EntityInterface~Bug6591\EnableableInterface (class Bug6591\DoctrineEntityHydrator, argument)', $object);
}

assertType('T of Bug6591\EntityInterface (class Bug6591\DoctrineEntityHydrator, argument)', $object);

return [...$data, ...$this->performExtraction($object)];
}

/**
* @param T $entity
* @return array<string, mixed>
*/
public function performExtraction(EntityInterface $entity): array
{
return [];
}
}
2 changes: 1 addition & 1 deletion tests/PHPStan/Rules/Methods/data/bug-6635.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ protected function sayHelloBug(mixed $block): mixed {
assertType('T of mixed~Bug6635\A (method Bug6635\HelloWorld::sayHelloBug(), argument)', $block);
}

assertType('(Bug6635\A&T (method Bug6635\HelloWorld::sayHelloBug(), argument))|T of mixed~Bug6635\A (method Bug6635\HelloWorld::sayHelloBug(), argument)', $block);
assertType('T (method Bug6635\HelloWorld::sayHelloBug(), argument)', $block);

return $block;
}
Expand Down

0 comments on commit 51e2df3

Please sign in to comment.