1: <?php declare(strict_types = 1);
2:
3: namespace PHPStan\Type;
4:
5: use PHPStan\Analyser\OutOfClassScope;
6: use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprStringNode;
7: use PHPStan\PhpDocParser\Ast\Type\ConstTypeNode;
8: use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
9: use PHPStan\PhpDocParser\Ast\Type\ObjectShapeItemNode;
10: use PHPStan\PhpDocParser\Ast\Type\ObjectShapeNode;
11: use PHPStan\PhpDocParser\Ast\Type\TypeNode;
12: use PHPStan\Reflection\ClassMemberAccessAnswerer;
13: use PHPStan\Reflection\ExtendedPropertyReflection;
14: use PHPStan\Reflection\MissingPropertyFromReflectionException;
15: use PHPStan\Reflection\Php\UniversalObjectCratesClassReflectionExtension;
16: use PHPStan\Reflection\ReflectionProviderStaticAccessor;
17: use PHPStan\Reflection\Type\CallbackUnresolvedPropertyPrototypeReflection;
18: use PHPStan\Reflection\Type\UnresolvedPropertyPrototypeReflection;
19: use PHPStan\ShouldNotHappenException;
20: use PHPStan\TrinaryLogic;
21: use PHPStan\Type\Accessory\HasPropertyType;
22: use PHPStan\Type\Constant\ConstantArrayType;
23: use PHPStan\Type\Constant\ConstantStringType;
24: use PHPStan\Type\Enum\EnumCaseObjectType;
25: use PHPStan\Type\Generic\TemplateTypeMap;
26: use PHPStan\Type\Generic\TemplateTypeVariance;
27: use PHPStan\Type\Traits\NonGeneralizableTypeTrait;
28: use PHPStan\Type\Traits\ObjectTypeTrait;
29: use PHPStan\Type\Traits\UndecidedComparisonTypeTrait;
30: use function array_filter;
31: use function array_key_exists;
32: use function array_values;
33: use function count;
34: use function implode;
35: use function in_array;
36: use function sprintf;
37:
38: /** @api */
39: class ObjectShapeType implements Type
40: {
41:
42: use ObjectTypeTrait;
43: use UndecidedComparisonTypeTrait;
44: use NonGeneralizableTypeTrait;
45:
46: /**
47: * @api
48: * @param array<int|string, Type> $properties
49: * @param list<int|string> $optionalProperties
50: */
51: public function __construct(private array $properties, private array $optionalProperties)
52: {
53: }
54:
55: /**
56: * @return array<int|string, Type>
57: */
58: public function getProperties(): array
59: {
60: return $this->properties;
61: }
62:
63: /**
64: * @return list<int|string>
65: */
66: public function getOptionalProperties(): array
67: {
68: return $this->optionalProperties;
69: }
70:
71: public function getReferencedClasses(): array
72: {
73: $classes = [];
74: foreach ($this->properties as $propertyType) {
75: foreach ($propertyType->getReferencedClasses() as $referencedClass) {
76: $classes[] = $referencedClass;
77: }
78: }
79:
80: return $classes;
81: }
82:
83: public function getObjectClassNames(): array
84: {
85: return [];
86: }
87:
88: public function getObjectClassReflections(): array
89: {
90: return [];
91: }
92:
93: public function hasProperty(string $propertyName): TrinaryLogic
94: {
95: return $this->hasInstanceProperty($propertyName);
96: }
97:
98: public function getProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection
99: {
100: return $this->getInstanceProperty($propertyName, $scope);
101: }
102:
103: public function getUnresolvedPropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection
104: {
105: return $this->getUnresolvedInstancePropertyPrototype($propertyName, $scope);
106: }
107:
108: public function hasInstanceProperty(string $propertyName): TrinaryLogic
109: {
110: if (!array_key_exists($propertyName, $this->properties)) {
111: return TrinaryLogic::createNo();
112: }
113:
114: if (in_array($propertyName, $this->optionalProperties, true)) {
115: return TrinaryLogic::createMaybe();
116: }
117:
118: return TrinaryLogic::createYes();
119: }
120:
121: public function getInstanceProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection
122: {
123: return $this->getUnresolvedInstancePropertyPrototype($propertyName, $scope)->getTransformedProperty();
124: }
125:
126: public function getUnresolvedInstancePropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection
127: {
128: if (!array_key_exists($propertyName, $this->properties)) {
129: throw new ShouldNotHappenException();
130: }
131:
132: $property = new ObjectShapePropertyReflection($propertyName, $this->properties[$propertyName]);
133: return new CallbackUnresolvedPropertyPrototypeReflection(
134: $property,
135: $property->getDeclaringClass(),
136: false,
137: static fn (Type $type): Type => $type,
138: );
139: }
140:
141: public function hasStaticProperty(string $propertyName): TrinaryLogic
142: {
143: return TrinaryLogic::createNo();
144: }
145:
146: public function getStaticProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection
147: {
148: throw new ShouldNotHappenException();
149: }
150:
151: public function getUnresolvedStaticPropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection
152: {
153: throw new ShouldNotHappenException();
154: }
155:
156: public function accepts(Type $type, bool $strictTypes): AcceptsResult
157: {
158: if ($type instanceof CompoundType) {
159: return $type->isAcceptedBy($this, $strictTypes);
160: }
161:
162: $reflectionProvider = ReflectionProviderStaticAccessor::getInstance();
163: foreach ($type->getObjectClassReflections() as $classReflection) {
164: if (!UniversalObjectCratesClassReflectionExtension::isUniversalObjectCrate(
165: $reflectionProvider,
166: $classReflection,
167: )) {
168: continue;
169: }
170:
171: return AcceptsResult::createMaybe();
172: }
173:
174: $result = AcceptsResult::createYes();
175: $scope = new OutOfClassScope();
176: foreach ($this->properties as $propertyName => $propertyType) {
177: $typeHasProperty = $type->hasInstanceProperty((string) $propertyName);
178: $hasProperty = new AcceptsResult(
179: $typeHasProperty,
180: $typeHasProperty->yes() ? [] : [
181: sprintf(
182: '%s %s have property $%s.',
183: $type->describe(VerbosityLevel::typeOnly()),
184: $typeHasProperty->no() ? 'does not' : 'might not',
185: $propertyName,
186: ),
187: ],
188: );
189: if (!$hasProperty->yes() && $type->hasStaticProperty((string) $propertyName)->yes()) {
190: $result = $result->and(new AcceptsResult(TrinaryLogic::createNo(), [
191: sprintf('Property %s::$%s is static.', $type->getStaticProperty((string) $propertyName, $scope)->getDeclaringClass()->getDisplayName(), $propertyName),
192: ]));
193: continue;
194: }
195: if ($hasProperty->no()) {
196: if (in_array($propertyName, $this->optionalProperties, true)) {
197: continue;
198: }
199: $result = $result->and($hasProperty);
200: continue;
201: }
202: if ($hasProperty->maybe()) {
203: if (!in_array($propertyName, $this->optionalProperties, true)) {
204: $result = $result->and($hasProperty);
205: continue;
206:
207: }
208:
209: $hasProperty = AcceptsResult::createYes();
210: }
211:
212: $result = $result->and($hasProperty);
213: try {
214: $otherProperty = $type->getInstanceProperty((string) $propertyName, $scope);
215: } catch (MissingPropertyFromReflectionException) {
216: continue;
217: }
218:
219: if (!$otherProperty->isPublic()) {
220: return new AcceptsResult(TrinaryLogic::createNo(), [
221: sprintf('Property %s::$%s is not public.', $otherProperty->getDeclaringClass()->getDisplayName(), $propertyName),
222: ]);
223: }
224:
225: if ($otherProperty->isStatic()) {
226: return new AcceptsResult(TrinaryLogic::createNo(), [
227: sprintf('Property %s::$%s is static.', $otherProperty->getDeclaringClass()->getDisplayName(), $propertyName),
228: ]);
229: }
230:
231: if (!$otherProperty->isReadable()) {
232: return new AcceptsResult(TrinaryLogic::createNo(), [
233: sprintf('Property %s::$%s is not readable.', $otherProperty->getDeclaringClass()->getDisplayName(), $propertyName),
234: ]);
235: }
236:
237: $otherPropertyType = $otherProperty->getReadableType();
238: $verbosity = VerbosityLevel::getRecommendedLevelByType($propertyType, $otherPropertyType);
239: $acceptsValue = $propertyType->accepts($otherPropertyType, $strictTypes)->decorateReasons(
240: static fn (string $reason) => sprintf(
241: 'Property ($%s) type %s does not accept type %s: %s',
242: $propertyName,
243: $propertyType->describe($verbosity),
244: $otherPropertyType->describe($verbosity),
245: $reason,
246: ),
247: );
248: if (!$acceptsValue->yes() && count($acceptsValue->reasons) === 0) {
249: $acceptsValue = new AcceptsResult($acceptsValue->result, [
250: sprintf(
251: 'Property ($%s) type %s does not accept type %s.',
252: $propertyName,
253: $propertyType->describe($verbosity),
254: $otherPropertyType->describe($verbosity),
255: ),
256: ]);
257: }
258: if ($acceptsValue->no()) {
259: return $acceptsValue;
260: }
261: $result = $result->and($acceptsValue);
262: }
263:
264: return $result->and(new AcceptsResult($type->isObject(), []));
265: }
266:
267: public function isSuperTypeOf(Type $type): IsSuperTypeOfResult
268: {
269: if ($type instanceof CompoundType) {
270: return $type->isSubTypeOf($this);
271: }
272:
273: if ($type instanceof ObjectWithoutClassType) {
274: return IsSuperTypeOfResult::createMaybe();
275: }
276:
277: $reflectionProvider = ReflectionProviderStaticAccessor::getInstance();
278: foreach ($type->getObjectClassReflections() as $classReflection) {
279: if (!UniversalObjectCratesClassReflectionExtension::isUniversalObjectCrate(
280: $reflectionProvider,
281: $classReflection,
282: )) {
283: continue;
284: }
285:
286: return IsSuperTypeOfResult::createMaybe();
287: }
288:
289: $result = IsSuperTypeOfResult::createYes();
290: $scope = new OutOfClassScope();
291: foreach ($this->properties as $propertyName => $propertyType) {
292: $hasProperty = new IsSuperTypeOfResult($type->hasInstanceProperty((string) $propertyName), []);
293: if ($hasProperty->no()) {
294: if (in_array($propertyName, $this->optionalProperties, true)) {
295: continue;
296: }
297: $result = $result->and($hasProperty);
298: continue;
299: }
300: if ($hasProperty->maybe()) {
301: if (!in_array($propertyName, $this->optionalProperties, true)) {
302: $result = $result->and($hasProperty);
303: continue;
304: }
305:
306: $hasProperty = IsSuperTypeOfResult::createYes();
307: }
308:
309: $result = $result->and($hasProperty);
310: try {
311: $otherProperty = $type->getInstanceProperty((string) $propertyName, $scope);
312: } catch (MissingPropertyFromReflectionException) {
313: continue;
314: }
315:
316: if (!$otherProperty->isPublic()) {
317: return IsSuperTypeOfResult::createNo();
318: }
319:
320: if ($otherProperty->isStatic()) {
321: return IsSuperTypeOfResult::createNo();
322: }
323:
324: if (!$otherProperty->isReadable()) {
325: return IsSuperTypeOfResult::createNo();
326: }
327:
328: $otherPropertyType = $otherProperty->getReadableType();
329: $isSuperType = $propertyType->isSuperTypeOf($otherPropertyType);
330: if ($isSuperType->no()) {
331: return $isSuperType;
332: }
333: $result = $result->and($isSuperType);
334: }
335:
336: return $result->and(new IsSuperTypeOfResult($type->isObject(), []));
337: }
338:
339: public function equals(Type $type): bool
340: {
341: if (!$type instanceof self) {
342: return false;
343: }
344:
345: if (count($this->properties) !== count($type->properties)) {
346: return false;
347: }
348:
349: foreach ($this->properties as $name => $propertyType) {
350: if (!array_key_exists($name, $type->properties)) {
351: return false;
352: }
353:
354: if (!$propertyType->equals($type->properties[$name])) {
355: return false;
356: }
357: }
358:
359: if (count($this->optionalProperties) !== count($type->optionalProperties)) {
360: return false;
361: }
362:
363: foreach ($this->optionalProperties as $name) {
364: if (in_array($name, $type->optionalProperties, true)) {
365: continue;
366: }
367:
368: return false;
369: }
370:
371: return true;
372: }
373:
374: public function tryRemove(Type $typeToRemove): ?Type
375: {
376: if ($typeToRemove instanceof HasPropertyType) {
377: $properties = $this->properties;
378: unset($properties[$typeToRemove->getPropertyName()]);
379: $optionalProperties = array_values(array_filter($this->optionalProperties, static fn (int|string $propertyName) => $propertyName !== $typeToRemove->getPropertyName()));
380:
381: return new self($properties, $optionalProperties);
382: }
383:
384: return null;
385: }
386:
387: public function makePropertyRequired(string $propertyName): self
388: {
389: if (array_key_exists($propertyName, $this->properties)) {
390: $optionalProperties = array_values(array_filter($this->optionalProperties, static fn (int|string $currentPropertyName) => $currentPropertyName !== $propertyName));
391:
392: return new self($this->properties, $optionalProperties);
393: }
394:
395: return $this;
396: }
397:
398: public function inferTemplateTypes(Type $receivedType): TemplateTypeMap
399: {
400: if ($receivedType instanceof UnionType || $receivedType instanceof IntersectionType) {
401: return $receivedType->inferTemplateTypesOn($this);
402: }
403:
404: if ($receivedType instanceof self) {
405: $typeMap = TemplateTypeMap::createEmpty();
406: $scope = new OutOfClassScope();
407: foreach ($this->properties as $name => $propertyType) {
408: if ($receivedType->hasInstanceProperty((string) $name)->no()) {
409: continue;
410: }
411:
412: try {
413: $receivedProperty = $receivedType->getInstanceProperty((string) $name, $scope);
414: } catch (MissingPropertyFromReflectionException) {
415: continue;
416: }
417: if (!$receivedProperty->isPublic()) {
418: continue;
419: }
420: if ($receivedProperty->isStatic()) {
421: continue;
422: }
423: $receivedPropertyType = $receivedProperty->getReadableType();
424: $typeMap = $typeMap->union($propertyType->inferTemplateTypes($receivedPropertyType));
425: }
426:
427: return $typeMap;
428: }
429:
430: return TemplateTypeMap::createEmpty();
431: }
432:
433: public function getReferencedTemplateTypes(TemplateTypeVariance $positionVariance): array
434: {
435: $variance = $positionVariance->compose(TemplateTypeVariance::createCovariant());
436: $references = [];
437: foreach ($this->properties as $propertyType) {
438: foreach ($propertyType->getReferencedTemplateTypes($variance) as $reference) {
439: $references[] = $reference;
440: }
441: }
442:
443: return $references;
444: }
445:
446: public function describe(VerbosityLevel $level): string
447: {
448: $callback = function () use ($level): string {
449: $items = [];
450: foreach ($this->properties as $name => $propertyType) {
451: $optional = in_array($name, $this->optionalProperties, true);
452: $items[] = sprintf('%s%s: %s', $name, $optional ? '?' : '', $propertyType->describe($level));
453: }
454: return sprintf('object{%s}', implode(', ', $items));
455: };
456: return $level->handle(
457: $callback,
458: $callback,
459: );
460: }
461:
462: public function getEnumCases(): array
463: {
464: return [];
465: }
466:
467: public function getEnumCaseObject(): ?EnumCaseObjectType
468: {
469: return null;
470: }
471:
472: public function traverse(callable $cb): Type
473: {
474: $properties = [];
475: $stillOriginal = true;
476:
477: foreach ($this->properties as $name => $propertyType) {
478: $transformed = $cb($propertyType);
479: if ($transformed !== $propertyType) {
480: $stillOriginal = false;
481: }
482:
483: $properties[$name] = $transformed;
484: }
485:
486: if ($stillOriginal) {
487: return $this;
488: }
489:
490: return new self($properties, $this->optionalProperties);
491: }
492:
493: public function traverseSimultaneously(Type $right, callable $cb): Type
494: {
495: if (!$right->isObject()->yes()) {
496: return $this;
497: }
498:
499: $properties = [];
500: $stillOriginal = true;
501:
502: $scope = new OutOfClassScope();
503: foreach ($this->properties as $name => $propertyType) {
504: if (!$right->hasInstanceProperty((string) $name)->yes()) {
505: return $this;
506: }
507: $transformed = $cb($propertyType, $right->getInstanceProperty((string) $name, $scope)->getReadableType());
508: if ($transformed !== $propertyType) {
509: $stillOriginal = false;
510: }
511:
512: $properties[$name] = $transformed;
513: }
514:
515: if ($stillOriginal) {
516: return $this;
517: }
518:
519: return new self($properties, $this->optionalProperties);
520: }
521:
522: public function exponentiate(Type $exponent): Type
523: {
524: if (!$exponent instanceof NeverType && !$this->isSuperTypeOf($exponent)->no()) {
525: return TypeCombinator::union($this, $exponent);
526: }
527:
528: return new BenevolentUnionType([
529: new FloatType(),
530: new IntegerType(),
531: ]);
532: }
533:
534: public function getFiniteTypes(): array
535: {
536: return [];
537: }
538:
539: public function toPhpDocNode(): TypeNode
540: {
541: $items = [];
542: foreach ($this->properties as $name => $type) {
543: if (ConstantArrayType::isValidIdentifier((string) $name)) {
544: $keyNode = new IdentifierTypeNode((string) $name);
545: } else {
546: $keyPhpDocNode = (new ConstantStringType((string) $name))->toPhpDocNode();
547: if (!$keyPhpDocNode instanceof ConstTypeNode) {
548: continue;
549: }
550:
551: /** @var ConstExprStringNode $keyNode */
552: $keyNode = $keyPhpDocNode->constExpr;
553: }
554: $items[] = new ObjectShapeItemNode(
555: $keyNode,
556: in_array($name, $this->optionalProperties, true),
557: $type->toPhpDocNode(),
558: );
559: }
560:
561: return new ObjectShapeNode($items);
562: }
563:
564: public function hasTemplateOrLateResolvableType(): bool
565: {
566: foreach ($this->properties as $property) {
567: if (!$property->hasTemplateOrLateResolvableType()) {
568: continue;
569: }
570:
571: return true;
572: }
573:
574: return false;
575: }
576:
577: }
578: