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