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