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