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