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