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: $methodReflection = $type->getMethod($methodName->getValue(), new OutOfClassScope());
568: if ($classOrObject->isString()->yes() && !$methodReflection->isStatic()) {
569: continue;
570: }
571: }
572:
573: if ($this->isOptionalKey(0) || $this->isOptionalKey(1)) {
574: $has = $has->and(TrinaryLogic::createMaybe());
575: }
576:
577: $typeAndMethods[] = ConstantArrayTypeAndMethod::createConcrete($type, $methodName->getValue(), $has);
578: }
579:
580: return $typeAndMethods;
581: }
582:
583: public function hasOffsetValueType(Type $offsetType): TrinaryLogic
584: {
585: $offsetArrayKeyType = $offsetType->toArrayKey();
586: if ($offsetArrayKeyType instanceof ErrorType) {
587: $allowedArrayKeys = AllowedArrayKeysTypes::getType();
588: $offsetArrayKeyType = TypeCombinator::intersect($allowedArrayKeys, $offsetType)->toArrayKey();
589: if ($offsetArrayKeyType instanceof NeverType) {
590: return TrinaryLogic::createNo();
591: }
592: }
593:
594: return $this->recursiveHasOffsetValueType($offsetArrayKeyType);
595: }
596:
597: private function recursiveHasOffsetValueType(Type $offsetType): TrinaryLogic
598: {
599: if ($offsetType instanceof UnionType) {
600: $results = [];
601: foreach ($offsetType->getTypes() as $innerType) {
602: $results[] = $this->recursiveHasOffsetValueType($innerType);
603: }
604:
605: return TrinaryLogic::extremeIdentity(...$results);
606: }
607: if ($offsetType instanceof IntegerRangeType) {
608: $finiteTypes = $offsetType->getFiniteTypes();
609: if ($finiteTypes !== []) {
610: $results = [];
611: foreach ($finiteTypes as $innerType) {
612: $results[] = $this->recursiveHasOffsetValueType($innerType);
613: }
614:
615: return TrinaryLogic::extremeIdentity(...$results);
616: }
617: }
618:
619: $result = TrinaryLogic::createNo();
620: foreach ($this->keyTypes as $i => $keyType) {
621: if (
622: $keyType instanceof ConstantIntegerType
623: && !$offsetType->isString()->no()
624: && $offsetType->isConstantScalarValue()->no()
625: ) {
626: return TrinaryLogic::createMaybe();
627: }
628:
629: $has = $keyType->isSuperTypeOf($offsetType);
630: if ($has->yes()) {
631: if ($this->isOptionalKey($i)) {
632: return TrinaryLogic::createMaybe();
633: }
634: return TrinaryLogic::createYes();
635: }
636: if (!$has->maybe()) {
637: continue;
638: }
639:
640: $result = TrinaryLogic::createMaybe();
641: }
642:
643: return $result;
644: }
645:
646: public function getOffsetValueType(Type $offsetType): Type
647: {
648: if (count($this->keyTypes) === 0) {
649: return new ErrorType();
650: }
651:
652: $offsetType = $offsetType->toArrayKey();
653: $matchingValueTypes = [];
654: $all = true;
655: $maybeAll = true;
656: foreach ($this->keyTypes as $i => $keyType) {
657: if ($keyType->isSuperTypeOf($offsetType)->no()) {
658: $all = false;
659:
660: if (
661: $keyType instanceof ConstantIntegerType
662: && !$offsetType->isString()->no()
663: && $offsetType->isConstantScalarValue()->no()
664: ) {
665: continue;
666: }
667: $maybeAll = false;
668: continue;
669: }
670:
671: $matchingValueTypes[] = $this->valueTypes[$i];
672: }
673:
674: if ($all) {
675: return $this->getIterableValueType();
676: }
677:
678: if (count($matchingValueTypes) > 0) {
679: $type = TypeCombinator::union(...$matchingValueTypes);
680: if ($type instanceof ErrorType) {
681: return new MixedType();
682: }
683:
684: return $type;
685: }
686:
687: if ($maybeAll) {
688: return $this->getIterableValueType();
689: }
690:
691: return new ErrorType(); // undefined offset
692: }
693:
694: public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $unionValues = true): Type
695: {
696: $builder = ConstantArrayTypeBuilder::createFromConstantArray($this);
697: $builder->setOffsetValueType($offsetType, $valueType);
698:
699: return $builder->getArray();
700: }
701:
702: public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type
703: {
704: $builder = ConstantArrayTypeBuilder::createFromConstantArray($this);
705: $builder->setOffsetValueType($offsetType, $valueType);
706:
707: return $builder->getArray();
708: }
709:
710: public function unsetOffset(Type $offsetType): Type
711: {
712: $offsetType = $offsetType->toArrayKey();
713: if ($offsetType instanceof ConstantIntegerType || $offsetType instanceof ConstantStringType) {
714: foreach ($this->keyTypes as $i => $keyType) {
715: if ($keyType->getValue() !== $offsetType->getValue()) {
716: continue;
717: }
718:
719: $keyTypes = $this->keyTypes;
720: unset($keyTypes[$i]);
721: $valueTypes = $this->valueTypes;
722: unset($valueTypes[$i]);
723:
724: $newKeyTypes = [];
725: $newValueTypes = [];
726: $newOptionalKeys = [];
727:
728: $k = 0;
729: foreach ($keyTypes as $j => $newKeyType) {
730: $newKeyTypes[] = $newKeyType;
731: $newValueTypes[] = $valueTypes[$j];
732: if (in_array($j, $this->optionalKeys, true)) {
733: $newOptionalKeys[] = $k;
734: }
735: $k++;
736: }
737:
738: return new self($newKeyTypes, $newValueTypes, $this->nextAutoIndexes, $newOptionalKeys, TrinaryLogic::createNo());
739: }
740:
741: return $this;
742: }
743:
744: $constantScalars = $offsetType->getConstantScalarTypes();
745: if (count($constantScalars) > 0) {
746: $optionalKeys = $this->optionalKeys;
747:
748: foreach ($constantScalars as $constantScalar) {
749: $constantScalar = $constantScalar->toArrayKey();
750: if (!$constantScalar instanceof ConstantIntegerType && !$constantScalar instanceof ConstantStringType) {
751: continue;
752: }
753:
754: foreach ($this->keyTypes as $i => $keyType) {
755: if ($keyType->getValue() !== $constantScalar->getValue()) {
756: continue;
757: }
758:
759: if (in_array($i, $optionalKeys, true)) {
760: continue 2;
761: }
762:
763: $optionalKeys[] = $i;
764: }
765: }
766:
767: return new self($this->keyTypes, $this->valueTypes, $this->nextAutoIndexes, $optionalKeys, TrinaryLogic::createNo());
768: }
769:
770: $optionalKeys = $this->optionalKeys;
771: $isList = $this->isList;
772: foreach ($this->keyTypes as $i => $keyType) {
773: if (!$offsetType->isSuperTypeOf($keyType)->yes()) {
774: continue;
775: }
776: $optionalKeys[] = $i;
777: $isList = TrinaryLogic::createNo();
778: }
779: $optionalKeys = array_values(array_unique($optionalKeys));
780:
781: return new self($this->keyTypes, $this->valueTypes, $this->nextAutoIndexes, $optionalKeys, $isList);
782: }
783:
784: public function chunkArray(Type $lengthType, TrinaryLogic $preserveKeys): Type
785: {
786: $biggerOne = IntegerRangeType::fromInterval(1, null);
787: $finiteTypes = $lengthType->getFiniteTypes();
788: if ($biggerOne->isSuperTypeOf($lengthType)->yes() && count($finiteTypes) < self::CHUNK_FINITE_TYPES_LIMIT) {
789: $results = [];
790: foreach ($finiteTypes as $finiteType) {
791: if (!$finiteType instanceof ConstantIntegerType || $finiteType->getValue() < 1) {
792: return $this->traitChunkArray($lengthType, $preserveKeys);
793: }
794:
795: $length = $finiteType->getValue();
796:
797: $builder = ConstantArrayTypeBuilder::createEmpty();
798:
799: $keyTypesCount = count($this->keyTypes);
800: for ($i = 0; $i < $keyTypesCount; $i += $length) {
801: $chunk = $this->sliceArray(new ConstantIntegerType($i), new ConstantIntegerType($length), TrinaryLogic::createYes());
802: $builder->setOffsetValueType(null, $preserveKeys->yes() ? $chunk : $chunk->getValuesArray());
803: }
804:
805: $results[] = $builder->getArray();
806: }
807:
808: return TypeCombinator::union(...$results);
809: }
810:
811: return $this->traitChunkArray($lengthType, $preserveKeys);
812: }
813:
814: public function fillKeysArray(Type $valueType): Type
815: {
816: $builder = ConstantArrayTypeBuilder::createEmpty();
817:
818: foreach ($this->valueTypes as $i => $keyType) {
819: if ($keyType->isInteger()->no()) {
820: $stringKeyType = $keyType->toString();
821: if ($stringKeyType instanceof ErrorType) {
822: return $stringKeyType;
823: }
824:
825: $builder->setOffsetValueType($stringKeyType, $valueType, $this->isOptionalKey($i));
826: } else {
827: $builder->setOffsetValueType($keyType, $valueType, $this->isOptionalKey($i));
828: }
829: }
830:
831: return $builder->getArray();
832: }
833:
834: public function flipArray(): Type
835: {
836: $builder = ConstantArrayTypeBuilder::createEmpty();
837:
838: foreach ($this->keyTypes as $i => $keyType) {
839: $valueType = $this->valueTypes[$i];
840: $builder->setOffsetValueType(
841: $valueType->toArrayKey(),
842: $keyType,
843: $this->isOptionalKey($i),
844: );
845: }
846:
847: return $builder->getArray();
848: }
849:
850: public function intersectKeyArray(Type $otherArraysType): Type
851: {
852: $builder = ConstantArrayTypeBuilder::createEmpty();
853:
854: foreach ($this->keyTypes as $i => $keyType) {
855: $valueType = $this->valueTypes[$i];
856: $has = $otherArraysType->hasOffsetValueType($keyType);
857: if ($has->no()) {
858: continue;
859: }
860: $builder->setOffsetValueType($keyType, $valueType, $this->isOptionalKey($i) || !$has->yes());
861: }
862:
863: return $builder->getArray();
864: }
865:
866: public function popArray(): Type
867: {
868: return $this->removeLastElements(1);
869: }
870:
871: public function reverseArray(TrinaryLogic $preserveKeys): Type
872: {
873: $builder = ConstantArrayTypeBuilder::createEmpty();
874:
875: for ($i = count($this->keyTypes) - 1; $i >= 0; $i--) {
876: $offsetType = $preserveKeys->yes() || $this->keyTypes[$i]->isInteger()->no()
877: ? $this->keyTypes[$i]
878: : null;
879: $builder->setOffsetValueType($offsetType, $this->valueTypes[$i], $this->isOptionalKey($i));
880: }
881:
882: return $builder->getArray();
883: }
884:
885: public function searchArray(Type $needleType, ?TrinaryLogic $strict = null): Type
886: {
887: $strict ??= TrinaryLogic::createMaybe();
888: $matches = [];
889: $hasIdenticalValue = false;
890:
891: foreach ($this->valueTypes as $index => $valueType) {
892: if ($strict->yes()) {
893: $isNeedleSuperType = $valueType->isSuperTypeOf($needleType);
894: if ($isNeedleSuperType->no()) {
895: continue;
896: }
897: }
898:
899: if ($needleType instanceof ConstantScalarType && $valueType instanceof ConstantScalarType) {
900: // @phpstan-ignore equal.notAllowed
901: $isLooseEqual = $needleType->getValue() == $valueType->getValue(); // phpcs:ignore
902: if (!$isLooseEqual) {
903: continue;
904: }
905: if (
906: ($strict->no() || $needleType->getValue() === $valueType->getValue())
907: && !$this->isOptionalKey($index)
908: ) {
909: $hasIdenticalValue = true;
910: }
911: }
912:
913: $matches[] = $this->keyTypes[$index];
914: }
915:
916: if (count($matches) > 0) {
917: if ($hasIdenticalValue) {
918: return TypeCombinator::union(...$matches);
919: }
920:
921: return TypeCombinator::union(new ConstantBooleanType(false), ...$matches);
922: }
923:
924: return new ConstantBooleanType(false);
925: }
926:
927: public function shiftArray(): Type
928: {
929: return $this->removeFirstElements(1);
930: }
931:
932: public function shuffleArray(): Type
933: {
934: return $this->getValuesArray()->degradeToGeneralArray();
935: }
936:
937: public function sliceArray(Type $offsetType, Type $lengthType, TrinaryLogic $preserveKeys): Type
938: {
939: $keyTypesCount = count($this->keyTypes);
940: if ($keyTypesCount === 0) {
941: return $this;
942: }
943:
944: $offset = $offsetType instanceof ConstantIntegerType ? $offsetType->getValue() : null;
945:
946: if ($lengthType instanceof ConstantIntegerType) {
947: $length = $lengthType->getValue();
948: } elseif ($lengthType->isNull()->yes()) {
949: $length = $keyTypesCount;
950: } else {
951: $length = null;
952: }
953:
954: if ($offset === null || $length === null) {
955: return $this->degradeToGeneralArray()
956: ->sliceArray($offsetType, $lengthType, $preserveKeys);
957: }
958:
959: if ($keyTypesCount + $offset <= 0) {
960: // A negative offset cannot reach left outside the array twice
961: $offset = 0;
962: }
963:
964: if ($keyTypesCount + $length <= 0) {
965: // A negative length cannot reach left outside the array twice
966: $length = 0;
967: }
968:
969: if ($length === 0 || ($offset < 0 && $length < 0 && $offset - $length >= 0)) {
970: // 0 / 0, 3 / 0 or e.g. -3 / -3 or -3 / -4 and so on never extract anything
971: return new self([], []);
972: }
973:
974: if ($length < 0) {
975: // Negative lengths prevent access to the most right n elements
976: return $this->removeLastElements($length * -1)
977: ->sliceArray($offsetType, new NullType(), $preserveKeys);
978: }
979:
980: if ($offset < 0) {
981: /*
982: * Transforms the problem with the negative offset in one with a positive offset using array reversion.
983: * The reason is below handling of optional keys which works only from left to right.
984: *
985: * e.g.
986: * array{a: 0, b: 1, c: 2, d: 3, e: 4}
987: * with offset -4 and length 2 (which would be sliced to array{b: 1, c: 2})
988: *
989: * is transformed via reversion to
990: *
991: * array{e: 4, d: 3, c: 2, b: 1, a: 0}
992: * with offset 2 and length 2 (which will be sliced to array{c: 2, b: 1} and then reversed again)
993: */
994: $offset *= -1;
995: $reversedLength = min($length, $offset);
996: $reversedOffset = $offset - $reversedLength;
997: return $this->reverseArray(TrinaryLogic::createYes())
998: ->sliceArray(new ConstantIntegerType($reversedOffset), new ConstantIntegerType($reversedLength), $preserveKeys)
999: ->reverseArray(TrinaryLogic::createYes());
1000: }
1001:
1002: if ($offset > 0) {
1003: return $this->removeFirstElements($offset, false)
1004: ->sliceArray(new ConstantIntegerType(0), $lengthType, $preserveKeys);
1005: }
1006:
1007: $builder = ConstantArrayTypeBuilder::createEmpty();
1008:
1009: $nonOptionalElementsCount = 0;
1010: $hasOptional = false;
1011: for ($i = 0; $nonOptionalElementsCount < $length && $i < $keyTypesCount; $i++) {
1012: $isOptional = $this->isOptionalKey($i);
1013: if (!$isOptional) {
1014: $nonOptionalElementsCount++;
1015: } else {
1016: $hasOptional = true;
1017: }
1018:
1019: $isLastElement = $nonOptionalElementsCount >= $length || $i + 1 >= $keyTypesCount;
1020: if ($isLastElement && $length < $keyTypesCount && $hasOptional) {
1021: // If the slice is not full yet, but has at least one optional key
1022: // the last non-optional element is going to be optional.
1023: // Otherwise, it would not fit into the slice if previous non-optional keys are there.
1024: $isOptional = true;
1025: }
1026:
1027: $offsetType = $preserveKeys->yes() || $this->keyTypes[$i]->isInteger()->no()
1028: ? $this->keyTypes[$i]
1029: : null;
1030:
1031: $builder->setOffsetValueType($offsetType, $this->valueTypes[$i], $isOptional);
1032: }
1033:
1034: return $builder->getArray();
1035: }
1036:
1037: public function spliceArray(Type $offsetType, Type $lengthType, Type $replacementType): Type
1038: {
1039: $keyTypesCount = count($this->keyTypes);
1040: if ($keyTypesCount === 0) {
1041: return $this;
1042: }
1043:
1044: $offset = $offsetType instanceof ConstantIntegerType ? $offsetType->getValue() : null;
1045:
1046: if ($lengthType instanceof ConstantIntegerType) {
1047: $length = $lengthType->getValue();
1048: } elseif ($lengthType->isNull()->yes()) {
1049: $length = $keyTypesCount;
1050: } else {
1051: $length = null;
1052: }
1053:
1054: if ($offset === null || $length === null) {
1055: return $this->degradeToGeneralArray()
1056: ->spliceArray($offsetType, $lengthType, $replacementType);
1057: }
1058:
1059: if ($keyTypesCount + $offset <= 0) {
1060: // A negative offset cannot reach left outside the array twice
1061: $offset = 0;
1062: }
1063:
1064: if ($keyTypesCount + $length <= 0) {
1065: // A negative length cannot reach left outside the array twice
1066: $length = 0;
1067: }
1068:
1069: $offsetWasNegative = false;
1070: if ($offset < 0) {
1071: $offsetWasNegative = true;
1072: $offset = $keyTypesCount + $offset;
1073: }
1074:
1075: if ($length < 0) {
1076: $length = $keyTypesCount - $offset + $length;
1077: }
1078:
1079: $extractType = $this->sliceArray($offsetType, $lengthType, TrinaryLogic::createYes());
1080:
1081: $types = [];
1082: foreach ($replacementType->toArray()->getArrays() as $replacementArrayType) {
1083: $removeKeysCount = 0;
1084: $optionalKeysBeforeReplacement = 0;
1085:
1086: $builder = ConstantArrayTypeBuilder::createEmpty();
1087: for ($i = 0;; $i++) {
1088: $isOptional = $this->isOptionalKey($i);
1089:
1090: if (!$offsetWasNegative && $i < $offset && $isOptional) {
1091: $optionalKeysBeforeReplacement++;
1092: }
1093:
1094: if ($i === $offset + $optionalKeysBeforeReplacement) {
1095: // When the offset is reached we have to a) put the replacement array in and b) remove $length elements
1096: $removeKeysCount = $length;
1097:
1098: if ($replacementArrayType instanceof self) {
1099: $valuesArray = $replacementArrayType->getValuesArray();
1100: for ($j = 0, $jMax = count($valuesArray->keyTypes); $j < $jMax; $j++) {
1101: $builder->setOffsetValueType(null, $valuesArray->valueTypes[$j], $valuesArray->isOptionalKey($j));
1102: }
1103: } else {
1104: $builder->degradeToGeneralArray();
1105: $builder->setOffsetValueType($replacementArrayType->getValuesArray()->getIterableKeyType(), $replacementArrayType->getIterableValueType(), true);
1106: }
1107: }
1108:
1109: if (!isset($this->keyTypes[$i])) {
1110: break;
1111: }
1112:
1113: if ($removeKeysCount > 0) {
1114: $extractTypeHasOffsetValueType = $extractType->hasOffsetValueType($this->keyTypes[$i]);
1115:
1116: if (
1117: (!$isOptional && $extractTypeHasOffsetValueType->yes())
1118: || ($isOptional && $extractTypeHasOffsetValueType->maybe())
1119: ) {
1120: $removeKeysCount--;
1121: continue;
1122: }
1123: }
1124:
1125: if (!$isOptional && $extractType->hasOffsetValueType($this->keyTypes[$i])->maybe()) {
1126: $isOptional = true;
1127: }
1128:
1129: $builder->setOffsetValueType(
1130: $this->keyTypes[$i]->isInteger()->no() ? $this->keyTypes[$i] : null,
1131: $this->valueTypes[$i],
1132: $isOptional,
1133: );
1134: }
1135:
1136: $types[] = $builder->getArray();
1137: }
1138:
1139: return TypeCombinator::union(...$types);
1140: }
1141:
1142: public function isIterableAtLeastOnce(): TrinaryLogic
1143: {
1144: $keysCount = count($this->keyTypes);
1145: if ($keysCount === 0) {
1146: return TrinaryLogic::createNo();
1147: }
1148:
1149: $optionalKeysCount = count($this->optionalKeys);
1150: if ($optionalKeysCount < $keysCount) {
1151: return TrinaryLogic::createYes();
1152: }
1153:
1154: return TrinaryLogic::createMaybe();
1155: }
1156:
1157: public function getArraySize(): Type
1158: {
1159: $optionalKeysCount = count($this->optionalKeys);
1160: $totalKeysCount = count($this->getKeyTypes());
1161: if ($optionalKeysCount === 0) {
1162: return new ConstantIntegerType($totalKeysCount);
1163: }
1164:
1165: return IntegerRangeType::fromInterval($totalKeysCount - $optionalKeysCount, $totalKeysCount);
1166: }
1167:
1168: public function getFirstIterableKeyType(): Type
1169: {
1170: $keyTypes = [];
1171: foreach ($this->keyTypes as $i => $keyType) {
1172: $keyTypes[] = $keyType;
1173: if (!$this->isOptionalKey($i)) {
1174: break;
1175: }
1176: }
1177:
1178: return TypeCombinator::union(...$keyTypes);
1179: }
1180:
1181: public function getLastIterableKeyType(): Type
1182: {
1183: $keyTypes = [];
1184: for ($i = count($this->keyTypes) - 1; $i >= 0; $i--) {
1185: $keyTypes[] = $this->keyTypes[$i];
1186: if (!$this->isOptionalKey($i)) {
1187: break;
1188: }
1189: }
1190:
1191: return TypeCombinator::union(...$keyTypes);
1192: }
1193:
1194: public function getFirstIterableValueType(): Type
1195: {
1196: $valueTypes = [];
1197: foreach ($this->valueTypes as $i => $valueType) {
1198: $valueTypes[] = $valueType;
1199: if (!$this->isOptionalKey($i)) {
1200: break;
1201: }
1202: }
1203:
1204: return TypeCombinator::union(...$valueTypes);
1205: }
1206:
1207: public function getLastIterableValueType(): Type
1208: {
1209: $valueTypes = [];
1210: for ($i = count($this->keyTypes) - 1; $i >= 0; $i--) {
1211: $valueTypes[] = $this->valueTypes[$i];
1212: if (!$this->isOptionalKey($i)) {
1213: break;
1214: }
1215: }
1216:
1217: return TypeCombinator::union(...$valueTypes);
1218: }
1219:
1220: public function isConstantArray(): TrinaryLogic
1221: {
1222: return TrinaryLogic::createYes();
1223: }
1224:
1225: public function isList(): TrinaryLogic
1226: {
1227: return $this->isList;
1228: }
1229:
1230: /** @param positive-int $length */
1231: private function removeLastElements(int $length): self
1232: {
1233: $keyTypesCount = count($this->keyTypes);
1234: if ($keyTypesCount === 0) {
1235: return $this;
1236: }
1237:
1238: $keyTypes = $this->keyTypes;
1239: $valueTypes = $this->valueTypes;
1240: $optionalKeys = $this->optionalKeys;
1241: $nextAutoindexes = $this->nextAutoIndexes;
1242:
1243: $optionalKeysRemoved = 0;
1244: $newLength = $keyTypesCount - $length;
1245: for ($i = $keyTypesCount - 1; $i >= 0; $i--) {
1246: $isOptional = $this->isOptionalKey($i);
1247:
1248: if ($i >= $newLength) {
1249: if ($isOptional) {
1250: $optionalKeysRemoved++;
1251: foreach ($optionalKeys as $key => $value) {
1252: if ($value === $i) {
1253: unset($optionalKeys[$key]);
1254: break;
1255: }
1256: }
1257: }
1258:
1259: $removedKeyType = array_pop($keyTypes);
1260: array_pop($valueTypes);
1261: $nextAutoindexes = $removedKeyType instanceof ConstantIntegerType
1262: ? [$removedKeyType->getValue()]
1263: : $this->nextAutoIndexes;
1264: continue;
1265: }
1266:
1267: if ($isOptional || $optionalKeysRemoved <= 0) {
1268: continue;
1269: }
1270:
1271: $optionalKeys[] = $i;
1272: $optionalKeysRemoved--;
1273: }
1274:
1275: return new self(
1276: $keyTypes,
1277: $valueTypes,
1278: $nextAutoindexes,
1279: array_values($optionalKeys),
1280: $this->isList,
1281: );
1282: }
1283:
1284: /** @param positive-int $length */
1285: private function removeFirstElements(int $length, bool $reindex = true): Type
1286: {
1287: $builder = ConstantArrayTypeBuilder::createEmpty();
1288:
1289: $optionalKeysIgnored = 0;
1290: foreach ($this->keyTypes as $i => $keyType) {
1291: $isOptional = $this->isOptionalKey($i);
1292: if ($i <= $length - 1) {
1293: if ($isOptional) {
1294: $optionalKeysIgnored++;
1295: }
1296: continue;
1297: }
1298:
1299: if (!$isOptional && $optionalKeysIgnored > 0) {
1300: $isOptional = true;
1301: $optionalKeysIgnored--;
1302: }
1303:
1304: $valueType = $this->valueTypes[$i];
1305: if ($reindex && $keyType instanceof ConstantIntegerType) {
1306: $keyType = null;
1307: }
1308:
1309: $builder->setOffsetValueType($keyType, $valueType, $isOptional);
1310: }
1311:
1312: return $builder->getArray();
1313: }
1314:
1315: public function toBoolean(): BooleanType
1316: {
1317: return $this->getArraySize()->toBoolean();
1318: }
1319:
1320: public function toInteger(): Type
1321: {
1322: return $this->toBoolean()->toInteger();
1323: }
1324:
1325: public function toFloat(): Type
1326: {
1327: return $this->toBoolean()->toFloat();
1328: }
1329:
1330: public function generalize(GeneralizePrecision $precision): Type
1331: {
1332: if (count($this->keyTypes) === 0) {
1333: return $this;
1334: }
1335:
1336: if ($precision->isTemplateArgument()) {
1337: return $this->traverse(static fn (Type $type) => $type->generalize($precision));
1338: }
1339:
1340: $arrayType = new ArrayType(
1341: $this->getIterableKeyType()->generalize($precision),
1342: $this->getIterableValueType()->generalize($precision),
1343: );
1344:
1345: $keyTypesCount = count($this->keyTypes);
1346: $optionalKeysCount = count($this->optionalKeys);
1347:
1348: $accessoryTypes = [];
1349: if ($precision->isMoreSpecific() && ($keyTypesCount - $optionalKeysCount) < 32) {
1350: foreach ($this->keyTypes as $i => $keyType) {
1351: if ($this->isOptionalKey($i)) {
1352: continue;
1353: }
1354:
1355: $accessoryTypes[] = new HasOffsetValueType($keyType, $this->valueTypes[$i]->generalize($precision));
1356: }
1357: } elseif ($keyTypesCount > $optionalKeysCount) {
1358: $accessoryTypes[] = new NonEmptyArrayType();
1359: }
1360:
1361: if ($this->isList()->yes()) {
1362: $arrayType = TypeCombinator::intersect($arrayType, new AccessoryArrayListType());
1363: }
1364:
1365: if (count($accessoryTypes) > 0) {
1366: return TypeCombinator::intersect($arrayType, ...$accessoryTypes);
1367: }
1368:
1369: return $arrayType;
1370: }
1371:
1372: public function generalizeValues(): self
1373: {
1374: $valueTypes = [];
1375: foreach ($this->valueTypes as $valueType) {
1376: $valueTypes[] = $valueType->generalize(GeneralizePrecision::lessSpecific());
1377: }
1378:
1379: return new self($this->keyTypes, $valueTypes, $this->nextAutoIndexes, $this->optionalKeys, $this->isList);
1380: }
1381:
1382: private function degradeToGeneralArray(): Type
1383: {
1384: $builder = ConstantArrayTypeBuilder::createFromConstantArray($this);
1385: $builder->degradeToGeneralArray();
1386:
1387: return $builder->getArray();
1388: }
1389:
1390: public function getKeysArrayFiltered(Type $filterValueType, TrinaryLogic $strict): Type
1391: {
1392: $keysArray = $this->getKeysOrValuesArray($this->keyTypes);
1393:
1394: return new IntersectionType([
1395: new ArrayType(
1396: IntegerRangeType::createAllGreaterThanOrEqualTo(0),
1397: $keysArray->getIterableValueType(),
1398: ),
1399: new AccessoryArrayListType(),
1400: ]);
1401: }
1402:
1403: public function getKeysArray(): self
1404: {
1405: return $this->getKeysOrValuesArray($this->keyTypes);
1406: }
1407:
1408: public function getValuesArray(): self
1409: {
1410: return $this->getKeysOrValuesArray($this->valueTypes);
1411: }
1412:
1413: /**
1414: * @param array<int, Type> $types
1415: */
1416: private function getKeysOrValuesArray(array $types): self
1417: {
1418: $count = count($types);
1419: $autoIndexes = range($count - count($this->optionalKeys), $count);
1420:
1421: if ($this->isList->yes()) {
1422: // Optimized version for lists: Assume that if a later key exists, then earlier keys also exist.
1423: $keyTypes = array_map(
1424: static fn (int $i): ConstantIntegerType => new ConstantIntegerType($i),
1425: array_keys($types),
1426: );
1427: return new self($keyTypes, $types, $autoIndexes, $this->optionalKeys, TrinaryLogic::createYes());
1428: }
1429:
1430: $keyTypes = [];
1431: $valueTypes = [];
1432: $optionalKeys = [];
1433: $maxIndex = 0;
1434:
1435: foreach ($types as $i => $type) {
1436: $keyTypes[] = new ConstantIntegerType($i);
1437:
1438: if ($this->isOptionalKey($maxIndex)) {
1439: // move $maxIndex to next non-optional key
1440: do {
1441: $maxIndex++;
1442: } while ($maxIndex < $count && $this->isOptionalKey($maxIndex));
1443: }
1444:
1445: if ($i === $maxIndex) {
1446: $valueTypes[] = $type;
1447: } else {
1448: $valueTypes[] = TypeCombinator::union(...array_slice($types, $i, $maxIndex - $i + 1));
1449: if ($maxIndex >= $count) {
1450: $optionalKeys[] = $i;
1451: }
1452: }
1453: $maxIndex++;
1454: }
1455:
1456: return new self($keyTypes, $valueTypes, $autoIndexes, $optionalKeys, TrinaryLogic::createYes());
1457: }
1458:
1459: public function describe(VerbosityLevel $level): string
1460: {
1461: $describeValue = function (bool $truncate) use ($level): string {
1462: $items = [];
1463: $values = [];
1464: $exportValuesOnly = true;
1465: foreach ($this->keyTypes as $i => $keyType) {
1466: $valueType = $this->valueTypes[$i];
1467: if ($keyType->getValue() !== $i) {
1468: $exportValuesOnly = false;
1469: }
1470:
1471: $isOptional = $this->isOptionalKey($i);
1472: if ($isOptional) {
1473: $exportValuesOnly = false;
1474: }
1475:
1476: $keyDescription = $keyType->getValue();
1477: if (is_string($keyDescription)) {
1478: if (str_contains($keyDescription, '"')) {
1479: $keyDescription = sprintf('\'%s\'', $keyDescription);
1480: } elseif (str_contains($keyDescription, '\'')) {
1481: $keyDescription = sprintf('"%s"', $keyDescription);
1482: } elseif (!self::isValidIdentifier($keyDescription)) {
1483: $keyDescription = sprintf('\'%s\'', $keyDescription);
1484: }
1485: }
1486:
1487: $valueTypeDescription = $valueType->describe($level);
1488: $items[] = sprintf('%s%s: %s', $keyDescription, $isOptional ? '?' : '', $valueTypeDescription);
1489: $values[] = $valueTypeDescription;
1490: }
1491:
1492: $append = '';
1493: if ($truncate && count($items) > self::DESCRIBE_LIMIT) {
1494: $items = array_slice($items, 0, self::DESCRIBE_LIMIT);
1495: $values = array_slice($values, 0, self::DESCRIBE_LIMIT);
1496: $append = ', ...';
1497: }
1498:
1499: return sprintf(
1500: 'array{%s%s}',
1501: implode(', ', $exportValuesOnly ? $values : $items),
1502: $append,
1503: );
1504: };
1505: return $level->handle(
1506: fn (): string => $this->isIterableAtLeastOnce()->no() ? 'array' : sprintf('array<%s, %s>', $this->getIterableKeyType()->describe($level), $this->getIterableValueType()->describe($level)),
1507: static fn (): string => $describeValue(true),
1508: static fn (): string => $describeValue(false),
1509: );
1510: }
1511:
1512: public function inferTemplateTypes(Type $receivedType): TemplateTypeMap
1513: {
1514: if ($receivedType instanceof UnionType || $receivedType instanceof IntersectionType) {
1515: return $receivedType->inferTemplateTypesOn($this);
1516: }
1517:
1518: if ($receivedType instanceof self) {
1519: $typeMap = TemplateTypeMap::createEmpty();
1520: foreach ($this->keyTypes as $i => $keyType) {
1521: $valueType = $this->valueTypes[$i];
1522: if ($receivedType->hasOffsetValueType($keyType)->no()) {
1523: continue;
1524: }
1525: $receivedValueType = $receivedType->getOffsetValueType($keyType);
1526: $typeMap = $typeMap->union($valueType->inferTemplateTypes($receivedValueType));
1527: }
1528:
1529: return $typeMap;
1530: }
1531:
1532: if ($receivedType->isArray()->yes()) {
1533: $keyTypeMap = $this->getIterableKeyType()->inferTemplateTypes($receivedType->getIterableKeyType());
1534: $itemTypeMap = $this->getIterableValueType()->inferTemplateTypes($receivedType->getIterableValueType());
1535:
1536: return $keyTypeMap->union($itemTypeMap);
1537: }
1538:
1539: return TemplateTypeMap::createEmpty();
1540: }
1541:
1542: public function getReferencedTemplateTypes(TemplateTypeVariance $positionVariance): array
1543: {
1544: $variance = $positionVariance->compose(TemplateTypeVariance::createCovariant());
1545: $references = [];
1546:
1547: foreach ($this->keyTypes as $type) {
1548: foreach ($type->getReferencedTemplateTypes($variance) as $reference) {
1549: $references[] = $reference;
1550: }
1551: }
1552:
1553: foreach ($this->valueTypes as $type) {
1554: foreach ($type->getReferencedTemplateTypes($variance) as $reference) {
1555: $references[] = $reference;
1556: }
1557: }
1558:
1559: return $references;
1560: }
1561:
1562: public function tryRemove(Type $typeToRemove): ?Type
1563: {
1564: if ($typeToRemove->isConstantArray()->yes() && $typeToRemove->isIterableAtLeastOnce()->no()) {
1565: return TypeCombinator::intersect($this, new NonEmptyArrayType());
1566: }
1567:
1568: if ($typeToRemove instanceof NonEmptyArrayType) {
1569: return new ConstantArrayType([], []);
1570: }
1571:
1572: if ($typeToRemove instanceof HasOffsetType) {
1573: return $this->unsetOffset($typeToRemove->getOffsetType());
1574: }
1575:
1576: if ($typeToRemove instanceof HasOffsetValueType) {
1577: return $this->unsetOffset($typeToRemove->getOffsetType());
1578: }
1579:
1580: return null;
1581: }
1582:
1583: public function traverse(callable $cb): Type
1584: {
1585: $valueTypes = [];
1586:
1587: $stillOriginal = true;
1588: foreach ($this->valueTypes as $valueType) {
1589: $transformedValueType = $cb($valueType);
1590: if ($transformedValueType !== $valueType) {
1591: $stillOriginal = false;
1592: }
1593:
1594: $valueTypes[] = $transformedValueType;
1595: }
1596:
1597: if ($stillOriginal) {
1598: return $this;
1599: }
1600:
1601: return new self($this->keyTypes, $valueTypes, $this->nextAutoIndexes, $this->optionalKeys, $this->isList);
1602: }
1603:
1604: public function traverseSimultaneously(Type $right, callable $cb): Type
1605: {
1606: if (!$right->isArray()->yes()) {
1607: return $this;
1608: }
1609:
1610: $valueTypes = [];
1611:
1612: $stillOriginal = true;
1613: foreach ($this->valueTypes as $i => $valueType) {
1614: $keyType = $this->keyTypes[$i];
1615: $transformedValueType = $cb($valueType, $right->getOffsetValueType($keyType));
1616: if ($transformedValueType !== $valueType) {
1617: $stillOriginal = false;
1618: }
1619:
1620: $valueTypes[] = $transformedValueType;
1621: }
1622:
1623: if ($stillOriginal) {
1624: return $this;
1625: }
1626:
1627: return new self($this->keyTypes, $valueTypes, $this->nextAutoIndexes, $this->optionalKeys, $this->isList);
1628: }
1629:
1630: public function isKeysSupersetOf(self $otherArray): bool
1631: {
1632: $keyTypesCount = count($this->keyTypes);
1633: $otherKeyTypesCount = count($otherArray->keyTypes);
1634:
1635: if ($keyTypesCount < $otherKeyTypesCount) {
1636: return false;
1637: }
1638:
1639: if ($otherKeyTypesCount === 0) {
1640: return $keyTypesCount === 0;
1641: }
1642:
1643: $failOnDifferentValueType = $keyTypesCount !== $otherKeyTypesCount || $keyTypesCount < 2;
1644:
1645: $keyTypes = $this->keyTypes;
1646:
1647: foreach ($otherArray->keyTypes as $j => $keyType) {
1648: $i = self::findKeyIndex($keyType, $keyTypes);
1649: if ($i === null) {
1650: return false;
1651: }
1652:
1653: unset($keyTypes[$i]);
1654:
1655: $valueType = $this->valueTypes[$i];
1656: $otherValueType = $otherArray->valueTypes[$j];
1657: if (!$otherValueType->isSuperTypeOf($valueType)->no()) {
1658: continue;
1659: }
1660:
1661: if ($failOnDifferentValueType) {
1662: return false;
1663: }
1664: $failOnDifferentValueType = true;
1665: }
1666:
1667: $requiredKeyCount = 0;
1668: foreach (array_keys($keyTypes) as $i) {
1669: if ($this->isOptionalKey($i)) {
1670: continue;
1671: }
1672:
1673: $requiredKeyCount++;
1674: if ($requiredKeyCount > 1) {
1675: return false;
1676: }
1677: }
1678:
1679: return true;
1680: }
1681:
1682: public function mergeWith(self $otherArray): self
1683: {
1684: // only call this after verifying isKeysSupersetOf, or if losing tagged unions is not an issue
1685: $valueTypes = $this->valueTypes;
1686: $optionalKeys = $this->optionalKeys;
1687: foreach ($this->keyTypes as $i => $keyType) {
1688: $otherIndex = $otherArray->getKeyIndex($keyType);
1689: if ($otherIndex === null) {
1690: $optionalKeys[] = $i;
1691: continue;
1692: }
1693: if ($otherArray->isOptionalKey($otherIndex)) {
1694: $optionalKeys[] = $i;
1695: }
1696: $otherValueType = $otherArray->valueTypes[$otherIndex];
1697: $valueTypes[$i] = TypeCombinator::union($valueTypes[$i], $otherValueType);
1698: }
1699:
1700: $optionalKeys = array_values(array_unique($optionalKeys));
1701:
1702: $nextAutoIndexes = array_values(array_unique(array_merge($this->nextAutoIndexes, $otherArray->nextAutoIndexes)));
1703: sort($nextAutoIndexes);
1704:
1705: return new self($this->keyTypes, $valueTypes, $nextAutoIndexes, $optionalKeys, $this->isList->and($otherArray->isList));
1706: }
1707:
1708: /**
1709: * @param ConstantIntegerType|ConstantStringType $otherKeyType
1710: */
1711: private function getKeyIndex($otherKeyType): ?int
1712: {
1713: return self::findKeyIndex($otherKeyType, $this->keyTypes);
1714: }
1715:
1716: /**
1717: * @param ConstantIntegerType|ConstantStringType $otherKeyType
1718: * @param array<int, ConstantIntegerType|ConstantStringType> $keyTypes
1719: */
1720: private static function findKeyIndex($otherKeyType, array $keyTypes): ?int
1721: {
1722: foreach ($keyTypes as $i => $keyType) {
1723: if ($keyType->equals($otherKeyType)) {
1724: return $i;
1725: }
1726: }
1727:
1728: return null;
1729: }
1730:
1731: public function makeOffsetRequired(Type $offsetType): self
1732: {
1733: $offsetType = $offsetType->toArrayKey();
1734: $optionalKeys = $this->optionalKeys;
1735: foreach ($this->keyTypes as $i => $keyType) {
1736: if (!$keyType->equals($offsetType)) {
1737: continue;
1738: }
1739:
1740: foreach ($optionalKeys as $j => $key) {
1741: if ($i === $key) {
1742: unset($optionalKeys[$j]);
1743: return new self($this->keyTypes, $this->valueTypes, $this->nextAutoIndexes, array_values($optionalKeys), $this->isList);
1744: }
1745: }
1746:
1747: break;
1748: }
1749:
1750: return $this;
1751: }
1752:
1753: public function toPhpDocNode(): TypeNode
1754: {
1755: $items = [];
1756: $values = [];
1757: $exportValuesOnly = true;
1758: foreach ($this->keyTypes as $i => $keyType) {
1759: if ($keyType->getValue() !== $i) {
1760: $exportValuesOnly = false;
1761: }
1762: $keyPhpDocNode = $keyType->toPhpDocNode();
1763: if (!$keyPhpDocNode instanceof ConstTypeNode) {
1764: continue;
1765: }
1766: $valueType = $this->valueTypes[$i];
1767:
1768: /** @var ConstExprStringNode|ConstExprIntegerNode $keyNode */
1769: $keyNode = $keyPhpDocNode->constExpr;
1770: if ($keyNode instanceof ConstExprStringNode) {
1771: $value = $keyNode->value;
1772: if (self::isValidIdentifier($value)) {
1773: $keyNode = new IdentifierTypeNode($value);
1774: }
1775: }
1776:
1777: $isOptional = $this->isOptionalKey($i);
1778: if ($isOptional) {
1779: $exportValuesOnly = false;
1780: }
1781: $items[] = new ArrayShapeItemNode(
1782: $keyNode,
1783: $isOptional,
1784: $valueType->toPhpDocNode(),
1785: );
1786: $values[] = new ArrayShapeItemNode(
1787: null,
1788: $isOptional,
1789: $valueType->toPhpDocNode(),
1790: );
1791: }
1792:
1793: return ArrayShapeNode::createSealed($exportValuesOnly ? $values : $items);
1794: }
1795:
1796: public static function isValidIdentifier(string $value): bool
1797: {
1798: $result = Strings::match($value, '~^(?:[\\\\]?+[a-z_\\x80-\\xFF][0-9a-z_\\x80-\\xFF-]*+)++$~si');
1799:
1800: return $result !== null;
1801: }
1802:
1803: public function getFiniteTypes(): array
1804: {
1805: $arraysArraysForCombinations = [];
1806: $count = 0;
1807: foreach ($this->getAllArrays() as $array) {
1808: $values = $array->getValueTypes();
1809: $arraysForCombinations = [];
1810: $combinationCount = 1;
1811: foreach ($values as $valueType) {
1812: $finiteTypes = $valueType->getFiniteTypes();
1813: if ($finiteTypes === []) {
1814: return [];
1815: }
1816: $arraysForCombinations[] = $finiteTypes;
1817: $combinationCount *= count($finiteTypes);
1818: }
1819: $arraysArraysForCombinations[] = $arraysForCombinations;
1820: $count += $combinationCount;
1821: }
1822:
1823: if ($count > InitializerExprTypeResolver::CALCULATE_SCALARS_LIMIT) {
1824: return [];
1825: }
1826:
1827: $finiteTypes = [];
1828: foreach ($arraysArraysForCombinations as $arraysForCombinations) {
1829: $combinations = CombinationsHelper::combinations($arraysForCombinations);
1830: foreach ($combinations as $combination) {
1831: $builder = ConstantArrayTypeBuilder::createEmpty();
1832: foreach ($combination as $i => $v) {
1833: $builder->setOffsetValueType($this->keyTypes[$i], $v);
1834: }
1835: $finiteTypes[] = $builder->getArray();
1836: }
1837: }
1838:
1839: return $finiteTypes;
1840: }
1841:
1842: public function hasTemplateOrLateResolvableType(): bool
1843: {
1844: foreach ($this->valueTypes as $valueType) {
1845: if (!$valueType->hasTemplateOrLateResolvableType()) {
1846: continue;
1847: }
1848:
1849: return true;
1850: }
1851:
1852: foreach ($this->keyTypes as $keyType) {
1853: if (!$keyType instanceof TemplateType) {
1854: continue;
1855: }
1856:
1857: return true;
1858: }
1859:
1860: return false;
1861: }
1862:
1863: }
1864: