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