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