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