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: | |
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: | |