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