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