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