1: <?php declare(strict_types = 1);
2:
3: namespace PHPStan\Type\Generic;
4:
5: use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode;
6: use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
7: use PHPStan\PhpDocParser\Ast\Type\TypeNode;
8: use PHPStan\Reflection\ClassMemberAccessAnswerer;
9: use PHPStan\Reflection\ClassReflection;
10: use PHPStan\Reflection\ExtendedMethodReflection;
11: use PHPStan\Reflection\PropertyReflection;
12: use PHPStan\Reflection\ReflectionProviderStaticAccessor;
13: use PHPStan\Reflection\Type\UnresolvedMethodPrototypeReflection;
14: use PHPStan\Reflection\Type\UnresolvedPropertyPrototypeReflection;
15: use PHPStan\ShouldNotHappenException;
16: use PHPStan\TrinaryLogic;
17: use PHPStan\Type\AcceptsResult;
18: use PHPStan\Type\CompoundType;
19: use PHPStan\Type\ErrorType;
20: use PHPStan\Type\IntersectionType;
21: use PHPStan\Type\ObjectType;
22: use PHPStan\Type\Type;
23: use PHPStan\Type\TypeWithClassName;
24: use PHPStan\Type\UnionType;
25: use PHPStan\Type\VerbosityLevel;
26: use function array_map;
27: use function count;
28: use function implode;
29: use function sprintf;
30:
31: /** @api */
32: class GenericObjectType extends ObjectType
33: {
34:
35: /**
36: * @api
37: * @param array<int, Type> $types
38: * @param array<int, TemplateTypeVariance> $variances
39: */
40: public function __construct(
41: string $mainType,
42: private array $types,
43: ?Type $subtractedType = null,
44: private ?ClassReflection $classReflection = null,
45: private array $variances = [],
46: )
47: {
48: parent::__construct($mainType, $subtractedType, $classReflection);
49: }
50:
51: public function describe(VerbosityLevel $level): string
52: {
53: return sprintf(
54: '%s<%s>',
55: parent::describe($level),
56: implode(', ', array_map(
57: static fn (Type $type, ?TemplateTypeVariance $variance = null): string => TypeProjectionHelper::describe($type, $variance, $level),
58: $this->types,
59: $this->variances,
60: )),
61: );
62: }
63:
64: public function equals(Type $type): bool
65: {
66: if (!$type instanceof self) {
67: return false;
68: }
69:
70: if (!parent::equals($type)) {
71: return false;
72: }
73:
74: if (count($this->types) !== count($type->types)) {
75: return false;
76: }
77:
78: foreach ($this->types as $i => $genericType) {
79: $otherGenericType = $type->types[$i];
80: if (!$genericType->equals($otherGenericType)) {
81: return false;
82: }
83:
84: $variance = $this->variances[$i] ?? TemplateTypeVariance::createInvariant();
85: $otherVariance = $type->variances[$i] ?? TemplateTypeVariance::createInvariant();
86: if (!$variance->equals($otherVariance)) {
87: return false;
88: }
89: }
90:
91: return true;
92: }
93:
94: /**
95: * @return string[]
96: */
97: public function getReferencedClasses(): array
98: {
99: $classes = parent::getReferencedClasses();
100: foreach ($this->types as $type) {
101: foreach ($type->getReferencedClasses() as $referencedClass) {
102: $classes[] = $referencedClass;
103: }
104: }
105:
106: return $classes;
107: }
108:
109: /** @return array<int, Type> */
110: public function getTypes(): array
111: {
112: return $this->types;
113: }
114:
115: /** @return array<int, TemplateTypeVariance> */
116: public function getVariances(): array
117: {
118: return $this->variances;
119: }
120:
121: public function accepts(Type $type, bool $strictTypes): TrinaryLogic
122: {
123: return $this->acceptsWithReason($type, $strictTypes)->result;
124: }
125:
126: public function acceptsWithReason(Type $type, bool $strictTypes): AcceptsResult
127: {
128: if ($type instanceof CompoundType) {
129: return $type->isAcceptedWithReasonBy($this, $strictTypes);
130: }
131:
132: return $this->isSuperTypeOfInternal($type, true);
133: }
134:
135: public function isSuperTypeOf(Type $type): TrinaryLogic
136: {
137: if ($type instanceof CompoundType) {
138: return $type->isSubTypeOf($this);
139: }
140:
141: return $this->isSuperTypeOfInternal($type, false)->result;
142: }
143:
144: private function isSuperTypeOfInternal(Type $type, bool $acceptsContext): AcceptsResult
145: {
146: $nakedSuperTypeOf = new AcceptsResult(parent::isSuperTypeOf($type), []);
147: if ($nakedSuperTypeOf->no()) {
148: return $nakedSuperTypeOf;
149: }
150:
151: if (!$type instanceof ObjectType) {
152: return $nakedSuperTypeOf;
153: }
154:
155: $ancestor = $type->getAncestorWithClassName($this->getClassName());
156: if ($ancestor === null) {
157: return $nakedSuperTypeOf;
158: }
159: if (!$ancestor instanceof self) {
160: if ($acceptsContext) {
161: return $nakedSuperTypeOf;
162: }
163:
164: return $nakedSuperTypeOf->and(AcceptsResult::createMaybe());
165: }
166:
167: if (count($this->types) !== count($ancestor->types)) {
168: return AcceptsResult::createNo();
169: }
170:
171: $classReflection = $this->getClassReflection();
172: if ($classReflection === null) {
173: return $nakedSuperTypeOf;
174: }
175:
176: $typeList = $classReflection->typeMapToList($classReflection->getTemplateTypeMap());
177: $results = [];
178: foreach ($typeList as $i => $templateType) {
179: if (!isset($ancestor->types[$i])) {
180: continue;
181: }
182: if (!isset($this->types[$i])) {
183: continue;
184: }
185: if ($templateType instanceof ErrorType) {
186: continue;
187: }
188: if (!$templateType instanceof TemplateType) {
189: throw new ShouldNotHappenException();
190: }
191:
192: $thisVariance = $this->variances[$i] ?? TemplateTypeVariance::createInvariant();
193: $ancestorVariance = $ancestor->variances[$i] ?? TemplateTypeVariance::createInvariant();
194: if (!$thisVariance->invariant()) {
195: $results[] = $thisVariance->isValidVarianceWithReason($templateType, $this->types[$i], $ancestor->types[$i]);
196: } else {
197: $results[] = $templateType->isValidVarianceWithReason($this->types[$i], $ancestor->types[$i]);
198: }
199:
200: $results[] = AcceptsResult::createFromBoolean($thisVariance->validPosition($ancestorVariance));
201: }
202:
203: if (count($results) === 0) {
204: return $nakedSuperTypeOf;
205: }
206:
207: $result = AcceptsResult::createYes();
208: foreach ($results as $innerResult) {
209: $result = $result->and($innerResult);
210: }
211:
212: return $result;
213: }
214:
215: public function getClassReflection(): ?ClassReflection
216: {
217: if ($this->classReflection !== null) {
218: return $this->classReflection;
219: }
220:
221: $reflectionProvider = ReflectionProviderStaticAccessor::getInstance();
222: if (!$reflectionProvider->hasClass($this->getClassName())) {
223: return null;
224: }
225:
226: return $this->classReflection = $reflectionProvider->getClass($this->getClassName())
227: ->withTypes($this->types)
228: ->withVariances($this->variances);
229: }
230:
231: public function getProperty(string $propertyName, ClassMemberAccessAnswerer $scope): PropertyReflection
232: {
233: return $this->getUnresolvedPropertyPrototype($propertyName, $scope)->getTransformedProperty();
234: }
235:
236: public function getUnresolvedPropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection
237: {
238: $prototype = parent::getUnresolvedPropertyPrototype($propertyName, $scope);
239:
240: return $prototype->doNotResolveTemplateTypeMapToBounds();
241: }
242:
243: public function getMethod(string $methodName, ClassMemberAccessAnswerer $scope): ExtendedMethodReflection
244: {
245: return $this->getUnresolvedMethodPrototype($methodName, $scope)->getTransformedMethod();
246: }
247:
248: public function getUnresolvedMethodPrototype(string $methodName, ClassMemberAccessAnswerer $scope): UnresolvedMethodPrototypeReflection
249: {
250: $prototype = parent::getUnresolvedMethodPrototype($methodName, $scope);
251:
252: return $prototype->doNotResolveTemplateTypeMapToBounds();
253: }
254:
255: public function inferTemplateTypes(Type $receivedType): TemplateTypeMap
256: {
257: if ($receivedType instanceof UnionType || $receivedType instanceof IntersectionType) {
258: return $receivedType->inferTemplateTypesOn($this);
259: }
260:
261: if (!$receivedType instanceof TypeWithClassName) {
262: return TemplateTypeMap::createEmpty();
263: }
264:
265: $ancestor = $receivedType->getAncestorWithClassName($this->getClassName());
266:
267: if ($ancestor === null) {
268: return TemplateTypeMap::createEmpty();
269: }
270: $ancestorClassReflection = $ancestor->getClassReflection();
271: if ($ancestorClassReflection === null) {
272: return TemplateTypeMap::createEmpty();
273: }
274:
275: $otherTypes = $ancestorClassReflection->typeMapToList($ancestorClassReflection->getActiveTemplateTypeMap());
276: $typeMap = TemplateTypeMap::createEmpty();
277:
278: foreach ($this->getTypes() as $i => $type) {
279: $other = $otherTypes[$i] ?? new ErrorType();
280: $typeMap = $typeMap->union($type->inferTemplateTypes($other));
281: }
282:
283: return $typeMap;
284: }
285:
286: public function getReferencedTemplateTypes(TemplateTypeVariance $positionVariance): array
287: {
288: $classReflection = $this->getClassReflection();
289: if ($classReflection !== null) {
290: $typeList = $classReflection->typeMapToList($classReflection->getTemplateTypeMap());
291: } else {
292: $typeList = [];
293: }
294:
295: $references = [];
296:
297: foreach ($this->types as $i => $type) {
298: $effectiveVariance = $this->variances[$i] ?? TemplateTypeVariance::createInvariant();
299: if ($effectiveVariance->invariant() && isset($typeList[$i]) && $typeList[$i] instanceof TemplateType) {
300: $effectiveVariance = $typeList[$i]->getVariance();
301: }
302:
303: $variance = $positionVariance->compose($effectiveVariance);
304: foreach ($type->getReferencedTemplateTypes($variance) as $reference) {
305: $references[] = $reference;
306: }
307: }
308:
309: return $references;
310: }
311:
312: public function traverse(callable $cb): Type
313: {
314: $subtractedType = $this->getSubtractedType() !== null ? $cb($this->getSubtractedType()) : null;
315:
316: $typesChanged = false;
317: $types = [];
318: foreach ($this->types as $type) {
319: $newType = $cb($type);
320: $types[] = $newType;
321: if ($newType === $type) {
322: continue;
323: }
324:
325: $typesChanged = true;
326: }
327:
328: if ($subtractedType !== $this->getSubtractedType() || $typesChanged) {
329: return $this->recreate($this->getClassName(), $types, $subtractedType, $this->variances);
330: }
331:
332: return $this;
333: }
334:
335: public function traverseSimultaneously(Type $right, callable $cb): Type
336: {
337: if (!$right instanceof TypeWithClassName) {
338: return $this;
339: }
340:
341: $ancestor = $right->getAncestorWithClassName($this->getClassName());
342: if (!$ancestor instanceof self) {
343: return $this;
344: }
345:
346: if (count($this->types) !== count($ancestor->types)) {
347: return $this;
348: }
349:
350: $typesChanged = false;
351: $types = [];
352: foreach ($this->types as $i => $leftType) {
353: $rightType = $ancestor->types[$i];
354: $newType = $cb($leftType, $rightType);
355: $types[] = $newType;
356: if ($newType === $leftType) {
357: continue;
358: }
359:
360: $typesChanged = true;
361: }
362:
363: if ($typesChanged) {
364: return $this->recreate($this->getClassName(), $types, null);
365: }
366:
367: return $this;
368: }
369:
370: /**
371: * @param Type[] $types
372: * @param TemplateTypeVariance[] $variances
373: */
374: protected function recreate(string $className, array $types, ?Type $subtractedType, array $variances = []): self
375: {
376: return new self(
377: $className,
378: $types,
379: $subtractedType,
380: null,
381: $variances,
382: );
383: }
384:
385: public function changeSubtractedType(?Type $subtractedType): Type
386: {
387: return new self($this->getClassName(), $this->types, $subtractedType, null, $this->variances);
388: }
389:
390: public function toPhpDocNode(): TypeNode
391: {
392: /** @var IdentifierTypeNode $parent */
393: $parent = parent::toPhpDocNode();
394: return new GenericTypeNode(
395: $parent,
396: array_map(static fn (Type $type) => $type->toPhpDocNode(), $this->types),
397: array_map(static fn (TemplateTypeVariance $variance) => $variance->toPhpDocNodeVariance(), $this->variances),
398: );
399: }
400:
401: /**
402: * @param mixed[] $properties
403: */
404: public static function __set_state(array $properties): Type
405: {
406: return new self(
407: $properties['className'],
408: $properties['types'],
409: $properties['subtractedType'] ?? null,
410: null,
411: $properties['variances'] ?? [],
412: );
413: }
414:
415: }
416: