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