Skip to content

Commit 4b978ba

Browse files
rvanvelzenondrejmirtes
authored andcommitted
Use narrowed conditional type if/else types for subtype checks
1 parent 8edfde0 commit 4b978ba

File tree

3 files changed

+97
-23
lines changed

3 files changed

+97
-23
lines changed

src/Type/ConditionalType.php

Lines changed: 62 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,14 @@ final class ConditionalType implements CompoundType, LateResolvableType
1818
use LateResolvableTypeTrait;
1919
use NonGeneralizableTypeTrait;
2020

21+
private ?Type $normalizedIf = null;
22+
23+
private ?Type $normalizedElse = null;
24+
25+
private ?Type $subjectWithTargetIntersectedType = null;
26+
27+
private ?Type $subjectWithTargetRemovedType = null;
28+
2129
public function __construct(
2230
private Type $subject,
2331
private Type $target,
@@ -113,37 +121,33 @@ protected function getResult(): Type
113121
{
114122
$isSuperType = $this->target->isSuperTypeOf($this->subject);
115123

116-
$intersectedType = TypeCombinator::intersect($this->subject, $this->target);
117-
$removedType = TypeCombinator::remove($this->subject, $this->target);
118-
119-
$yesType = fn () => TypeTraverser::map(
120-
!$this->negated ? $this->if : $this->else,
121-
fn (Type $type, callable $traverse) => $type === $this->subject ? (!$this->negated ? $intersectedType : $removedType) : $traverse($type),
122-
);
123-
$noType = fn () => TypeTraverser::map(
124-
!$this->negated ? $this->else : $this->if,
125-
fn (Type $type, callable $traverse) => $type === $this->subject ? (!$this->negated ? $removedType : $intersectedType) : $traverse($type),
126-
);
127-
128124
if ($isSuperType->yes()) {
129-
return $yesType();
125+
return !$this->negated ? $this->getNormalizedIf() : $this->getNormalizedElse();
130126
}
131127

132128
if ($isSuperType->no()) {
133-
return $noType();
129+
return !$this->negated ? $this->getNormalizedElse() : $this->getNormalizedIf();
134130
}
135131

136-
return TypeCombinator::union($yesType(), $noType());
132+
return TypeCombinator::union(
133+
$this->getNormalizedIf(),
134+
$this->getNormalizedElse(),
135+
);
137136
}
138137

139138
public function traverse(callable $cb): Type
140139
{
141140
$subject = $cb($this->subject);
142141
$target = $cb($this->target);
143-
$if = $cb($this->if);
144-
$else = $cb($this->else);
145-
146-
if ($this->subject === $subject && $this->target === $target && $this->if === $if && $this->else === $else) {
142+
$if = $cb($this->getNormalizedIf());
143+
$else = $cb($this->getNormalizedElse());
144+
145+
if (
146+
$this->subject === $subject
147+
&& $this->target === $target
148+
&& $this->getNormalizedIf() === $if
149+
&& $this->getNormalizedElse() === $else
150+
) {
147151
return $this;
148152
}
149153

@@ -158,10 +162,15 @@ public function traverseSimultaneously(Type $right, callable $cb): Type
158162

159163
$subject = $cb($this->subject, $right->subject);
160164
$target = $cb($this->target, $right->target);
161-
$if = $cb($this->if, $right->if);
162-
$else = $cb($this->else, $right->else);
163-
164-
if ($this->subject === $subject && $this->target === $target && $this->if === $if && $this->else === $else) {
165+
$if = $cb($this->getNormalizedIf(), $right->getNormalizedIf());
166+
$else = $cb($this->getNormalizedElse(), $right->getNormalizedElse());
167+
168+
if (
169+
$this->subject === $subject
170+
&& $this->target === $target
171+
&& $this->getNormalizedIf() === $if
172+
&& $this->getNormalizedElse() === $else
173+
) {
165174
return $this;
166175
}
167176

@@ -193,4 +202,34 @@ public static function __set_state(array $properties): Type
193202
);
194203
}
195204

205+
private function getNormalizedIf(): Type
206+
{
207+
return $this->normalizedIf ??= TypeTraverser::map(
208+
$this->if,
209+
fn (Type $type, callable $traverse) => $type === $this->subject
210+
? (!$this->negated ? $this->getSubjectWithTargetIntersectedType() : $this->getSubjectWithTargetRemovedType())
211+
: $traverse($type),
212+
);
213+
}
214+
215+
private function getNormalizedElse(): Type
216+
{
217+
return $this->normalizedElse ??= TypeTraverser::map(
218+
$this->else,
219+
fn (Type $type, callable $traverse) => $type === $this->subject
220+
? (!$this->negated ? $this->getSubjectWithTargetRemovedType() : $this->getSubjectWithTargetIntersectedType())
221+
: $traverse($type),
222+
);
223+
}
224+
225+
private function getSubjectWithTargetIntersectedType(): Type
226+
{
227+
return $this->subjectWithTargetIntersectedType ??= TypeCombinator::intersect($this->subject, $this->target);
228+
}
229+
230+
private function getSubjectWithTargetRemovedType(): Type
231+
{
232+
return $this->subjectWithTargetRemovedType ??= TypeCombinator::remove($this->subject, $this->target);
233+
}
234+
196235
}

tests/PHPStan/Rules/PhpDoc/IncompatiblePhpDocTypeRuleTest.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -420,4 +420,9 @@ public function testGenericCallables(): void
420420
]);
421421
}
422422

423+
public function testBug10622(): void
424+
{
425+
$this->analyse([__DIR__ . '/data/bug-10622.php'], []);
426+
}
427+
423428
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php
2+
3+
namespace Bug10622;
4+
5+
class Model {}
6+
7+
/**
8+
* @template TKey of array-key
9+
* @template TModel
10+
*
11+
*/
12+
class SupportCollection {}
13+
14+
/**
15+
* @template TKey of array-key
16+
* @template TModel of Model
17+
*
18+
*/
19+
class Collection
20+
{
21+
/**
22+
* Run a map over each of the items.
23+
*
24+
* @template TMapValue
25+
*
26+
* @param callable(TModel, TKey): TMapValue $callback
27+
* @return (TMapValue is Model ? self<TKey, TMapValue> : SupportCollection<TKey, TMapValue>)
28+
*/
29+
public function map(callable $callback) {}
30+
}

0 commit comments

Comments
 (0)