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