1: <?php declare(strict_types = 1);
2:
3: namespace PHPStan\Type;
4:
5: use PHPStan\Internal\CombinationsHelper;
6: use PHPStan\Php\PhpVersion;
7: use PHPStan\PhpDocParser\Ast\Type\ArrayShapeNode;
8: use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode;
9: use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
10: use PHPStan\PhpDocParser\Ast\Type\IntersectionTypeNode;
11: use PHPStan\PhpDocParser\Ast\Type\TypeNode;
12: use PHPStan\Reflection\Callables\CallableParametersAcceptor;
13: use PHPStan\Reflection\ClassConstantReflection;
14: use PHPStan\Reflection\ClassMemberAccessAnswerer;
15: use PHPStan\Reflection\ExtendedMethodReflection;
16: use PHPStan\Reflection\ExtendedPropertyReflection;
17: use PHPStan\Reflection\InitializerExprTypeResolver;
18: use PHPStan\Reflection\MissingConstantFromReflectionException;
19: use PHPStan\Reflection\MissingMethodFromReflectionException;
20: use PHPStan\Reflection\MissingPropertyFromReflectionException;
21: use PHPStan\Reflection\ParametersAcceptorSelector;
22: use PHPStan\Reflection\ReflectionProvider;
23: use PHPStan\Reflection\TrivialParametersAcceptor;
24: use PHPStan\Reflection\Type\IntersectionTypeUnresolvedMethodPrototypeReflection;
25: use PHPStan\Reflection\Type\IntersectionTypeUnresolvedPropertyPrototypeReflection;
26: use PHPStan\Reflection\Type\UnresolvedMethodPrototypeReflection;
27: use PHPStan\Reflection\Type\UnresolvedPropertyPrototypeReflection;
28: use PHPStan\ShouldNotHappenException;
29: use PHPStan\TrinaryLogic;
30: use PHPStan\Type\Accessory\AccessoryArrayListType;
31: use PHPStan\Type\Accessory\AccessoryDecimalIntegerStringType;
32: use PHPStan\Type\Accessory\AccessoryLiteralStringType;
33: use PHPStan\Type\Accessory\AccessoryLowercaseStringType;
34: use PHPStan\Type\Accessory\AccessoryNonEmptyStringType;
35: use PHPStan\Type\Accessory\AccessoryNonFalsyStringType;
36: use PHPStan\Type\Accessory\AccessoryNumericStringType;
37: use PHPStan\Type\Accessory\AccessoryType;
38: use PHPStan\Type\Accessory\AccessoryUppercaseStringType;
39: use PHPStan\Type\Accessory\HasOffsetType;
40: use PHPStan\Type\Accessory\HasOffsetValueType;
41: use PHPStan\Type\Accessory\NonEmptyArrayType;
42: use PHPStan\Type\Constant\ConstantArrayType;
43: use PHPStan\Type\Constant\ConstantArrayTypeBuilder;
44: use PHPStan\Type\Constant\ConstantIntegerType;
45: use PHPStan\Type\Constant\ConstantStringType;
46: use PHPStan\Type\Enum\EnumCaseObjectType;
47: use PHPStan\Type\Generic\TemplateArrayType;
48: use PHPStan\Type\Generic\TemplateType;
49: use PHPStan\Type\Generic\TemplateTypeMap;
50: use PHPStan\Type\Generic\TemplateTypeVariance;
51: use PHPStan\Type\Traits\NonGeneralizableTypeTrait;
52: use PHPStan\Type\Traits\NonRemoveableTypeTrait;
53: use function array_filter;
54: use function array_intersect_key;
55: use function array_map;
56: use function array_shift;
57: use function array_unique;
58: use function array_values;
59: use function count;
60: use function implode;
61: use function in_array;
62: use function is_int;
63: use function ksort;
64: use function sprintf;
65: use function str_starts_with;
66: use function strcasecmp;
67: use function strlen;
68: use function substr;
69: use function usort;
70:
71: /** @api */
72: class IntersectionType implements CompoundType
73: {
74:
75: use NonRemoveableTypeTrait;
76: use NonGeneralizableTypeTrait;
77:
78: private bool $sortedTypes = false;
79:
80: private ?TrinaryLogic $isBoolean = null;
81:
82: private ?TrinaryLogic $isFloat = null;
83:
84: private ?TrinaryLogic $isInteger = null;
85:
86: private ?TrinaryLogic $isString = null;
87:
88: private ?TrinaryLogic $isArray = null;
89:
90: private ?TrinaryLogic $isList = null;
91:
92: private ?TrinaryLogic $isConstantArray = null;
93:
94: private ?TrinaryLogic $isOversizedArray = null;
95:
96: private ?TrinaryLogic $isOffsetAccessible = null;
97:
98: private ?TrinaryLogic $isIterableAtLeastOnce = null;
99:
100: private ?TrinaryLogic $isConstantScalarValue = null;
101:
102: private ?TrinaryLogic $isCallable = null;
103:
104: /** @var array<string, Type> */
105: private array $cachedGetOffsetValueType = [];
106:
107: /** @var array<string, TrinaryLogic> */
108: private array $cachedHasOffsetValueType = [];
109:
110: /** @var array<int, string> */
111: private array $cachedDescriptions = [];
112:
113: /**
114: * @api
115: * @param list<Type> $types
116: */
117: public function __construct(private array $types)
118: {
119: if (count($types) < 2) {
120: throw new ShouldNotHappenException(sprintf(
121: 'Cannot create %s with: %s',
122: self::class,
123: implode(', ', array_map(static fn (Type $type): string => $type->describe(VerbosityLevel::value()), $types)),
124: ));
125: }
126: }
127:
128: /**
129: * @return list<Type>
130: */
131: public function getTypes(): array
132: {
133: return $this->types;
134: }
135:
136: /**
137: * @return list<Type>
138: */
139: private function getSortedTypes(): array
140: {
141: if ($this->sortedTypes) {
142: return $this->types;
143: }
144:
145: $this->types = UnionTypeHelper::sortTypes($this->types);
146: $this->sortedTypes = true;
147:
148: return $this->types;
149: }
150:
151: public function inferTemplateTypesOn(Type $templateType): TemplateTypeMap
152: {
153: $types = TemplateTypeMap::createEmpty();
154:
155: foreach ($this->types as $type) {
156: $types = $types->intersect($templateType->inferTemplateTypes($type));
157: }
158:
159: return $types;
160: }
161:
162: public function getReferencedClasses(): array
163: {
164: $classes = [];
165: foreach ($this->types as $type) {
166: foreach ($type->getReferencedClasses() as $className) {
167: $classes[] = $className;
168: }
169: }
170:
171: return $classes;
172: }
173:
174: public function getObjectClassNames(): array
175: {
176: $objectClassNames = [];
177: foreach ($this->types as $type) {
178: $innerObjectClassNames = $type->getObjectClassNames();
179: foreach ($innerObjectClassNames as $innerObjectClassName) {
180: $objectClassNames[] = $innerObjectClassName;
181: }
182: }
183:
184: return array_values(array_unique($objectClassNames));
185: }
186:
187: public function getObjectClassReflections(): array
188: {
189: $reflections = [];
190: foreach ($this->types as $type) {
191: foreach ($type->getObjectClassReflections() as $reflection) {
192: $reflections[] = $reflection;
193: }
194: }
195:
196: return $reflections;
197: }
198:
199: public function getArrays(): array
200: {
201: $arrays = [];
202: foreach ($this->types as $type) {
203: foreach ($type->getArrays() as $array) {
204: $arrays[] = $array;
205: }
206: }
207:
208: return $arrays;
209: }
210:
211: public function getConstantArrays(): array
212: {
213: if ($this->isCallable()->yes() && $this->isArray()->yes()) {
214: $builder = ConstantArrayTypeBuilder::createEmpty();
215: $zero = new ConstantIntegerType(0);
216: $builder->setOffsetValueType(
217: $zero,
218: $this->getOffsetValueType($zero),
219: );
220: $one = new ConstantIntegerType(1);
221: $builder->setOffsetValueType(
222: $one,
223: $this->getOffsetValueType($one),
224: );
225: $constantArray = $builder->getArray();
226: if (!$constantArray instanceof ConstantArrayType) {
227: throw new ShouldNotHappenException();
228: }
229:
230: return [$builder->getArray()];
231: }
232:
233: $constantArrays = [];
234: foreach ($this->types as $type) {
235: foreach ($type->getConstantArrays() as $constantArray) {
236: $constantArrays[] = $constantArray;
237: }
238: }
239:
240: return $constantArrays;
241: }
242:
243: public function getConstantStrings(): array
244: {
245: $strings = [];
246: foreach ($this->types as $type) {
247: foreach ($type->getConstantStrings() as $string) {
248: $strings[] = $string;
249: }
250: }
251:
252: return $strings;
253: }
254:
255: public function accepts(Type $otherType, bool $strictTypes): AcceptsResult
256: {
257: $result = AcceptsResult::createYes();
258: foreach ($this->types as $type) {
259: $result = $result->and($type->accepts($otherType, $strictTypes));
260: }
261:
262: if (!$result->yes()) {
263: $isList = $otherType->isList();
264: $reasons = $result->reasons;
265: $verbosity = VerbosityLevel::getRecommendedLevelByType($this, $otherType);
266: if ($this->isList()->yes() && !$isList->yes()) {
267: $reasons[] = sprintf(
268: '%s %s a list.',
269: $otherType->describe($verbosity),
270: $isList->no() ? 'is not' : 'might not be',
271: );
272: }
273:
274: $isNonEmpty = $otherType->isIterableAtLeastOnce();
275: if ($this->isIterableAtLeastOnce()->yes() && !$isNonEmpty->yes()) {
276: $reasons[] = sprintf(
277: '%s %s empty.',
278: $otherType->describe($verbosity),
279: $isNonEmpty->no() ? 'is' : 'might be',
280: );
281: }
282:
283: if (count($reasons) > 0) {
284: return new AcceptsResult($result->result, $reasons);
285: }
286: }
287:
288: return $result;
289: }
290:
291: public function isSuperTypeOf(Type $otherType): IsSuperTypeOfResult
292: {
293: if ($otherType instanceof IntersectionType && $this->equals($otherType)) {
294: return IsSuperTypeOfResult::createYes();
295: }
296:
297: if ($otherType instanceof NeverType) {
298: return IsSuperTypeOfResult::createYes();
299: }
300:
301: return IsSuperTypeOfResult::createYes()->and(...array_map(static fn (Type $innerType) => $innerType->isSuperTypeOf($otherType), $this->types));
302: }
303:
304: public function isSubTypeOf(Type $otherType): IsSuperTypeOfResult
305: {
306: if (($otherType instanceof self || $otherType instanceof UnionType) && !$otherType instanceof TemplateType) {
307: return $otherType->isSuperTypeOf($this);
308: }
309:
310: $result = IsSuperTypeOfResult::lazyMaxMin(
311: $this->types,
312: static fn (Type $innerType) => $otherType->isSuperTypeOf($innerType),
313: );
314:
315: if (
316: !$result->no()
317: && $this->isOversizedArray()->yes()
318: && !$otherType->isIterableAtLeastOnce()->no()
319: ) {
320: return IsSuperTypeOfResult::createYes();
321: }
322:
323: return $result;
324: }
325:
326: public function isAcceptedBy(Type $acceptingType, bool $strictTypes): AcceptsResult
327: {
328: $result = AcceptsResult::lazyMaxMin(
329: $this->types,
330: static fn (Type $innerType) => $acceptingType->accepts($innerType, $strictTypes),
331: );
332:
333: // lazyMaxMin can short-circuit to Yes when array<mixed> (inside e.g. array&callable
334: // or array&hasOffsetValue) is accepted by a specific array type like array<int>,
335: // because MixedType::isAcceptedBy() always returns Yes. The isSuperTypeOf check
336: // considers the intersection holistically and catches these false positives.
337: if ($result->yes()) {
338: $isSuperType = $acceptingType->isSuperTypeOf($this);
339: if ($isSuperType->no()) {
340: return $isSuperType->toAcceptsResult();
341: }
342: }
343:
344: if ($this->isOversizedArray()->yes()) {
345: if (!$result->no()) {
346: return AcceptsResult::createYes();
347: }
348: }
349:
350: return $result;
351: }
352:
353: public function equals(Type $type): bool
354: {
355: if (!$type instanceof static) {
356: return false;
357: }
358:
359: if (count($this->types) !== count($type->types)) {
360: return false;
361: }
362:
363: $otherTypes = $type->types;
364: foreach ($this->types as $innerType) {
365: $match = false;
366: foreach ($otherTypes as $i => $otherType) {
367: if (!$innerType->equals($otherType)) {
368: continue;
369: }
370:
371: $match = true;
372: unset($otherTypes[$i]);
373: break;
374: }
375:
376: if (!$match) {
377: return false;
378: }
379: }
380:
381: return count($otherTypes) === 0;
382: }
383:
384: public function describe(VerbosityLevel $level): string
385: {
386: if (isset($this->cachedDescriptions[$level->getLevelValue()])) {
387: return $this->cachedDescriptions[$level->getLevelValue()];
388: }
389:
390: return $this->cachedDescriptions[$level->getLevelValue()] = $level->handle(
391: fn (): string => $this->describeType($level),
392: fn (): string => $this->describeItself($level, true),
393: fn (): string => $this->describeItself($level, false),
394: );
395: }
396:
397: private function describeType(VerbosityLevel $level): string
398: {
399: $typeNames = [];
400: $isList = $this->isList()->yes();
401: $valueType = null;
402: foreach ($this->getSortedTypes() as $type) {
403: if ($isList) {
404: if ($type instanceof ArrayType || $type instanceof ConstantArrayType) {
405: $valueType = $type->getIterableValueType();
406: continue;
407: }
408: if ($type instanceof NonEmptyArrayType) {
409: continue;
410: }
411: }
412: if ($type instanceof AccessoryType) {
413: continue;
414: }
415: $typeNames[] = $type->generalize(GeneralizePrecision::lessSpecific())->describe($level);
416: }
417:
418: if ($isList) {
419: $isMixedValueType = $valueType instanceof MixedType && $valueType->describe(VerbosityLevel::precise()) === 'mixed' && !$valueType->isExplicitMixed();
420: $innerType = '';
421: if ($valueType !== null && !$isMixedValueType) {
422: $innerType = sprintf('<%s>', $valueType->describe($level));
423: }
424:
425: $typeNames[] = 'list' . $innerType;
426: }
427:
428: usort($typeNames, static function ($a, $b) {
429: $cmp = strcasecmp($a, $b);
430: if ($cmp !== 0) {
431: return $cmp;
432: }
433:
434: return $a <=> $b;
435: });
436:
437: return implode('&', $typeNames);
438: }
439:
440: private function describeItself(VerbosityLevel $level, bool $skipAccessoryTypes): string
441: {
442: $baseTypes = [];
443: $typesToDescribe = [];
444: $skipTypeNames = [];
445:
446: $nonEmptyStr = false;
447: $nonFalsyStr = false;
448: $isList = $this->isList()->yes();
449: $isArray = $this->isArray()->yes();
450: $isNonEmptyArray = $this->isIterableAtLeastOnce()->yes();
451: // When a TemplateArrayType carries the array refinement, we describe
452: // it via its own describe() (e.g. "T of array") rather than collapsing
453: // it into a generic `array<...>` prefix. In that case the
454: // `NonEmptyArrayType` and `AccessoryArrayListType` markers must
455: // describe themselves explicitly — they cannot be absorbed into a
456: // non-existent `non-empty-array` prefix.
457: $hasTemplateArray = false;
458: if ($isArray || $isList) {
459: foreach ($this->types as $type) {
460: if ($type instanceof TemplateArrayType) {
461: $hasTemplateArray = true;
462: break;
463: }
464: }
465: }
466: $describedTypes = [];
467: foreach ($this->getSortedTypes() as $i => $type) {
468: if ($type instanceof AccessoryNonEmptyStringType
469: || $type instanceof AccessoryLiteralStringType
470: || $type instanceof AccessoryNumericStringType
471: || $type instanceof AccessoryNonFalsyStringType
472: || $type instanceof AccessoryLowercaseStringType
473: || $type instanceof AccessoryUppercaseStringType
474: || $type instanceof AccessoryDecimalIntegerStringType
475: ) {
476: if (
477: ($type instanceof AccessoryLowercaseStringType || $type instanceof AccessoryUppercaseStringType)
478: && !$level->isPrecise()
479: && !$level->isCache()
480: ) {
481: continue;
482: }
483: if ($type instanceof AccessoryNonFalsyStringType) {
484: $nonFalsyStr = true;
485: }
486: if ($type instanceof AccessoryNonEmptyStringType) {
487: $nonEmptyStr = true;
488: }
489: if ($nonEmptyStr && $nonFalsyStr) {
490: // prevent redundant 'non-empty-string&non-falsy-string'
491: foreach ($typesToDescribe as $key => $typeToDescribe) {
492: if (!($typeToDescribe instanceof AccessoryNonEmptyStringType)) {
493: continue;
494: }
495:
496: unset($typesToDescribe[$key]);
497: }
498: }
499:
500: $typesToDescribe[$i] = $type;
501: $skipTypeNames[] = 'string';
502: continue;
503: }
504: if ($isList || $isArray) {
505: if ($type instanceof TemplateArrayType) {
506: // Preserve the template's own describe (e.g. "T of array")
507: // instead of collapsing it to a generic array shape — the
508: // other intersection members already carry the array
509: // refinement.
510: $describedTypes[$i] = $type->describe($level);
511: continue;
512: }
513: if ($type instanceof ArrayType) {
514: $keyType = $type->getKeyType();
515: $valueType = $type->getItemType();
516: if ($isList) {
517: $isMixedValueType = $valueType instanceof MixedType && $valueType->describe(VerbosityLevel::precise()) === 'mixed' && !$valueType->isExplicitMixed();
518: $valueTypeDescription = '';
519: if (!$isMixedValueType) {
520: $valueTypeDescription = sprintf('<%s>', $valueType->describe($level));
521: }
522:
523: $describedTypes[$i] = ($isNonEmptyArray ? 'non-empty-list' : 'list') . $valueTypeDescription;
524: } else {
525: $isMixedKeyType = $keyType instanceof MixedType && $keyType->describe(VerbosityLevel::precise()) === 'mixed' && !$keyType->isExplicitMixed();
526: $isMixedValueType = $valueType instanceof MixedType && $valueType->describe(VerbosityLevel::precise()) === 'mixed' && !$valueType->isExplicitMixed();
527: $typeDescription = '';
528: if (!$isMixedKeyType) {
529: $typeDescription = sprintf('<%s, %s>', $keyType->describe($level), $valueType->describe($level));
530: } elseif (!$isMixedValueType) {
531: $typeDescription = sprintf('<%s>', $valueType->describe($level));
532: }
533:
534: $describedTypes[$i] = ($isNonEmptyArray ? 'non-empty-array' : 'array') . $typeDescription;
535: }
536: continue;
537: } elseif ($type instanceof ConstantArrayType) {
538: $description = $type->describe($level);
539: $kind = str_starts_with($description, 'list') ? 'list' : 'array';
540: $descriptionWithoutKind = substr($description, strlen($kind));
541: $begin = $isList ? 'list' : 'array';
542: if ($isNonEmptyArray && !$type->isIterableAtLeastOnce()->yes()) {
543: $begin = 'non-empty-' . $begin;
544: }
545:
546: $describedTypes[$i] = $begin . $descriptionWithoutKind;
547: continue;
548: }
549: if ($type instanceof NonEmptyArrayType || $type instanceof AccessoryArrayListType) {
550: if ($hasTemplateArray) {
551: $describedTypes[$i] = $type->describe($level);
552: }
553: continue;
554: }
555: }
556:
557: if ($type instanceof CallableType && $type->isCommonCallable()) {
558: $typesToDescribe[$i] = $type;
559: $skipTypeNames[] = 'object';
560: $skipTypeNames[] = 'string';
561: continue;
562: }
563:
564: if (!$type instanceof AccessoryType) {
565: $baseTypes[$i] = $type;
566: continue;
567: }
568:
569: if ($skipAccessoryTypes) {
570: continue;
571: }
572:
573: $typesToDescribe[$i] = $type;
574: }
575:
576: foreach ($baseTypes as $i => $type) {
577: $typeDescription = $type->describe($level);
578:
579: if (in_array($typeDescription, ['object', 'string'], true) && in_array($typeDescription, $skipTypeNames, true)) {
580: foreach ($typesToDescribe as $j => $typeToDescribe) {
581: if ($typeToDescribe instanceof CallableType && $typeToDescribe->isCommonCallable()) {
582: $describedTypes[$i] = 'callable-' . $typeDescription;
583: unset($typesToDescribe[$j]);
584: continue 2;
585: }
586: }
587: }
588:
589: if (in_array($typeDescription, $skipTypeNames, true)) {
590: continue;
591: }
592:
593: $describedTypes[$i] = $type->describe($level);
594: }
595:
596: foreach ($typesToDescribe as $i => $typeToDescribe) {
597: $describedTypes[$i] = $typeToDescribe->describe($level);
598: }
599:
600: ksort($describedTypes);
601:
602: return implode('&', $describedTypes);
603: }
604:
605: public function getTemplateType(string $ancestorClassName, string $templateTypeName): Type
606: {
607: return $this->intersectTypes(static fn (Type $type): Type => $type->getTemplateType($ancestorClassName, $templateTypeName));
608: }
609:
610: public function isObject(): TrinaryLogic
611: {
612: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isObject());
613: }
614:
615: public function getClassStringType(): Type
616: {
617: return $this->intersectTypes(static fn (Type $type): Type => $type->getClassStringType());
618: }
619:
620: public function isEnum(): TrinaryLogic
621: {
622: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isEnum());
623: }
624:
625: public function canAccessProperties(): TrinaryLogic
626: {
627: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->canAccessProperties());
628: }
629:
630: public function hasProperty(string $propertyName): TrinaryLogic
631: {
632: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->hasProperty($propertyName));
633: }
634:
635: public function getProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection
636: {
637: return $this->getUnresolvedPropertyPrototype($propertyName, $scope)->getTransformedProperty();
638: }
639:
640: public function getUnresolvedPropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection
641: {
642: $propertyPrototypes = [];
643: foreach ($this->types as $type) {
644: if (!$type->hasProperty($propertyName)->yes()) {
645: continue;
646: }
647:
648: $propertyPrototypes[] = $type->getUnresolvedPropertyPrototype($propertyName, $scope)->withFechedOnType($this);
649: }
650:
651: $propertiesCount = count($propertyPrototypes);
652: if ($propertiesCount === 0) {
653: throw new MissingPropertyFromReflectionException($this->describe(VerbosityLevel::typeOnly()), $propertyName);
654: }
655:
656: if ($propertiesCount === 1) {
657: return $propertyPrototypes[0];
658: }
659:
660: return new IntersectionTypeUnresolvedPropertyPrototypeReflection($propertyPrototypes);
661: }
662:
663: public function hasInstanceProperty(string $propertyName): TrinaryLogic
664: {
665: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->hasInstanceProperty($propertyName));
666: }
667:
668: public function getInstanceProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection
669: {
670: return $this->getUnresolvedInstancePropertyPrototype($propertyName, $scope)->getTransformedProperty();
671: }
672:
673: public function getUnresolvedInstancePropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection
674: {
675: $propertyPrototypes = [];
676: foreach ($this->types as $type) {
677: if (!$type->hasInstanceProperty($propertyName)->yes()) {
678: continue;
679: }
680:
681: $propertyPrototypes[] = $type->getUnresolvedInstancePropertyPrototype($propertyName, $scope)->withFechedOnType($this);
682: }
683:
684: $propertiesCount = count($propertyPrototypes);
685: if ($propertiesCount === 0) {
686: throw new MissingPropertyFromReflectionException($this->describe(VerbosityLevel::typeOnly()), $propertyName);
687: }
688:
689: if ($propertiesCount === 1) {
690: return $propertyPrototypes[0];
691: }
692:
693: return new IntersectionTypeUnresolvedPropertyPrototypeReflection($propertyPrototypes);
694: }
695:
696: public function hasStaticProperty(string $propertyName): TrinaryLogic
697: {
698: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->hasStaticProperty($propertyName));
699: }
700:
701: public function getStaticProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection
702: {
703: return $this->getUnresolvedStaticPropertyPrototype($propertyName, $scope)->getTransformedProperty();
704: }
705:
706: public function getUnresolvedStaticPropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection
707: {
708: $propertyPrototypes = [];
709: foreach ($this->types as $type) {
710: if (!$type->hasStaticProperty($propertyName)->yes()) {
711: continue;
712: }
713:
714: $propertyPrototypes[] = $type->getUnresolvedStaticPropertyPrototype($propertyName, $scope)->withFechedOnType($this);
715: }
716:
717: $propertiesCount = count($propertyPrototypes);
718: if ($propertiesCount === 0) {
719: throw new MissingPropertyFromReflectionException($this->describe(VerbosityLevel::typeOnly()), $propertyName);
720: }
721:
722: if ($propertiesCount === 1) {
723: return $propertyPrototypes[0];
724: }
725:
726: return new IntersectionTypeUnresolvedPropertyPrototypeReflection($propertyPrototypes);
727: }
728:
729: public function canCallMethods(): TrinaryLogic
730: {
731: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->canCallMethods());
732: }
733:
734: public function hasMethod(string $methodName): TrinaryLogic
735: {
736: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->hasMethod($methodName));
737: }
738:
739: public function getMethod(string $methodName, ClassMemberAccessAnswerer $scope): ExtendedMethodReflection
740: {
741: return $this->getUnresolvedMethodPrototype($methodName, $scope)->getTransformedMethod();
742: }
743:
744: public function getUnresolvedMethodPrototype(string $methodName, ClassMemberAccessAnswerer $scope): UnresolvedMethodPrototypeReflection
745: {
746: $methodPrototypes = [];
747: foreach ($this->types as $type) {
748: if (!$type->hasMethod($methodName)->yes()) {
749: continue;
750: }
751:
752: $methodPrototypes[] = $type->getUnresolvedMethodPrototype($methodName, $scope)->withCalledOnType($this);
753: }
754:
755: $methodsCount = count($methodPrototypes);
756: if ($methodsCount === 0) {
757: throw new MissingMethodFromReflectionException($this->describe(VerbosityLevel::typeOnly()), $methodName);
758: }
759:
760: if ($methodsCount === 1) {
761: return $methodPrototypes[0];
762: }
763:
764: return new IntersectionTypeUnresolvedMethodPrototypeReflection($methodName, $methodPrototypes);
765: }
766:
767: public function canAccessConstants(): TrinaryLogic
768: {
769: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->canAccessConstants());
770: }
771:
772: public function hasConstant(string $constantName): TrinaryLogic
773: {
774: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->hasConstant($constantName));
775: }
776:
777: public function getConstant(string $constantName): ClassConstantReflection
778: {
779: foreach ($this->types as $type) {
780: if ($type->hasConstant($constantName)->yes()) {
781: return $type->getConstant($constantName);
782: }
783: }
784:
785: throw new MissingConstantFromReflectionException($this->describe(VerbosityLevel::typeOnly()), $constantName);
786: }
787:
788: public function isIterable(): TrinaryLogic
789: {
790: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isIterable());
791: }
792:
793: public function isIterableAtLeastOnce(): TrinaryLogic
794: {
795: if ($this->isCallable()->yes() && $this->isArray()->yes()) {
796: return TrinaryLogic::createYes();
797: }
798:
799: return $this->isIterableAtLeastOnce ??= $this->intersectResults(
800: static fn (Type $type): TrinaryLogic => $type->isIterableAtLeastOnce(),
801: static fn (Type $type): bool => !$type->isIterable()->no(),
802: );
803: }
804:
805: public function getArraySize(): Type
806: {
807: if ($this->isCallable()->yes() && $this->isArray()->yes()) {
808: return new ConstantIntegerType(2);
809: }
810:
811: $arraySize = $this->intersectTypes(static fn (Type $type): Type => $type->getArraySize());
812:
813: $knownOffsets = [];
814: foreach ($this->types as $type) {
815: if (!($type instanceof HasOffsetValueType) && !($type instanceof HasOffsetType)) {
816: continue;
817: }
818:
819: $knownOffsets[$type->getOffsetType()->getValue()] = true;
820: }
821:
822: if ($this->isList()->yes() && $this->isIterableAtLeastOnce()->yes()) {
823: $knownOffsets[0] = true;
824: }
825:
826: if ($knownOffsets !== []) {
827: return TypeCombinator::intersect($arraySize, IntegerRangeType::fromInterval(count($knownOffsets), null));
828: }
829:
830: return $arraySize;
831: }
832:
833: public function getIterableKeyType(): Type
834: {
835: if ($this->isCallable()->yes() && $this->isArray()->yes()) {
836: return new UnionType([new ConstantIntegerType(0), new ConstantIntegerType(1)]);
837: }
838: return $this->intersectTypes(static fn (Type $type): Type => $type->getIterableKeyType());
839: }
840:
841: public function getFirstIterableKeyType(): Type
842: {
843: return $this->intersectTypes(static fn (Type $type): Type => $type->getIterableKeyType());
844: }
845:
846: public function getLastIterableKeyType(): Type
847: {
848: return $this->intersectTypes(static fn (Type $type): Type => $type->getIterableKeyType());
849: }
850:
851: public function getIterableValueType(): Type
852: {
853: $result = $this->intersectTypes(static fn (Type $type): Type => $type->getIterableValueType());
854: if ($this->isCallable()->yes() && $this->isArray()->yes()) {
855: return TypeCombinator::intersect(
856: $result,
857: new UnionType([
858: new ObjectWithoutClassType(),
859: new IntersectionType([new StringType(), new AccessoryNonFalsyStringType()]),
860: ]),
861: );
862: }
863: return $result;
864: }
865:
866: public function getFirstIterableValueType(): Type
867: {
868: return $this->intersectTypes(static fn (Type $type): Type => $type->getIterableValueType());
869: }
870:
871: public function getLastIterableValueType(): Type
872: {
873: return $this->intersectTypes(static fn (Type $type): Type => $type->getIterableValueType());
874: }
875:
876: public function isArray(): TrinaryLogic
877: {
878: return $this->isArray ??= $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isArray());
879: }
880:
881: public function isConstantArray(): TrinaryLogic
882: {
883: if ($this->isCallable()->yes() && $this->isArray()->yes()) {
884: return TrinaryLogic::createYes();
885: }
886: return $this->isConstantArray ??= $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isConstantArray());
887: }
888:
889: public function isOversizedArray(): TrinaryLogic
890: {
891: return $this->isOversizedArray ??= $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isOversizedArray());
892: }
893:
894: public function isList(): TrinaryLogic
895: {
896: if ($this->isCallable()->yes() && $this->isArray()->yes()) {
897: return TrinaryLogic::createYes();
898: }
899:
900: return $this->isList ??= $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isList());
901: }
902:
903: public function isString(): TrinaryLogic
904: {
905: return $this->isString ??= $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isString());
906: }
907:
908: public function isNumericString(): TrinaryLogic
909: {
910: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isNumericString());
911: }
912:
913: public function isDecimalIntegerString(): TrinaryLogic
914: {
915: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isDecimalIntegerString());
916: }
917:
918: public function isNonEmptyString(): TrinaryLogic
919: {
920: if ($this->isCallable()->yes() && $this->isString()->yes()) {
921: return TrinaryLogic::createYes();
922: }
923: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isNonEmptyString());
924: }
925:
926: public function isNonFalsyString(): TrinaryLogic
927: {
928: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isNonFalsyString());
929: }
930:
931: public function isLiteralString(): TrinaryLogic
932: {
933: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isLiteralString());
934: }
935:
936: public function isLowercaseString(): TrinaryLogic
937: {
938: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isLowercaseString());
939: }
940:
941: public function isUppercaseString(): TrinaryLogic
942: {
943: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isUppercaseString());
944: }
945:
946: public function isClassString(): TrinaryLogic
947: {
948: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isClassString());
949: }
950:
951: public function getClassStringObjectType(): Type
952: {
953: return $this->intersectTypes(static fn (Type $type): Type => $type->getClassStringObjectType());
954: }
955:
956: public function getObjectTypeOrClassStringObjectType(): Type
957: {
958: return $this->intersectTypes(static fn (Type $type): Type => $type->getObjectTypeOrClassStringObjectType());
959: }
960:
961: public function isVoid(): TrinaryLogic
962: {
963: return TrinaryLogic::createNo();
964: }
965:
966: public function isScalar(): TrinaryLogic
967: {
968: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isScalar());
969: }
970:
971: public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType
972: {
973: return $this->intersectResults(
974: static fn (Type $innerType): TrinaryLogic => $innerType->looseCompare($type, $phpVersion)->toTrinaryLogic(),
975: )->toBooleanType();
976: }
977:
978: public function isOffsetAccessible(): TrinaryLogic
979: {
980: return $this->isOffsetAccessible ??= $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isOffsetAccessible());
981: }
982:
983: public function isOffsetAccessLegal(): TrinaryLogic
984: {
985: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isOffsetAccessLegal());
986: }
987:
988: public function hasOffsetValueType(Type $offsetType): TrinaryLogic
989: {
990: $cacheKey = $offsetType->describe(VerbosityLevel::cache());
991: if (isset($this->cachedHasOffsetValueType[$cacheKey])) {
992: return $this->cachedHasOffsetValueType[$cacheKey];
993: }
994: return $this->cachedHasOffsetValueType[$cacheKey] = $this->doHasOffsetValueType($offsetType);
995: }
996:
997: private function doHasOffsetValueType(Type $offsetType): TrinaryLogic
998: {
999: if ($this->isCallable()->yes() && $this->isArray()->yes()) {
1000: $arrayKeyOffsetType = $offsetType->toArrayKey();
1001: $callableArrayOffsetType = new UnionType([new ConstantIntegerType(0), new ConstantIntegerType(1)]);
1002:
1003: return $callableArrayOffsetType->isSuperTypeOf($arrayKeyOffsetType)->result;
1004: }
1005:
1006: if ($this->isList()->yes()) {
1007: $arrayKeyOffsetType = $offsetType->toArrayKey();
1008:
1009: $negative = IntegerRangeType::fromInterval(null, -1);
1010: if ($negative->isSuperTypeOf($arrayKeyOffsetType)->yes()) {
1011: return TrinaryLogic::createNo();
1012: }
1013:
1014: $size = $this->getArraySize();
1015: if ($size instanceof IntegerRangeType && $size->getMin() !== null) {
1016: $knownOffsets = IntegerRangeType::fromInterval(0, $size->getMin() - 1);
1017: } elseif ($size instanceof ConstantIntegerType) {
1018: $knownOffsets = IntegerRangeType::fromInterval(0, $size->getValue() - 1);
1019: } elseif ($this->isIterableAtLeastOnce()->yes()) {
1020: $knownOffsets = new ConstantIntegerType(0);
1021: } else {
1022: $knownOffsets = null;
1023: }
1024:
1025: if ($knownOffsets !== null && $knownOffsets->isSuperTypeOf($arrayKeyOffsetType)->yes()) {
1026: return TrinaryLogic::createYes();
1027: }
1028:
1029: foreach ($this->types as $type) {
1030: if (!$type instanceof HasOffsetValueType && !$type instanceof HasOffsetType) {
1031: continue;
1032: }
1033:
1034: foreach ($type->getOffsetType()->getConstantScalarValues() as $constantScalarValue) {
1035: if (!is_int($constantScalarValue)) {
1036: continue;
1037: }
1038: if (IntegerRangeType::fromInterval(0, $constantScalarValue)->isSuperTypeOf($arrayKeyOffsetType)->yes()) {
1039: return TrinaryLogic::createYes();
1040: }
1041: }
1042: }
1043: }
1044:
1045: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->hasOffsetValueType($offsetType));
1046: }
1047:
1048: public function getOffsetValueType(Type $offsetType): Type
1049: {
1050: $cacheKey = $offsetType->describe(VerbosityLevel::cache());
1051: if (isset($this->cachedGetOffsetValueType[$cacheKey])) {
1052: return $this->cachedGetOffsetValueType[$cacheKey];
1053: }
1054: return $this->cachedGetOffsetValueType[$cacheKey] = $this->doGetOffsetValueType($offsetType);
1055: }
1056:
1057: private function doGetOffsetValueType(Type $offsetType): Type
1058: {
1059: $result = $this->intersectTypes(static fn (Type $type): Type => $type->getOffsetValueType($offsetType));
1060: if ($this->isOversizedArray()->yes()) {
1061: return TypeUtils::toBenevolentUnion($result);
1062: }
1063:
1064: if ($this->isCallable()->yes() && $this->isArray()->yes()) {
1065: $arrayKeyOffsetType = $offsetType->toArrayKey();
1066: if ((new ConstantIntegerType(0))->isSuperTypeOf($arrayKeyOffsetType)->yes()) {
1067: $narrowedType = new UnionType([new ClassStringType(), new ObjectWithoutClassType()]);
1068: } elseif ((new ConstantIntegerType(1))->isSuperTypeOf($arrayKeyOffsetType)->yes()) {
1069: $narrowedType = new IntersectionType([new StringType(), new AccessoryNonFalsyStringType()]);
1070: } else {
1071: $narrowedType = new UnionType([new IntersectionType([new StringType(), new AccessoryNonFalsyStringType()]), new ObjectWithoutClassType()]);
1072: }
1073: $result = TypeCombinator::intersect($result, $narrowedType);
1074: }
1075:
1076: return $result;
1077: }
1078:
1079: public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $unionValues = true): Type
1080: {
1081: if ($this->isOversizedArray()->yes()) {
1082: return $this->intersectTypes(static function (Type $type) use ($offsetType, $valueType, $unionValues): Type {
1083: // avoid new HasOffsetValueType being intersected with oversized array
1084: if (!$type instanceof ArrayType) {
1085: return $type->setOffsetValueType($offsetType, $valueType, $unionValues);
1086: }
1087:
1088: if (!$offsetType instanceof ConstantStringType && !$offsetType instanceof ConstantIntegerType) {
1089: return $type->setOffsetValueType($offsetType, $valueType, $unionValues);
1090: }
1091:
1092: if (!$offsetType->isSuperTypeOf($type->getKeyType())->yes()) {
1093: return $type->setOffsetValueType($offsetType, $valueType, $unionValues);
1094: }
1095:
1096: return new IntersectionType([
1097: new ArrayType(
1098: TypeCombinator::union($type->getKeyType(), $offsetType),
1099: TypeCombinator::union($type->getItemType(), $valueType),
1100: ),
1101: new NonEmptyArrayType(),
1102: ]);
1103: });
1104: }
1105:
1106: $result = $this->intersectTypes(static fn (Type $type): Type => $type->setOffsetValueType($offsetType, $valueType, $unionValues));
1107:
1108: if (
1109: $offsetType !== null
1110: && $this->isList()->yes()
1111: && !$result->isList()->yes()
1112: ) {
1113: if ($this->isIterableAtLeastOnce()->yes() && (new ConstantIntegerType(1))->isSuperTypeOf($offsetType)->yes()) {
1114: $result = TypeCombinator::intersect($result, new AccessoryArrayListType());
1115: } else {
1116: foreach ($this->types as $type) {
1117: if (!$type instanceof HasOffsetValueType && !$type instanceof HasOffsetType) {
1118: continue;
1119: }
1120:
1121: foreach ($type->getOffsetType()->getConstantScalarValues() as $constantScalarValue) {
1122: if (!is_int($constantScalarValue)) {
1123: continue;
1124: }
1125: if (IntegerRangeType::fromInterval(0, $constantScalarValue + 1)->isSuperTypeOf($offsetType)->yes()) {
1126: $result = TypeCombinator::intersect($result, new AccessoryArrayListType());
1127: break 2;
1128: }
1129: }
1130: }
1131: }
1132: }
1133:
1134: if (
1135: $this->isList()->yes()
1136: && $offsetType !== null
1137: && $offsetType->toArrayKey()->isInteger()->yes()
1138: && $this->getIterableValueType()->isArray()->yes()
1139: ) {
1140: $result = TypeCombinator::intersect($result, new AccessoryArrayListType());
1141: }
1142:
1143: return $result;
1144: }
1145:
1146: public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type
1147: {
1148: return $this->intersectTypes(static fn (Type $type): Type => $type->setExistingOffsetValueType($offsetType, $valueType));
1149: }
1150:
1151: public function unsetOffset(Type $offsetType): Type
1152: {
1153: return $this->intersectTypes(static fn (Type $type): Type => $type->unsetOffset($offsetType));
1154: }
1155:
1156: public function getKeysArrayFiltered(Type $filterValueType, TrinaryLogic $strict): Type
1157: {
1158: return $this->intersectTypes(static fn (Type $type): Type => $type->getKeysArrayFiltered($filterValueType, $strict));
1159: }
1160:
1161: public function getKeysArray(): Type
1162: {
1163: return $this->intersectTypes(static fn (Type $type): Type => $type->getKeysArray());
1164: }
1165:
1166: public function getValuesArray(): Type
1167: {
1168: $cb = static fn (Type $type): Type => $type->getValuesArray();
1169: if ($this->isList()->yes()) {
1170: return $this;
1171: }
1172: return $this->intersectTypes($cb);
1173: }
1174:
1175: public function chunkArray(Type $lengthType, TrinaryLogic $preserveKeys): Type
1176: {
1177: return $this->intersectTypes(static fn (Type $type): Type => $type->chunkArray($lengthType, $preserveKeys));
1178: }
1179:
1180: public function fillKeysArray(Type $valueType): Type
1181: {
1182: return $this->intersectTypes(static fn (Type $type): Type => $type->fillKeysArray($valueType));
1183: }
1184:
1185: public function flipArray(): Type
1186: {
1187: return $this->intersectTypes(static fn (Type $type): Type => $type->flipArray());
1188: }
1189:
1190: public function intersectKeyArray(Type $otherArraysType): Type
1191: {
1192: return $this->intersectTypesPreserveTemplateType(static fn (Type $type): Type => $type->intersectKeyArray($otherArraysType));
1193: }
1194:
1195: public function popArray(): Type
1196: {
1197: return $this->intersectTypesPreserveTemplateType(static fn (Type $type): Type => $type->popArray());
1198: }
1199:
1200: public function reverseArray(TrinaryLogic $preserveKeys): Type
1201: {
1202: return $this->intersectTypesPreserveTemplateType(static fn (Type $type): Type => $type->reverseArray($preserveKeys));
1203: }
1204:
1205: public function searchArray(Type $needleType, ?TrinaryLogic $strict = null): Type
1206: {
1207: return $this->intersectTypes(static fn (Type $type): Type => $type->searchArray($needleType, $strict));
1208: }
1209:
1210: public function shiftArray(): Type
1211: {
1212: return $this->intersectTypesPreserveTemplateType(static fn (Type $type): Type => $type->shiftArray());
1213: }
1214:
1215: public function shuffleArray(): Type
1216: {
1217: $cb = static fn (Type $type): Type => $type->shuffleArray();
1218: if ($this->isList()->yes()) {
1219: return $this->intersectTypesPreserveTemplateType($cb);
1220: }
1221: return $this->intersectTypes($cb);
1222: }
1223:
1224: public function sliceArray(Type $offsetType, Type $lengthType, TrinaryLogic $preserveKeys): Type
1225: {
1226: $result = $this->intersectTypesPreserveTemplateType(static fn (Type $type): Type => $type->sliceArray($offsetType, $lengthType, $preserveKeys));
1227:
1228: if (
1229: $this->isList()->yes()
1230: && $this->isIterableAtLeastOnce()->yes()
1231: && (new ConstantIntegerType(0))->isSuperTypeOf($offsetType)->yes()
1232: && IntegerRangeType::fromInterval(1, null)->isSuperTypeOf($lengthType)->yes()
1233: ) {
1234: $result = TypeCombinator::intersect($result, new NonEmptyArrayType());
1235: }
1236:
1237: return $result;
1238: }
1239:
1240: public function spliceArray(Type $offsetType, Type $lengthType, Type $replacementType): Type
1241: {
1242: return $this->intersectTypesPreserveTemplateType(static fn (Type $type): Type => $type->spliceArray($offsetType, $lengthType, $replacementType));
1243: }
1244:
1245: public function truncateListToSize(Type $sizeType): Type
1246: {
1247: return $this->intersectTypesPreserveTemplateType(static fn (Type $type): Type => $type->truncateListToSize($sizeType));
1248: }
1249:
1250: public function makeListMaybe(): Type
1251: {
1252: return $this->intersectTypes(static fn (Type $type): Type => $type->makeListMaybe());
1253: }
1254:
1255: public function mapValueType(callable $cb): Type
1256: {
1257: return $this->intersectTypesPreserveTemplateType(static fn (Type $type): Type => $type->mapValueType($cb));
1258: }
1259:
1260: public function mapKeyType(callable $cb): Type
1261: {
1262: return $this->intersectTypesPreserveTemplateType(static fn (Type $type): Type => $type->mapKeyType($cb));
1263: }
1264:
1265: public function makeAllArrayKeysOptional(): Type
1266: {
1267: return $this->intersectTypes(static fn (Type $type): Type => $type->makeAllArrayKeysOptional());
1268: }
1269:
1270: public function changeKeyCaseArray(?int $case): Type
1271: {
1272: return $this->intersectTypesPreserveTemplateType(static fn (Type $type): Type => $type->changeKeyCaseArray($case));
1273: }
1274:
1275: public function filterArrayRemovingFalsey(): Type
1276: {
1277: return $this->intersectTypesPreserveTemplateType(static fn (Type $type): Type => $type->filterArrayRemovingFalsey());
1278: }
1279:
1280: public function getEnumCases(): array
1281: {
1282: $compare = [];
1283: foreach ($this->types as $type) {
1284: $oneType = [];
1285: foreach ($type->getEnumCases() as $enumCase) {
1286: $oneType[$enumCase->getClassName() . '::' . $enumCase->getEnumCaseName()] = $enumCase;
1287: }
1288: $compare[] = $oneType;
1289: }
1290:
1291: return array_values(array_intersect_key(...$compare));
1292: }
1293:
1294: public function getEnumCaseObject(): ?EnumCaseObjectType
1295: {
1296: $singleCase = null;
1297: foreach ($this->types as $type) {
1298: $caseObject = $type->getEnumCaseObject();
1299: if ($caseObject === null) {
1300: continue;
1301: }
1302:
1303: if ($singleCase !== null) {
1304: return null;
1305: }
1306:
1307: $singleCase = $caseObject;
1308: }
1309:
1310: return $singleCase;
1311: }
1312:
1313: public function isCallable(): TrinaryLogic
1314: {
1315: return $this->isCallable ??= $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isCallable());
1316: }
1317:
1318: public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope): array
1319: {
1320: $yesAcceptors = [];
1321:
1322: foreach ($this->types as $type) {
1323: if (!$type->isCallable()->yes()) {
1324: continue;
1325: }
1326: $yesAcceptors[] = $type->getCallableParametersAcceptors($scope);
1327: }
1328:
1329: if (count($yesAcceptors) === 0) {
1330: if ($this->isCallable()->no()) {
1331: throw new ShouldNotHappenException();
1332: }
1333:
1334: return [new TrivialParametersAcceptor()];
1335: }
1336:
1337: $result = [];
1338: $combinations = CombinationsHelper::combinations($yesAcceptors);
1339: foreach ($combinations as $combination) {
1340: $combined = ParametersAcceptorSelector::combineAcceptors($combination);
1341: if (!$combined instanceof CallableParametersAcceptor) {
1342: throw new ShouldNotHappenException();
1343: }
1344: $result[] = $combined;
1345: }
1346:
1347: return $result;
1348: }
1349:
1350: public function isCloneable(): TrinaryLogic
1351: {
1352: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isCloneable());
1353: }
1354:
1355: public function isSmallerThan(Type $otherType, PhpVersion $phpVersion): TrinaryLogic
1356: {
1357: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isSmallerThan($otherType, $phpVersion));
1358: }
1359:
1360: public function isSmallerThanOrEqual(Type $otherType, PhpVersion $phpVersion): TrinaryLogic
1361: {
1362: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isSmallerThanOrEqual($otherType, $phpVersion));
1363: }
1364:
1365: public function isNull(): TrinaryLogic
1366: {
1367: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isNull());
1368: }
1369:
1370: public function isConstantValue(): TrinaryLogic
1371: {
1372: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isConstantValue());
1373: }
1374:
1375: public function isConstantScalarValue(): TrinaryLogic
1376: {
1377: return $this->isConstantScalarValue ??= $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isConstantScalarValue());
1378: }
1379:
1380: public function getConstantScalarTypes(): array
1381: {
1382: $scalarTypes = [];
1383: foreach ($this->types as $type) {
1384: foreach ($type->getConstantScalarTypes() as $scalarType) {
1385: $scalarTypes[] = $scalarType;
1386: }
1387: }
1388:
1389: return $scalarTypes;
1390: }
1391:
1392: public function getConstantScalarValues(): array
1393: {
1394: $values = [];
1395: foreach ($this->types as $type) {
1396: foreach ($type->getConstantScalarValues() as $value) {
1397: $values[] = $value;
1398: }
1399: }
1400:
1401: return $values;
1402: }
1403:
1404: public function isTrue(): TrinaryLogic
1405: {
1406: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isTrue());
1407: }
1408:
1409: public function isFalse(): TrinaryLogic
1410: {
1411: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isFalse());
1412: }
1413:
1414: public function isBoolean(): TrinaryLogic
1415: {
1416: return $this->isBoolean ??= $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isBoolean());
1417: }
1418:
1419: public function isFloat(): TrinaryLogic
1420: {
1421: return $this->isFloat ??= $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isFloat());
1422: }
1423:
1424: public function isInteger(): TrinaryLogic
1425: {
1426: return $this->isInteger ??= $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isInteger());
1427: }
1428:
1429: public function isGreaterThan(Type $otherType, PhpVersion $phpVersion): TrinaryLogic
1430: {
1431: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $otherType->isSmallerThan($type, $phpVersion));
1432: }
1433:
1434: public function isGreaterThanOrEqual(Type $otherType, PhpVersion $phpVersion): TrinaryLogic
1435: {
1436: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $otherType->isSmallerThanOrEqual($type, $phpVersion));
1437: }
1438:
1439: public function getSmallerType(PhpVersion $phpVersion): Type
1440: {
1441: return $this->intersectTypes(static fn (Type $type): Type => $type->getSmallerType($phpVersion));
1442: }
1443:
1444: public function getSmallerOrEqualType(PhpVersion $phpVersion): Type
1445: {
1446: return $this->intersectTypes(static fn (Type $type): Type => $type->getSmallerOrEqualType($phpVersion));
1447: }
1448:
1449: public function getGreaterType(PhpVersion $phpVersion): Type
1450: {
1451: return $this->intersectTypes(static fn (Type $type): Type => $type->getGreaterType($phpVersion));
1452: }
1453:
1454: public function getGreaterOrEqualType(PhpVersion $phpVersion): Type
1455: {
1456: return $this->intersectTypes(static fn (Type $type): Type => $type->getGreaterOrEqualType($phpVersion));
1457: }
1458:
1459: public function toBoolean(): BooleanType
1460: {
1461: $type = $this->intersectTypes(static fn (Type $type): BooleanType => $type->toBoolean());
1462:
1463: if (!$type instanceof BooleanType) {
1464: return new BooleanType();
1465: }
1466:
1467: return $type;
1468: }
1469:
1470: public function toNumber(): Type
1471: {
1472: $type = $this->intersectTypes(static fn (Type $type): Type => $type->toNumber());
1473:
1474: return $type;
1475: }
1476:
1477: public function toBitwiseNotType(): Type
1478: {
1479: return $this->intersectTypes(static fn (Type $type): Type => $type->toBitwiseNotType());
1480: }
1481:
1482: public function toGetClassResultType(): Type
1483: {
1484: return $this->intersectTypes(static fn (Type $type): Type => $type->toGetClassResultType());
1485: }
1486:
1487: public function toClassConstantType(ReflectionProvider $reflectionProvider): Type
1488: {
1489: return $this->intersectTypes(static fn (Type $type): Type => $type->toClassConstantType($reflectionProvider));
1490: }
1491:
1492: public function toObjectTypeForInstanceofCheck(): ClassNameToObjectTypeResult
1493: {
1494: $types = [];
1495: $uncertainty = false;
1496: foreach ($this->getTypes() as $innerType) {
1497: $result = $innerType->toObjectTypeForInstanceofCheck();
1498: $types[] = $result->type;
1499: if (!$result->uncertainty) {
1500: continue;
1501: }
1502:
1503: $uncertainty = true;
1504: }
1505:
1506: return new ClassNameToObjectTypeResult(TypeCombinator::intersect(...$types), $uncertainty);
1507: }
1508:
1509: public function toObjectTypeForIsACheck(Type $objectOrClassType, bool $allowString, bool $allowSameClass): ClassNameToObjectTypeResult
1510: {
1511: $types = [];
1512: $uncertainty = false;
1513: foreach ($this->getTypes() as $innerType) {
1514: $result = $innerType->toObjectTypeForIsACheck($objectOrClassType, $allowString, $allowSameClass);
1515: $types[] = $result->type;
1516: if (!$result->uncertainty) {
1517: continue;
1518: }
1519:
1520: $uncertainty = true;
1521: }
1522:
1523: return new ClassNameToObjectTypeResult(TypeCombinator::intersect(...$types), $uncertainty);
1524: }
1525:
1526: public function toAbsoluteNumber(): Type
1527: {
1528: $type = $this->intersectTypes(static fn (Type $type): Type => $type->toAbsoluteNumber());
1529:
1530: return $type;
1531: }
1532:
1533: public function toString(): Type
1534: {
1535: $type = $this->intersectTypes(static fn (Type $type): Type => $type->toString());
1536:
1537: return $type;
1538: }
1539:
1540: public function toInteger(): Type
1541: {
1542: $type = $this->intersectTypes(static fn (Type $type): Type => $type->toInteger());
1543:
1544: return $type;
1545: }
1546:
1547: public function toFloat(): Type
1548: {
1549: $type = $this->intersectTypes(static fn (Type $type): Type => $type->toFloat());
1550:
1551: return $type;
1552: }
1553:
1554: public function toArray(): Type
1555: {
1556: $type = $this->intersectTypes(static fn (Type $type): Type => $type->toArray());
1557:
1558: return $type;
1559: }
1560:
1561: public function toArrayKey(): Type
1562: {
1563: if ($this->isDecimalIntegerString()->yes()) {
1564: return new IntegerType();
1565: }
1566:
1567: if ($this->isNumericString()->yes()) {
1568: return TypeCombinator::union(
1569: new IntegerType(),
1570: $this,
1571: );
1572: }
1573:
1574: if ($this->isString()->yes()) {
1575: return $this;
1576: }
1577:
1578: return $this->intersectTypes(static fn (Type $type): Type => $type->toArrayKey());
1579: }
1580:
1581: public function toCoercedArgumentType(bool $strictTypes): Type
1582: {
1583: return $this->intersectTypes(static fn (Type $type): Type => $type->toCoercedArgumentType($strictTypes));
1584: }
1585:
1586: public function inferTemplateTypes(Type $receivedType): TemplateTypeMap
1587: {
1588: $types = TemplateTypeMap::createEmpty();
1589:
1590: foreach ($this->types as $type) {
1591: $types = $types->intersect($type->inferTemplateTypes($receivedType));
1592: }
1593:
1594: return $types;
1595: }
1596:
1597: public function getReferencedTemplateTypes(TemplateTypeVariance $positionVariance): array
1598: {
1599: $references = [];
1600:
1601: foreach ($this->types as $type) {
1602: foreach ($type->getReferencedTemplateTypes($positionVariance) as $reference) {
1603: $references[] = $reference;
1604: }
1605: }
1606:
1607: return $references;
1608: }
1609:
1610: public function traverse(callable $cb): Type
1611: {
1612: $types = [];
1613: $changed = false;
1614:
1615: foreach ($this->types as $type) {
1616: $newType = $cb($type);
1617: if ($type !== $newType) {
1618: $changed = true;
1619: }
1620: $types[] = $newType;
1621: }
1622:
1623: if ($changed) {
1624: $result = $types[0];
1625: for ($i = 1, $count = count($types); $i < $count; $i++) {
1626: $result = TypeCombinator::intersect($result, $types[$i]);
1627: }
1628: return $result;
1629: }
1630:
1631: return $this;
1632: }
1633:
1634: public function traverseSimultaneously(Type $right, callable $cb): Type
1635: {
1636: if ($this->isArray()->yes() && $right->isArray()->yes()) {
1637: $changed = false;
1638: $newTypes = [];
1639:
1640: foreach ($this->types as $innerType) {
1641: $newKeyType = $cb($innerType->getIterableKeyType(), $right->getIterableKeyType());
1642: $newValueType = $cb($innerType->getIterableValueType(), $right->getIterableValueType());
1643: if ($newKeyType === $innerType->getIterableKeyType() && $newValueType === $innerType->getIterableValueType()) {
1644: $newTypes[] = $innerType;
1645: continue;
1646: }
1647:
1648: $changed = true;
1649: $newTypes[] = TypeTraverser::map($innerType, static function (Type $type, callable $traverse) use ($innerType, $newKeyType, $newValueType): Type {
1650: if ($type === $innerType->getIterableKeyType()) {
1651: return $newKeyType;
1652: }
1653: if ($type === $innerType->getIterableValueType()) {
1654: return $newValueType;
1655: }
1656:
1657: return $traverse($type);
1658: });
1659: }
1660:
1661: if (!$changed) {
1662: return $this;
1663: }
1664:
1665: $result = $newTypes[0];
1666: for ($i = 1, $count = count($newTypes); $i < $count; $i++) {
1667: $result = TypeCombinator::intersect($result, $newTypes[$i]);
1668: }
1669: return $result;
1670: }
1671:
1672: return $this;
1673: }
1674:
1675: public function tryRemove(Type $typeToRemove): ?Type
1676: {
1677: return $this->intersectTypes(static fn (Type $type): Type => TypeCombinator::remove($type, $typeToRemove));
1678: }
1679:
1680: public function exponentiate(Type $exponent): Type
1681: {
1682: return $this->intersectTypes(static fn (Type $type): Type => $type->exponentiate($exponent));
1683: }
1684:
1685: public function getFiniteTypes(): array
1686: {
1687: $compare = [];
1688: foreach ($this->types as $type) {
1689: $oneType = [];
1690: foreach ($type->getFiniteTypes() as $finiteType) {
1691: if ($finiteType instanceof EnumCaseObjectType) {
1692: $oneType[$finiteType->getClassName() . '::' . $finiteType->getEnumCaseName()] = $finiteType;
1693: continue;
1694: }
1695: $oneType[$finiteType->describe(VerbosityLevel::typeOnly())] = $finiteType;
1696: }
1697: $compare[] = $oneType;
1698: }
1699:
1700: $result = array_values(array_intersect_key(...$compare));
1701:
1702: if (count($result) > InitializerExprTypeResolver::CALCULATE_SCALARS_LIMIT) {
1703: return [];
1704: }
1705:
1706: return $result;
1707: }
1708:
1709: /**
1710: * @param callable(Type $type): TrinaryLogic $getResult
1711: * @param (callable(Type $type): bool)|null $filter
1712: */
1713: private function intersectResults(
1714: callable $getResult,
1715: ?callable $filter = null,
1716: ): TrinaryLogic
1717: {
1718: $types = $this->types;
1719: if ($filter !== null) {
1720: $types = array_filter($types, $filter);
1721: }
1722: if (count($types) === 0) {
1723: return TrinaryLogic::createNo();
1724: }
1725:
1726: return TrinaryLogic::lazyMaxMin($types, $getResult);
1727: }
1728:
1729: /**
1730: * @param callable(Type $type): Type $getType
1731: */
1732: private function intersectTypes(callable $getType): Type
1733: {
1734: $operands = array_map($getType, $this->types);
1735: $result = $operands[0];
1736: for ($i = 1, $count = count($operands); $i < $count; $i++) {
1737: $result = TypeCombinator::intersect($result, $operands[$i]);
1738: }
1739: return $result;
1740: }
1741:
1742: /**
1743: * @param callable(Type $type): Type $getType
1744: */
1745: private function intersectTypesPreserveTemplateType(callable $getType): Type
1746: {
1747: return $this->intersectTypes(static function (Type $type) use ($getType): Type {
1748: if ($type instanceof TemplateType) {
1749: return $type;
1750: }
1751: return $getType($type);
1752: });
1753: }
1754:
1755: public function toPhpDocNode(): TypeNode
1756: {
1757: $baseTypes = [];
1758: $typesToDescribe = [];
1759: $skipTypeNames = [];
1760:
1761: $nonEmptyStr = false;
1762: $nonFalsyStr = false;
1763: $isList = $this->isList()->yes();
1764: $isArray = $this->isArray()->yes();
1765: $isNonEmptyArray = $this->isIterableAtLeastOnce()->yes();
1766: $describedTypes = [];
1767:
1768: foreach ($this->getSortedTypes() as $i => $type) {
1769: if ($type instanceof AccessoryNonEmptyStringType
1770: || $type instanceof AccessoryLiteralStringType
1771: || $type instanceof AccessoryNumericStringType
1772: || $type instanceof AccessoryNonFalsyStringType
1773: || $type instanceof AccessoryLowercaseStringType
1774: || $type instanceof AccessoryUppercaseStringType
1775: || $type instanceof AccessoryDecimalIntegerStringType
1776: ) {
1777: if ($type instanceof AccessoryNonFalsyStringType) {
1778: $nonFalsyStr = true;
1779: }
1780: if ($type instanceof AccessoryNonEmptyStringType) {
1781: $nonEmptyStr = true;
1782: }
1783: if ($nonEmptyStr && $nonFalsyStr) {
1784: // prevent redundant 'non-empty-string&non-falsy-string'
1785: foreach ($typesToDescribe as $key => $typeToDescribe) {
1786: if (!($typeToDescribe instanceof AccessoryNonEmptyStringType)) {
1787: continue;
1788: }
1789:
1790: unset($typesToDescribe[$key]);
1791: }
1792: }
1793:
1794: $typesToDescribe[$i] = $type;
1795: $skipTypeNames[] = 'string';
1796: continue;
1797: }
1798:
1799: if ($isList || $isArray) {
1800: if ($type instanceof ArrayType) {
1801: $keyType = $type->getKeyType();
1802: $valueType = $type->getItemType();
1803: if ($isList) {
1804: $isMixedValueType = $valueType instanceof MixedType && $valueType->describe(VerbosityLevel::precise()) === 'mixed' && !$valueType->isExplicitMixed();
1805: $identifierTypeNode = new IdentifierTypeNode($isNonEmptyArray ? 'non-empty-list' : 'list');
1806: if (!$isMixedValueType) {
1807: $describedTypes[$i] = new GenericTypeNode($identifierTypeNode, [
1808: $valueType->toPhpDocNode(),
1809: ]);
1810: } else {
1811: $describedTypes[$i] = $identifierTypeNode;
1812: }
1813: } else {
1814: $isMixedKeyType = $keyType instanceof MixedType && $keyType->describe(VerbosityLevel::precise()) === 'mixed' && !$keyType->isExplicitMixed();
1815: $isMixedValueType = $valueType instanceof MixedType && $valueType->describe(VerbosityLevel::precise()) === 'mixed' && !$valueType->isExplicitMixed();
1816: $identifierTypeNode = new IdentifierTypeNode($isNonEmptyArray ? 'non-empty-array' : 'array');
1817: if (!$isMixedKeyType) {
1818: $describedTypes[$i] = new GenericTypeNode($identifierTypeNode, [
1819: $keyType->toPhpDocNode(),
1820: $valueType->toPhpDocNode(),
1821: ]);
1822: } elseif (!$isMixedValueType) {
1823: $describedTypes[$i] = new GenericTypeNode($identifierTypeNode, [
1824: $valueType->toPhpDocNode(),
1825: ]);
1826: } else {
1827: $describedTypes[$i] = $identifierTypeNode;
1828: }
1829: }
1830: continue;
1831: } elseif ($type instanceof ConstantArrayType) {
1832: $constantArrayTypeNode = $type->toPhpDocNode();
1833: if ($constantArrayTypeNode instanceof ArrayShapeNode) {
1834: $newKind = $constantArrayTypeNode->kind;
1835: if ($isList) {
1836: if ($isNonEmptyArray && !$type->isIterableAtLeastOnce()->yes()) {
1837: $newKind = ArrayShapeNode::KIND_NON_EMPTY_LIST;
1838: } else {
1839: $newKind = ArrayShapeNode::KIND_LIST;
1840: }
1841: } elseif ($isNonEmptyArray && !$type->isIterableAtLeastOnce()->yes()) {
1842: $newKind = ArrayShapeNode::KIND_NON_EMPTY_ARRAY;
1843: }
1844:
1845: if ($newKind !== $constantArrayTypeNode->kind) {
1846: if ($constantArrayTypeNode->sealed) {
1847: $constantArrayTypeNode = ArrayShapeNode::createSealed($constantArrayTypeNode->items, $newKind);
1848: } else {
1849: $constantArrayTypeNode = ArrayShapeNode::createUnsealed($constantArrayTypeNode->items, $constantArrayTypeNode->unsealedType, $newKind);
1850: }
1851: }
1852:
1853: $describedTypes[$i] = $constantArrayTypeNode;
1854: continue;
1855: }
1856: }
1857: if ($type instanceof NonEmptyArrayType || $type instanceof AccessoryArrayListType) {
1858: continue;
1859: }
1860: }
1861:
1862: if (!$type instanceof AccessoryType) {
1863: $baseTypes[$i] = $type;
1864: continue;
1865: }
1866:
1867: $accessoryPhpDocNode = $type->toPhpDocNode();
1868: if ($accessoryPhpDocNode instanceof IdentifierTypeNode && $accessoryPhpDocNode->name === '') {
1869: continue;
1870: }
1871:
1872: $typesToDescribe[$i] = $type;
1873: }
1874:
1875: foreach ($baseTypes as $i => $type) {
1876: $typeNode = $type->toPhpDocNode();
1877: if ($typeNode instanceof GenericTypeNode && $typeNode->type->name === 'array') {
1878: $nonEmpty = false;
1879: $typeName = 'array';
1880: foreach ($typesToDescribe as $j => $typeToDescribe) {
1881: if ($typeToDescribe instanceof AccessoryArrayListType) {
1882: $typeName = 'list';
1883: if (count($typeNode->genericTypes) > 1) {
1884: array_shift($typeNode->genericTypes);
1885: }
1886: } elseif ($typeToDescribe instanceof NonEmptyArrayType) {
1887: $nonEmpty = true;
1888: } else {
1889: continue;
1890: }
1891:
1892: unset($typesToDescribe[$j]);
1893: }
1894:
1895: if ($nonEmpty) {
1896: $typeName = 'non-empty-' . $typeName;
1897: }
1898:
1899: $describedTypes[$i] = new GenericTypeNode(
1900: new IdentifierTypeNode($typeName),
1901: $typeNode->genericTypes,
1902: );
1903: continue;
1904: }
1905:
1906: if ($typeNode instanceof IdentifierTypeNode && in_array($typeNode->name, $skipTypeNames, true)) {
1907: continue;
1908: }
1909:
1910: $describedTypes[$i] = $typeNode;
1911: }
1912:
1913: foreach ($typesToDescribe as $i => $typeToDescribe) {
1914: $describedTypes[$i] = $typeToDescribe->toPhpDocNode();
1915: }
1916:
1917: ksort($describedTypes);
1918:
1919: $describedTypes = array_values($describedTypes);
1920:
1921: if (count($describedTypes) === 1) {
1922: return $describedTypes[0];
1923: }
1924:
1925: if (count($describedTypes) === 0) {
1926: throw new ShouldNotHappenException(sprintf('Intersection consists of %s but there should be at least one base type.', implode('&', array_map(static fn (Type $type) => $type->describe(VerbosityLevel::precise()), $this->types))));
1927: }
1928:
1929: return new IntersectionTypeNode($describedTypes);
1930: }
1931:
1932: public function hasTemplateOrLateResolvableType(): bool
1933: {
1934: foreach ($this->types as $type) {
1935: if (!$type->hasTemplateOrLateResolvableType()) {
1936: continue;
1937: }
1938:
1939: return true;
1940: }
1941:
1942: return false;
1943: }
1944:
1945: }
1946: