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