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