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