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: return !TypeUtils::containsTemplateType($this->subject) && !TypeUtils::containsTemplateType($this->target);
117: }
118:
119: protected function getResult(): Type
120: {
121: $isSuperType = $this->target->isSuperTypeOf($this->subject);
122:
123: if ($isSuperType->yes()) {
124: return !$this->negated ? $this->getNormalizedIf() : $this->getNormalizedElse();
125: }
126:
127: if ($isSuperType->no()) {
128: return !$this->negated ? $this->getNormalizedElse() : $this->getNormalizedIf();
129: }
130:
131: return TypeCombinator::union(
132: $this->getNormalizedIf(),
133: $this->getNormalizedElse(),
134: );
135: }
136:
137: public function traverse(callable $cb): Type
138: {
139: $subject = $cb($this->subject);
140: $target = $cb($this->target);
141: $if = $cb($this->getNormalizedIf());
142: $else = $cb($this->getNormalizedElse());
143:
144: if (
145: $this->subject === $subject
146: && $this->target === $target
147: && $this->getNormalizedIf() === $if
148: && $this->getNormalizedElse() === $else
149: ) {
150: return $this;
151: }
152:
153: return new self($subject, $target, $if, $else, $this->negated);
154: }
155:
156: public function traverseSimultaneously(Type $right, callable $cb): Type
157: {
158: if (!$right instanceof self) {
159: return $this;
160: }
161:
162: $subject = $cb($this->subject, $right->subject);
163: $target = $cb($this->target, $right->target);
164: $if = $cb($this->getNormalizedIf(), $right->getNormalizedIf());
165: $else = $cb($this->getNormalizedElse(), $right->getNormalizedElse());
166:
167: if (
168: $this->subject === $subject
169: && $this->target === $target
170: && $this->getNormalizedIf() === $if
171: && $this->getNormalizedElse() === $else
172: ) {
173: return $this;
174: }
175:
176: return new self($subject, $target, $if, $else, $this->negated);
177: }
178:
179: public function toPhpDocNode(): TypeNode
180: {
181: return new ConditionalTypeNode(
182: $this->subject->toPhpDocNode(),
183: $this->target->toPhpDocNode(),
184: $this->if->toPhpDocNode(),
185: $this->else->toPhpDocNode(),
186: $this->negated,
187: );
188: }
189:
190: private function getNormalizedIf(): Type
191: {
192: return $this->normalizedIf ??= TypeTraverser::map(
193: $this->if,
194: fn (Type $type, callable $traverse) => $type === $this->subject
195: ? (!$this->negated ? $this->getSubjectWithTargetIntersectedType() : $this->getSubjectWithTargetRemovedType())
196: : $traverse($type),
197: );
198: }
199:
200: private function getNormalizedElse(): Type
201: {
202: return $this->normalizedElse ??= TypeTraverser::map(
203: $this->else,
204: fn (Type $type, callable $traverse) => $type === $this->subject
205: ? (!$this->negated ? $this->getSubjectWithTargetRemovedType() : $this->getSubjectWithTargetIntersectedType())
206: : $traverse($type),
207: );
208: }
209:
210: private function getSubjectWithTargetIntersectedType(): Type
211: {
212: return $this->subjectWithTargetIntersectedType ??= TypeCombinator::intersect($this->subject, $this->target);
213: }
214:
215: private function getSubjectWithTargetRemovedType(): Type
216: {
217: return $this->subjectWithTargetRemovedType ??= TypeCombinator::remove($this->subject, $this->target);
218: }
219:
220: }
221: