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: // Resolve the branches first, then convert — avoids infinite loop when
121: // the subject contains a ConditionalTypeForParameter with a colliding parameter name.
122: return $type->resolveWithSubject($subjectType, $getSubjectType);
123: }
124: }
125:
126: return $traverse($type);
127: });
128: }
129:
130: /**
131: * A condition on the same parameter nested in a branch sees the subject as the branch
132: * knows it: `subject & target` where this condition holds, `subject ~ target` where it
133: * does not. `($key is Model ? A : ($key is Arrayable ? B : C))` asks whether the
134: * non-Model part of the argument is Arrayable.
135: *
136: * @param callable(string): ?Type $getSubjectType
137: */
138: private function resolveWithSubject(Type $subject, callable $getSubjectType): Type
139: {
140: $target = self::resolveInType($this->target, $getSubjectType);
141: $getNarrowedSubjectType = fn (bool $conditionHolds): callable => fn (string $parameterName): ?Type => $parameterName === $this->parameterName
142: ? ($conditionHolds ? TypeCombinator::intersect($subject, $target) : TypeCombinator::remove($subject, $target))
143: : $getSubjectType($parameterName);
144:
145: return new ConditionalType(
146: $subject,
147: $target,
148: self::resolveInType($this->getNormalizedIf(), $getNarrowedSubjectType(!$this->negated)),
149: self::resolveInType($this->getNormalizedElse(), $getNarrowedSubjectType($this->negated)),
150: $this->negated,
151: );
152: }
153:
154: public function toConditional(Type $subject): Type
155: {
156: return new ConditionalType(
157: $subject,
158: $this->target,
159: $this->getNormalizedIf(),
160: $this->getNormalizedElse(),
161: $this->negated,
162: );
163: }
164:
165: public function isSuperTypeOf(Type $type): IsSuperTypeOfResult
166: {
167: if ($type instanceof self) {
168: return $this->if->isSuperTypeOf($type->if)
169: ->and($this->else->isSuperTypeOf($type->else));
170: }
171:
172: return $this->isSuperTypeOfDefault($type);
173: }
174:
175: public function getReferencedClasses(): array
176: {
177: return array_merge(
178: $this->target->getReferencedClasses(),
179: $this->if->getReferencedClasses(),
180: $this->else->getReferencedClasses(),
181: );
182: }
183:
184: public function getReferencedTemplateTypes(TemplateTypeVariance $positionVariance): array
185: {
186: return array_merge(
187: $this->target->getReferencedTemplateTypes($positionVariance),
188: $this->if->getReferencedTemplateTypes($positionVariance),
189: $this->else->getReferencedTemplateTypes($positionVariance),
190: );
191: }
192:
193: public function equals(Type $type): bool
194: {
195: return $type instanceof self
196: && $this->parameterName === $type->parameterName
197: && $this->target->equals($type->target)
198: && $this->if->equals($type->if)
199: && $this->else->equals($type->else);
200: }
201:
202: public function describe(VerbosityLevel $level): string
203: {
204: return sprintf(
205: '(%s %s %s ? %s : %s)',
206: $this->parameterName,
207: $this->negated ? 'is not' : 'is',
208: $this->target->describe($level),
209: $this->if->describe($level),
210: $this->else->describe($level),
211: );
212: }
213:
214: public function isResolvable(): bool
215: {
216: return false;
217: }
218:
219: protected function getResult(): Type
220: {
221: return TypeCombinator::union($this->getNormalizedIf(), $this->getNormalizedElse());
222: }
223:
224: public function traverse(callable $cb): Type
225: {
226: $target = $cb($this->target);
227: $if = $cb($this->getNormalizedIf());
228: $else = $cb($this->getNormalizedElse());
229:
230: if (
231: $this->target === $target
232: && $this->getNormalizedIf() === $if
233: && $this->getNormalizedElse() === $else
234: ) {
235: return $this;
236: }
237:
238: return new self($this->parameterName, $target, $if, $else, $this->negated);
239: }
240:
241: public function traverseSimultaneously(Type $right, callable $cb): Type
242: {
243: if (!$right instanceof self) {
244: return $this;
245: }
246:
247: $target = $cb($this->target, $right->target);
248: $if = $cb($this->getNormalizedIf(), $right->getNormalizedIf());
249: $else = $cb($this->getNormalizedElse(), $right->getNormalizedElse());
250:
251: if (
252: $this->target === $target
253: && $this->getNormalizedIf() === $if
254: && $this->getNormalizedElse() === $else
255: ) {
256: return $this;
257: }
258:
259: return new self($this->parameterName, $target, $if, $else, $this->negated);
260: }
261:
262: private function getNormalizedIf(): Type
263: {
264: return $this->normalizedIf ??= $this->parameterTemplateType === null
265: ? $this->if
266: : NarrowedSubjectType::narrowReferences($this->if, $this->parameterTemplateType, $this->target, !$this->negated);
267: }
268:
269: private function getNormalizedElse(): Type
270: {
271: return $this->normalizedElse ??= $this->parameterTemplateType === null
272: ? $this->else
273: : NarrowedSubjectType::narrowReferences($this->else, $this->parameterTemplateType, $this->target, $this->negated);
274: }
275:
276: public function toPhpDocNode(): TypeNode
277: {
278: return new ConditionalTypeForParameterNode(
279: $this->parameterName,
280: $this->target->toPhpDocNode(),
281: $this->if->toPhpDocNode(),
282: $this->else->toPhpDocNode(),
283: $this->negated,
284: );
285: }
286:
287: }
288: