Skip to content

Commit 51e2df3

Browse files
committed
Fix template type subtraction and union
1 parent ec1aa02 commit 51e2df3

File tree

5 files changed

+154
-35
lines changed

5 files changed

+154
-35
lines changed

src/Type/Generic/TemplateTypeTrait.php

+59-14
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
use PHPStan\Type\GeneralizePrecision;
77
use PHPStan\Type\IntersectionType;
88
use PHPStan\Type\MixedType;
9+
use PHPStan\Type\SubtractableType;
910
use PHPStan\Type\Type;
1011
use PHPStan\Type\TypeCombinator;
1112
use PHPStan\Type\UnionType;
@@ -89,19 +90,70 @@ public function isValidVariance(Type $a, Type $b): TrinaryLogic
8990
return $this->variance->isValidVariance($a, $b);
9091
}
9192

92-
public function subtract(Type $type): Type
93+
public function subtract(Type $typeToRemove): Type
9394
{
94-
return $this;
95+
$removedBound = TypeCombinator::remove($this->getBound(), $typeToRemove);
96+
$type = TemplateTypeFactory::create(
97+
$this->getScope(),
98+
$this->getName(),
99+
$removedBound,
100+
$this->getVariance(),
101+
);
102+
if ($this->isArgument()) {
103+
return TemplateTypeHelper::toArgument($type);
104+
}
105+
106+
return $type;
95107
}
96108

97109
public function getTypeWithoutSubtractedType(): Type
98110
{
99-
return $this;
111+
$bound = $this->getBound();
112+
if (!$bound instanceof SubtractableType) { // @phpstan-ignore-line
113+
return $this;
114+
}
115+
116+
$type = TemplateTypeFactory::create(
117+
$this->getScope(),
118+
$this->getName(),
119+
$bound->getTypeWithoutSubtractedType(),
120+
$this->getVariance(),
121+
);
122+
if ($this->isArgument()) {
123+
return TemplateTypeHelper::toArgument($type);
124+
}
125+
126+
return $type;
100127
}
101128

102129
public function changeSubtractedType(?Type $subtractedType): Type
103130
{
104-
return $this;
131+
$bound = $this->getBound();
132+
if (!$bound instanceof SubtractableType) { // @phpstan-ignore-line
133+
return $this;
134+
}
135+
136+
$type = TemplateTypeFactory::create(
137+
$this->getScope(),
138+
$this->getName(),
139+
$bound->changeSubtractedType($subtractedType),
140+
$this->getVariance(),
141+
);
142+
if ($this->isArgument()) {
143+
return TemplateTypeHelper::toArgument($type);
144+
}
145+
146+
return $type;
147+
}
148+
149+
public function getSubtractedType(): ?Type
150+
{
151+
$bound = $this->getBound();
152+
if (!$bound instanceof SubtractableType) { // @phpstan-ignore-line
153+
return null;
154+
}
155+
156+
return $bound->getSubtractedType();
105157
}
106158

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

