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