1: <?php declare(strict_types = 1);
2:
3: namespace PHPStan\Type\Constant;
4:
5: use Nette\Utils\Strings;
6: use PHPStan\Analyser\OutOfClassScope;
7: use PHPStan\Php\PhpVersion;
8: use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprIntegerNode;
9: use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprStringNode;
10: use PHPStan\PhpDocParser\Ast\Type\ArrayShapeItemNode;
11: use PHPStan\PhpDocParser\Ast\Type\ArrayShapeNode;
12: use PHPStan\PhpDocParser\Ast\Type\ConstTypeNode;
13: use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
14: use PHPStan\PhpDocParser\Ast\Type\TypeNode;
15: use PHPStan\Reflection\Callables\FunctionCallableVariant;
16: use PHPStan\Reflection\ClassMemberAccessAnswerer;
17: use PHPStan\Reflection\InaccessibleMethod;
18: use PHPStan\Reflection\InitializerExprTypeResolver;
19: use PHPStan\Reflection\PhpVersionStaticAccessor;
20: use PHPStan\Reflection\TrivialParametersAcceptor;
21: use PHPStan\Rules\Arrays\AllowedArrayKeysTypes;
22: use PHPStan\ShouldNotHappenException;
23: use PHPStan\TrinaryLogic;
24: use PHPStan\Type\AcceptsResult;
25: use PHPStan\Type\Accessory\AccessoryArrayListType;
26: use PHPStan\Type\Accessory\HasOffsetType;
27: use PHPStan\Type\Accessory\HasOffsetValueType;
28: use PHPStan\Type\Accessory\NonEmptyArrayType;
29: use PHPStan\Type\ArrayType;
30: use PHPStan\Type\BooleanType;
31: use PHPStan\Type\CompoundType;
32: use PHPStan\Type\ConstantScalarType;
33: use PHPStan\Type\ErrorType;
34: use PHPStan\Type\GeneralizePrecision;
35: use PHPStan\Type\Generic\TemplateType;
36: use PHPStan\Type\Generic\TemplateTypeMap;
37: use PHPStan\Type\Generic\TemplateTypeVariance;
38: use PHPStan\Type\IntegerRangeType;
39: use PHPStan\Type\IntersectionType;
40: use PHPStan\Type\IsSuperTypeOfResult;
41: use PHPStan\Type\MixedType;
42: use PHPStan\Type\NeverType;
43: use PHPStan\Type\NullType;
44: use PHPStan\Type\RecursionGuard;
45: use PHPStan\Type\StaticTypeFactory;
46: use PHPStan\Type\Traits\ArrayTypeTrait;
47: use PHPStan\Type\Traits\NonObjectTypeTrait;
48: use PHPStan\Type\Traits\UndecidedComparisonTypeTrait;
49: use PHPStan\Type\Type;
50: use PHPStan\Type\TypeCombinator;
51: use PHPStan\Type\UnionType;
52: use PHPStan\Type\VerbosityLevel;
53: use function array_keys;
54: use function array_map;
55: use function array_merge;
56: use function array_pop;
57: use function array_push;
58: use function array_slice;
59: use function array_unique;
60: use function array_values;
61: use function assert;
62: use function count;
63: use function implode;
64: use function in_array;
65: use function is_int;
66: use function is_string;
67: use function min;
68: use function pow;
69: use function range;
70: use function sort;
71: use function sprintf;
72: use function str_contains;
73: use function strtolower;
74: use function strtoupper;
75: use const CASE_LOWER;
76: use const CASE_UPPER;
77:
78: /**
79: * @api
80: */
81: class ConstantArrayType implements Type
82: {
83:
84: use ArrayTypeTrait {
85: chunkArray as traitChunkArray;
86: }
87: use NonObjectTypeTrait;
88: use UndecidedComparisonTypeTrait;
89:
90: private const DESCRIBE_LIMIT = 8;
91: private const CHUNK_FINITE_TYPES_LIMIT = 5;
92:
93: private TrinaryLogic $isList;
94:
95: /** @var self[]|null */
96: private ?array $allArrays = null;
97:
98: private ?Type $iterableKeyType = null;
99:
100: private ?Type $iterableValueType = null;
101:
102: /** @var array<int|string, int>|null */
103: private ?array $keyIndexMap = null;
104:
105: /**
106: * @api
107: * @param list<ConstantIntegerType|ConstantStringType> $keyTypes
108: * @param array<int, Type> $valueTypes
109: * @param list<int> $nextAutoIndexes
110: * @param int[] $optionalKeys
111: */
112: public function __construct(
113: private array $keyTypes,
114: private array $valueTypes,
115: private array $nextAutoIndexes = [0],
116: private array $optionalKeys = [],
117: ?TrinaryLogic $isList = null,
118: )
119: {
120: assert(count($keyTypes) === count($valueTypes));
121:
122: $keyTypesCount = count($this->keyTypes);
123: if ($keyTypesCount === 0) {
124: $isList = TrinaryLogic::createYes();
125: }
126:
127: if ($isList === null) {
128: $isList = TrinaryLogic::createNo();
129: }
130: $this->isList = $isList;
131: }
132:
133: /**
134: * @param list<ConstantIntegerType|ConstantStringType> $keyTypes
135: * @param array<int, Type> $valueTypes
136: * @param list<int> $nextAutoIndexes
137: * @param int[] $optionalKeys
138: */
139: protected function recreate(
140: array $keyTypes,
141: array $valueTypes,
142: array $nextAutoIndexes = [0],
143: array $optionalKeys = [],
144: ?TrinaryLogic $isList = null,
145: ): self
146: {
147: return new self($keyTypes, $valueTypes, $nextAutoIndexes, $optionalKeys, $isList);
148: }
149:
150: public function getConstantArrays(): array
151: {
152: return [$this];
153: }
154:
155: public function getReferencedClasses(): array
156: {
157: $referencedClasses = [];
158: foreach ($this->getKeyTypes() as $keyType) {
159: foreach ($keyType->getReferencedClasses() as $referencedClass) {
160: $referencedClasses[] = $referencedClass;
161: }
162: }
163:
164: foreach ($this->getValueTypes() as $valueType) {
165: foreach ($valueType->getReferencedClasses() as $referencedClass) {
166: $referencedClasses[] = $referencedClass;
167: }
168: }
169:
170: return $referencedClasses;
171: }
172:
173: public function getIterableKeyType(): Type
174: {
175: if ($this->iterableKeyType !== null) {
176: return $this->iterableKeyType;
177: }
178:
179: $keyTypesCount = count($this->keyTypes);
180: if ($keyTypesCount === 0) {
181: $keyType = new NeverType(true);
182: } elseif ($keyTypesCount === 1) {
183: $keyType = $this->keyTypes[0];
184: } else {
185: $keyType = new UnionType($this->keyTypes);
186: }
187:
188: return $this->iterableKeyType = $keyType;
189: }
190:
191: public function getIterableValueType(): Type
192: {
193: if ($this->iterableValueType !== null) {
194: return $this->iterableValueType;
195: }
196:
197: return $this->iterableValueType = count($this->valueTypes) > 0 ? TypeCombinator::union(...$this->valueTypes) : new NeverType(true);
198: }
199:
200: public function getKeyType(): Type
201: {
202: return $this->getIterableKeyType();
203: }
204:
205: public function getItemType(): Type
206: {
207: return $this->getIterableValueType();
208: }
209:
210: public function isConstantValue(): TrinaryLogic
211: {
212: return TrinaryLogic::createYes();
213: }
214:
215: /**
216: * @return list<int>
217: */
218: public function getNextAutoIndexes(): array
219: {
220: return $this->nextAutoIndexes;
221: }
222:
223: /**
224: * @return int[]
225: */
226: public function getOptionalKeys(): array
227: {
228: return $this->optionalKeys;
229: }
230:
231: /**
232: * @return self[]
233: */
234: public function getAllArrays(): array
235: {
236: if ($this->allArrays !== null) {
237: return $this->allArrays;
238: }
239:
240: if (count($this->optionalKeys) <= 10) {
241: $optionalKeysCombinations = $this->powerSet($this->optionalKeys);
242: } else {
243: $optionalKeysCombinations = [
244: [],
245: array_slice($this->optionalKeys, 0, 1, true),
246: array_slice($this->optionalKeys, -1, 1, true),
247: $this->optionalKeys,
248: ];
249: }
250:
251: $requiredKeys = [];
252: foreach (array_keys($this->keyTypes) as $i) {
253: if (in_array($i, $this->optionalKeys, true)) {
254: continue;
255: }
256: $requiredKeys[] = $i;
257: }
258:
259: $arrays = [];
260: foreach ($optionalKeysCombinations as $combination) {
261: $keys = array_merge($requiredKeys, $combination);
262: sort($keys);
263:
264: if ($this->isList->yes() && array_keys($keys) !== $keys) {
265: continue;
266: }
267:
268: $builder = ConstantArrayTypeBuilder::createEmpty();
269: $builder->disableArrayDegradation();
270: foreach ($keys as $i) {
271: $builder->setOffsetValueType($this->keyTypes[$i], $this->valueTypes[$i]);
272: }
273:
274: $array = $builder->getArray();
275: if (!$array instanceof self) {
276: throw new ShouldNotHappenException();
277: }
278:
279: $arrays[] = $array;
280: }
281:
282: return $this->allArrays = $arrays;
283: }
284:
285: /**
286: * @template T
287: * @param T[] $in
288: * @return T[][]
289: */
290: private function powerSet(array $in): array
291: {
292: $count = count($in);
293: $members = pow(2, $count);
294: $return = [];
295: for ($i = 0; $i < $members; $i++) {
296: $b = sprintf('%0' . $count . 'b', $i);
297: $out = [];
298: for ($j = 0; $j < $count; $j++) {
299: if ($b[$j] !== '1') {
300: continue;
301: }
302:
303: $out[] = $in[$j];
304: }
305: $return[] = $out;
306: }
307:
308: return $return;
309: }
310:
311: /**
312: * @return list<ConstantIntegerType|ConstantStringType>
313: */
314: public function getKeyTypes(): array
315: {
316: return $this->keyTypes;
317: }
318:
319: /**
320: * @return array<int, Type>
321: */
322: public function getValueTypes(): array
323: {
324: return $this->valueTypes;
325: }
326:
327: public function isOptionalKey(int $i): bool
328: {
329: return in_array($i, $this->optionalKeys, true);
330: }
331:
332: public function accepts(Type $type, bool $strictTypes): AcceptsResult
333: {
334: if ($type instanceof CompoundType && !$type instanceof IntersectionType) {
335: return $type->isAcceptedBy($this, $strictTypes);
336: }
337:
338: if ($type instanceof self && count($this->keyTypes) === 0) {
339: return AcceptsResult::createFromBoolean(count($type->keyTypes) === 0);
340: }
341:
342: $result = AcceptsResult::createYes();
343: foreach ($this->keyTypes as $i => $keyType) {
344: $valueType = $this->valueTypes[$i];
345: $hasOffsetValueType = $type->hasOffsetValueType($keyType);
346: $hasOffset = new AcceptsResult(
347: $hasOffsetValueType,
348: $hasOffsetValueType->yes() || !$type->isConstantArray()->yes() ? [] : [sprintf('Array %s have offset %s.', $hasOffsetValueType->no() ? 'does not' : 'might not', $keyType->describe(VerbosityLevel::value()))],
349: );
350: if ($hasOffset->no()) {
351: if ($this->isOptionalKey($i)) {
352: continue;
353: }
354: return $hasOffset;
355: }
356: if ($hasOffset->maybe() && $this->isOptionalKey($i)) {
357: $hasOffset = AcceptsResult::createYes();
358: }
359:
360: $result = $result->and($hasOffset);
361: $otherValueType = $type->getOffsetValueType($keyType);
362: $verbosity = VerbosityLevel::getRecommendedLevelByType($valueType, $otherValueType);
363: $acceptsValue = $valueType->accepts($otherValueType, $strictTypes)->decorateReasons(
364: static fn (string $reason) => sprintf(
365: 'Offset %s (%s) does not accept type %s: %s',
366: $keyType->describe(VerbosityLevel::precise()),
367: $valueType->describe($verbosity),
368: $otherValueType->describe($verbosity),
369: $reason,
370: ),
371: );
372: if (!$acceptsValue->yes() && count($acceptsValue->reasons) === 0 && $type->isConstantArray()->yes()) {
373: $acceptsValue = new AcceptsResult($acceptsValue->result, [
374: sprintf(
375: 'Offset %s (%s) does not accept type %s.',
376: $keyType->describe(VerbosityLevel::precise()),
377: $valueType->describe($verbosity),
378: $otherValueType->describe($verbosity),
379: ),
380: ]);
381: }
382: if ($acceptsValue->no()) {
383: return $acceptsValue;
384: }
385: $result = $result->and($acceptsValue);
386: }
387:
388: $result = $result->and(new AcceptsResult($type->isArray(), []));
389: if ($type->isOversizedArray()->yes()) {
390: if (!$result->no()) {
391: return AcceptsResult::createYes();
392: }
393: }
394:
395: return $result;
396: }
397:
398: public function isSuperTypeOf(Type $type): IsSuperTypeOfResult
399: {
400: if ($type instanceof self) {
401: if (count($this->keyTypes) === 0) {
402: return new IsSuperTypeOfResult($type->isIterableAtLeastOnce()->negate(), []);
403: }
404:
405: $results = [];
406: foreach ($this->keyTypes as $i => $keyType) {
407: $hasOffset = $type->hasOffsetValueType($keyType);
408: if ($hasOffset->no()) {
409: if (!$this->isOptionalKey($i)) {
410: return IsSuperTypeOfResult::createNo();
411: }
412:
413: $results[] = IsSuperTypeOfResult::createYes();
414: continue;
415: } elseif ($hasOffset->maybe() && !$this->isOptionalKey($i)) {
416: $results[] = IsSuperTypeOfResult::createMaybe();
417: }
418:
419: $isValueSuperType = $this->valueTypes[$i]->isSuperTypeOf($type->getOffsetValueType($keyType));
420: if ($isValueSuperType->no()) {
421: return $isValueSuperType->decorateReasons(static fn (string $reason) => sprintf('Offset %s: %s', $keyType->describe(VerbosityLevel::value()), $reason));
422: }
423: $results[] = $isValueSuperType;
424: }
425:
426: return IsSuperTypeOfResult::createYes()->and(...$results);
427: }
428:
429: if ($type instanceof ArrayType) {
430: $result = IsSuperTypeOfResult::createMaybe();
431: if (count($this->keyTypes) === 0) {
432: return $result;
433: }
434:
435: $isKeySuperType = $this->getKeyType()->isSuperTypeOf($type->getKeyType());
436: if ($isKeySuperType->no()) {
437: return $isKeySuperType;
438: }
439:
440: return $result->and($isKeySuperType, $this->getItemType()->isSuperTypeOf($type->getItemType()));
441: }
442:
443: if ($type instanceof CompoundType) {
444: return $type->isSubTypeOf($this);
445: }
446:
447: return IsSuperTypeOfResult::createNo();
448: }
449:
450: public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType
451: {
452: if ($type->isInteger()->yes()) {
453: return new ConstantBooleanType(false);
454: }
455:
456: if ($this->isIterableAtLeastOnce()->no()) {
457: if ($type->isIterableAtLeastOnce()->yes()) {
458: return new ConstantBooleanType(false);
459: }
460:
461: $constantScalarValues = $type->getConstantScalarValues();
462: if (count($constantScalarValues) > 0) {
463: $results = [];
464: foreach ($constantScalarValues as $constantScalarValue) {
465: // @phpstan-ignore equal.invalid, equal.notAllowed
466: $results[] = TrinaryLogic::createFromBoolean($constantScalarValue == []); // phpcs:ignore
467: }
468:
469: return TrinaryLogic::extremeIdentity(...$results)->toBooleanType();
470: }
471: }
472:
473: return new BooleanType();
474: }
475:
476: public function equals(Type $type): bool
477: {
478: if (!$type instanceof self) {
479: return false;
480: }
481:
482: if (count($this->keyTypes) !== count($type->keyTypes)) {
483: return false;
484: }
485:
486: foreach ($this->keyTypes as $i => $keyType) {
487: $valueType = $this->valueTypes[$i];
488: if (!$valueType->equals($type->valueTypes[$i])) {
489: return false;
490: }
491: if (!$keyType->equals($type->keyTypes[$i])) {
492: return false;
493: }
494: }
495:
496: if ($this->optionalKeys !== $type->optionalKeys) {
497: return false;
498: }
499:
500: return true;
501: }
502:
503: public function isCallable(): TrinaryLogic
504: {
505: $result = RecursionGuard::run($this, function (): TrinaryLogic {
506: $hasNonExistentMethod = false;
507: $typeAndMethods = $this->doFindTypeAndMethodNames($hasNonExistentMethod);
508: if ($typeAndMethods === []) {
509: return TrinaryLogic::createNo();
510: }
511:
512: $results = array_map(
513: static fn (ConstantArrayTypeAndMethod $typeAndMethod): TrinaryLogic => $typeAndMethod->getCertainty(),
514: $typeAndMethods,
515: );
516:
517: $result = TrinaryLogic::createYes()->and(...$results);
518:
519: if ($hasNonExistentMethod) {
520: $result = $result->and(TrinaryLogic::createMaybe());
521: }
522:
523: return $result;
524: });
525:
526: if ($result instanceof ErrorType) {
527: return TrinaryLogic::createNo();
528: }
529:
530: return $result;
531: }
532:
533: public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope): array
534: {
535: $typeAndMethodNames = $this->findTypeAndMethodNames();
536: if ($typeAndMethodNames === []) {
537: throw new ShouldNotHappenException();
538: }
539:
540: $acceptors = [];
541: foreach ($typeAndMethodNames as $typeAndMethodName) {
542: if ($typeAndMethodName->isUnknown() || !$typeAndMethodName->getCertainty()->yes()) {
543: $acceptors[] = new TrivialParametersAcceptor();
544: continue;
545: }
546:
547: $method = $typeAndMethodName->getType()
548: ->getMethod($typeAndMethodName->getMethod(), $scope);
549:
550: if (!$scope->canCallMethod($method)) {
551: $acceptors[] = new InaccessibleMethod($method);
552: continue;
553: }
554:
555: array_push($acceptors, ...FunctionCallableVariant::createFromVariants($method, $method->getVariants()));
556: }
557:
558: return $acceptors;
559: }
560:
561: /** @return ConstantArrayTypeAndMethod[] */
562: public function findTypeAndMethodNames(): array
563: {
564: return $this->doFindTypeAndMethodNames();
565: }
566:
567: /** @return ConstantArrayTypeAndMethod[] */
568: private function doFindTypeAndMethodNames(bool &$hasNonExistentMethod = false): array
569: {
570: if (count($this->keyTypes) !== 2) {
571: return [];
572: }
573:
574: $classOrObject = null;
575: $method = null;
576: foreach ($this->keyTypes as $i => $keyType) {
577: if ($keyType->isSuperTypeOf(new ConstantIntegerType(0))->yes()) {
578: $classOrObject = $this->valueTypes[$i];
579: continue;
580: }
581:
582: if (!$keyType->isSuperTypeOf(new ConstantIntegerType(1))->yes()) {
583: continue;
584: }
585:
586: $method = $this->valueTypes[$i];
587: }
588:
589: if ($classOrObject === null || $method === null) {
590: return [];
591: }
592:
593: $callableArray = [$classOrObject, $method];
594:
595: [$classOrObject, $methods] = $callableArray;
596: if (count($methods->getConstantStrings()) === 0) {
597: return [ConstantArrayTypeAndMethod::createUnknown()];
598: }
599:
600: $type = $classOrObject->getObjectTypeOrClassStringObjectType();
601: if (!$type->isObject()->yes()) {
602: return [ConstantArrayTypeAndMethod::createUnknown()];
603: }
604:
605: $typeAndMethods = [];
606: $phpVersion = PhpVersionStaticAccessor::getInstance();
607: foreach ($methods->getConstantStrings() as $methodName) {
608: $has = $type->hasMethod($methodName->getValue());
609: if ($has->no()) {
610: $hasNonExistentMethod = true;
611: continue;
612: }
613:
614: if (
615: $has->yes()
616: && !$phpVersion->supportsCallableInstanceMethods()
617: ) {
618: $isString = $classOrObject->isString();
619: if ($isString->yes()) {
620: $methodReflection = $type->getMethod($methodName->getValue(), new OutOfClassScope());
621:
622: if (!$methodReflection->isStatic()) {
623: continue;
624: }
625: } elseif ($isString->maybe()) {
626: $has = $has->and(TrinaryLogic::createMaybe());
627: }
628: }
629:
630: if ($this->isOptionalKey(0) || $this->isOptionalKey(1)) {
631: $has = $has->and(TrinaryLogic::createMaybe());
632: }
633:
634: $typeAndMethods[] = ConstantArrayTypeAndMethod::createConcrete($type, $methodName->getValue(), $has);
635: }
636:
637: return $typeAndMethods;
638: }
639:
640: public function hasOffsetValueType(Type $offsetType): TrinaryLogic
641: {
642: $offsetArrayKeyType = $offsetType->toArrayKey();
643: if ($offsetArrayKeyType instanceof ErrorType) {
644: $allowedArrayKeys = AllowedArrayKeysTypes::getType();
645: $offsetArrayKeyType = TypeCombinator::intersect($allowedArrayKeys, $offsetType)->toArrayKey();
646: if ($offsetArrayKeyType instanceof NeverType) {
647: return TrinaryLogic::createNo();
648: }
649: }
650:
651: return $this->recursiveHasOffsetValueType($offsetArrayKeyType);
652: }
653:
654: private function recursiveHasOffsetValueType(Type $offsetType): TrinaryLogic
655: {
656: if ($offsetType instanceof UnionType) {
657: $results = [];
658: foreach ($offsetType->getTypes() as $innerType) {
659: $results[] = $this->recursiveHasOffsetValueType($innerType);
660: }
661:
662: return TrinaryLogic::extremeIdentity(...$results);
663: }
664: if ($offsetType instanceof IntegerRangeType) {
665: $finiteTypes = $offsetType->getFiniteTypes();
666: if ($finiteTypes !== []) {
667: $results = [];
668: foreach ($finiteTypes as $innerType) {
669: $results[] = $this->recursiveHasOffsetValueType($innerType);
670: }
671:
672: return TrinaryLogic::extremeIdentity(...$results);
673: }
674: }
675:
676: $result = TrinaryLogic::createNo();
677: foreach ($this->keyTypes as $i => $keyType) {
678: if (
679: $keyType instanceof ConstantIntegerType
680: && !$offsetType->isString()->no()
681: && $offsetType->isConstantScalarValue()->no()
682: ) {
683: return TrinaryLogic::createMaybe();
684: }
685:
686: $has = $keyType->isSuperTypeOf($offsetType);
687: if ($has->yes()) {
688: if ($this->isOptionalKey($i)) {
689: return TrinaryLogic::createMaybe();
690: }
691: return TrinaryLogic::createYes();
692: }
693: if (!$has->maybe()) {
694: continue;
695: }
696:
697: $result = TrinaryLogic::createMaybe();
698: }
699:
700: return $result;
701: }
702:
703: public function getOffsetValueType(Type $offsetType): Type
704: {
705: if (count($this->keyTypes) === 0) {
706: return new ErrorType();
707: }
708:
709: $offsetType = $offsetType->toArrayKey();
710: $matchingValueTypes = [];
711: $all = true;
712: $maybeAll = true;
713: foreach ($this->keyTypes as $i => $keyType) {
714: if ($keyType->isSuperTypeOf($offsetType)->no()) {
715: $all = false;
716:
717: if (
718: $keyType instanceof ConstantIntegerType
719: && !$offsetType->isString()->no()
720: && $offsetType->isConstantScalarValue()->no()
721: ) {
722: continue;
723: }
724: $maybeAll = false;
725: continue;
726: }
727:
728: $matchingValueTypes[] = $this->valueTypes[$i];
729: }
730:
731: if ($all) {
732: return $this->getIterableValueType();
733: }
734:
735: if (count($matchingValueTypes) > 0) {
736: $type = TypeCombinator::union(...$matchingValueTypes);
737: if ($type instanceof ErrorType) {
738: return new MixedType();
739: }
740:
741: return $type;
742: }
743:
744: if ($maybeAll) {
745: return $this->getIterableValueType();
746: }
747:
748: return new ErrorType(); // undefined offset
749: }
750:
751: public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $unionValues = true): Type
752: {
753: if ($offsetType === null && count($this->nextAutoIndexes) === 0) {
754: return new ErrorType();
755: }
756:
757: $builder = ConstantArrayTypeBuilder::createFromConstantArray($this);
758: $builder->setOffsetValueType($offsetType, $valueType);
759:
760: return $builder->getArray();
761: }
762:
763: public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type
764: {
765: $builder = ConstantArrayTypeBuilder::createFromConstantArray($this);
766: $builder->setOffsetValueType($offsetType, $valueType);
767:
768: return $builder->getArray();
769: }
770:
771: /**
772: * Removes or marks as optional the key(s) matching the given offset type from this constant array.
773: *
774: * By default, the method assumes an actual `unset()` call was made, which actively modifies the
775: * array and weakens its list certainty to "maybe". However, in some contexts, such as the else
776: * branch of an array_key_exists() check, the key is statically known to be absent without any
777: * modification, so list certainty should be preserved as-is.
778: */
779: public function unsetOffset(Type $offsetType, bool $preserveListCertainty = false): Type
780: {
781: $offsetType = $offsetType->toArrayKey();
782: if ($offsetType instanceof ConstantIntegerType || $offsetType instanceof ConstantStringType) {
783: foreach ($this->keyTypes as $i => $keyType) {
784: if ($keyType->getValue() !== $offsetType->getValue()) {
785: continue;
786: }
787:
788: $keyTypes = $this->keyTypes;
789: unset($keyTypes[$i]);
790: $valueTypes = $this->valueTypes;
791: unset($valueTypes[$i]);
792:
793: $newKeyTypes = [];
794: $newValueTypes = [];
795: $newOptionalKeys = [];
796:
797: $k = 0;
798: foreach ($keyTypes as $j => $newKeyType) {
799: $newKeyTypes[] = $newKeyType;
800: $newValueTypes[] = $valueTypes[$j];
801: if (in_array($j, $this->optionalKeys, true)) {
802: $newOptionalKeys[] = $k;
803: }
804: $k++;
805: }
806:
807: $newIsList = self::isListAfterUnset(
808: $newKeyTypes,
809: $newOptionalKeys,
810: $this->isList,
811: in_array($i, $this->optionalKeys, true),
812: );
813: if (!$preserveListCertainty) {
814: $newIsList = $newIsList->and(TrinaryLogic::createMaybe());
815: } elseif ($this->isList->yes() && $newIsList->no()) {
816: return new NeverType();
817: }
818:
819: return $this->recreate($newKeyTypes, $newValueTypes, $this->nextAutoIndexes, $newOptionalKeys, $newIsList);
820: }
821:
822: return $this;
823: }
824:
825: $constantScalars = $offsetType->getConstantScalarTypes();
826: if (count($constantScalars) > 0) {
827: $optionalKeys = $this->optionalKeys;
828:
829: $arrayHasChanged = false;
830: foreach ($constantScalars as $constantScalar) {
831: $constantScalar = $constantScalar->toArrayKey();
832: if (!$constantScalar instanceof ConstantIntegerType && !$constantScalar instanceof ConstantStringType) {
833: continue;
834: }
835:
836: foreach ($this->keyTypes as $i => $keyType) {
837: if ($keyType->getValue() !== $constantScalar->getValue()) {
838: continue;
839: }
840:
841: $arrayHasChanged = true;
842: if (in_array($i, $optionalKeys, true)) {
843: continue 2;
844: }
845:
846: $optionalKeys[] = $i;
847: }
848: }
849:
850: if (!$arrayHasChanged) {
851: return $this;
852: }
853:
854: $newIsList = self::isListAfterUnset(
855: $this->keyTypes,
856: $optionalKeys,
857: $this->isList,
858: count($optionalKeys) === count($this->optionalKeys),
859: );
860: if (!$preserveListCertainty) {
861: $newIsList = $newIsList->and(TrinaryLogic::createMaybe());
862: } elseif ($this->isList->yes() && $newIsList->no()) {
863: return new NeverType();
864: }
865:
866: return $this->recreate($this->keyTypes, $this->valueTypes, $this->nextAutoIndexes, $optionalKeys, $newIsList);
867: }
868:
869: $optionalKeys = $this->optionalKeys;
870: $arrayHasChanged = false;
871: foreach ($this->keyTypes as $i => $keyType) {
872: if (!$offsetType->isSuperTypeOf($keyType)->yes()) {
873: continue;
874: }
875: $arrayHasChanged = true;
876: $optionalKeys[] = $i;
877: }
878: $optionalKeys = array_values(array_unique($optionalKeys));
879:
880: if (!$arrayHasChanged) {
881: return $this;
882: }
883:
884: $newIsList = self::isListAfterUnset(
885: $this->keyTypes,
886: $optionalKeys,
887: $this->isList,
888: count($optionalKeys) === count($this->optionalKeys),
889: );
890: if (!$preserveListCertainty) {
891: $newIsList = $newIsList->and(TrinaryLogic::createMaybe());
892: } elseif ($this->isList->yes() && $newIsList->no()) {
893: return new NeverType();
894: }
895:
896: return $this->recreate($this->keyTypes, $this->valueTypes, $this->nextAutoIndexes, $optionalKeys, $newIsList);
897: }
898:
899: /**
900: * When we're unsetting something not on the array, it will be untouched,
901: * So the nextAutoIndexes won't change, and the array might still be a list even with PHPStan definition.
902: *
903: * @param list<ConstantIntegerType|ConstantStringType> $newKeyTypes
904: * @param int[] $newOptionalKeys
905: */
906: private static function isListAfterUnset(array $newKeyTypes, array $newOptionalKeys, TrinaryLogic $arrayIsList, bool $unsetOptionalKey): TrinaryLogic
907: {
908: if (!$unsetOptionalKey || $arrayIsList->no()) {
909: return TrinaryLogic::createNo();
910: }
911:
912: $isListOnlyIfKeysAreOptional = false;
913: foreach ($newKeyTypes as $k2 => $newKeyType2) {
914: if (!$newKeyType2 instanceof ConstantIntegerType || $newKeyType2->getValue() !== $k2) {
915: // We found a non-optional key that implies that the array is never a list.
916: if (!in_array($k2, $newOptionalKeys, true)) {
917: return TrinaryLogic::createNo();
918: }
919:
920: // The array can still be a list if all the following keys are also optional.
921: $isListOnlyIfKeysAreOptional = true;
922: continue;
923: }
924:
925: if ($isListOnlyIfKeysAreOptional && !in_array($k2, $newOptionalKeys, true)) {
926: return TrinaryLogic::createNo();
927: }
928: }
929:
930: return $arrayIsList;
931: }
932:
933: public function chunkArray(Type $lengthType, TrinaryLogic $preserveKeys): Type
934: {
935: $biggerOne = IntegerRangeType::fromInterval(1, null);
936: $finiteTypes = $lengthType->getFiniteTypes();
937: if ($biggerOne->isSuperTypeOf($lengthType)->yes() && count($finiteTypes) < self::CHUNK_FINITE_TYPES_LIMIT) {
938: $results = [];
939: foreach ($finiteTypes as $finiteType) {
940: if (!$finiteType instanceof ConstantIntegerType || $finiteType->getValue() < 1) {
941: return $this->traitChunkArray($lengthType, $preserveKeys);
942: }
943:
944: $length = $finiteType->getValue();
945:
946: $builder = ConstantArrayTypeBuilder::createEmpty();
947:
948: $keyTypesCount = count($this->keyTypes);
949: for ($i = 0; $i < $keyTypesCount; $i += $length) {
950: $chunk = $this->sliceArray(new ConstantIntegerType($i), new ConstantIntegerType($length), TrinaryLogic::createYes());
951: $builder->setOffsetValueType(null, $preserveKeys->yes() ? $chunk : $chunk->getValuesArray());
952: }
953:
954: $results[] = $builder->getArray();
955: }
956:
957: return TypeCombinator::union(...$results);
958: }
959:
960: return $this->traitChunkArray($lengthType, $preserveKeys);
961: }
962:
963: public function fillKeysArray(Type $valueType): Type
964: {
965: $builder = ConstantArrayTypeBuilder::createEmpty();
966:
967: foreach ($this->valueTypes as $i => $keyType) {
968: if ($keyType->isInteger()->no()) {
969: $stringKeyType = $keyType->toString();
970: if ($stringKeyType instanceof ErrorType) {
971: return $stringKeyType;
972: }
973:
974: $builder->setOffsetValueType($stringKeyType, $valueType, $this->isOptionalKey($i));
975: } else {
976: $builder->setOffsetValueType($keyType, $valueType, $this->isOptionalKey($i));
977: }
978: }
979:
980: return $builder->getArray();
981: }
982:
983: public function flipArray(): Type
984: {
985: $builder = ConstantArrayTypeBuilder::createEmpty();
986:
987: foreach ($this->keyTypes as $i => $keyType) {
988: $valueType = $this->valueTypes[$i];
989: $builder->setOffsetValueType(
990: $valueType->toArrayKey(),
991: $keyType,
992: $this->isOptionalKey($i),
993: );
994: }
995:
996: return $builder->getArray();
997: }
998:
999: public function intersectKeyArray(Type $otherArraysType): Type
1000: {
1001: $builder = ConstantArrayTypeBuilder::createEmpty();
1002:
1003: foreach ($this->keyTypes as $i => $keyType) {
1004: $valueType = $this->valueTypes[$i];
1005: $has = $otherArraysType->hasOffsetValueType($keyType);
1006: if ($has->no()) {
1007: continue;
1008: }
1009: $builder->setOffsetValueType($keyType, $valueType, $this->isOptionalKey($i) || !$has->yes());
1010: }
1011:
1012: return $builder->getArray();
1013: }
1014:
1015: public function popArray(): Type
1016: {
1017: return $this->removeLastElements(1);
1018: }
1019:
1020: public function reverseArray(TrinaryLogic $preserveKeys): Type
1021: {
1022: $builder = ConstantArrayTypeBuilder::createEmpty();
1023:
1024: for ($i = count($this->keyTypes) - 1; $i >= 0; $i--) {
1025: $offsetType = $preserveKeys->yes() || $this->keyTypes[$i]->isInteger()->no()
1026: ? $this->keyTypes[$i]
1027: : null;
1028: $builder->setOffsetValueType($offsetType, $this->valueTypes[$i], $this->isOptionalKey($i));
1029: }
1030:
1031: return $builder->getArray();
1032: }
1033:
1034: public function searchArray(Type $needleType, ?TrinaryLogic $strict = null): Type
1035: {
1036: $strict ??= TrinaryLogic::createMaybe();
1037: $matches = [];
1038: $hasIdenticalValue = false;
1039:
1040: foreach ($this->valueTypes as $index => $valueType) {
1041: if ($strict->yes()) {
1042: $isNeedleSuperType = $valueType->isSuperTypeOf($needleType);
1043: if ($isNeedleSuperType->no()) {
1044: continue;
1045: }
1046: }
1047:
1048: if ($needleType instanceof ConstantScalarType && $valueType instanceof ConstantScalarType) {
1049: // @phpstan-ignore equal.notAllowed
1050: $isLooseEqual = $needleType->getValue() == $valueType->getValue(); // phpcs:ignore
1051: if (!$isLooseEqual) {
1052: continue;
1053: }
1054: if (
1055: ($strict->no() || $needleType->getValue() === $valueType->getValue())
1056: && !$this->isOptionalKey($index)
1057: ) {
1058: $hasIdenticalValue = true;
1059: }
1060: }
1061:
1062: $matches[] = $this->keyTypes[$index];
1063: }
1064:
1065: if (count($matches) > 0) {
1066: if ($hasIdenticalValue) {
1067: return TypeCombinator::union(...$matches);
1068: }
1069:
1070: return TypeCombinator::union(new ConstantBooleanType(false), ...$matches);
1071: }
1072:
1073: return new ConstantBooleanType(false);
1074: }
1075:
1076: public function shiftArray(): Type
1077: {
1078: return $this->removeFirstElements(1);
1079: }
1080:
1081: public function shuffleArray(): Type
1082: {
1083: return $this->getValuesArray()->degradeToGeneralArray();
1084: }
1085:
1086: public function sliceArray(Type $offsetType, Type $lengthType, TrinaryLogic $preserveKeys): Type
1087: {
1088: $keyTypesCount = count($this->keyTypes);
1089: if ($keyTypesCount === 0) {
1090: return $this;
1091: }
1092:
1093: $offset = $offsetType instanceof ConstantIntegerType ? $offsetType->getValue() : null;
1094:
1095: if ($lengthType instanceof ConstantIntegerType) {
1096: $length = $lengthType->getValue();
1097: } elseif ($lengthType->isNull()->yes()) {
1098: $length = $keyTypesCount;
1099: } else {
1100: $length = null;
1101: }
1102:
1103: if ($offset === null || $length === null) {
1104: return $this->degradeToGeneralArray()
1105: ->sliceArray($offsetType, $lengthType, $preserveKeys);
1106: }
1107:
1108: if ($keyTypesCount + $offset <= 0) {
1109: // A negative offset cannot reach left outside the array twice
1110: $offset = 0;
1111: }
1112:
1113: if ($keyTypesCount + $length <= 0) {
1114: // A negative length cannot reach left outside the array twice
1115: $length = 0;
1116: }
1117:
1118: if ($length === 0 || ($offset < 0 && $length < 0 && $offset - $length >= 0)) {
1119: // 0 / 0, 3 / 0 or e.g. -3 / -3 or -3 / -4 and so on never extract anything
1120: return $this->recreate([], []);
1121: }
1122:
1123: if ($length < 0) {
1124: // Negative lengths prevent access to the most right n elements
1125: return $this->removeLastElements($length * -1)
1126: ->sliceArray($offsetType, new NullType(), $preserveKeys);
1127: }
1128:
1129: if ($offset < 0) {
1130: /*
1131: * Transforms the problem with the negative offset in one with a positive offset using array reversion.
1132: * The reason is below handling of optional keys which works only from left to right.
1133: *
1134: * e.g.
1135: * array{a: 0, b: 1, c: 2, d: 3, e: 4}
1136: * with offset -4 and length 2 (which would be sliced to array{b: 1, c: 2})
1137: *
1138: * is transformed via reversion to
1139: *
1140: * array{e: 4, d: 3, c: 2, b: 1, a: 0}
1141: * with offset 2 and length 2 (which will be sliced to array{c: 2, b: 1} and then reversed again)
1142: */
1143: $offset *= -1;
1144: $reversedLength = min($length, $offset);
1145: $reversedOffset = $offset - $reversedLength;
1146: return $this->reverseArray(TrinaryLogic::createYes())
1147: ->sliceArray(new ConstantIntegerType($reversedOffset), new ConstantIntegerType($reversedLength), $preserveKeys)
1148: ->reverseArray(TrinaryLogic::createYes());
1149: }
1150:
1151: if ($offset > 0) {
1152: return $this->removeFirstElements($offset, false)
1153: ->sliceArray(new ConstantIntegerType(0), $lengthType, $preserveKeys);
1154: }
1155:
1156: $builder = ConstantArrayTypeBuilder::createEmpty();
1157:
1158: $nonOptionalElementsCount = 0;
1159: $hasOptional = false;
1160: for ($i = 0; $nonOptionalElementsCount < $length && $i < $keyTypesCount; $i++) {
1161: $isOptional = $this->isOptionalKey($i);
1162: if (!$isOptional) {
1163: $nonOptionalElementsCount++;
1164: } else {
1165: $hasOptional = true;
1166: }
1167:
1168: $isLastElement = $nonOptionalElementsCount >= $length || $i + 1 >= $keyTypesCount;
1169: if ($isLastElement && $length < $keyTypesCount && $hasOptional) {
1170: // If the slice is not full yet, but has at least one optional key
1171: // the last non-optional element is going to be optional.
1172: // Otherwise, it would not fit into the slice if previous non-optional keys are there.
1173: $isOptional = true;
1174: }
1175:
1176: $offsetType = $preserveKeys->yes() || $this->keyTypes[$i]->isInteger()->no()
1177: ? $this->keyTypes[$i]
1178: : null;
1179:
1180: $builder->setOffsetValueType($offsetType, $this->valueTypes[$i], $isOptional);
1181: }
1182:
1183: return $builder->getArray();
1184: }
1185:
1186: public function spliceArray(Type $offsetType, Type $lengthType, Type $replacementType): Type
1187: {
1188: $keyTypesCount = count($this->keyTypes);
1189: if ($keyTypesCount === 0) {
1190: return $this;
1191: }
1192:
1193: $offset = $offsetType instanceof ConstantIntegerType ? $offsetType->getValue() : null;
1194:
1195: if ($lengthType instanceof ConstantIntegerType) {
1196: $length = $lengthType->getValue();
1197: } elseif ($lengthType->isNull()->yes()) {
1198: $length = $keyTypesCount;
1199: } else {
1200: $length = null;
1201: }
1202:
1203: if ($offset === null || $length === null) {
1204: return $this->degradeToGeneralArray()
1205: ->spliceArray($offsetType, $lengthType, $replacementType);
1206: }
1207:
1208: $allKeysInteger = $this->getIterableKeyType()->isInteger()->yes();
1209:
1210: if ($keyTypesCount + $offset <= 0) {
1211: // A negative offset cannot reach left outside the array twice
1212: $offset = 0;
1213: }
1214:
1215: if ($keyTypesCount + $length <= 0) {
1216: // A negative length cannot reach left outside the array twice
1217: $length = 0;
1218: }
1219:
1220: $offsetWasNegative = false;
1221: if ($offset < 0) {
1222: $offsetWasNegative = true;
1223: $offset = $keyTypesCount + $offset;
1224: }
1225:
1226: if ($length < 0) {
1227: $length = $keyTypesCount - $offset + $length;
1228: }
1229:
1230: $extractType = $this->sliceArray($offsetType, $lengthType, TrinaryLogic::createYes());
1231:
1232: $types = [];
1233: foreach ($replacementType->toArray()->getArrays() as $replacementArrayType) {
1234: $removeKeysCount = 0;
1235: $optionalKeysBeforeReplacement = 0;
1236:
1237: $builder = ConstantArrayTypeBuilder::createEmpty();
1238: for ($i = 0;; $i++) {
1239: $isOptional = $this->isOptionalKey($i);
1240:
1241: if (!$offsetWasNegative && $i < $offset && $isOptional) {
1242: $optionalKeysBeforeReplacement++;
1243: }
1244:
1245: if ($i === $offset + $optionalKeysBeforeReplacement) {
1246: // When the offset is reached we have to a) put the replacement array in and b) remove $length elements
1247: $removeKeysCount = $length;
1248:
1249: if ($replacementArrayType instanceof self) {
1250: $valuesArray = $replacementArrayType->getValuesArray();
1251: for ($j = 0, $jMax = count($valuesArray->keyTypes); $j < $jMax; $j++) {
1252: $builder->setOffsetValueType(null, $valuesArray->valueTypes[$j], $valuesArray->isOptionalKey($j));
1253: }
1254: } else {
1255: $builder->degradeToGeneralArray();
1256: $builder->setOffsetValueType($replacementArrayType->getValuesArray()->getIterableKeyType(), $replacementArrayType->getIterableValueType(), true);
1257: }
1258: }
1259:
1260: if (!isset($this->keyTypes[$i])) {
1261: break;
1262: }
1263:
1264: if ($removeKeysCount > 0) {
1265: $extractTypeHasOffsetValueType = $extractType->hasOffsetValueType($this->keyTypes[$i]);
1266:
1267: if (
1268: (!$isOptional && $extractTypeHasOffsetValueType->yes())
1269: || ($isOptional && $extractTypeHasOffsetValueType->maybe())
1270: ) {
1271: $removeKeysCount--;
1272: continue;
1273: }
1274: }
1275:
1276: if (!$isOptional && $extractType->hasOffsetValueType($this->keyTypes[$i])->maybe()) {
1277: $isOptional = true;
1278: }
1279:
1280: $builder->setOffsetValueType(
1281: $this->keyTypes[$i]->isInteger()->no() ? $this->keyTypes[$i] : null,
1282: $this->valueTypes[$i],
1283: $isOptional,
1284: );
1285: }
1286:
1287: $builtType = $builder->getArray();
1288: if ($allKeysInteger && !$builtType->isList()->yes()) {
1289: $builtType = TypeCombinator::intersect($builtType, new AccessoryArrayListType());
1290: }
1291: $types[] = $builtType;
1292: }
1293:
1294: return TypeCombinator::union(...$types);
1295: }
1296:
1297: public function isIterableAtLeastOnce(): TrinaryLogic
1298: {
1299: $keysCount = count($this->keyTypes);
1300: if ($keysCount === 0) {
1301: return TrinaryLogic::createNo();
1302: }
1303:
1304: $optionalKeysCount = count($this->optionalKeys);
1305: if ($optionalKeysCount < $keysCount) {
1306: return TrinaryLogic::createYes();
1307: }
1308:
1309: return TrinaryLogic::createMaybe();
1310: }
1311:
1312: public function getArraySize(): Type
1313: {
1314: $optionalKeysCount = count($this->optionalKeys);
1315: $totalKeysCount = count($this->getKeyTypes());
1316: if ($optionalKeysCount === 0) {
1317: return new ConstantIntegerType($totalKeysCount);
1318: }
1319:
1320: return IntegerRangeType::fromInterval($totalKeysCount - $optionalKeysCount, $totalKeysCount);
1321: }
1322:
1323: public function getFirstIterableKeyType(): Type
1324: {
1325: $keyTypes = [];
1326: foreach ($this->keyTypes as $i => $keyType) {
1327: $keyTypes[] = $keyType;
1328: if (!$this->isOptionalKey($i)) {
1329: break;
1330: }
1331: }
1332:
1333: return TypeCombinator::union(...$keyTypes);
1334: }
1335:
1336: public function getLastIterableKeyType(): Type
1337: {
1338: $keyTypes = [];
1339: for ($i = count($this->keyTypes) - 1; $i >= 0; $i--) {
1340: $keyTypes[] = $this->keyTypes[$i];
1341: if (!$this->isOptionalKey($i)) {
1342: break;
1343: }
1344: }
1345:
1346: return TypeCombinator::union(...$keyTypes);
1347: }
1348:
1349: public function getFirstIterableValueType(): Type
1350: {
1351: $valueTypes = [];
1352: foreach ($this->valueTypes as $i => $valueType) {
1353: $valueTypes[] = $valueType;
1354: if (!$this->isOptionalKey($i)) {
1355: break;
1356: }
1357: }
1358:
1359: return TypeCombinator::union(...$valueTypes);
1360: }
1361:
1362: public function getLastIterableValueType(): Type
1363: {
1364: $valueTypes = [];
1365: for ($i = count($this->keyTypes) - 1; $i >= 0; $i--) {
1366: $valueTypes[] = $this->valueTypes[$i];
1367: if (!$this->isOptionalKey($i)) {
1368: break;
1369: }
1370: }
1371:
1372: return TypeCombinator::union(...$valueTypes);
1373: }
1374:
1375: public function isConstantArray(): TrinaryLogic
1376: {
1377: return TrinaryLogic::createYes();
1378: }
1379:
1380: public function isList(): TrinaryLogic
1381: {
1382: return $this->isList;
1383: }
1384:
1385: /** @param positive-int $length */
1386: private function removeLastElements(int $length): self
1387: {
1388: $keyTypesCount = count($this->keyTypes);
1389: if ($keyTypesCount === 0) {
1390: return $this;
1391: }
1392:
1393: $keyTypes = $this->keyTypes;
1394: $valueTypes = $this->valueTypes;
1395: $optionalKeys = $this->optionalKeys;
1396: $nextAutoindexes = $this->nextAutoIndexes;
1397:
1398: $optionalKeysRemoved = 0;
1399: $newLength = $keyTypesCount - $length;
1400: for ($i = $keyTypesCount - 1; $i >= 0; $i--) {
1401: $isOptional = $this->isOptionalKey($i);
1402:
1403: if ($i >= $newLength) {
1404: if ($isOptional) {
1405: $optionalKeysRemoved++;
1406: foreach ($optionalKeys as $key => $value) {
1407: if ($value === $i) {
1408: unset($optionalKeys[$key]);
1409: break;
1410: }
1411: }
1412: }
1413:
1414: $removedKeyType = array_pop($keyTypes);
1415: array_pop($valueTypes);
1416: $nextAutoindexes = $removedKeyType instanceof ConstantIntegerType
1417: ? [$removedKeyType->getValue()]
1418: : $this->nextAutoIndexes;
1419: continue;
1420: }
1421:
1422: if ($isOptional || $optionalKeysRemoved <= 0) {
1423: continue;
1424: }
1425:
1426: $optionalKeys[] = $i;
1427: $optionalKeysRemoved--;
1428: }
1429:
1430: return $this->recreate(
1431: $keyTypes,
1432: $valueTypes,
1433: $nextAutoindexes,
1434: array_values($optionalKeys),
1435: $this->isList,
1436: );
1437: }
1438:
1439: /** @param positive-int $length */
1440: private function removeFirstElements(int $length, bool $reindex = true): Type
1441: {
1442: $builder = ConstantArrayTypeBuilder::createEmpty();
1443:
1444: $optionalKeysIgnored = 0;
1445: foreach ($this->keyTypes as $i => $keyType) {
1446: $isOptional = $this->isOptionalKey($i);
1447: if ($i <= $length - 1) {
1448: if ($isOptional) {
1449: $optionalKeysIgnored++;
1450: }
1451: continue;
1452: }
1453:
1454: if (!$isOptional && $optionalKeysIgnored > 0) {
1455: $isOptional = true;
1456: $optionalKeysIgnored--;
1457: }
1458:
1459: $valueType = $this->valueTypes[$i];
1460: if ($reindex && $keyType instanceof ConstantIntegerType) {
1461: $keyType = null;
1462: }
1463:
1464: $builder->setOffsetValueType($keyType, $valueType, $isOptional);
1465: }
1466:
1467: return $builder->getArray();
1468: }
1469:
1470: public function toBoolean(): BooleanType
1471: {
1472: return $this->getArraySize()->toBoolean();
1473: }
1474:
1475: public function toInteger(): Type
1476: {
1477: return $this->toBoolean()->toInteger();
1478: }
1479:
1480: public function toFloat(): Type
1481: {
1482: return $this->toBoolean()->toFloat();
1483: }
1484:
1485: public function generalize(GeneralizePrecision $precision): Type
1486: {
1487: if (count($this->keyTypes) === 0) {
1488: return $this;
1489: }
1490:
1491: if ($precision->isTemplateArgument()) {
1492: return $this->traverse(static fn (Type $type) => $type->generalize($precision));
1493: }
1494:
1495: $arrayType = new ArrayType(
1496: $this->getIterableKeyType()->generalize($precision),
1497: $this->getIterableValueType()->generalize($precision),
1498: );
1499:
1500: $keyTypesCount = count($this->keyTypes);
1501: $optionalKeysCount = count($this->optionalKeys);
1502:
1503: $accessoryTypes = [];
1504: if ($precision->isMoreSpecific() && ($keyTypesCount - $optionalKeysCount) < 32) {
1505: foreach ($this->keyTypes as $i => $keyType) {
1506: if ($this->isOptionalKey($i)) {
1507: continue;
1508: }
1509:
1510: $accessoryTypes[] = new HasOffsetValueType($keyType, $this->valueTypes[$i]->generalize($precision));
1511: }
1512: } elseif ($keyTypesCount > $optionalKeysCount) {
1513: $accessoryTypes[] = new NonEmptyArrayType();
1514: }
1515:
1516: if ($this->isList()->yes()) {
1517: $arrayType = TypeCombinator::intersect($arrayType, new AccessoryArrayListType());
1518: }
1519:
1520: if (count($accessoryTypes) > 0) {
1521: return TypeCombinator::intersect($arrayType, ...$accessoryTypes);
1522: }
1523:
1524: return $arrayType;
1525: }
1526:
1527: public function generalizeValues(): self
1528: {
1529: $valueTypes = [];
1530: foreach ($this->valueTypes as $valueType) {
1531: $valueTypes[] = $valueType->generalize(GeneralizePrecision::lessSpecific());
1532: }
1533:
1534: return $this->recreate($this->keyTypes, $valueTypes, $this->nextAutoIndexes, $this->optionalKeys, $this->isList);
1535: }
1536:
1537: private function degradeToGeneralArray(): Type
1538: {
1539: $builder = ConstantArrayTypeBuilder::createFromConstantArray($this);
1540: $builder->degradeToGeneralArray();
1541:
1542: return $builder->getArray();
1543: }
1544:
1545: public function getKeysArrayFiltered(Type $filterValueType, TrinaryLogic $strict): Type
1546: {
1547: $keysArray = $this->getKeysOrValuesArray($this->keyTypes);
1548:
1549: return new IntersectionType([
1550: new ArrayType(
1551: IntegerRangeType::createAllGreaterThanOrEqualTo(0),
1552: $keysArray->getIterableValueType(),
1553: ),
1554: new AccessoryArrayListType(),
1555: ]);
1556: }
1557:
1558: public function getKeysArray(): self
1559: {
1560: return $this->getKeysOrValuesArray($this->keyTypes);
1561: }
1562:
1563: public function getValuesArray(): self
1564: {
1565: return $this->getKeysOrValuesArray($this->valueTypes);
1566: }
1567:
1568: /**
1569: * @param array<int, Type> $types
1570: */
1571: private function getKeysOrValuesArray(array $types): self
1572: {
1573: $count = count($types);
1574: $autoIndexes = range($count - count($this->optionalKeys), $count);
1575:
1576: if ($this->isList->yes()) {
1577: // Optimized version for lists: Assume that if a later key exists, then earlier keys also exist.
1578: $keyTypes = array_map(
1579: static fn (int $i): ConstantIntegerType => new ConstantIntegerType($i),
1580: array_keys($types),
1581: );
1582: return $this->recreate($keyTypes, $types, $autoIndexes, $this->optionalKeys, TrinaryLogic::createYes());
1583: }
1584:
1585: $keyTypes = [];
1586: $valueTypes = [];
1587: $optionalKeys = [];
1588: $maxIndex = 0;
1589:
1590: foreach ($types as $i => $type) {
1591: $keyTypes[] = new ConstantIntegerType($i);
1592:
1593: if ($this->isOptionalKey($maxIndex)) {
1594: // move $maxIndex to next non-optional key
1595: do {
1596: $maxIndex++;
1597: } while ($maxIndex < $count && $this->isOptionalKey($maxIndex));
1598: }
1599:
1600: if ($i === $maxIndex) {
1601: $valueTypes[] = $type;
1602: } else {
1603: $valueTypes[] = TypeCombinator::union(...array_slice($types, $i, $maxIndex - $i + 1));
1604: if ($maxIndex >= $count) {
1605: $optionalKeys[] = $i;
1606: }
1607: }
1608: $maxIndex++;
1609: }
1610:
1611: return $this->recreate($keyTypes, $valueTypes, $autoIndexes, $optionalKeys, TrinaryLogic::createYes());
1612: }
1613:
1614: public function describe(VerbosityLevel $level): string
1615: {
1616: $arrayName = $this->shouldBeDescribedAsAList() ? 'list' : 'array';
1617:
1618: $describeValue = function (bool $truncate) use ($level, $arrayName): string {
1619: $items = [];
1620: $values = [];
1621: $exportValuesOnly = true;
1622: foreach ($this->keyTypes as $i => $keyType) {
1623: $valueType = $this->valueTypes[$i];
1624: if ($keyType->getValue() !== $i) {
1625: $exportValuesOnly = false;
1626: }
1627:
1628: $isOptional = $this->isOptionalKey($i);
1629: if ($isOptional) {
1630: $exportValuesOnly = false;
1631: }
1632:
1633: $keyDescription = $keyType->getValue();
1634: if (is_string($keyDescription)) {
1635: if (str_contains($keyDescription, '"')) {
1636: $keyDescription = sprintf('\'%s\'', $keyDescription);
1637: } elseif (str_contains($keyDescription, '\'')) {
1638: $keyDescription = sprintf('"%s"', $keyDescription);
1639: } elseif (!self::isValidIdentifier($keyDescription)) {
1640: $keyDescription = sprintf('\'%s\'', $keyDescription);
1641: }
1642: }
1643:
1644: $valueTypeDescription = $valueType->describe($level);
1645: $items[] = sprintf('%s%s: %s', $keyDescription, $isOptional ? '?' : '', $valueTypeDescription);
1646: $values[] = $valueTypeDescription;
1647: }
1648:
1649: $append = '';
1650: if ($truncate && count($items) > self::DESCRIBE_LIMIT) {
1651: $items = array_slice($items, 0, self::DESCRIBE_LIMIT);
1652: $values = array_slice($values, 0, self::DESCRIBE_LIMIT);
1653: $append = ', ...';
1654: }
1655:
1656: return sprintf(
1657: '%s{%s%s}',
1658: $arrayName,
1659: implode(', ', $exportValuesOnly ? $values : $items),
1660: $append,
1661: );
1662: };
1663: return $level->handle(
1664: fn (): string => $this->isIterableAtLeastOnce()->no() ? $arrayName : sprintf('%s<%s, %s>', $arrayName, $this->getIterableKeyType()->describe($level), $this->getIterableValueType()->describe($level)),
1665: static fn (): string => $describeValue(true),
1666: static fn (): string => $describeValue(false),
1667: );
1668: }
1669:
1670: private function shouldBeDescribedAsAList(): bool
1671: {
1672: if (!$this->isList->yes()) {
1673: return false;
1674: }
1675:
1676: if (count($this->optionalKeys) === 0) {
1677: return false;
1678: }
1679:
1680: if (count($this->optionalKeys) > 1) {
1681: return true;
1682: }
1683:
1684: return $this->optionalKeys[0] !== count($this->keyTypes) - 1;
1685: }
1686:
1687: public function inferTemplateTypes(Type $receivedType): TemplateTypeMap
1688: {
1689: if ($receivedType instanceof UnionType || $receivedType instanceof IntersectionType) {
1690: return $receivedType->inferTemplateTypesOn($this);
1691: }
1692:
1693: if ($receivedType instanceof self) {
1694: $typeMap = TemplateTypeMap::createEmpty();
1695: foreach ($this->keyTypes as $i => $keyType) {
1696: $valueType = $this->valueTypes[$i];
1697: if ($receivedType->hasOffsetValueType($keyType)->no()) {
1698: continue;
1699: }
1700: $receivedValueType = $receivedType->getOffsetValueType($keyType);
1701: $typeMap = $typeMap->union($valueType->inferTemplateTypes($receivedValueType));
1702: }
1703:
1704: return $typeMap;
1705: }
1706:
1707: if ($receivedType->isArray()->yes()) {
1708: $keyTypeMap = $this->getIterableKeyType()->inferTemplateTypes($receivedType->getIterableKeyType());
1709: $itemTypeMap = $this->getIterableValueType()->inferTemplateTypes($receivedType->getIterableValueType());
1710:
1711: return $keyTypeMap->union($itemTypeMap);
1712: }
1713:
1714: return TemplateTypeMap::createEmpty();
1715: }
1716:
1717: public function getReferencedTemplateTypes(TemplateTypeVariance $positionVariance): array
1718: {
1719: $variance = $positionVariance->compose(TemplateTypeVariance::createCovariant());
1720: $references = [];
1721:
1722: foreach ($this->keyTypes as $type) {
1723: foreach ($type->getReferencedTemplateTypes($variance) as $reference) {
1724: $references[] = $reference;
1725: }
1726: }
1727:
1728: foreach ($this->valueTypes as $type) {
1729: foreach ($type->getReferencedTemplateTypes($variance) as $reference) {
1730: $references[] = $reference;
1731: }
1732: }
1733:
1734: return $references;
1735: }
1736:
1737: public function tryRemove(Type $typeToRemove): ?Type
1738: {
1739: if ($typeToRemove->isConstantArray()->yes() && $typeToRemove->isIterableAtLeastOnce()->no()) {
1740: return TypeCombinator::intersect($this, new NonEmptyArrayType());
1741: }
1742:
1743: if ($typeToRemove instanceof NonEmptyArrayType) {
1744: return new ConstantArrayType([], []);
1745: }
1746:
1747: if ($typeToRemove instanceof HasOffsetType) {
1748: return $this->unsetOffset($typeToRemove->getOffsetType(), true);
1749: }
1750:
1751: if ($typeToRemove instanceof HasOffsetValueType) {
1752: return $this->unsetOffset($typeToRemove->getOffsetType(), true);
1753: }
1754:
1755: return null;
1756: }
1757:
1758: public function traverse(callable $cb): Type
1759: {
1760: $valueTypes = [];
1761:
1762: $stillOriginal = true;
1763: foreach ($this->valueTypes as $valueType) {
1764: $transformedValueType = $cb($valueType);
1765: if ($transformedValueType !== $valueType) {
1766: $stillOriginal = false;
1767: }
1768:
1769: $valueTypes[] = $transformedValueType;
1770: }
1771:
1772: if ($stillOriginal) {
1773: return $this;
1774: }
1775:
1776: return $this->recreate($this->keyTypes, $valueTypes, $this->nextAutoIndexes, $this->optionalKeys, $this->isList);
1777: }
1778:
1779: public function traverseSimultaneously(Type $right, callable $cb): Type
1780: {
1781: if (!$right->isArray()->yes()) {
1782: return $this;
1783: }
1784:
1785: $valueTypes = [];
1786:
1787: $stillOriginal = true;
1788: foreach ($this->valueTypes as $i => $valueType) {
1789: $keyType = $this->keyTypes[$i];
1790: $transformedValueType = $cb($valueType, $right->getOffsetValueType($keyType));
1791: if ($transformedValueType !== $valueType) {
1792: $stillOriginal = false;
1793: }
1794:
1795: $valueTypes[] = $transformedValueType;
1796: }
1797:
1798: if ($stillOriginal) {
1799: return $this;
1800: }
1801:
1802: return $this->recreate($this->keyTypes, $valueTypes, $this->nextAutoIndexes, $this->optionalKeys, $this->isList);
1803: }
1804:
1805: public function isKeysSupersetOf(self $otherArray): bool
1806: {
1807: $keyTypesCount = count($this->keyTypes);
1808: $otherKeyTypesCount = count($otherArray->keyTypes);
1809:
1810: if ($keyTypesCount < $otherKeyTypesCount) {
1811: return false;
1812: }
1813:
1814: if ($otherKeyTypesCount === 0) {
1815: return $keyTypesCount === 0;
1816: }
1817:
1818: $failOnDifferentValueType = $keyTypesCount !== $otherKeyTypesCount || $keyTypesCount < 2;
1819:
1820: $keyIndexMap = $this->getKeyIndexMap();
1821: $otherKeyValues = [];
1822:
1823: foreach ($otherArray->keyTypes as $j => $keyType) {
1824: $keyValue = $keyType->getValue();
1825: $i = $keyIndexMap[$keyValue] ?? null;
1826: if ($i === null) {
1827: return false;
1828: }
1829:
1830: $otherKeyValues[$keyValue] = true;
1831:
1832: $valueType = $this->valueTypes[$i];
1833: $otherValueType = $otherArray->valueTypes[$j];
1834: if (!$otherValueType->isSuperTypeOf($valueType)->no()) {
1835: continue;
1836: }
1837:
1838: if ($failOnDifferentValueType) {
1839: return false;
1840: }
1841: $failOnDifferentValueType = true;
1842: }
1843:
1844: $requiredKeyCount = 0;
1845: foreach ($this->keyTypes as $i => $keyType) {
1846: if (isset($otherKeyValues[$keyType->getValue()])) {
1847: continue;
1848: }
1849: if ($this->isOptionalKey($i)) {
1850: continue;
1851: }
1852:
1853: $requiredKeyCount++;
1854: if ($requiredKeyCount > 1) {
1855: return false;
1856: }
1857: }
1858:
1859: return true;
1860: }
1861:
1862: public function mergeWith(self $otherArray): self
1863: {
1864: // only call this after verifying isKeysSupersetOf, or if losing tagged unions is not an issue
1865: $valueTypes = $this->valueTypes;
1866: $optionalKeys = $this->optionalKeys;
1867: foreach ($this->keyTypes as $i => $keyType) {
1868: $otherIndex = $otherArray->getKeyIndex($keyType);
1869: if ($otherIndex === null) {
1870: $optionalKeys[] = $i;
1871: continue;
1872: }
1873: if ($otherArray->isOptionalKey($otherIndex)) {
1874: $optionalKeys[] = $i;
1875: }
1876: $otherValueType = $otherArray->valueTypes[$otherIndex];
1877: $valueTypes[$i] = TypeCombinator::union($valueTypes[$i], $otherValueType);
1878: }
1879:
1880: $optionalKeys = array_values(array_unique($optionalKeys));
1881:
1882: $nextAutoIndexes = array_values(array_unique(array_merge($this->nextAutoIndexes, $otherArray->nextAutoIndexes)));
1883: sort($nextAutoIndexes);
1884:
1885: return $this->recreate($this->keyTypes, $valueTypes, $nextAutoIndexes, $optionalKeys, $this->isList->and($otherArray->isList));
1886: }
1887:
1888: /**
1889: * @return array<int|string, int>
1890: */
1891: private function getKeyIndexMap(): array
1892: {
1893: if ($this->keyIndexMap !== null) {
1894: return $this->keyIndexMap;
1895: }
1896:
1897: $map = [];
1898: foreach ($this->keyTypes as $i => $keyType) {
1899: $map[$keyType->getValue()] = $i;
1900: }
1901:
1902: return $this->keyIndexMap = $map;
1903: }
1904:
1905: /**
1906: * @param ConstantIntegerType|ConstantStringType $otherKeyType
1907: */
1908: private function getKeyIndex($otherKeyType): ?int
1909: {
1910: return $this->getKeyIndexMap()[$otherKeyType->getValue()] ?? null;
1911: }
1912:
1913: public function makeOffsetRequired(Type $offsetType): self
1914: {
1915: $offsetType = $offsetType->toArrayKey();
1916: $optionalKeys = $this->optionalKeys;
1917: $isList = $this->isList->yes();
1918: foreach ($this->keyTypes as $i => $keyType) {
1919: if (!$keyType->equals($offsetType)) {
1920: continue;
1921: }
1922:
1923: $keyValue = $keyType->getValue();
1924: foreach ($optionalKeys as $j => $key) {
1925: if (
1926: $i !== $key
1927: && (
1928: !$isList
1929: || !is_int($keyValue)
1930: || !is_int($this->keyTypes[$key]->getValue())
1931: || $this->keyTypes[$key]->getValue() >= $keyValue
1932: )
1933: ) {
1934: continue;
1935: }
1936:
1937: unset($optionalKeys[$j]);
1938: }
1939:
1940: if (count($this->optionalKeys) !== count($optionalKeys)) {
1941: return $this->recreate($this->keyTypes, $this->valueTypes, $this->nextAutoIndexes, array_values($optionalKeys), $this->isList);
1942: }
1943:
1944: break;
1945: }
1946:
1947: return $this;
1948: }
1949:
1950: public function makeList(): Type
1951: {
1952: if ($this->isList->yes()) {
1953: return $this;
1954: }
1955:
1956: if ($this->isList->no()) {
1957: return new NeverType();
1958: }
1959:
1960: return $this->recreate($this->keyTypes, $this->valueTypes, $this->nextAutoIndexes, $this->optionalKeys, TrinaryLogic::createYes());
1961: }
1962:
1963: public function makeListMaybe(): Type
1964: {
1965: if (!$this->isList->yes()) {
1966: return $this;
1967: }
1968:
1969: return $this->recreate(
1970: $this->keyTypes,
1971: $this->valueTypes,
1972: $this->nextAutoIndexes,
1973: $this->optionalKeys,
1974: TrinaryLogic::createMaybe(),
1975: );
1976: }
1977:
1978: public function mapValueType(callable $cb): Type
1979: {
1980: $newValueTypes = [];
1981: foreach ($this->valueTypes as $valueType) {
1982: $newValueTypes[] = $cb($valueType);
1983: }
1984:
1985: return $this->recreate(
1986: $this->keyTypes,
1987: $newValueTypes,
1988: $this->nextAutoIndexes,
1989: $this->optionalKeys,
1990: $this->isList,
1991: );
1992: }
1993:
1994: public function mapKeyType(callable $cb): Type
1995: {
1996: // Constant array shapes already encode precise per-slot keys; a
1997: // blanket key-type rewrite (the prior `TypeTraverser`-based pattern
1998: // in `NodeScopeResolver`) would coerce constants into a broader
1999: // type and lose precision. Pass through unchanged.
2000: return $this;
2001: }
2002:
2003: public function makeAllArrayKeysOptional(): Type
2004: {
2005: $keyCount = count($this->keyTypes);
2006: if ($keyCount === 0) {
2007: return $this;
2008: }
2009:
2010: return $this->recreate(
2011: $this->keyTypes,
2012: $this->valueTypes,
2013: $this->nextAutoIndexes,
2014: range(0, $keyCount - 1),
2015: $this->isList,
2016: );
2017: }
2018:
2019: public function changeKeyCaseArray(?int $case): Type
2020: {
2021: $builder = ConstantArrayTypeBuilder::createEmpty();
2022: foreach ($this->keyTypes as $i => $keyType) {
2023: if ($keyType instanceof ConstantStringType) {
2024: $newKeyType = self::foldConstantStringKeyCase($keyType, $case);
2025: } else {
2026: $newKeyType = $keyType;
2027: }
2028: $builder->setOffsetValueType($newKeyType, $this->valueTypes[$i], $this->isOptionalKey($i));
2029: }
2030: $result = $builder->getArray();
2031: if ($this->isList()->yes()) {
2032: $result = TypeCombinator::intersect($result, new AccessoryArrayListType());
2033: }
2034: return $result;
2035: }
2036:
2037: public function filterArrayRemovingFalsey(): Type
2038: {
2039: $falseyTypes = StaticTypeFactory::falsey();
2040: $builder = ConstantArrayTypeBuilder::createEmpty();
2041: foreach ($this->keyTypes as $i => $keyType) {
2042: $value = $this->valueTypes[$i];
2043: $isFalsey = $falseyTypes->isSuperTypeOf($value);
2044: if ($isFalsey->yes()) {
2045: continue;
2046: }
2047: if ($isFalsey->maybe()) {
2048: $builder->setOffsetValueType($keyType, TypeCombinator::remove($value, $falseyTypes), true);
2049: continue;
2050: }
2051: $builder->setOffsetValueType($keyType, $value, $this->isOptionalKey($i));
2052: }
2053:
2054: return $builder->getArray();
2055: }
2056:
2057: private static function foldConstantStringKeyCase(ConstantStringType $type, ?int $case): Type
2058: {
2059: if ($case === CASE_LOWER) {
2060: return new ConstantStringType(strtolower($type->getValue()));
2061: }
2062: if ($case === CASE_UPPER) {
2063: return new ConstantStringType(strtoupper($type->getValue()));
2064: }
2065:
2066: return TypeCombinator::union(
2067: new ConstantStringType(strtolower($type->getValue())),
2068: new ConstantStringType(strtoupper($type->getValue())),
2069: );
2070: }
2071:
2072: public function toPhpDocNode(): TypeNode
2073: {
2074: $items = [];
2075: $values = [];
2076: $exportValuesOnly = true;
2077: foreach ($this->keyTypes as $i => $keyType) {
2078: if ($keyType->getValue() !== $i) {
2079: $exportValuesOnly = false;
2080: }
2081: $keyPhpDocNode = $keyType->toPhpDocNode();
2082: if (!$keyPhpDocNode instanceof ConstTypeNode) {
2083: continue;
2084: }
2085: $valueType = $this->valueTypes[$i];
2086:
2087: /** @var ConstExprStringNode|ConstExprIntegerNode $keyNode */
2088: $keyNode = $keyPhpDocNode->constExpr;
2089: if ($keyNode instanceof ConstExprStringNode) {
2090: $value = $keyNode->value;
2091: if (self::isValidIdentifier($value)) {
2092: $keyNode = new IdentifierTypeNode($value);
2093: }
2094: }
2095:
2096: $isOptional = $this->isOptionalKey($i);
2097: if ($isOptional) {
2098: $exportValuesOnly = false;
2099: }
2100: $items[] = new ArrayShapeItemNode(
2101: $keyNode,
2102: $isOptional,
2103: $valueType->toPhpDocNode(),
2104: );
2105: $values[] = new ArrayShapeItemNode(
2106: null,
2107: $isOptional,
2108: $valueType->toPhpDocNode(),
2109: );
2110: }
2111:
2112: return ArrayShapeNode::createSealed(
2113: $exportValuesOnly ? $values : $items,
2114: $this->shouldBeDescribedAsAList() ? ArrayShapeNode::KIND_LIST : ArrayShapeNode::KIND_ARRAY,
2115: );
2116: }
2117:
2118: public static function isValidIdentifier(string $value): bool
2119: {
2120: $result = Strings::match($value, '~^(?:[\\\\]?+[a-z_\\x80-\\xFF][0-9a-z_\\x80-\\xFF-]*+)++$~si');
2121:
2122: return $result !== null;
2123: }
2124:
2125: public function getFiniteTypes(): array
2126: {
2127: $limit = InitializerExprTypeResolver::CALCULATE_SCALARS_LIMIT;
2128:
2129: // Build finite array types incrementally, processing one key at a time.
2130: // For optional keys, fork each partial result into with/without variants.
2131: // This avoids generating 2^N ConstantArrayType objects via getAllArrays().
2132: /** @var list<ConstantArrayTypeBuilder> $partials */
2133: $partials = [ConstantArrayTypeBuilder::createEmpty()];
2134:
2135: foreach ($this->keyTypes as $i => $keyType) {
2136: $finiteValueTypes = $this->valueTypes[$i]->getFiniteTypes();
2137: if ($finiteValueTypes === []) {
2138: return [];
2139: }
2140:
2141: $isOptional = $this->isOptionalKey($i);
2142: $newPartials = [];
2143:
2144: foreach ($partials as $partial) {
2145: if ($isOptional) {
2146: $newPartials[] = clone $partial;
2147: }
2148: foreach ($finiteValueTypes as $finiteValueType) {
2149: $newPartial = clone $partial;
2150: $newPartial->setOffsetValueType($keyType, $finiteValueType);
2151: $newPartials[] = $newPartial;
2152: }
2153: }
2154:
2155: $partials = $newPartials;
2156: if (count($partials) > $limit) {
2157: return [];
2158: }
2159: }
2160:
2161: $finiteTypes = [];
2162: foreach ($partials as $partial) {
2163: $finiteTypes[] = $partial->getArray();
2164: }
2165:
2166: return $finiteTypes;
2167: }
2168:
2169: public function hasTemplateOrLateResolvableType(): bool
2170: {
2171: foreach ($this->valueTypes as $valueType) {
2172: if (!$valueType->hasTemplateOrLateResolvableType()) {
2173: continue;
2174: }
2175:
2176: return true;
2177: }
2178:
2179: foreach ($this->keyTypes as $keyType) {
2180: if (!$keyType instanceof TemplateType) {
2181: continue;
2182: }
2183:
2184: return true;
2185: }
2186:
2187: return false;
2188: }
2189:
2190: }
2191: