1: <?php declare(strict_types = 1);
2:
3: namespace PHPStan\Type;
4:
5: use PHPStan\PhpDocParser\Ast\Type\ConditionalTypeForParameterNode;
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/ConditionalTypeForParameter.cpp')]
17: final class ConditionalTypeForParameter implements CompoundType, LateResolvableType
18: {
19:
20: use LateResolvableTypeTrait;
21: use NonGeneralizableTypeTrait;
22:
23: private ?TemplateType $parameterTemplateType = null;
24:
25: private ?Type $normalizedIf = null;
26:
27: private ?Type $normalizedElse = null;
28:
29: public function __construct(
30: private string $parameterName,
31: private Type $target,
32: private Type $if,
33: private Type $else,
34: private bool $negated,
35: )
36: {
37: }
38:
39: public function getParameterName(): string
40: {
41: return $this->parameterName;
42: }
43:
44: public function getTarget(): Type
45: {
46: return $this->target;
47: }
48:
49: public function getIf(): Type
50: {
51: return $this->if;
52: }
53:
54: public function getElse(): Type
55: {
56: return $this->else;
57: }
58:
59: public function isNegated(): bool
60: {
61: return $this->negated;
62: }
63:
64: public function changeParameterName(string $parameterName): self
65: {
66: $type = new self(
67: $parameterName,
68: $this->target,
69: $this->if,
70: $this->else,
71: $this->negated,
72: );
73: $type->parameterTemplateType = $this->parameterTemplateType;
74:
75: return $type;
76: }
77:
78: /**
79: * Narrows the references to the template type the parameter is declared with along with
80: * the parameter: `@param T $param` makes `($param is X ? A : B)` narrow T to `T & X` in A
81: * and to `T ~ X` in B, the way `(T is X ? A : B)` does (see NarrowedSubjectType). Only
82: * sound when nothing but the parameter binds T.
83: */
84: public function narrowTemplateType(TemplateType $templateType): self
85: {
86: $type = new self(
87: $this->parameterName,
88: $this->target,
89: $this->if,
90: $this->else,
91: $this->negated,
92: );
93: $type->parameterTemplateType = $templateType;
94:
95: return $type;
96: }
97:
98: /**
99: * Replaces every ConditionalTypeForParameter inside $type with the ConditionalType on the
100: * subject its parameter resolves to. $getSubjectType is called with the parameter name
101: * including the leading `$`; returning null leaves that conditional unresolved.
102: *
103: * Shared by everything that resolves a declared conditional type against concrete
104: * subjects: ResolvedFunctionVariant (`@return`, `@param`, `@param-out`,
105: * `@param-closure-this`), TypeSpecifier (`@phpstan-assert`) and ConditionalTypeResolver
106: * (`@throws`, `@phpstan-self-out`).
107: *
108: * @param callable(string): ?Type $getSubjectType
109: */
110: public static function resolveInType(Type $type, callable $getSubjectType): Type
111: {
112: if (!$type->hasTemplateOrLateResolvableType()) {
113: return $type;
114: }
115:
116: return TypeTraverser::map($type, static function (Type $type, callable $traverse) use ($getSubjectType): Type {
117: if ($type instanceof self) {
118: $subjectType = $getSubjectType($type->getParameterName());
119: if ($subjectType !== null) {
120: // Traverse children first, then convert — avoids infinite loop when
121: // the subject contains a ConditionalTypeForParameter with a colliding parameter name.
122: $type = $traverse($type);
123: if ($type instanceof self) {
124: return $type->toConditional($subjectType);
125: }
126:
127: return $type;
128: }
129: }
130:
131: return $traverse($type);
132: });
133: }
134:
135: public function toConditional(Type $subject): Type
136: {
137: return new ConditionalType(
138: $subject,
139: $this->target,
140: $this->getNormalizedIf(),
141: $this->getNormalizedElse(),
142: $this->negated,
143: );
144: }
145:
146: public function isSuperTypeOf(Type $type): IsSuperTypeOfResult
147: {
148: if ($type instanceof self) {
149: return $this->if->isSuperTypeOf($type->if)
150: ->and($this->else->isSuperTypeOf($type->else));
151: }
152:
153: return $this->isSuperTypeOfDefault($type);
154: }
155:
156: public function getReferencedClasses(): array
157: {
158: return array_merge(
159: $this->target->getReferencedClasses(),
160: $this->if->getReferencedClasses(),
161: $this->else->getReferencedClasses(),
162: );
163: }
164:
165: public function getReferencedTemplateTypes(TemplateTypeVariance $positionVariance): array
166: {
167: return array_merge(
168: $this->target->getReferencedTemplateTypes($positionVariance),
169: $this->if->getReferencedTemplateTypes($positionVariance),
170: $this->else->getReferencedTemplateTypes($positionVariance),
171: );
172: }
173:
174: public function equals(Type $type): bool
175: {
176: return $type instanceof self
177: && $this->parameterName === $type->parameterName
178: && $this->target->equals($type->target)
179: && $this->if->equals($type->if)
180: && $this->else->equals($type->else);
181: }
182:
183: public function describe(VerbosityLevel $level): string
184: {
185: return sprintf(
186: '(%s %s %s ? %s : %s)',
187: $this->parameterName,
188: $this->negated ? 'is not' : 'is',
189: $this->target->describe($level),
190: $this->if->describe($level),
191: $this->else->describe($level),
192: );
193: }
194:
195: public function isResolvable(): bool
196: {
197: return false;
198: }
199:
200: protected function getResult(): Type
201: {
202: return TypeCombinator::union($this->getNormalizedIf(), $this->getNormalizedElse());
203: }
204:
205: public function traverse(callable $cb): Type
206: {
207: $target = $cb($this->target);
208: $if = $cb($this->getNormalizedIf());
209: $else = $cb($this->getNormalizedElse());
210:
211: if (
212: $this->target === $target
213: && $this->getNormalizedIf() === $if
214: && $this->getNormalizedElse() === $else
215: ) {
216: return $this;
217: }
218:
219: return new self($this->parameterName, $target, $if, $else, $this->negated);
220: }
221:
222: public function traverseSimultaneously(Type $right, callable $cb): Type
223: {
224: if (!$right instanceof self) {
225: return $this;
226: }
227:
228: $target = $cb($this->target, $right->target);
229: $if = $cb($this->getNormalizedIf(), $right->getNormalizedIf());
230: $else = $cb($this->getNormalizedElse(), $right->getNormalizedElse());
231:
232: if (
233: $this->target === $target
234: && $this->getNormalizedIf() === $if
235: && $this->getNormalizedElse() === $else
236: ) {
237: return $this;
238: }
239:
240: return new self($this->parameterName, $target, $if, $else, $this->negated);
241: }
242:
243: private function getNormalizedIf(): Type
244: {
245: return $this->normalizedIf ??= $this->parameterTemplateType === null
246: ? $this->if
247: : NarrowedSubjectType::narrowReferences($this->if, $this->parameterTemplateType, $this->target, !$this->negated);
248: }
249:
250: private function getNormalizedElse(): Type
251: {
252: return $this->normalizedElse ??= $this->parameterTemplateType === null
253: ? $this->else
254: : NarrowedSubjectType::narrowReferences($this->else, $this->parameterTemplateType, $this->target, $this->negated);
255: }
256:
257: public function toPhpDocNode(): TypeNode
258: {
259: return new ConditionalTypeForParameterNode(
260: $this->parameterName,
261: $this->target->toPhpDocNode(),
262: $this->if->toPhpDocNode(),
263: $this->else->toPhpDocNode(),
264: $this->negated,
265: );
266: }
267:
268: }
269: