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