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\Turbo\ShadowedByTurboExtension;
8: use PHPStan\Type\Generic\TemplateType;
9: use PHPStan\Type\Generic\TemplateTypeVariance;
10: use PHPStan\Type\Traits\LateResolvableTypeTrait;
11: use PHPStan\Type\Traits\NonGeneralizableTypeTrait;
12: use function array_merge;
13: use function sprintf;
14:
15: /** @api */
16: #[ShadowedByTurboExtension(implementation: __DIR__ . '/../../turbo-ext/src/ConditionalType.cpp')]
17: final class ConditionalType implements CompoundType, LateResolvableType
18: {
19:
20: use LateResolvableTypeTrait;
21: use NonGeneralizableTypeTrait;
22:
23: private ?Type $normalizedIf = null;
24:
25: private ?Type $normalizedElse = null;
26:
27: public function __construct(
28: private Type $subject,
29: private Type $target,
30: private Type $if,
31: private Type $else,
32: private bool $negated,
33: )
34: {
35: }
36:
37: public function getSubject(): Type
38: {
39: return $this->subject;
40: }
41:
42: public function getTarget(): Type
43: {
44: return $this->target;
45: }
46:
47: public function getIf(): Type
48: {
49: return $this->if;
50: }
51:
52: public function getElse(): Type
53: {
54: return $this->else;
55: }
56:
57: public function isNegated(): bool
58: {
59: return $this->negated;
60: }
61:
62: public function isSuperTypeOf(Type $type): IsSuperTypeOfResult
63: {
64: if ($type instanceof self) {
65: return $this->if->isSuperTypeOf($type->if)
66: ->and($this->else->isSuperTypeOf($type->else));
67: }
68:
69: return $this->isSuperTypeOfDefault($type);
70: }
71:
72: public function getReferencedClasses(): array
73: {
74: return array_merge(
75: $this->subject->getReferencedClasses(),
76: $this->target->getReferencedClasses(),
77: $this->if->getReferencedClasses(),
78: $this->else->getReferencedClasses(),
79: );
80: }
81:
82: public function getReferencedTemplateTypes(TemplateTypeVariance $positionVariance): array
83: {
84: return array_merge(
85: $this->subject->getReferencedTemplateTypes($positionVariance),
86: $this->target->getReferencedTemplateTypes($positionVariance),
87: $this->if->getReferencedTemplateTypes($positionVariance),
88: $this->else->getReferencedTemplateTypes($positionVariance),
89: );
90: }
91:
92: public function equals(Type $type): bool
93: {
94: return $type instanceof self
95: && $this->subject->equals($type->subject)
96: && $this->target->equals($type->target)
97: && $this->if->equals($type->if)
98: && $this->else->equals($type->else);
99: }
100:
101: public function describe(VerbosityLevel $level): string
102: {
103: return sprintf(
104: '(%s %s %s ? %s : %s)',
105: $this->subject->describe($level),
106: $this->negated ? 'is not' : 'is',
107: $this->target->describe($level),
108: $this->if->describe($level),
109: $this->else->describe($level),
110: );
111: }
112:
113: public function isResolvable(): bool
114: {
115: if (!TypeUtils::containsTemplateType($this->subject) && !TypeUtils::containsTemplateType($this->target)) {
116: return true;
117: }
118:
119: $isSuperType = $this->target->isSuperTypeOf($this->subject);
120:
121: return $isSuperType->yes() || $isSuperType->no();
122: }
123:
124: protected function getResult(): Type
125: {
126: $isSuperType = $this->target->isSuperTypeOf($this->subject);
127:
128: if ($isSuperType->yes()) {
129: return !$this->negated ? $this->getNormalizedIf() : $this->getNormalizedElse();
130: }
131:
132: if ($isSuperType->no()) {
133: return !$this->negated ? $this->getNormalizedElse() : $this->getNormalizedIf();
134: }
135:
136: return TypeCombinator::union(
137: $this->getNormalizedIf(),
138: $this->getNormalizedElse(),
139: );
140: }
141:
142: public function traverse(callable $cb): Type
143: {
144: $subject = $cb($this->subject);
145: $target = $cb($this->target);
146: $if = $cb($this->getNormalizedIf());
147: $else = $cb($this->getNormalizedElse());
148:
149: if (
150: $this->subject === $subject
151: && $this->target === $target
152: && $this->getNormalizedIf() === $if
153: && $this->getNormalizedElse() === $else
154: ) {
155: return $this;
156: }
157:
158: return new self($subject, $target, $if, $else, $this->negated);
159: }
160:
161: public function traverseSimultaneously(Type $right, callable $cb): Type
162: {
163: if (!$right instanceof self) {
164: return $this;
165: }
166:
167: $subject = $cb($this->subject, $right->subject);
168: $target = $cb($this->target, $right->target);
169: $if = $cb($this->getNormalizedIf(), $right->getNormalizedIf());
170: $else = $cb($this->getNormalizedElse(), $right->getNormalizedElse());
171:
172: if (
173: $this->subject === $subject
174: && $this->target === $target
175: && $this->getNormalizedIf() === $if
176: && $this->getNormalizedElse() === $else
177: ) {
178: return $this;
179: }
180:
181: return new self($subject, $target, $if, $else, $this->negated);
182: }
183:
184: public function toPhpDocNode(): TypeNode
185: {
186: return new ConditionalTypeNode(
187: $this->subject->toPhpDocNode(),
188: $this->target->toPhpDocNode(),
189: $this->if->toPhpDocNode(),
190: $this->else->toPhpDocNode(),
191: $this->negated,
192: );
193: }
194:
195: private function getNormalizedIf(): Type
196: {
197: return $this->normalizedIf ??= $this->narrowSubjectIn($this->if, !$this->negated);
198: }
199:
200: private function getNormalizedElse(): Type
201: {
202: return $this->normalizedElse ??= $this->narrowSubjectIn($this->else, $this->negated);
203: }
204:
205: /**
206: * Replaces the references to the subject in a branch with what the branch knows about
207: * it: `subject & target` where the condition holds, `subject ~ target` where it does not
208: * (see NarrowedSubjectType).
209: *
210: * A branch can only reference the subject while it is a template type. Once the subject
211: * resolves to a concrete type, an equal type inside the branch is not a mention of it -
212: * the `mixed` value type of an `array` branch has nothing to do with a `mixed` subject -
213: * so nothing is narrowed here; the references narrowed earlier follow the resolution on
214: * their own. Matching by identity instead would still hit such unrelated types whenever
215: * they happen to share an instance - the turbo extension memoizes TypeCombinator
216: * results, so each `int|false` resolved from a PHPDoc is the same object.
217: */
218: private function narrowSubjectIn(Type $branch, bool $conditionHolds): Type
219: {
220: if (!$this->subject instanceof TemplateType) {
221: return $branch;
222: }
223:
224: return NarrowedSubjectType::narrowReferences($branch, $this->subject, $this->target, $conditionHolds);
225: }
226:
227: }
228: