1: <?php declare(strict_types = 1);
2:
3: namespace PHPStan\Type\Generic;
4:
5: use PHPStan\Reflection\ClassMemberAccessAnswerer;
6: use PHPStan\Reflection\ClassReflection;
7: use PHPStan\Reflection\MethodReflection;
8: use PHPStan\Reflection\PropertyReflection;
9: use PHPStan\Reflection\ReflectionProviderStaticAccessor;
10: use PHPStan\Reflection\Type\UnresolvedMethodPrototypeReflection;
11: use PHPStan\Reflection\Type\UnresolvedPropertyPrototypeReflection;
12: use PHPStan\ShouldNotHappenException;
13: use PHPStan\TrinaryLogic;
14: use PHPStan\Type\CompoundType;
15: use PHPStan\Type\ErrorType;
16: use PHPStan\Type\IntersectionType;
17: use PHPStan\Type\ObjectType;
18: use PHPStan\Type\Type;
19: use PHPStan\Type\TypeWithClassName;
20: use PHPStan\Type\UnionType;
21: use PHPStan\Type\VerbosityLevel;
22: use function array_map;
23: use function count;
24: use function implode;
25: use function sprintf;
26:
27: /** @api */
28: class GenericObjectType extends ObjectType
29: {
30:
31: /**
32: * @api
33: * @param array<int, Type> $types
34: */
35: public function __construct(
36: string $mainType,
37: private array $types,
38: ?Type $subtractedType = null,
39: private ?ClassReflection $classReflection = null,
40: )
41: {
42: parent::__construct($mainType, $subtractedType, $classReflection);
43: }
44:
45: public function describe(VerbosityLevel $level): string
46: {
47: return sprintf(
48: '%s<%s>',
49: parent::describe($level),
50: implode(', ', array_map(static fn (Type $type): string => $type->describe($level), $this->types)),
51: );
52: }
53:
54: public function equals(Type $type): bool
55: {
56: if (!$type instanceof self) {
57: return false;
58: }
59:
60: if (!parent::equals($type)) {
61: return false;
62: }
63:
64: if (count($this->types) !== count($type->types)) {
65: return false;
66: }
67:
68: foreach ($this->types as $i => $genericType) {
69: $otherGenericType = $type->types[$i];
70: if (!$genericType->equals($otherGenericType)) {
71: return false;
72: }
73: }
74:
75: return true;
76: }
77:
78: /**
79: * @return string[]
80: */
81: public function getReferencedClasses(): array
82: {
83: $classes = parent::getReferencedClasses();
84: foreach ($this->types as $type) {
85: foreach ($type->getReferencedClasses() as $referencedClass) {
86: $classes[] = $referencedClass;
87: }
88: }
89:
90: return $classes;
91: }
92:
93: /** @return array<int, Type> */
94: public function getTypes(): array
95: {
96: return $this->types;
97: }
98:
99: public function accepts(Type $type, bool $strictTypes): TrinaryLogic
100: {
101: if ($type instanceof CompoundType) {
102: return $type->isAcceptedBy($this, $strictTypes);
103: }
104:
105: return $this->isSuperTypeOfInternal($type, true);
106: }
107:
108: public function isSuperTypeOf(Type $type): TrinaryLogic
109: {
110: if ($type instanceof CompoundType) {
111: return $type->isSubTypeOf($this);
112: }
113:
114: return $this->isSuperTypeOfInternal($type, false);
115: }
116:
117: private function isSuperTypeOfInternal(Type $type, bool $acceptsContext): TrinaryLogic
118: {
119: $nakedSuperTypeOf = parent::isSuperTypeOf($type);
120: if ($nakedSuperTypeOf->no()) {
121: return $nakedSuperTypeOf;
122: }
123:
124: if (!$type instanceof ObjectType) {
125: return $nakedSuperTypeOf;
126: }
127:
128: $ancestor = $type->getAncestorWithClassName($this->getClassName());
129: if ($ancestor === null) {
130: return $nakedSuperTypeOf;
131: }
132: if (!$ancestor instanceof self) {
133: if ($acceptsContext) {
134: return $nakedSuperTypeOf;
135: }
136:
137: return $nakedSuperTypeOf->and(TrinaryLogic::createMaybe());
138: }
139:
140: if (count($this->types) !== count($ancestor->types)) {
141: return TrinaryLogic::createNo();
142: }
143:
144: $classReflection = $this->getClassReflection();
145: if ($classReflection === null) {
146: return $nakedSuperTypeOf;
147: }
148:
149: $typeList = $classReflection->typeMapToList($classReflection->getTemplateTypeMap());
150: $results = [];
151: foreach ($typeList as $i => $templateType) {
152: if (!isset($ancestor->types[$i])) {
153: continue;
154: }
155: if (!isset($this->types[$i])) {
156: continue;
157: }
158: if ($templateType instanceof ErrorType) {
159: continue;
160: }
161: if (!$templateType instanceof TemplateType) {
162: throw new ShouldNotHappenException();
163: }
164:
165: $results[] = $templateType->isValidVariance($this->types[$i], $ancestor->types[$i]);
166: }
167:
168: if (count($results) === 0) {
169: return $nakedSuperTypeOf;
170: }
171:
172: return $nakedSuperTypeOf->and(...$results);
173: }
174:
175: public function getClassReflection(): ?ClassReflection
176: {
177: if ($this->classReflection !== null) {
178: return $this->classReflection;
179: }
180:
181: $reflectionProvider = ReflectionProviderStaticAccessor::getInstance();
182: if (!$reflectionProvider->hasClass($this->getClassName())) {
183: return null;
184: }
185:
186: return $this->classReflection = $reflectionProvider->getClass($this->getClassName())->withTypes($this->types);
187: }
188:
189: public function getProperty(string $propertyName, ClassMemberAccessAnswerer $scope): PropertyReflection
190: {
191: return $this->getUnresolvedPropertyPrototype($propertyName, $scope)->getTransformedProperty();
192: }
193:
194: public function getUnresolvedPropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection
195: {
196: $prototype = parent::getUnresolvedPropertyPrototype($propertyName, $scope);
197:
198: return $prototype->doNotResolveTemplateTypeMapToBounds();
199: }
200:
201: public function getMethod(string $methodName, ClassMemberAccessAnswerer $scope): MethodReflection
202: {
203: return $this->getUnresolvedMethodPrototype($methodName, $scope)->getTransformedMethod();
204: }
205:
206: public function getUnresolvedMethodPrototype(string $methodName, ClassMemberAccessAnswerer $scope): UnresolvedMethodPrototypeReflection
207: {
208: $prototype = parent::getUnresolvedMethodPrototype($methodName, $scope);
209:
210: return $prototype->doNotResolveTemplateTypeMapToBounds();
211: }
212:
213: public function inferTemplateTypes(Type $receivedType): TemplateTypeMap
214: {
215: if ($receivedType instanceof UnionType || $receivedType instanceof IntersectionType) {
216: return $receivedType->inferTemplateTypesOn($this);
217: }
218:
219: if (!$receivedType instanceof TypeWithClassName) {
220: return TemplateTypeMap::createEmpty();
221: }
222:
223: $ancestor = $receivedType->getAncestorWithClassName($this->getClassName());
224:
225: if ($ancestor === null) {
226: return TemplateTypeMap::createEmpty();
227: }
228: $ancestorClassReflection = $ancestor->getClassReflection();
229: if ($ancestorClassReflection === null) {
230: return TemplateTypeMap::createEmpty();
231: }
232:
233: $otherTypes = $ancestorClassReflection->typeMapToList($ancestorClassReflection->getActiveTemplateTypeMap());
234: $typeMap = TemplateTypeMap::createEmpty();
235:
236: foreach ($this->getTypes() as $i => $type) {
237: $other = $otherTypes[$i] ?? new ErrorType();
238: $typeMap = $typeMap->union($type->inferTemplateTypes($other));
239: }
240:
241: return $typeMap;
242: }
243:
244: public function getReferencedTemplateTypes(TemplateTypeVariance $positionVariance): array
245: {
246: $classReflection = $this->getClassReflection();
247: if ($classReflection !== null) {
248: $typeList = $classReflection->typeMapToList($classReflection->getTemplateTypeMap());
249: } else {
250: $typeList = [];
251: }
252:
253: $references = [];
254:
255: foreach ($this->types as $i => $type) {
256: $variance = $positionVariance->compose(
257: isset($typeList[$i]) && $typeList[$i] instanceof TemplateType
258: ? $typeList[$i]->getVariance()
259: : TemplateTypeVariance::createInvariant(),
260: );
261: foreach ($type->getReferencedTemplateTypes($variance) as $reference) {
262: $references[] = $reference;
263: }
264: }
265:
266: return $references;
267: }
268:
269: public function traverse(callable $cb): Type
270: {
271: $subtractedType = $this->getSubtractedType() !== null ? $cb($this->getSubtractedType()) : null;
272:
273: $typesChanged = false;
274: $types = [];
275: foreach ($this->types as $type) {
276: $newType = $cb($type);
277: $types[] = $newType;
278: if ($newType === $type) {
279: continue;
280: }
281:
282: $typesChanged = true;
283: }
284:
285: if ($subtractedType !== $this->getSubtractedType() || $typesChanged) {
286: return $this->recreate($this->getClassName(), $types, $subtractedType);
287: }
288:
289: return $this;
290: }
291:
292: /**
293: * @param Type[] $types
294: */
295: protected function recreate(string $className, array $types, ?Type $subtractedType): self
296: {
297: return new self(
298: $className,
299: $types,
300: $subtractedType,
301: );
302: }
303:
304: public function changeSubtractedType(?Type $subtractedType): Type
305: {
306: return new self($this->getClassName(), $this->types, $subtractedType);
307: }
308:
309: /**
310: * @param mixed[] $properties
311: */
312: public static function __set_state(array $properties): Type
313: {
314: return new self(
315: $properties['className'],
316: $properties['types'],
317: $properties['subtractedType'] ?? null,
318: );
319: }
320:
321: }
322: