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