1: <?php declare(strict_types = 1);
2:
3: namespace PHPStan\Type\Constant;
4:
5: use PHPStan\Reflection\ClassMemberAccessAnswerer;
6: use PHPStan\Reflection\InaccessibleMethod;
7: use PHPStan\Reflection\ParametersAcceptor;
8: use PHPStan\Reflection\ReflectionProviderStaticAccessor;
9: use PHPStan\Reflection\TrivialParametersAcceptor;
10: use PHPStan\ShouldNotHappenException;
11: use PHPStan\TrinaryLogic;
12: use PHPStan\Type\Accessory\HasOffsetValueType;
13: use PHPStan\Type\Accessory\NonEmptyArrayType;
14: use PHPStan\Type\ArrayType;
15: use PHPStan\Type\BooleanType;
16: use PHPStan\Type\CompoundType;
17: use PHPStan\Type\ConstantType;
18: use PHPStan\Type\ErrorType;
19: use PHPStan\Type\GeneralizePrecision;
20: use PHPStan\Type\Generic\GenericClassStringType;
21: use PHPStan\Type\Generic\TemplateMixedType;
22: use PHPStan\Type\Generic\TemplateTypeMap;
23: use PHPStan\Type\Generic\TemplateTypeVariance;
24: use PHPStan\Type\IntegerRangeType;
25: use PHPStan\Type\IntersectionType;
26: use PHPStan\Type\MixedType;
27: use PHPStan\Type\NeverType;
28: use PHPStan\Type\ObjectType;
29: use PHPStan\Type\ObjectWithoutClassType;
30: use PHPStan\Type\StringType;
31: use PHPStan\Type\Type;
32: use PHPStan\Type\TypeCombinator;
33: use PHPStan\Type\TypeUtils;
34: use PHPStan\Type\UnionType;
35: use PHPStan\Type\VerbosityLevel;
36: use function array_keys;
37: use function array_map;
38: use function array_merge;
39: use function array_pop;
40: use function array_push;
41: use function array_reverse;
42: use function array_slice;
43: use function array_unique;
44: use function array_values;
45: use function assert;
46: use function count;
47: use function implode;
48: use function in_array;
49: use function is_int;
50: use function is_string;
51: use function min;
52: use function pow;
53: use function sort;
54: use function sprintf;
55: use function strpos;
56:
57: /**
58: * @api
59: */
60: class ConstantArrayType extends ArrayType implements ConstantType
61: {
62:
63: private const DESCRIBE_LIMIT = 8;
64:
65: /** @var self[]|null */
66: private ?array $allArrays = null;
67:
68: /** @var non-empty-list<int> */
69: private array $nextAutoIndexes;
70:
71: /**
72: * @api
73: * @param array<int, ConstantIntegerType|ConstantStringType> $keyTypes
74: * @param array<int, Type> $valueTypes
75: * @param non-empty-list<int>|int $nextAutoIndexes
76: * @param int[] $optionalKeys
77: */
78: public function __construct(
79: private array $keyTypes,
80: private array $valueTypes,
81: int|array $nextAutoIndexes = [0],
82: private array $optionalKeys = [],
83: )
84: {
85: assert(count($keyTypes) === count($valueTypes));
86:
87: if (is_int($nextAutoIndexes)) {
88: $nextAutoIndexes = [$nextAutoIndexes];
89: }
90:
91: $this->nextAutoIndexes = $nextAutoIndexes;
92:
93: $keyTypesCount = count($this->keyTypes);
94: if ($keyTypesCount === 0) {
95: $keyType = new NeverType(true);
96: } elseif ($keyTypesCount === 1) {
97: $keyType = $this->keyTypes[0];
98: } else {
99: $keyType = new UnionType($this->keyTypes);
100: }
101:
102: parent::__construct(
103: $keyType,
104: count($valueTypes) > 0 ? TypeCombinator::union(...$valueTypes) : new NeverType(true),
105: );
106: }
107:
108: public function isEmpty(): bool
109: {
110: return count($this->keyTypes) === 0;
111: }
112:
113: /**
114: * @return non-empty-list<int>
115: */
116: public function getNextAutoIndexes(): array
117: {
118: return $this->nextAutoIndexes;
119: }
120:
121: /**
122: * @deprecated
123: */
124: public function getNextAutoIndex(): int
125: {
126: return $this->nextAutoIndexes[count($this->nextAutoIndexes) - 1];
127: }
128:
129: /**
130: * @return int[]
131: */
132: public function getOptionalKeys(): array
133: {
134: return $this->optionalKeys;
135: }
136:
137: /**
138: * @return self[]
139: */
140: public function getAllArrays(): array
141: {
142: if ($this->allArrays !== null) {
143: return $this->allArrays;
144: }
145:
146: if (count($this->optionalKeys) <= 10) {
147: $optionalKeysCombinations = $this->powerSet($this->optionalKeys);
148: } else {
149: $optionalKeysCombinations = [
150: [],
151: $this->optionalKeys,
152: ];
153: }
154:
155: $requiredKeys = [];
156: foreach (array_keys($this->keyTypes) as $i) {
157: if (in_array($i, $this->optionalKeys, true)) {
158: continue;
159: }
160: $requiredKeys[] = $i;
161: }
162:
163: $arrays = [];
164: foreach ($optionalKeysCombinations as $combination) {
165: $keys = array_merge($requiredKeys, $combination);
166: $builder = ConstantArrayTypeBuilder::createEmpty();
167: foreach ($keys as $i) {
168: $builder->setOffsetValueType($this->keyTypes[$i], $this->valueTypes[$i]);
169: }
170:
171: $array = $builder->getArray();
172: if (!$array instanceof ConstantArrayType) {
173: throw new ShouldNotHappenException();
174: }
175:
176: $arrays[] = $array;
177: }
178:
179: return $this->allArrays = $arrays;
180: }
181:
182: /**
183: * @template T
184: * @param T[] $in
185: * @return T[][]
186: */
187: private function powerSet(array $in): array
188: {
189: $count = count($in);
190: $members = pow(2, $count);
191: $return = [];
192: for ($i = 0; $i < $members; $i++) {
193: $b = sprintf('%0' . $count . 'b', $i);
194: $out = [];
195: for ($j = 0; $j < $count; $j++) {
196: if ($b[$j] !== '1') {
197: continue;
198: }
199:
200: $out[] = $in[$j];
201: }
202: $return[] = $out;
203: }
204:
205: return $return;
206: }
207:
208: /**
209: * @return array<int, ConstantIntegerType|ConstantStringType>
210: */
211: public function getKeyTypes(): array
212: {
213: return $this->keyTypes;
214: }
215:
216: public function getFirstKeyType(): Type
217: {
218: $keyTypes = [];
219: foreach ($this->keyTypes as $i => $keyType) {
220: $keyTypes[] = $keyType;
221: if (!$this->isOptionalKey($i)) {
222: break;
223: }
224: }
225:
226: return TypeCombinator::union(...$keyTypes);
227: }
228:
229: public function getLastKeyType(): Type
230: {
231: $keyTypes = [];
232: for ($i = count($this->keyTypes) - 1; $i >= 0; $i--) {
233: $keyTypes[] = $this->keyTypes[$i];
234: if (!$this->isOptionalKey($i)) {
235: break;
236: }
237: }
238:
239: return TypeCombinator::union(...$keyTypes);
240: }
241:
242: /**
243: * @return array<int, Type>
244: */
245: public function getValueTypes(): array
246: {
247: return $this->valueTypes;
248: }
249:
250: public function getFirstValueType(): Type
251: {
252: $valueTypes = [];
253: foreach ($this->valueTypes as $i => $valueType) {
254: $valueTypes[] = $valueType;
255: if (!$this->isOptionalKey($i)) {
256: break;
257: }
258: }
259:
260: return TypeCombinator::union(...$valueTypes);
261: }
262:
263: public function getLastValueType(): Type
264: {
265: $valueTypes = [];
266: for ($i = count($this->keyTypes) - 1; $i >= 0; $i--) {
267: $valueTypes[] = $this->valueTypes[$i];
268: if (!$this->isOptionalKey($i)) {
269: break;
270: }
271: }
272:
273: return TypeCombinator::union(...$valueTypes);
274: }
275:
276: public function isOptionalKey(int $i): bool
277: {
278: return in_array($i, $this->optionalKeys, true);
279: }
280:
281: public function accepts(Type $type, bool $strictTypes): TrinaryLogic
282: {
283: if ($type instanceof MixedType && !$type instanceof TemplateMixedType) {
284: return $type->isAcceptedBy($this, $strictTypes);
285: }
286:
287: if ($type instanceof self && count($this->keyTypes) === 0) {
288: return TrinaryLogic::createFromBoolean(count($type->keyTypes) === 0);
289: }
290:
291: $result = TrinaryLogic::createYes();
292: foreach ($this->keyTypes as $i => $keyType) {
293: $valueType = $this->valueTypes[$i];
294: $hasOffset = $type->hasOffsetValueType($keyType);
295: if ($hasOffset->no()) {
296: if ($this->isOptionalKey($i)) {
297: continue;
298: }
299: return $hasOffset;
300: }
301: if ($hasOffset->maybe() && $this->isOptionalKey($i)) {
302: $hasOffset = TrinaryLogic::createYes();
303: }
304:
305: $result = $result->and($hasOffset);
306: $otherValueType = $type->getOffsetValueType($keyType);
307: $acceptsValue = $valueType->accepts($otherValueType, $strictTypes);
308: if ($acceptsValue->no()) {
309: return $acceptsValue;
310: }
311: $result = $result->and($acceptsValue);
312: }
313:
314: return $result->and($type->isArray());
315: }
316:
317: public function isSuperTypeOf(Type $type): TrinaryLogic
318: {
319: if ($type instanceof self) {
320: if (count($this->keyTypes) === 0) {
321: if (count($type->keyTypes) > 0) {
322: if (count($type->optionalKeys) > 0) {
323: return TrinaryLogic::createMaybe();
324: }
325: return TrinaryLogic::createNo();
326: }
327:
328: return TrinaryLogic::createYes();
329: }
330:
331: $results = [];
332: foreach ($this->keyTypes as $i => $keyType) {
333: $hasOffset = $type->hasOffsetValueType($keyType);
334: if ($hasOffset->no()) {
335: if (!$this->isOptionalKey($i)) {
336: return TrinaryLogic::createNo();
337: }
338:
339: $results[] = TrinaryLogic::createMaybe();
340: continue;
341: } elseif ($hasOffset->maybe() && !$this->isOptionalKey($i)) {
342: $results[] = TrinaryLogic::createMaybe();
343: }
344: $results[] = $this->valueTypes[$i]->isSuperTypeOf($type->getOffsetValueType($keyType));
345: }
346:
347: return TrinaryLogic::createYes()->and(...$results);
348: }
349:
350: if ($type instanceof ArrayType) {
351: $result = TrinaryLogic::createMaybe();
352: if (count($this->keyTypes) === 0) {
353: return $result;
354: }
355:
356: return $result->and(
357: $this->getKeyType()->isSuperTypeOf($type->getKeyType()),
358: $this->getItemType()->isSuperTypeOf($type->getItemType()),
359: );
360: }
361:
362: if ($type instanceof CompoundType) {
363: return $type->isSubTypeOf($this);
364: }
365:
366: return TrinaryLogic::createNo();
367: }
368:
369: public function equals(Type $type): bool
370: {
371: if (!$type instanceof self) {
372: return false;
373: }
374:
375: if (count($this->keyTypes) !== count($type->keyTypes)) {
376: return false;
377: }
378:
379: foreach ($this->keyTypes as $i => $keyType) {
380: $valueType = $this->valueTypes[$i];
381: if (!$valueType->equals($type->valueTypes[$i])) {
382: return false;
383: }
384: if (!$keyType->equals($type->keyTypes[$i])) {
385: return false;
386: }
387: }
388:
389: if ($this->optionalKeys !== $type->optionalKeys) {
390: return false;
391: }
392:
393: return true;
394: }
395:
396: public function isCallable(): TrinaryLogic
397: {
398: $typeAndMethods = $this->findTypeAndMethodNames();
399: if ($typeAndMethods === []) {
400: return TrinaryLogic::createNo();
401: }
402:
403: $results = array_map(
404: static fn (ConstantArrayTypeAndMethod $typeAndMethod): TrinaryLogic => $typeAndMethod->getCertainty(),
405: $typeAndMethods,
406: );
407:
408: return TrinaryLogic::createYes()->and(...$results);
409: }
410:
411: /**
412: * @return ParametersAcceptor[]
413: */
414: public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope): array
415: {
416: $typeAndMethodNames = $this->findTypeAndMethodNames();
417: if ($typeAndMethodNames === []) {
418: throw new ShouldNotHappenException();
419: }
420:
421: $acceptors = [];
422: foreach ($typeAndMethodNames as $typeAndMethodName) {
423: if ($typeAndMethodName->isUnknown() || !$typeAndMethodName->getCertainty()->yes()) {
424: $acceptors[] = new TrivialParametersAcceptor();
425: continue;
426: }
427:
428: $method = $typeAndMethodName->getType()
429: ->getMethod($typeAndMethodName->getMethod(), $scope);
430:
431: if (!$scope->canCallMethod($method)) {
432: $acceptors[] = new InaccessibleMethod($method);
433: continue;
434: }
435:
436: array_push($acceptors, ...$method->getVariants());
437: }
438:
439: return $acceptors;
440: }
441:
442: /** @deprecated Use findTypeAndMethodNames() instead */
443: public function findTypeAndMethodName(): ?ConstantArrayTypeAndMethod
444: {
445: if (count($this->keyTypes) !== 2) {
446: return null;
447: }
448:
449: if ($this->keyTypes[0]->isSuperTypeOf(new ConstantIntegerType(0))->no()) {
450: return null;
451: }
452:
453: if ($this->keyTypes[1]->isSuperTypeOf(new ConstantIntegerType(1))->no()) {
454: return null;
455: }
456:
457: [$classOrObject, $method] = $this->valueTypes;
458:
459: if (!$method instanceof ConstantStringType) {
460: return ConstantArrayTypeAndMethod::createUnknown();
461: }
462:
463: if ($classOrObject instanceof ConstantStringType) {
464: $reflectionProvider = ReflectionProviderStaticAccessor::getInstance();
465: if (!$reflectionProvider->hasClass($classOrObject->getValue())) {
466: return null;
467: }
468: $type = new ObjectType($reflectionProvider->getClass($classOrObject->getValue())->getName());
469: } elseif ($classOrObject instanceof GenericClassStringType) {
470: $type = $classOrObject->getGenericType();
471: } elseif ((new ObjectWithoutClassType())->isSuperTypeOf($classOrObject)->yes()) {
472: $type = $classOrObject;
473: } else {
474: return ConstantArrayTypeAndMethod::createUnknown();
475: }
476:
477: $has = $type->hasMethod($method->getValue());
478: if (!$has->no()) {
479: if ($this->isOptionalKey(0) || $this->isOptionalKey(1)) {
480: $has = $has->and(TrinaryLogic::createMaybe());
481: }
482:
483: return ConstantArrayTypeAndMethod::createConcrete($type, $method->getValue(), $has);
484: }
485:
486: return null;
487: }
488:
489: /** @return ConstantArrayTypeAndMethod[] */
490: public function findTypeAndMethodNames(): array
491: {
492: if (count($this->keyTypes) !== 2) {
493: return [];
494: }
495:
496: if ($this->keyTypes[0]->isSuperTypeOf(new ConstantIntegerType(0))->no()) {
497: return [];
498: }
499:
500: if ($this->keyTypes[1]->isSuperTypeOf(new ConstantIntegerType(1))->no()) {
501: return [];
502: }
503:
504: [$classOrObjects, $methods] = $this->valueTypes;
505: $classOrObjects = TypeUtils::flattenTypes($classOrObjects);
506: $methods = TypeUtils::flattenTypes($methods);
507:
508: $typeAndMethods = [];
509: foreach ($classOrObjects as $classOrObject) {
510: if ($classOrObject instanceof ConstantStringType) {
511: $reflectionProvider = ReflectionProviderStaticAccessor::getInstance();
512: if (!$reflectionProvider->hasClass($classOrObject->getValue())) {
513: continue;
514: }
515: $type = new ObjectType($reflectionProvider->getClass($classOrObject->getValue())->getName());
516: } elseif ($classOrObject instanceof GenericClassStringType) {
517: $type = $classOrObject->getGenericType();
518: } elseif ((new ObjectWithoutClassType())->isSuperTypeOf($classOrObject)->yes()) {
519: $type = $classOrObject;
520: } else {
521: $typeAndMethods[] = ConstantArrayTypeAndMethod::createUnknown();
522: continue;
523: }
524:
525: foreach ($methods as $method) {
526: if (!$method instanceof ConstantStringType) {
527: $typeAndMethods[] = ConstantArrayTypeAndMethod::createUnknown();
528: continue;
529: }
530:
531: $has = $type->hasMethod($method->getValue());
532: if ($has->no()) {
533: continue;
534: }
535:
536: if ($this->isOptionalKey(0) || $this->isOptionalKey(1)) {
537: $has = $has->and(TrinaryLogic::createMaybe());
538: }
539:
540: $typeAndMethods[] = ConstantArrayTypeAndMethod::createConcrete($type, $method->getValue(), $has);
541: }
542: }
543:
544: return $typeAndMethods;
545: }
546:
547: public function hasOffsetValueType(Type $offsetType): TrinaryLogic
548: {
549: $offsetType = ArrayType::castToArrayKeyType($offsetType);
550: if ($offsetType instanceof UnionType) {
551: return TrinaryLogic::lazyExtremeIdentity($offsetType->getTypes(), fn (Type $innerType) => $this->hasOffsetValueType($innerType));
552: }
553:
554: $result = TrinaryLogic::createNo();
555: foreach ($this->keyTypes as $i => $keyType) {
556: if (
557: $keyType instanceof ConstantIntegerType
558: && $offsetType instanceof StringType
559: && !$offsetType instanceof ConstantStringType
560: ) {
561: return TrinaryLogic::createMaybe();
562: }
563:
564: $has = $keyType->isSuperTypeOf($offsetType);
565: if ($has->yes()) {
566: if ($this->isOptionalKey($i)) {
567: return TrinaryLogic::createMaybe();
568: }
569: return TrinaryLogic::createYes();
570: }
571: if (!$has->maybe()) {
572: continue;
573: }
574:
575: $result = TrinaryLogic::createMaybe();
576: }
577:
578: return $result;
579: }
580:
581: public function getOffsetValueType(Type $offsetType): Type
582: {
583: $offsetType = ArrayType::castToArrayKeyType($offsetType);
584: $matchingValueTypes = [];
585: $all = true;
586: foreach ($this->keyTypes as $i => $keyType) {
587: if ($keyType->isSuperTypeOf($offsetType)->no()) {
588: $all = false;
589: continue;
590: }
591:
592: $matchingValueTypes[] = $this->valueTypes[$i];
593: }
594:
595: if ($all) {
596: if (count($this->keyTypes) === 0) {
597: return new ErrorType();
598: }
599:
600: return $this->getIterableValueType();
601: }
602:
603: if (count($matchingValueTypes) > 0) {
604: $type = TypeCombinator::union(...$matchingValueTypes);
605: if ($type instanceof ErrorType) {
606: return new MixedType();
607: }
608:
609: return $type;
610: }
611:
612: return new ErrorType(); // undefined offset
613: }
614:
615: public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $unionValues = true): Type
616: {
617: $builder = ConstantArrayTypeBuilder::createFromConstantArray($this);
618: $builder->setOffsetValueType($offsetType, $valueType);
619:
620: return $builder->getArray();
621: }
622:
623: public function unsetOffset(Type $offsetType): Type
624: {
625: $offsetType = ArrayType::castToArrayKeyType($offsetType);
626: if ($offsetType instanceof ConstantIntegerType || $offsetType instanceof ConstantStringType) {
627: foreach ($this->keyTypes as $i => $keyType) {
628: if ($keyType->getValue() !== $offsetType->getValue()) {
629: continue;
630: }
631:
632: $keyTypes = $this->keyTypes;
633: unset($keyTypes[$i]);
634: $valueTypes = $this->valueTypes;
635: unset($valueTypes[$i]);
636:
637: $newKeyTypes = [];
638: $newValueTypes = [];
639: $newOptionalKeys = [];
640:
641: $k = 0;
642: foreach ($keyTypes as $j => $newKeyType) {
643: $newKeyTypes[] = $newKeyType;
644: $newValueTypes[] = $valueTypes[$j];
645: if (in_array($j, $this->optionalKeys, true)) {
646: $newOptionalKeys[] = $k;
647: }
648: $k++;
649: }
650:
651: return new self($newKeyTypes, $newValueTypes, $this->nextAutoIndexes, $newOptionalKeys);
652: }
653:
654: return $this;
655: }
656:
657: $constantScalars = TypeUtils::getConstantScalars($offsetType);
658: if (count($constantScalars) > 0) {
659: $optionalKeys = $this->optionalKeys;
660:
661: foreach ($constantScalars as $constantScalar) {
662: $constantScalar = ArrayType::castToArrayKeyType($constantScalar);
663: if (!$constantScalar instanceof ConstantIntegerType && !$constantScalar instanceof ConstantStringType) {
664: continue;
665: }
666:
667: foreach ($this->keyTypes as $i => $keyType) {
668: if ($keyType->getValue() !== $constantScalar->getValue()) {
669: continue;
670: }
671:
672: if (in_array($i, $optionalKeys, true)) {
673: continue 2;
674: }
675:
676: $optionalKeys[] = $i;
677: }
678: }
679:
680: return new self($this->keyTypes, $this->valueTypes, $this->nextAutoIndexes, $optionalKeys);
681: }
682:
683: return new ArrayType($this->getKeyType(), $this->getItemType());
684: }
685:
686: public function isIterableAtLeastOnce(): TrinaryLogic
687: {
688: $keysCount = count($this->keyTypes);
689: if ($keysCount === 0) {
690: return TrinaryLogic::createNo();
691: }
692:
693: $optionalKeysCount = count($this->optionalKeys);
694: if ($optionalKeysCount < $keysCount) {
695: return TrinaryLogic::createYes();
696: }
697:
698: return TrinaryLogic::createMaybe();
699: }
700:
701: public function removeLast(): self
702: {
703: return $this->removeLastElements(1);
704: }
705:
706: /** @param positive-int $length */
707: private function removeLastElements(int $length): self
708: {
709: $keyTypesCount = count($this->keyTypes);
710: if ($keyTypesCount === 0) {
711: return $this;
712: }
713:
714: $keyTypes = $this->keyTypes;
715: $valueTypes = $this->valueTypes;
716: $optionalKeys = $this->optionalKeys;
717: $nextAutoindex = $this->nextAutoIndexes;
718:
719: $optionalKeysRemoved = 0;
720: $newLength = $keyTypesCount - $length;
721: for ($i = $keyTypesCount - 1; $i >= 0; $i--) {
722: $isOptional = $this->isOptionalKey($i);
723:
724: if ($i >= $newLength) {
725: if ($isOptional) {
726: $optionalKeysRemoved++;
727: foreach ($optionalKeys as $key => $value) {
728: if ($value === $i) {
729: unset($optionalKeys[$key]);
730: break;
731: }
732: }
733: }
734:
735: $removedKeyType = array_pop($keyTypes);
736: array_pop($valueTypes);
737: $nextAutoindex = $removedKeyType instanceof ConstantIntegerType
738: ? $removedKeyType->getValue()
739: : $this->getNextAutoIndex(); // @phpstan-ignore-line
740: continue;
741: }
742:
743: if ($isOptional || $optionalKeysRemoved <= 0) {
744: continue;
745: }
746:
747: $optionalKeys[] = $i;
748: $optionalKeysRemoved--;
749: }
750:
751: return new self(
752: $keyTypes,
753: $valueTypes,
754: $nextAutoindex,
755: array_values($optionalKeys),
756: );
757: }
758:
759: public function removeFirst(): self
760: {
761: return $this->removeFirstElements(1);
762: }
763:
764: /** @param positive-int $length */
765: private function removeFirstElements(int $length, bool $reindex = true): self
766: {
767: $builder = ConstantArrayTypeBuilder::createEmpty();
768:
769: $optionalKeysIgnored = 0;
770: foreach ($this->keyTypes as $i => $keyType) {
771: $isOptional = $this->isOptionalKey($i);
772: if ($i <= $length - 1) {
773: if ($isOptional) {
774: $optionalKeysIgnored++;
775: }
776: continue;
777: }
778:
779: if (!$isOptional && $optionalKeysIgnored > 0) {
780: $isOptional = true;
781: $optionalKeysIgnored--;
782: }
783:
784: $valueType = $this->valueTypes[$i];
785: if ($reindex && $keyType instanceof ConstantIntegerType) {
786: $keyType = null;
787: }
788:
789: $builder->setOffsetValueType($keyType, $valueType, $isOptional);
790: }
791:
792: $array = $builder->getArray();
793: if (!$array instanceof self) {
794: throw new ShouldNotHappenException();
795: }
796:
797: return $array;
798: }
799:
800: public function slice(int $offset, ?int $limit, bool $preserveKeys = false): self
801: {
802: $keyTypesCount = count($this->keyTypes);
803: if ($keyTypesCount === 0) {
804: return $this;
805: }
806:
807: $limit ??= $keyTypesCount;
808: if ($limit < 0) {
809: // Negative limits prevent access to the most right n elements
810: return $this->removeLastElements($limit * -1)
811: ->slice($offset, null, $preserveKeys);
812: }
813:
814: if ($keyTypesCount + $offset <= 0) {
815: // A negative offset cannot reach left outside the array
816: $offset = 0;
817: }
818:
819: if ($offset < 0) {
820: /*
821: * Transforms the problem with the negative offset in one with a positive offset using array reversion.
822: * The reason is belows handling of optional keys which works only from left to right.
823: *
824: * e.g.
825: * array{a: 0, b: 1, c: 2, d: 3, e: 4}
826: * with offset -4 and limit 2 (which would be sliced to array{b: 1, c: 2})
827: *
828: * is transformed via reversion to
829: *
830: * array{e: 4, d: 3, c: 2, b: 1, a: 0}
831: * with offset 2 and limit 2 (which will be sliced to array{c: 2, b: 1} and then reversed again)
832: */
833: $offset *= -1;
834: $reversedLimit = min($limit, $offset);
835: $reversedOffset = $offset - $reversedLimit;
836: return $this->reverse(true)
837: ->slice($reversedOffset, $reversedLimit, $preserveKeys)
838: ->reverse(true);
839: }
840:
841: if ($offset > 0) {
842: return $this->removeFirstElements($offset, false)
843: ->slice(0, $limit, $preserveKeys);
844: }
845:
846: $builder = ConstantArrayTypeBuilder::createEmpty();
847:
848: $nonOptionalElementsCount = 0;
849: $hasOptional = false;
850: for ($i = 0; $nonOptionalElementsCount < $limit && $i < $keyTypesCount; $i++) {
851: $isOptional = $this->isOptionalKey($i);
852: if (!$isOptional) {
853: $nonOptionalElementsCount++;
854: } else {
855: $hasOptional = true;
856: }
857:
858: $isLastElement = $nonOptionalElementsCount >= $limit || $i + 1 >= $keyTypesCount;
859: if ($isLastElement && $limit < $keyTypesCount && $hasOptional) {
860: // If the slice is not full yet, but has at least one optional key
861: // the last non-optional element is going to be optional.
862: // Otherwise, it would not fit into the slice if previous non-optional keys are there.
863: $isOptional = true;
864: }
865:
866: $builder->setOffsetValueType($this->keyTypes[$i], $this->valueTypes[$i], $isOptional);
867: }
868:
869: $slice = $builder->getArray();
870: if (!$slice instanceof self) {
871: throw new ShouldNotHappenException();
872: }
873:
874: return $preserveKeys ? $slice : $slice->reindex();
875: }
876:
877: public function reverse(bool $preserveKeys = false): self
878: {
879: $keyTypesReversed = array_reverse($this->keyTypes, true);
880: $keyTypes = array_values($keyTypesReversed);
881: $keyTypesReversedKeys = array_keys($keyTypesReversed);
882: $optionalKeys = array_map(static fn (int $optionalKey): int => $keyTypesReversedKeys[$optionalKey], $this->optionalKeys);
883:
884: $reversed = new self($keyTypes, array_reverse($this->valueTypes), $this->nextAutoIndexes, $optionalKeys);
885:
886: return $preserveKeys ? $reversed : $reversed->reindex();
887: }
888:
889: /** @param positive-int $length */
890: public function chunk(int $length, bool $preserveKeys = false): self
891: {
892: $builder = ConstantArrayTypeBuilder::createEmpty();
893:
894: $keyTypesCount = count($this->keyTypes);
895: for ($i = 0; $i < $keyTypesCount; $i += $length) {
896: $chunk = $this->slice($i, $length, true);
897: $builder->setOffsetValueType(null, $preserveKeys ? $chunk : $chunk->getValuesArray());
898: }
899:
900: $chunks = $builder->getArray();
901: if (!$chunks instanceof self) {
902: throw new ShouldNotHappenException();
903: }
904:
905: return $chunks;
906: }
907:
908: private function reindex(): self
909: {
910: $keyTypes = [];
911: $autoIndex = 0;
912:
913: foreach ($this->keyTypes as $keyType) {
914: if (!$keyType instanceof ConstantIntegerType) {
915: $keyTypes[] = $keyType;
916: continue;
917: }
918:
919: $keyTypes[] = new ConstantIntegerType($autoIndex);
920: $autoIndex++;
921: }
922:
923: return new self($keyTypes, $this->valueTypes, [$autoIndex], $this->optionalKeys);
924: }
925:
926: public function toBoolean(): BooleanType
927: {
928: return $this->count()->toBoolean();
929: }
930:
931: public function toInteger(): Type
932: {
933: return $this->toBoolean()->toInteger();
934: }
935:
936: public function toFloat(): Type
937: {
938: return $this->toBoolean()->toFloat();
939: }
940:
941: public function generalize(GeneralizePrecision $precision): Type
942: {
943: if (count($this->keyTypes) === 0) {
944: return $this;
945: }
946:
947: if ($precision->isTemplateArgument()) {
948: return $this->traverse(static fn (Type $type) => $type->generalize($precision));
949: }
950:
951: $arrayType = new ArrayType(
952: $this->getKeyType()->generalize($precision),
953: $this->getItemType()->generalize($precision),
954: );
955:
956: $keyTypesCount = count($this->keyTypes);
957: $optionalKeysCount = count($this->optionalKeys);
958:
959: if ($precision->isMoreSpecific() && ($keyTypesCount - $optionalKeysCount) < 32) {
960: $accessoryTypes = [];
961: foreach ($this->keyTypes as $i => $keyType) {
962: if ($this->isOptionalKey($i)) {
963: continue;
964: }
965:
966: $accessoryTypes[] = new HasOffsetValueType($keyType, $this->valueTypes[$i]->generalize($precision));
967: }
968:
969: return TypeCombinator::intersect($arrayType, ...$accessoryTypes);
970: }
971:
972: if ($keyTypesCount > $optionalKeysCount) {
973: return TypeCombinator::intersect($arrayType, new NonEmptyArrayType());
974: }
975:
976: return $arrayType;
977: }
978:
979: /**
980: * @return self
981: */
982: public function generalizeValues(): ArrayType
983: {
984: $valueTypes = [];
985: foreach ($this->valueTypes as $valueType) {
986: $valueTypes[] = $valueType->generalize(GeneralizePrecision::lessSpecific());
987: }
988:
989: return new self($this->keyTypes, $valueTypes, $this->nextAutoIndexes, $this->optionalKeys);
990: }
991:
992: public function generalizeToArray(): Type
993: {
994: if ($this->isEmpty()) {
995: return $this;
996: }
997:
998: $arrayType = new ArrayType($this->getKeyType(), $this->getItemType());
999: if ($this->isIterableAtLeastOnce()->yes()) {
1000: return TypeCombinator::intersect($arrayType, new NonEmptyArrayType());
1001: }
1002:
1003: return $arrayType;
1004: }
1005:
1006: /**
1007: * @return self
1008: */
1009: public function getKeysArray(): ArrayType
1010: {
1011: $keyTypes = [];
1012: $valueTypes = [];
1013: $optionalKeys = [];
1014: $autoIndex = 0;
1015:
1016: foreach ($this->keyTypes as $i => $keyType) {
1017: $keyTypes[] = new ConstantIntegerType($i);
1018: $valueTypes[] = $keyType;
1019: $autoIndex++;
1020:
1021: if (!$this->isOptionalKey($i)) {
1022: continue;
1023: }
1024:
1025: $optionalKeys[] = $i;
1026: }
1027:
1028: return new self($keyTypes, $valueTypes, $autoIndex, $optionalKeys);
1029: }
1030:
1031: /**
1032: * @return self
1033: */
1034: public function getValuesArray(): ArrayType
1035: {
1036: $keyTypes = [];
1037: $valueTypes = [];
1038: $optionalKeys = [];
1039: $autoIndex = 0;
1040:
1041: foreach ($this->valueTypes as $i => $valueType) {
1042: $keyTypes[] = new ConstantIntegerType($i);
1043: $valueTypes[] = $valueType;
1044: $autoIndex++;
1045:
1046: if (!$this->isOptionalKey($i)) {
1047: continue;
1048: }
1049:
1050: $optionalKeys[] = $i;
1051: }
1052:
1053: return new self($keyTypes, $valueTypes, $autoIndex, $optionalKeys);
1054: }
1055:
1056: public function count(): Type
1057: {
1058: $optionalKeysCount = count($this->optionalKeys);
1059: $totalKeysCount = count($this->getKeyTypes());
1060: if ($optionalKeysCount === 0) {
1061: return new ConstantIntegerType($totalKeysCount);
1062: }
1063:
1064: return IntegerRangeType::fromInterval($totalKeysCount - $optionalKeysCount, $totalKeysCount);
1065: }
1066:
1067: public function describe(VerbosityLevel $level): string
1068: {
1069: $describeValue = function (bool $truncate) use ($level): string {
1070: $items = [];
1071: $values = [];
1072: $exportValuesOnly = true;
1073: foreach ($this->keyTypes as $i => $keyType) {
1074: $valueType = $this->valueTypes[$i];
1075: if ($keyType->getValue() !== $i) {
1076: $exportValuesOnly = false;
1077: }
1078:
1079: $isOptional = $this->isOptionalKey($i);
1080: if ($isOptional) {
1081: $exportValuesOnly = false;
1082: }
1083:
1084: $keyDescription = $keyType->getValue();
1085: if (is_string($keyDescription)) {
1086: if (strpos($keyDescription, '"') !== false) {
1087: $keyDescription = sprintf('\'%s\'', $keyDescription);
1088: } elseif (strpos($keyDescription, '\'') !== false) {
1089: $keyDescription = sprintf('"%s"', $keyDescription);
1090: }
1091: }
1092:
1093: $valueTypeDescription = $valueType->describe($level);
1094: $items[] = sprintf('%s%s: %s', $keyDescription, $isOptional ? '?' : '', $valueTypeDescription);
1095: $values[] = $valueTypeDescription;
1096: }
1097:
1098: $append = '';
1099: if ($truncate && count($items) > self::DESCRIBE_LIMIT) {
1100: $items = array_slice($items, 0, self::DESCRIBE_LIMIT);
1101: $values = array_slice($values, 0, self::DESCRIBE_LIMIT);
1102: $append = ', ...';
1103: }
1104:
1105: return sprintf(
1106: 'array{%s%s}',
1107: implode(', ', $exportValuesOnly ? $values : $items),
1108: $append,
1109: );
1110: };
1111: return $level->handle(
1112: fn (): string => parent::describe($level),
1113: static fn (): string => $describeValue(true),
1114: static fn (): string => $describeValue(false),
1115: );
1116: }
1117:
1118: public function inferTemplateTypes(Type $receivedType): TemplateTypeMap
1119: {
1120: if ($receivedType instanceof UnionType || $receivedType instanceof IntersectionType) {
1121: return $receivedType->inferTemplateTypesOn($this);
1122: }
1123:
1124: if ($receivedType instanceof self) {
1125: $typeMap = TemplateTypeMap::createEmpty();
1126: foreach ($this->keyTypes as $i => $keyType) {
1127: $valueType = $this->valueTypes[$i];
1128: if ($receivedType->hasOffsetValueType($keyType)->no()) {
1129: continue;
1130: }
1131: $receivedValueType = $receivedType->getOffsetValueType($keyType);
1132: $typeMap = $typeMap->union($valueType->inferTemplateTypes($receivedValueType));
1133: }
1134:
1135: return $typeMap;
1136: }
1137:
1138: return parent::inferTemplateTypes($receivedType);
1139: }
1140:
1141: public function getReferencedTemplateTypes(TemplateTypeVariance $positionVariance): array
1142: {
1143: $variance = $positionVariance->compose(TemplateTypeVariance::createInvariant());
1144: $references = [];
1145:
1146: foreach ($this->keyTypes as $type) {
1147: foreach ($type->getReferencedTemplateTypes($variance) as $reference) {
1148: $references[] = $reference;
1149: }
1150: }
1151:
1152: foreach ($this->valueTypes as $type) {
1153: foreach ($type->getReferencedTemplateTypes($variance) as $reference) {
1154: $references[] = $reference;
1155: }
1156: }
1157:
1158: return $references;
1159: }
1160:
1161: public function traverse(callable $cb): Type
1162: {
1163: $valueTypes = [];
1164:
1165: $stillOriginal = true;
1166: foreach ($this->valueTypes as $valueType) {
1167: $transformedValueType = $cb($valueType);
1168: if ($transformedValueType !== $valueType) {
1169: $stillOriginal = false;
1170: }
1171:
1172: $valueTypes[] = $transformedValueType;
1173: }
1174:
1175: if ($stillOriginal) {
1176: return $this;
1177: }
1178:
1179: return new self($this->keyTypes, $valueTypes, $this->nextAutoIndexes, $this->optionalKeys);
1180: }
1181:
1182: public function isKeysSupersetOf(self $otherArray): bool
1183: {
1184: $keyTypesCount = count($this->keyTypes);
1185: $otherKeyTypesCount = count($otherArray->keyTypes);
1186:
1187: if ($keyTypesCount < $otherKeyTypesCount) {
1188: return false;
1189: }
1190:
1191: if ($otherKeyTypesCount === 0) {
1192: return $keyTypesCount === 0;
1193: }
1194:
1195: $failOnDifferentValueType = $keyTypesCount !== $otherKeyTypesCount || $keyTypesCount < 2;
1196:
1197: $keyTypes = $this->keyTypes;
1198:
1199: foreach ($otherArray->keyTypes as $j => $keyType) {
1200: $i = self::findKeyIndex($keyType, $keyTypes);
1201: if ($i === null) {
1202: return false;
1203: }
1204:
1205: unset($keyTypes[$i]);
1206:
1207: $valueType = $this->valueTypes[$i];
1208: $otherValueType = $otherArray->valueTypes[$j];
1209: if (!$otherValueType->isSuperTypeOf($valueType)->no()) {
1210: continue;
1211: }
1212:
1213: if ($failOnDifferentValueType) {
1214: return false;
1215: }
1216: $failOnDifferentValueType = true;
1217: }
1218:
1219: $requiredKeyCount = 0;
1220: foreach (array_keys($keyTypes) as $i) {
1221: if ($this->isOptionalKey($i)) {
1222: continue;
1223: }
1224:
1225: $requiredKeyCount++;
1226: if ($requiredKeyCount > 1) {
1227: return false;
1228: }
1229: }
1230:
1231: return true;
1232: }
1233:
1234: public function mergeWith(self $otherArray): self
1235: {
1236: // only call this after verifying isKeysSupersetOf
1237: $valueTypes = $this->valueTypes;
1238: $optionalKeys = $this->optionalKeys;
1239: foreach ($this->keyTypes as $i => $keyType) {
1240: $otherIndex = $otherArray->getKeyIndex($keyType);
1241: if ($otherIndex === null) {
1242: $optionalKeys[] = $i;
1243: continue;
1244: }
1245: if ($otherArray->isOptionalKey($otherIndex)) {
1246: $optionalKeys[] = $i;
1247: }
1248: $otherValueType = $otherArray->valueTypes[$otherIndex];
1249: $valueTypes[$i] = TypeCombinator::union($valueTypes[$i], $otherValueType);
1250: }
1251:
1252: $optionalKeys = array_values(array_unique($optionalKeys));
1253:
1254: $nextAutoIndexes = array_unique(array_merge($this->nextAutoIndexes, $otherArray->nextAutoIndexes));
1255: sort($nextAutoIndexes);
1256:
1257: return new self($this->keyTypes, $valueTypes, $nextAutoIndexes, $optionalKeys);
1258: }
1259:
1260: /**
1261: * @param ConstantIntegerType|ConstantStringType $otherKeyType
1262: */
1263: private function getKeyIndex($otherKeyType): ?int
1264: {
1265: return self::findKeyIndex($otherKeyType, $this->keyTypes);
1266: }
1267:
1268: /**
1269: * @param ConstantIntegerType|ConstantStringType $otherKeyType
1270: * @param array<int, ConstantIntegerType|ConstantStringType> $keyTypes
1271: */
1272: private static function findKeyIndex($otherKeyType, array $keyTypes): ?int
1273: {
1274: foreach ($keyTypes as $i => $keyType) {
1275: if ($keyType->equals($otherKeyType)) {
1276: return $i;
1277: }
1278: }
1279:
1280: return null;
1281: }
1282:
1283: public function makeOffsetRequired(Type $offsetType): self
1284: {
1285: $offsetType = ArrayType::castToArrayKeyType($offsetType);
1286: $optionalKeys = $this->optionalKeys;
1287: foreach ($this->keyTypes as $i => $keyType) {
1288: if (!$keyType->equals($offsetType)) {
1289: continue;
1290: }
1291:
1292: foreach ($optionalKeys as $j => $key) {
1293: if ($i === $key) {
1294: unset($optionalKeys[$j]);
1295: return new self($this->keyTypes, $this->valueTypes, $this->nextAutoIndexes, array_values($optionalKeys));
1296: }
1297: }
1298:
1299: break;
1300: }
1301:
1302: return $this;
1303: }
1304:
1305: /**
1306: * @param mixed[] $properties
1307: */
1308: public static function __set_state(array $properties): Type
1309: {
1310: return new self($properties['keyTypes'], $properties['valueTypes'], $properties['nextAutoIndexes'] ?? $properties['nextAutoIndex'], $properties['optionalKeys'] ?? []);
1311: }
1312:
1313: }
1314: