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