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: /** @api */
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: * @param mixed[] $properties
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: