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: $hasProperty = new IsSuperTypeOfResult($type->hasInstanceProperty((string) $propertyName), []);
299: if ($hasProperty->no()) {
300: if (in_array($propertyName, $this->optionalProperties, true)) {
301: continue;
302: }
303: $result = $result->and($hasProperty);
304: continue;
305: }
306: if ($hasProperty->maybe()) {
307: if (!in_array($propertyName, $this->optionalProperties, true)) {
308: $result = $result->and($hasProperty);
309: continue;
310: }
311:
312: $hasProperty = IsSuperTypeOfResult::createYes();
313: }
314:
315: $result = $result->and($hasProperty);
316: try {
317: $otherProperty = $type->getInstanceProperty((string) $propertyName, $scope);
318: } catch (MissingPropertyFromReflectionException) {
319: continue;
320: }
321:
322: if (!$otherProperty->isPublic()) {
323: return IsSuperTypeOfResult::createNo();
324: }
325:
326: if ($otherProperty->isStatic()) {
327: return IsSuperTypeOfResult::createNo();
328: }
329:
330: if (!$otherProperty->isReadable()) {
331: return IsSuperTypeOfResult::createNo();
332: }
333:
334: $otherPropertyType = $otherProperty->getReadableType();
335: $isSuperType = $propertyType->isSuperTypeOf($otherPropertyType);
336: if ($isSuperType->no()) {
337: return $isSuperType;
338: }
339: $result = $result->and($isSuperType);
340: }
341:
342: return $result->and(new IsSuperTypeOfResult($type->isObject(), []));
343: }
344:
345: public function equals(Type $type): bool
346: {
347: if (!$type instanceof self) {
348: return false;
349: }
350:
351: if (count($this->properties) !== count($type->properties)) {
352: return false;
353: }
354:
355: foreach ($this->properties as $name => $propertyType) {
356: if (!array_key_exists($name, $type->properties)) {
357: return false;
358: }
359:
360: if (!$propertyType->equals($type->properties[$name])) {
361: return false;
362: }
363: }
364:
365: if (count($this->optionalProperties) !== count($type->optionalProperties)) {
366: return false;
367: }
368:
369: foreach ($this->optionalProperties as $name) {
370: if (in_array($name, $type->optionalProperties, true)) {
371: continue;
372: }
373:
374: return false;
375: }
376:
377: return true;
378: }
379:
380: public function tryRemove(Type $typeToRemove): ?Type
381: {
382: if ($typeToRemove instanceof HasPropertyType) {
383: $properties = $this->properties;
384: unset($properties[$typeToRemove->getPropertyName()]);
385: $optionalProperties = array_values(array_filter($this->optionalProperties, static fn (int|string $propertyName) => $propertyName !== $typeToRemove->getPropertyName()));
386:
387: return new self($properties, $optionalProperties);
388: }
389:
390: return null;
391: }
392:
393: public function makePropertyRequired(string $propertyName): self
394: {
395: if (array_key_exists($propertyName, $this->properties)) {
396: $optionalProperties = array_values(array_filter($this->optionalProperties, static fn (int|string $currentPropertyName) => $currentPropertyName !== $propertyName));
397:
398: return new self($this->properties, $optionalProperties);
399: }
400:
401: return $this;
402: }
403:
404: public function inferTemplateTypes(Type $receivedType): TemplateTypeMap
405: {
406: if ($receivedType instanceof UnionType || $receivedType instanceof IntersectionType) {
407: return $receivedType->inferTemplateTypesOn($this);
408: }
409:
410: if ($receivedType instanceof self) {
411: $typeMap = TemplateTypeMap::createEmpty();
412: $scope = new OutOfClassScope();
413: foreach ($this->properties as $name => $propertyType) {
414: if ($receivedType->hasInstanceProperty((string) $name)->no()) {
415: continue;
416: }
417:
418: try {
419: $receivedProperty = $receivedType->getInstanceProperty((string) $name, $scope);
420: } catch (MissingPropertyFromReflectionException) {
421: continue;
422: }
423: if (!$receivedProperty->isPublic()) {
424: continue;
425: }
426: if ($receivedProperty->isStatic()) {
427: continue;
428: }
429: $receivedPropertyType = $receivedProperty->getReadableType();
430: $typeMap = $typeMap->union($propertyType->inferTemplateTypes($receivedPropertyType));
431: }
432:
433: return $typeMap;
434: }
435:
436: return TemplateTypeMap::createEmpty();
437: }
438:
439: public function getReferencedTemplateTypes(TemplateTypeVariance $positionVariance): array
440: {
441: $variance = $positionVariance->compose(TemplateTypeVariance::createCovariant());
442: $references = [];
443: foreach ($this->properties as $propertyType) {
444: foreach ($propertyType->getReferencedTemplateTypes($variance) as $reference) {
445: $references[] = $reference;
446: }
447: }
448:
449: return $references;
450: }
451:
452: public function describe(VerbosityLevel $level): string
453: {
454: $callback = function () use ($level): string {
455: $items = [];
456: foreach ($this->properties as $name => $propertyType) {
457: $optional = in_array($name, $this->optionalProperties, true);
458: $items[] = sprintf('%s%s: %s', $name, $optional ? '?' : '', $propertyType->describe($level));
459: }
460: return sprintf('object{%s}', implode(', ', $items));
461: };
462: return $level->handle(
463: $callback,
464: $callback,
465: );
466: }
467:
468: public function getEnumCases(): array
469: {
470: return [];
471: }
472:
473: public function getEnumCaseObject(): ?EnumCaseObjectType
474: {
475: return null;
476: }
477:
478: public function traverse(callable $cb): Type
479: {
480: $properties = [];
481: $stillOriginal = true;
482:
483: foreach ($this->properties as $name => $propertyType) {
484: $transformed = $cb($propertyType);
485: if ($transformed !== $propertyType) {
486: $stillOriginal = false;
487: }
488:
489: $properties[$name] = $transformed;
490: }
491:
492: if ($stillOriginal) {
493: return $this;
494: }
495:
496: return new self($properties, $this->optionalProperties);
497: }
498:
499: public function traverseSimultaneously(Type $right, callable $cb): Type
500: {
501: if (!$right->isObject()->yes()) {
502: return $this;
503: }
504:
505: $properties = [];
506: $stillOriginal = true;
507:
508: $scope = new OutOfClassScope();
509: foreach ($this->properties as $name => $propertyType) {
510: if (!$right->hasInstanceProperty((string) $name)->yes()) {
511: return $this;
512: }
513: $transformed = $cb($propertyType, $right->getInstanceProperty((string) $name, $scope)->getReadableType());
514: if ($transformed !== $propertyType) {
515: $stillOriginal = false;
516: }
517:
518: $properties[$name] = $transformed;
519: }
520:
521: if ($stillOriginal) {
522: return $this;
523: }
524:
525: return new self($properties, $this->optionalProperties);
526: }
527:
528: public function exponentiate(Type $exponent): Type
529: {
530: if (!$exponent instanceof NeverType && !$this->isSuperTypeOf($exponent)->no()) {
531: return TypeCombinator::union($this, $exponent);
532: }
533:
534: return new BenevolentUnionType([
535: new FloatType(),
536: new IntegerType(),
537: ]);
538: }
539:
540: public function getFiniteTypes(): array
541: {
542: return [];
543: }
544:
545: public function toPhpDocNode(): TypeNode
546: {
547: $items = [];
548: foreach ($this->properties as $name => $type) {
549: if (ConstantArrayType::isValidIdentifier((string) $name)) {
550: $keyNode = new IdentifierTypeNode((string) $name);
551: } else {
552: $keyPhpDocNode = (new ConstantStringType((string) $name))->toPhpDocNode();
553: if (!$keyPhpDocNode instanceof ConstTypeNode) {
554: continue;
555: }
556:
557: /** @var ConstExprStringNode $keyNode */
558: $keyNode = $keyPhpDocNode->constExpr;
559: }
560: $items[] = new ObjectShapeItemNode(
561: $keyNode,
562: in_array($name, $this->optionalProperties, true),
563: $type->toPhpDocNode(),
564: );
565: }
566:
567: return new ObjectShapeNode($items);
568: }
569:
570: public function hasTemplateOrLateResolvableType(): bool
571: {
572: foreach ($this->properties as $property) {
573: if (!$property->hasTemplateOrLateResolvableType()) {
574: continue;
575: }
576:
577: return true;
578: }
579:
580: return false;
581: }
582:
583: }
584: