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