221273
public function tryRemove(Type $typeToRemove): ?Type
222274
{
223-
$removedBound = TypeCombinator::remove($this->getBound(), $typeToRemove);
224-
$type = TemplateTypeFactory::create(
225-
$this->getScope(),
226-
$this->getName(),
227-
$removedBound,
228-
$this->getVariance(),
229-
);
230-
if ($this->isArgument()) {
231-
return TemplateTypeHelper::toArgument($type);
275+
if ($this->getBound()->isSuperTypeOf($typeToRemove)->yes()) {
276+
return $this->subtract($typeToRemove);
232277
}
233278

234-
return $type;
279+
return null;
235280
}
236281

237282
/**

src/Type/TypeCombinator.php

+19-20
Original file line numberDiff line numberDiff line change
@@ -360,12 +360,7 @@ private static function compareTypesInUnion(Type $a, Type $b): ?array
360360
$isSuperType = $typeWithoutSubtractedTypeA->isSuperTypeOf($b);
361361
}
362362
if ($isSuperType->yes()) {
363-
if ($b instanceof SubtractableType) {
364-
$subtractedType = $b->getSubtractedType();
365-
} else {
366-
$subtractedType = new MixedType(false, $b);
367-
}
368-
$a = self::intersectWithSubtractedType($a, $subtractedType);
363+
$a = self::intersectWithSubtractedType($a, $b);
369364
return [$a, null];
370365
}
371366
}
@@ -378,12 +373,7 @@ private static function compareTypesInUnion(Type $a, Type $b): ?array
378373
$isSuperType = $typeWithoutSubtractedTypeB->isSuperTypeOf($a);
379374
}
380375
if ($isSuperType->yes()) {
381-
if ($a instanceof SubtractableType) {
382-
$subtractedType = $a->getSubtractedType();
383-
} else {
384-
$subtractedType = new MixedType(false, $a);
385-
}
386-
$b = self::intersectWithSubtractedType($b, $subtractedType);
376+
$b = self::intersectWithSubtractedType($b, $a);
387377
return [null, $b];
388378
}
389379
}
@@ -454,27 +444,36 @@ private static function unionWithSubtractedType(
454444
}
455445

456446
private static function intersectWithSubtractedType(
457-
SubtractableType $subtractableType,
458-
?Type $subtractedType,
447+
SubtractableType $a,
448+
Type $b,
459449
): Type
460450
{
461-
if ($subtractableType->getSubtractedType() === null) {
462-
return $subtractableType;
451+
if ($a->getSubtractedType() === null) {
452+
return $a;
463453
}
464454

465-
if ($subtractedType === null) {
466-
return $subtractableType->getTypeWithoutSubtractedType();
455+
if ($b instanceof SubtractableType) {
456+
$subtractedType = $b->getSubtractedType();
457+
if ($subtractedType === null) {
458+
return $a->getTypeWithoutSubtractedType();
459+
}
460+
} else {
461+
$subtractedTypeTmp = self::intersect($a->getTypeWithoutSubtractedType(), $a->getSubtractedType());
462+
if ($b->isSuperTypeOf($subtractedTypeTmp)->yes()) {
463+
return $a->getTypeWithoutSubtractedType();
464+
}
465+
$subtractedType = new MixedType(false, $b);
467466
}
468467

469468
$subtractedType = self::intersect(
470-
$subtractableType->getSubtractedType(),
469+
$a->getSubtractedType(),
471470
$subtractedType,
472471
);
473472
if ($subtractedType instanceof NeverType) {
474473
$subtractedType = null;
475474
}
476475

477-
return $subtractableType->changeSubtractedType($subtractedType);
476+
return $a->changeSubtractedType($subtractedType);
478477
}
479478

480479
/**

tests/PHPStan/Analyser/NodeScopeResolverTest.php

+1
Original file line numberDiff line numberDiff line change
@@ -775,6 +775,7 @@ public function dataFileAsserts(): iterable
775775
yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Methods/data/bug-6635.php');
776776
}
777777
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6584.php');
778+
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6591.php');
778779
}
779780

780781
/**
+74
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace Bug6591;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
interface HydratorInterface {
8+
/**
9+
* @return array<string, mixed>
10+
*/
11+
public function extract(object $object): array;
12+
}
13+
14+
interface EntityInterface {
15+
public const IDENTITY = 'identity';
16+
public const CREATED = 'created';
17+
public function getIdentity(): string;
18+
public function getCreated(): \DateTimeImmutable;
19+
}
20+
interface UpdatableInterface extends EntityInterface {
21+
public const UPDATED = 'updated';
22+
public function getUpdated(): \DateTimeImmutable;
23+
public function setUpdated(\DateTimeImmutable $updated): void;
24+
}
25+
interface EnableableInterface extends UpdatableInterface {
26+
public const ENABLED = 'enabled';
27+
public function isEnabled(): bool;
28+
public function setEnabled(bool $enabled): void;
29+
}
30+
31+
32+
/**
33+
* @template T of EntityInterface
34+
*/
35+
class DoctrineEntityHydrator implements HydratorInterface
36+
{
37+
/** @param T $object */
38+
public function extract(object $object): array
39+
{
40+
$data = [
41+
EntityInterface::IDENTITY => $object->getIdentity(),
42+
EntityInterface::CREATED => $object->getCreated()->format('c'),
43+
];
44+
assertType('T of Bug6591\EntityInterface (class Bug6591\DoctrineEntityHydrator, argument)', $object);
45+
if ($object instanceof UpdatableInterface) {
46+
assertType('Bug6591\UpdatableInterface&T of Bug6591\EntityInterface (class Bug6591\DoctrineEntityHydrator, argument)', $object);
47+
$data[UpdatableInterface::UPDATED] = $object->getUpdated()->format('c');
48+
} else {
49+
assertType('T of Bug6591\EntityInterface~Bug6591\UpdatableInterface (class Bug6591\DoctrineEntityHydrator, argument)', $object);
50+
}
51+
52+
assertType('T of Bug6591\EntityInterface (class Bug6591\DoctrineEntityHydrator, argument)', $object);
53+
54+
if ($object instanceof EnableableInterface) {
55+
assertType('Bug6591\EnableableInterface&T of Bug6591\EntityInterface (class Bug6591\DoctrineEntityHydrator, argument)', $object);
56+
$data[EnableableInterface::ENABLED] = $object->isEnabled();
57+
} else {
58+
assertType('T of Bug6591\EntityInterface~Bug6591\EnableableInterface (class Bug6591\DoctrineEntityHydrator, argument)', $object);
59+
}
60+
61+
assertType('T of Bug6591\EntityInterface (class Bug6591\DoctrineEntityHydrator, argument)', $object);
62+
63+
return [...$data, ...$this->performExtraction($object)];
64+
}
65+
66+
/**
67+
* @param T $entity
68+
* @return array<string, mixed>
69+
*/
70+
public function performExtraction(EntityInterface $entity): array
71+
{
72+
return [];
73+
}
74+
}

tests/PHPStan/Rules/Methods/data/bug-6635.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ protected function sayHelloBug(mixed $block): mixed {
2626
assertType('T of mixed~Bug6635\A (method Bug6635\HelloWorld::sayHelloBug(), argument)', $block);
2727
}
2828

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

3131
return $block;
3232
}

0 commit comments

Comments
 (0)