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