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