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