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