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\Type\Generic\TemplateTypeVariance;
8: use PHPStan\Type\Traits\LateResolvableTypeTrait;
9: use PHPStan\Type\Traits\NonGeneralizableTypeTrait;
10: use function array_merge;
11: use function sprintf;
12:
13: /** @api */
14: final class ConditionalType implements CompoundType, LateResolvableType
15: {
16:
17: use LateResolvableTypeTrait;
18: use NonGeneralizableTypeTrait;
19:
20: private ?Type $normalizedIf = null;
21:
22: private ?Type $normalizedElse = null;
23:
24: private ?Type $subjectWithTargetIntersectedType = null;
25:
26: private ?Type $subjectWithTargetRemovedType = null;
27:
28: public function __construct(
29: private Type $subject,
30: private Type $target,
31: private Type $if,
32: private Type $else,
33: private bool $negated,
34: )
35: {
36: }
37:
38: public function getSubject(): Type
39: {
40: return $this->subject;
41: }
42:
43: public function getTarget(): Type
44: {
45: return $this->target;
46: }
47:
48: public function getIf(): Type
49: {
50: return $this->if;
51: }
52:
53: public function getElse(): Type
54: {
55: return $this->else;
56: }
57:
58: public function isNegated(): bool
59: {
60: return $this->negated;
61: }
62:
63: public function isSuperTypeOf(Type $type): IsSuperTypeOfResult
64: {
65: if ($type instanceof self) {
66: return $this->if->isSuperTypeOf($type->if)
67: ->and($this->else->isSuperTypeOf($type->else));
68: }
69:
70: return $this->isSuperTypeOfDefault($type);
71: }
72:
73: public function getReferencedClasses(): array
74: {
75: return array_merge(
76: $this->subject->getReferencedClasses(),
77: $this->target->getReferencedClasses(),
78: $this->if->getReferencedClasses(),
79: $this->else->getReferencedClasses(),
80: );
81: }
82:
83: public function getReferencedTemplateTypes(TemplateTypeVariance $positionVariance): array
84: {
85: return array_merge(
86: $this->subject->getReferencedTemplateTypes($positionVariance),
87: $this->target->getReferencedTemplateTypes($positionVariance),
88: $this->if->getReferencedTemplateTypes($positionVariance),
89: $this->else->getReferencedTemplateTypes($positionVariance),
90: );
91: }
92:
93: public function equals(Type $type): bool
94: {
95: return $type instanceof self
96: && $this->subject->equals($type->subject)
97: && $this->target->equals($type->target)
98: && $this->if->equals($type->if)
99: && $this->else->equals($type->else);
100: }
101:
102: public function describe(VerbosityLevel $level): string
103: {
104: return sprintf(
105: '(%s %s %s ? %s : %s)',
106: $this->subject->describe($level),
107: $this->negated ? 'is not' : 'is',
108: $this->target->describe($level),
109: $this->if->describe($level),
110: $this->else->describe($level),
111: );
112: }
113:
114: public function isResolvable(): bool
115: {
116: if (!TypeUtils::containsTemplateType($this->subject) && !TypeUtils::containsTemplateType($this->target)) {
117: return true;
118: }
119:
120: $isSuperType = $this->target->isSuperTypeOf($this->subject);
121:
122: return $isSuperType->yes() || $isSuperType->no();
123: }
124:
125: protected function getResult(): Type
126: {
127: $isSuperType = $this->target->isSuperTypeOf($this->subject);
128:
129: if ($isSuperType->yes()) {
130: return !$this->negated ? $this->getNormalizedIf() : $this->getNormalizedElse();
131: }
132:
133: if ($isSuperType->no()) {
134: return !$this->negated ? $this->getNormalizedElse() : $this->getNormalizedIf();
135: }
136:
137: return TypeCombinator::union(
138: $this->getNormalizedIf(),
139: $this->getNormalizedElse(),
140: );
141: }
142:
143: public function traverse(callable $cb): Type
144: {
145: $subject = $cb($this->subject);
146: $target = $cb($this->target);
147: $if = $cb($this->getNormalizedIf());
148: $else = $cb($this->getNormalizedElse());
149:
150: if (
151: $this->subject === $subject
152: && $this->target === $target
153: && $this->getNormalizedIf() === $if
154: && $this->getNormalizedElse() === $else
155: ) {
156: return $this;
157: }
158:
159: return new self($subject, $target, $if, $else, $this->negated);
160: }
161:
162: public function traverseSimultaneously(Type $right, callable $cb): Type
163: {
164: if (!$right instanceof self) {
165: return $this;
166: }
167:
168: $subject = $cb($this->subject, $right->subject);
169: $target = $cb($this->target, $right->target);
170: $if = $cb($this->getNormalizedIf(), $right->getNormalizedIf());
171: $else = $cb($this->getNormalizedElse(), $right->getNormalizedElse());
172:
173: if (
174: $this->subject === $subject
175: && $this->target === $target
176: && $this->getNormalizedIf() === $if
177: && $this->getNormalizedElse() === $else
178: ) {
179: return $this;
180: }
181:
182: return new self($subject, $target, $if, $else, $this->negated);
183: }
184:
185: public function toPhpDocNode(): TypeNode
186: {
187: return new ConditionalTypeNode(
188: $this->subject->toPhpDocNode(),
189: $this->target->toPhpDocNode(),
190: $this->if->toPhpDocNode(),
191: $this->else->toPhpDocNode(),
192: $this->negated,
193: );
194: }
195:
196: private function getNormalizedIf(): Type
197: {
198: return $this->normalizedIf ??= TypeTraverser::map(
199: $this->if,
200: fn (Type $type, callable $traverse) => $type === $this->subject
201: ? (!$this->negated ? $this->getSubjectWithTargetIntersectedType() : $this->getSubjectWithTargetRemovedType())
202: : $traverse($type),
203: );
204: }
205:
206: private function getNormalizedElse(): Type
207: {
208: return $this->normalizedElse ??= TypeTraverser::map(
209: $this->else,
210: fn (Type $type, callable $traverse) => $type === $this->subject
211: ? (!$this->negated ? $this->getSubjectWithTargetRemovedType() : $this->getSubjectWithTargetIntersectedType())
212: : $traverse($type),
213: );
214: }
215:
216: private function getSubjectWithTargetIntersectedType(): Type
217: {
218: return $this->subjectWithTargetIntersectedType ??= TypeCombinator::intersect($this->subject, $this->target);
219: }
220:
221: private function getSubjectWithTargetRemovedType(): Type
222: {
223: return $this->subjectWithTargetRemovedType ??= TypeCombinator::remove($this->subject, $this->target);
224: }
225:
226: }
227: