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