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