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: public function __construct(
22: private Type $subject,
23: private Type $target,
24: private Type $if,
25: private Type $else,
26: private bool $negated,
27: )
28: {
29: }
30:
31: public function getSubject(): Type
32: {
33: return $this->subject;
34: }
35:
36: public function getTarget(): Type
37: {
38: return $this->target;
39: }
40:
41: public function getIf(): Type
42: {
43: return $this->if;
44: }
45:
46: public function getElse(): Type
47: {
48: return $this->else;
49: }
50:
51: public function isNegated(): bool
52: {
53: return $this->negated;
54: }
55:
56: public function isSuperTypeOf(Type $type): TrinaryLogic
57: {
58: if ($type instanceof self) {
59: return $this->if->isSuperTypeOf($type->if)
60: ->and($this->else->isSuperTypeOf($type->else));
61: }
62:
63: return $this->isSuperTypeOfDefault($type);
64: }
65:
66: public function getReferencedClasses(): array
67: {
68: return array_merge(
69: $this->subject->getReferencedClasses(),
70: $this->target->getReferencedClasses(),
71: $this->if->getReferencedClasses(),
72: $this->else->getReferencedClasses(),
73: );
74: }
75:
76: public function getReferencedTemplateTypes(TemplateTypeVariance $positionVariance): array
77: {
78: return array_merge(
79: $this->subject->getReferencedTemplateTypes($positionVariance),
80: $this->target->getReferencedTemplateTypes($positionVariance),
81: $this->if->getReferencedTemplateTypes($positionVariance),
82: $this->else->getReferencedTemplateTypes($positionVariance),
83: );
84: }
85:
86: public function equals(Type $type): bool
87: {
88: return $type instanceof self
89: && $this->subject->equals($type->subject)
90: && $this->target->equals($type->target)
91: && $this->if->equals($type->if)
92: && $this->else->equals($type->else);
93: }
94:
95: public function describe(VerbosityLevel $level): string
96: {
97: return sprintf(
98: '(%s %s %s ? %s : %s)',
99: $this->subject->describe($level),
100: $this->negated ? 'is not' : 'is',
101: $this->target->describe($level),
102: $this->if->describe($level),
103: $this->else->describe($level),
104: );
105: }
106:
107: public function isResolvable(): bool
108: {
109: return !TypeUtils::containsTemplateType($this->subject) && !TypeUtils::containsTemplateType($this->target);
110: }
111:
112: protected function getResult(): Type
113: {
114: $isSuperType = $this->target->isSuperTypeOf($this->subject);
115:
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:
128: if ($isSuperType->yes()) {
129: return $yesType();
130: }
131:
132: if ($isSuperType->no()) {
133: return $noType();
134: }
135:
136: return TypeCombinator::union($yesType(), $noType());
137: }
138:
139: public function traverse(callable $cb): Type
140: {
141: $subject = $cb($this->subject);
142: $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) {
147: return $this;
148: }
149:
150: return new self($subject, $target, $if, $else, $this->negated);
151: }
152:
153: public function traverseSimultaneously(Type $right, callable $cb): Type
154: {
155: if (!$right instanceof self) {
156: return $this;
157: }
158:
159: $subject = $cb($this->subject, $right->subject);
160: $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: return $this;
166: }
167:
168: return new self($subject, $target, $if, $else, $this->negated);
169: }
170:
171: public function toPhpDocNode(): TypeNode
172: {
173: return new ConditionalTypeNode(
174: $this->subject->toPhpDocNode(),
175: $this->target->toPhpDocNode(),
176: $this->if->toPhpDocNode(),
177: $this->else->toPhpDocNode(),
178: $this->negated,
179: );
180: }
181:
182: /**
183: * @param mixed[] $properties
184: */
185: public static function __set_state(array $properties): Type
186: {
187: return new self(
188: $properties['subject'],
189: $properties['target'],
190: $properties['if'],
191: $properties['else'],
192: $properties['negated'],
193: );
194: }
195:
196: }
197: