1: | <?php declare(strict_types = 1); |
2: | |
3: | namespace PHPStan\Type; |
4: | |
5: | use PHPStan\PhpDocParser\Ast\Type\ConditionalTypeNode; |
6: | use PHPStan\PhpDocParser\Ast\Type\TypeNode; |
7: | use PHPStan\TrinaryLogic; |
8: | use PHPStan\Type\Generic\TemplateTypeVariance; |
9: | use PHPStan\Type\Traits\LateResolvableTypeTrait; |
10: | use PHPStan\Type\Traits\NonGeneralizableTypeTrait; |
11: | use function array_merge; |
12: | use function sprintf; |
13: | |
14: | |
15: | final class ConditionalType implements CompoundType, LateResolvableType |
16: | { |
17: | |
18: | use LateResolvableTypeTrait; |
19: | use NonGeneralizableTypeTrait; |
20: | |
21: | private ?Type $normalizedIf = null; |
22: | |
23: | private ?Type $normalizedElse = null; |
24: | |
25: | private ?Type $subjectWithTargetIntersectedType = null; |
26: | |
27: | private ?Type $subjectWithTargetRemovedType = null; |
28: | |
29: | public function __construct( |
30: | private Type $subject, |
31: | private Type $target, |
32: | private Type $if, |
33: | private Type $else, |
34: | private bool $negated, |
35: | ) |
36: | { |
37: | } |
38: | |
39: | public function getSubject(): Type |
40: | { |
41: | return $this->subject; |
42: | } |
43: | |
44: | public function getTarget(): Type |
45: | { |
46: | return $this->target; |
47: | } |
48: | |
49: | public function getIf(): Type |
50: | { |
51: | return $this->if; |
52: | } |
53: | |
54: | public function getElse(): Type |
55: | { |
56: | return $this->else; |
57: | } |
58: | |
59: | public function isNegated(): bool |
60: | { |
61: | return $this->negated; |
62: | } |
63: | |
64: | public function isSuperTypeOf(Type $type): TrinaryLogic |
65: | { |
66: | if ($type instanceof self) { |
67: | return $this->if->isSuperTypeOf($type->if) |
68: | ->and($this->else->isSuperTypeOf($type->else)); |
69: | } |
70: | |
71: | return $this->isSuperTypeOfDefault($type); |
72: | } |
73: | |
74: | public function getReferencedClasses(): array |
75: | { |
76: | return array_merge( |
77: | $this->subject->getReferencedClasses(), |
78: | $this->target->getReferencedClasses(), |
79: | $this->if->getReferencedClasses(), |
80: | $this->else->getReferencedClasses(), |
81: | ); |
82: | } |
83: | |
84: | public function getReferencedTemplateTypes(TemplateTypeVariance $positionVariance): array |
85: | { |
86: | return array_merge( |
87: | $this->subject->getReferencedTemplateTypes($positionVariance), |
88: | $this->target->getReferencedTemplateTypes($positionVariance), |
89: | $this->if->getReferencedTemplateTypes($positionVariance), |
90: | $this->else->getReferencedTemplateTypes($positionVariance), |
91: | ); |
92: | } |
93: | |
94: | public function equals(Type $type): bool |
95: | { |
96: | return $type instanceof self |
97: | && $this->subject->equals($type->subject) |
98: | && $this->target->equals($type->target) |
99: | && $this->if->equals($type->if) |
100: | && $this->else->equals($type->else); |
101: | } |
102: | |
103: | public function describe(VerbosityLevel $level): string |
104: | { |
105: | return sprintf( |
106: | '(%s %s %s ? %s : %s)', |
107: | $this->subject->describe($level), |
108: | $this->negated ? 'is not' : 'is', |
109: | $this->target->describe($level), |
110: | $this->if->describe($level), |
111: | $this->else->describe($level), |
112: | ); |
113: | } |
114: | |
115: | public function isResolvable(): bool |
116: | { |
117: | return !TypeUtils::containsTemplateType($this->subject) && !TypeUtils::containsTemplateType($this->target); |
118: | } |
119: | |
120: | protected function getResult(): Type |
121: | { |
122: | $isSuperType = $this->target->isSuperTypeOf($this->subject); |
123: | |
124: | if ($isSuperType->yes()) { |
125: | return !$this->negated ? $this->getNormalizedIf() : $this->getNormalizedElse(); |
126: | } |
127: | |
128: | if ($isSuperType->no()) { |
129: | return !$this->negated ? $this->getNormalizedElse() : $this->getNormalizedIf(); |
130: | } |
131: | |
132: | return TypeCombinator::union( |
133: | $this->getNormalizedIf(), |
134: | $this->getNormalizedElse(), |
135: | ); |
136: | } |
137: | |
138: | public function traverse(callable $cb): Type |
139: | { |
140: | $subject = $cb($this->subject); |
141: | $target = $cb($this->target); |
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: | ) { |
151: | return $this; |
152: | } |
153: | |
154: | return new self($subject, $target, $if, $else, $this->negated); |
155: | } |
156: | |
157: | public function traverseSimultaneously(Type $right, callable $cb): Type |
158: | { |
159: | if (!$right instanceof self) { |
160: | return $this; |
161: | } |
162: | |
163: | $subject = $cb($this->subject, $right->subject); |
164: | $target = $cb($this->target, $right->target); |
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: | ) { |
174: | return $this; |
175: | } |
176: | |
177: | return new self($subject, $target, $if, $else, $this->negated); |
178: | } |
179: | |
180: | public function toPhpDocNode(): TypeNode |
181: | { |
182: | return new ConditionalTypeNode( |
183: | $this->subject->toPhpDocNode(), |
184: | $this->target->toPhpDocNode(), |
185: | $this->if->toPhpDocNode(), |
186: | $this->else->toPhpDocNode(), |
187: | $this->negated, |
188: | ); |
189: | } |
190: | |
191: | |
192: | |
193: | |
194: | public static function __set_state(array $properties): Type |
195: | { |
196: | return new self( |
197: | $properties['subject'], |
198: | $properties['target'], |
199: | $properties['if'], |
200: | $properties['else'], |
201: | $properties['negated'], |
202: | ); |
203: | } |
204: | |
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: | |
235: | } |
236: | |