1: <?php declare(strict_types = 1);
2:
3: namespace PHPStan\Type;
4:
5: use PHPStan\Php\PhpVersion;
6: use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode;
7: use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
8: use PHPStan\PhpDocParser\Ast\Type\TypeNode;
9: use PHPStan\Reflection\ClassMemberAccessAnswerer;
10: use PHPStan\Reflection\TrivialParametersAcceptor;
11: use PHPStan\Rules\Arrays\AllowedArrayKeysTypes;
12: use PHPStan\ShouldNotHappenException;
13: use PHPStan\TrinaryLogic;
14: use PHPStan\Type\Accessory\AccessoryArrayListType;
15: use PHPStan\Type\Accessory\AccessoryLowercaseStringType;
16: use PHPStan\Type\Accessory\AccessoryNonEmptyStringType;
17: use PHPStan\Type\Accessory\AccessoryNonFalsyStringType;
18: use PHPStan\Type\Accessory\AccessoryNumericStringType;
19: use PHPStan\Type\Accessory\AccessoryUppercaseStringType;
20: use PHPStan\Type\Accessory\HasOffsetType;
21: use PHPStan\Type\Accessory\HasOffsetValueType;
22: use PHPStan\Type\Accessory\NonEmptyArrayType;
23: use PHPStan\Type\Constant\ConstantArrayType;
24: use PHPStan\Type\Constant\ConstantArrayTypeBuilder;
25: use PHPStan\Type\Constant\ConstantBooleanType;
26: use PHPStan\Type\Constant\ConstantFloatType;
27: use PHPStan\Type\Constant\ConstantIntegerType;
28: use PHPStan\Type\Constant\ConstantStringType;
29: use PHPStan\Type\Generic\TemplateMixedType;
30: use PHPStan\Type\Generic\TemplateStrictMixedType;
31: use PHPStan\Type\Generic\TemplateTypeMap;
32: use PHPStan\Type\Generic\TemplateTypeVariance;
33: use PHPStan\Type\Traits\ArrayTypeTrait;
34: use PHPStan\Type\Traits\MaybeCallableTypeTrait;
35: use PHPStan\Type\Traits\NonGeneralizableTypeTrait;
36: use PHPStan\Type\Traits\NonObjectTypeTrait;
37: use PHPStan\Type\Traits\UndecidedBooleanTypeTrait;
38: use PHPStan\Type\Traits\UndecidedComparisonTypeTrait;
39: use PHPStan\Type\Traverser\UnsafeArrayStringKeyCastingTraverser;
40: use function array_map;
41: use function array_merge;
42: use function count;
43: use function in_array;
44: use function sprintf;
45: use function strtolower;
46: use function strtoupper;
47: use const CASE_LOWER;
48: use const CASE_UPPER;
49:
50: /** @api */
51: class ArrayType implements Type
52: {
53:
54: use ArrayTypeTrait;
55: use MaybeCallableTypeTrait;
56: use NonObjectTypeTrait;
57: use UndecidedBooleanTypeTrait;
58: use UndecidedComparisonTypeTrait;
59: use NonGeneralizableTypeTrait;
60:
61: private const TRUNCATE_ACCESSORIES_LIMIT = 8;
62:
63: private Type $keyType;
64:
65: private ?Type $cachedIterableKeyType = null;
66:
67: private ?TrinaryLogic $isList = null;
68:
69: /** @api */
70: public function __construct(Type $keyType, private Type $itemType)
71: {
72: if (in_array($keyType->describe(VerbosityLevel::value()), ['(int|string)', '(int|non-decimal-int-string)'], true)) {
73: $keyType = new MixedType();
74: }
75: if ($keyType instanceof StrictMixedType && !$keyType instanceof TemplateStrictMixedType) {
76: $keyType = (new UnionType([new StringType(), new IntegerType()]))->toArrayKey();
77: }
78:
79: $this->keyType = $keyType;
80: }
81:
82: public function getKeyType(): Type
83: {
84: return $this->keyType;
85: }
86:
87: public function getItemType(): Type
88: {
89: return $this->itemType;
90: }
91:
92: /**
93: * Build a same-kind array with new key/item types. Subclasses
94: * (e.g. {@see TemplateArrayType}) override this to preserve their
95: * extra metadata across array-mutating operations such as offset
96: * writes and unsets.
97: */
98: protected function withTypes(Type $keyType, Type $itemType): self
99: {
100: return new self($keyType, $itemType);
101: }
102:
103: public function getReferencedClasses(): array
104: {
105: return array_merge(
106: $this->keyType->getReferencedClasses(),
107: $this->getItemType()->getReferencedClasses(),
108: );
109: }
110:
111: public function getConstantArrays(): array
112: {
113: return [];
114: }
115:
116: public function accepts(Type $type, bool $strictTypes): AcceptsResult
117: {
118: if ($type instanceof CompoundType) {
119: return $type->isAcceptedBy($this, $strictTypes);
120: }
121:
122: if ($type instanceof ConstantArrayType) {
123: $result = AcceptsResult::createYes();
124: $thisKeyType = $this->keyType;
125: $itemType = $this->getItemType();
126: foreach ($type->getKeyTypes() as $i => $keyType) {
127: $valueType = $type->getValueTypes()[$i];
128: $acceptsKey = $thisKeyType->accepts($keyType, $strictTypes);
129: $acceptsValue = $itemType->accepts($valueType, $strictTypes);
130: $result = $result->and($acceptsKey)->and($acceptsValue);
131: }
132:
133: return $result;
134: }
135:
136: if ($type instanceof ArrayType) {
137: return $this->getItemType()->accepts($type->getItemType(), $strictTypes)
138: ->and($this->keyType->accepts($type->keyType, $strictTypes));
139: }
140:
141: return AcceptsResult::createNo();
142: }
143:
144: public function isSuperTypeOf(Type $type): IsSuperTypeOfResult
145: {
146: if ($type instanceof self || $type instanceof ConstantArrayType) {
147: return $this->getItemType()->isSuperTypeOf($type->getItemType())
148: ->and($this->getIterableKeyType()->isSuperTypeOf($type->getIterableKeyType()));
149: }
150:
151: if ($type instanceof CompoundType) {
152: return $type->isSubTypeOf($this);
153: }
154:
155: return IsSuperTypeOfResult::createNo();
156: }
157:
158: public function equals(Type $type): bool
159: {
160: return $type instanceof self
161: && $this->getItemType()->equals($type->getIterableValueType())
162: && $this->keyType->equals($type->keyType);
163: }
164:
165: public function describe(VerbosityLevel $level): string
166: {
167: $isMixedKeyType = $this->keyType instanceof MixedType && $this->keyType->describe(VerbosityLevel::precise()) === 'mixed' && !$this->keyType->isExplicitMixed();
168: $isMixedItemType = $this->itemType instanceof MixedType && $this->itemType->describe(VerbosityLevel::precise()) === 'mixed' && !$this->itemType->isExplicitMixed();
169:
170: $valueHandler = function () use ($level, $isMixedKeyType, $isMixedItemType): string {
171: if ($isMixedKeyType || $this->keyType instanceof NeverType) {
172: if ($isMixedItemType || $this->itemType instanceof NeverType) {
173: return 'array';
174: }
175:
176: return sprintf('array<%s>', $this->itemType->describe($level));
177: }
178:
179: return sprintf('array<%s, %s>', $this->keyType->describe($level), $this->itemType->describe($level));
180: };
181:
182: return $level->handle(
183: $valueHandler,
184: $valueHandler,
185: function () use ($level, $isMixedKeyType, $isMixedItemType): string {
186: if ($isMixedKeyType) {
187: if ($isMixedItemType) {
188: return 'array';
189: }
190:
191: return sprintf('array<%s>', $this->itemType->describe($level));
192: }
193:
194: return sprintf('array<%s, %s>', $this->keyType->describe($level), $this->itemType->describe($level));
195: },
196: );
197: }
198:
199: public function generalizeValues(): self
200: {
201: return new self($this->keyType, $this->itemType->generalize(GeneralizePrecision::lessSpecific()));
202: }
203:
204: public function getKeysArrayFiltered(Type $filterValueType, TrinaryLogic $strict): Type
205: {
206: return $this->getKeysArray();
207: }
208:
209: public function getKeysArray(): Type
210: {
211: return TypeCombinator::intersect(new self(new IntegerType(), $this->getIterableKeyType()), new AccessoryArrayListType());
212: }
213:
214: public function getValuesArray(): Type
215: {
216: return TypeCombinator::intersect(new self(new IntegerType(), $this->itemType), new AccessoryArrayListType());
217: }
218:
219: public function isIterableAtLeastOnce(): TrinaryLogic
220: {
221: return TrinaryLogic::createMaybe();
222: }
223:
224: public function getArraySize(): Type
225: {
226: return IntegerRangeType::fromInterval(0, null);
227: }
228:
229: public function getIterableKeyType(): Type
230: {
231: if ($this->cachedIterableKeyType !== null) {
232: return $this->cachedIterableKeyType;
233: }
234: $keyType = $this->keyType;
235: if ($keyType instanceof MixedType && !$keyType instanceof TemplateMixedType) {
236: $keyType = (new BenevolentUnionType([new IntegerType(), new StringType()]))->toArrayKey();
237: }
238: if ($keyType instanceof StrictMixedType) {
239: $keyType = (new BenevolentUnionType([new IntegerType(), new StringType()]))->toArrayKey();
240: }
241:
242: return $this->cachedIterableKeyType = UnsafeArrayStringKeyCastingTraverser::castKeyType($keyType);
243: }
244:
245: public function getFirstIterableKeyType(): Type
246: {
247: return $this->getIterableKeyType();
248: }
249:
250: public function getLastIterableKeyType(): Type
251: {
252: return $this->getIterableKeyType();
253: }
254:
255: public function getIterableValueType(): Type
256: {
257: return $this->getItemType();
258: }
259:
260: public function getFirstIterableValueType(): Type
261: {
262: return $this->getItemType();
263: }
264:
265: public function getLastIterableValueType(): Type
266: {
267: return $this->getItemType();
268: }
269:
270: public function isConstantArray(): TrinaryLogic
271: {
272: return TrinaryLogic::createNo();
273: }
274:
275: public function isList(): TrinaryLogic
276: {
277: if ($this->isList === null) {
278: if (IntegerRangeType::fromInterval(0, null)->isSuperTypeOf($this->getKeyType())->no()) {
279: return $this->isList = TrinaryLogic::createNo();
280: }
281:
282: if ($this->getKeyType()->isSuperTypeOf(new ConstantIntegerType(0))->no()) {
283: return $this->isList = TrinaryLogic::createNo();
284: }
285:
286: return $this->isList = TrinaryLogic::createMaybe();
287: }
288:
289: return $this->isList;
290: }
291:
292: public function isConstantValue(): TrinaryLogic
293: {
294: return TrinaryLogic::createNo();
295: }
296:
297: public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType
298: {
299: if ($type->isInteger()->yes()) {
300: return new ConstantBooleanType(false);
301: }
302:
303: return new BooleanType();
304: }
305:
306: public function hasOffsetValueType(Type $offsetType): TrinaryLogic
307: {
308: $offsetArrayKeyType = $offsetType->toArrayKey();
309: if ($offsetArrayKeyType instanceof ErrorType) {
310: $allowedArrayKeys = AllowedArrayKeysTypes::getType();
311: $offsetArrayKeyType = TypeCombinator::intersect($allowedArrayKeys, $offsetType)->toArrayKey();
312: if ($offsetArrayKeyType instanceof NeverType) {
313: return TrinaryLogic::createNo();
314: }
315: }
316: $offsetType = $offsetArrayKeyType;
317:
318: if ($this->getKeyType()->isSuperTypeOf($offsetType)->no()
319: && ($offsetType->isString()->no() || !$offsetType->isConstantScalarValue()->no())
320: ) {
321: return TrinaryLogic::createNo();
322: }
323:
324: return TrinaryLogic::createMaybe();
325: }
326:
327: public function getOffsetValueType(Type $offsetType): Type
328: {
329: $offsetType = $offsetType->toArrayKey();
330: if ($this->getKeyType()->isSuperTypeOf($offsetType)->no()
331: && ($offsetType->isString()->no() || !$offsetType->isConstantScalarValue()->no())
332: ) {
333: return new ErrorType();
334: }
335:
336: $type = $this->getItemType();
337: if ($type instanceof ErrorType) {
338: return new MixedType();
339: }
340:
341: return $type;
342: }
343:
344: public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $unionValues = true): Type
345: {
346: if ($offsetType === null) {
347: $isKeyTypeInteger = $this->keyType->isInteger();
348: if ($isKeyTypeInteger->no()) {
349: $offsetType = new IntegerType();
350: } elseif ($isKeyTypeInteger->yes()) {
351: /** @var list<ConstantIntegerType> $constantScalars */
352: $constantScalars = $this->keyType->getConstantScalarTypes();
353: if (count($constantScalars) > 0) {
354: foreach ($constantScalars as $constantScalar) {
355: $constantScalars[] = ConstantTypeHelper::getTypeFromValue($constantScalar->getValue() + 1);
356: }
357:
358: $offsetType = TypeCombinator::union(...$constantScalars);
359: } else {
360: $offsetType = $this->keyType;
361: }
362: } else {
363: $integerTypes = [];
364: TypeTraverser::map($this->keyType, static function (Type $type, callable $traverse) use (&$integerTypes): Type {
365: if ($type instanceof UnionType) {
366: return $traverse($type);
367: }
368:
369: $isInteger = $type->isInteger();
370: if ($isInteger->yes()) {
371: $integerTypes[] = $type;
372: }
373:
374: return $type;
375: });
376: if (count($integerTypes) === 0) {
377: $offsetType = $this->keyType;
378: } else {
379: $offsetType = TypeCombinator::union(...$integerTypes);
380: }
381: }
382: } else {
383: $offsetType = $offsetType->toArrayKey();
384: }
385:
386: if ($offsetType instanceof ConstantStringType || $offsetType instanceof ConstantIntegerType) {
387: if ($offsetType->isSuperTypeOf($this->keyType)->yes()) {
388: $builder = ConstantArrayTypeBuilder::createEmpty();
389: $builder->setOffsetValueType($offsetType, $valueType);
390: return $builder->getArray();
391: }
392:
393: return new IntersectionType([
394: $this->withTypes(
395: TypeCombinator::union($this->keyType, $offsetType),
396: TypeCombinator::union($this->itemType, $valueType),
397: ),
398: new HasOffsetValueType($offsetType, $valueType),
399: new NonEmptyArrayType(),
400: ]);
401: }
402:
403: return new IntersectionType([
404: $this->withTypes(
405: TypeCombinator::union($this->keyType, $offsetType),
406: $unionValues ? TypeCombinator::union($this->itemType, $valueType) : $valueType,
407: ),
408: new NonEmptyArrayType(),
409: ]);
410: }
411:
412: public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type
413: {
414: if ($this->itemType->isConstantArray()->yes() && $valueType->isConstantArray()->yes()) {
415: $newItemTypes = [];
416:
417: foreach ($valueType->getConstantArrays() as $constArray) {
418: $newItemType = $this->itemType;
419: $optionalKeyTypes = [];
420: foreach ($constArray->getKeyTypes() as $i => $keyType) {
421: $newItemType = $newItemType->setExistingOffsetValueType($keyType, $constArray->getOffsetValueType($keyType));
422:
423: if (!$constArray->isOptionalKey($i)) {
424: continue;
425: }
426:
427: $optionalKeyTypes[] = $keyType;
428: }
429: $newItemTypes[] = $newItemType;
430:
431: if ($optionalKeyTypes === []) {
432: continue;
433: }
434:
435: foreach ($optionalKeyTypes as $keyType) {
436: $newItemType = $newItemType->unsetOffset($keyType);
437: }
438: $newItemTypes[] = $newItemType;
439: }
440:
441: $newItemType = TypeCombinator::union(...$newItemTypes);
442: if ($newItemType !== $this->itemType) {
443: return new self(
444: $this->keyType,
445: $newItemType,
446: );
447: }
448: }
449:
450: return new self(
451: $this->keyType,
452: TypeCombinator::union($this->itemType, $valueType),
453: );
454: }
455:
456: public function unsetOffset(Type $offsetType): Type
457: {
458: $offsetType = $offsetType->toArrayKey();
459:
460: if (
461: ($offsetType instanceof ConstantIntegerType || $offsetType instanceof ConstantStringType)
462: && !$this->keyType->isSuperTypeOf($offsetType)->no()
463: ) {
464: $keyType = TypeCombinator::remove($this->keyType, $offsetType);
465: if ($keyType instanceof NeverType) {
466: return new ConstantArrayType([], []);
467: }
468:
469: return new self($keyType, $this->itemType);
470: }
471:
472: return $this;
473: }
474:
475: public function fillKeysArray(Type $valueType): Type
476: {
477: $itemType = $this->getItemType();
478: if ($itemType->isInteger()->no()) {
479: $stringKeyType = $itemType->toString();
480: if ($stringKeyType instanceof ErrorType) {
481: return $stringKeyType;
482: }
483:
484: return new ArrayType($stringKeyType, $valueType);
485: }
486:
487: return new ArrayType($itemType, $valueType);
488: }
489:
490: public function flipArray(): Type
491: {
492: return new self($this->getIterableValueType()->toArrayKey(), $this->getIterableKeyType());
493: }
494:
495: public function intersectKeyArray(Type $otherArraysType): Type
496: {
497: $isKeySuperType = $otherArraysType->getIterableKeyType()->isSuperTypeOf($this->getIterableKeyType());
498: if ($isKeySuperType->no()) {
499: return ConstantArrayTypeBuilder::createEmpty()->getArray();
500: }
501:
502: if ($isKeySuperType->yes()) {
503: return $this;
504: }
505:
506: $constantArrays = $otherArraysType->getConstantArrays();
507: if (count($constantArrays) > 0) {
508: // When the other operand is one or more array shapes with a known
509: // sealedness, the result is a (possibly unsealed) array shape too:
510: // it can only contain the keys present in those shapes, each
511: // optional because the general first array may or may not have it.
512: $allSealednessKnown = true;
513: foreach ($constantArrays as $constantArray) {
514: if ($constantArray->isUnsealed()->maybe()) {
515: $allSealednessKnown = false;
516: break;
517: }
518: }
519:
520: if ($allSealednessKnown) {
521: $results = [];
522: foreach ($constantArrays as $constantArray) {
523: $results[] = $this->intersectConstantArrayShape($constantArray);
524: }
525:
526: return TypeCombinator::union(...$results);
527: }
528: }
529:
530: return $this->withTypes($otherArraysType->getIterableKeyType(), $this->getIterableValueType());
531: }
532:
533: private function intersectConstantArrayShape(ConstantArrayType $constantArray): Type
534: {
535: $builder = ConstantArrayTypeBuilder::createEmpty();
536:
537: $valueType = $this->getIterableValueType();
538: $keyType = $this->getIterableKeyType();
539: foreach ($constantArray->getKeyTypes() as $shapeKeyType) {
540: if (TypeCombinator::intersect($shapeKeyType, $keyType) instanceof NeverType) {
541: continue;
542: }
543: $builder->setOffsetValueType($shapeKeyType, $valueType, true);
544: }
545:
546: $unsealed = $constantArray->getUnsealedTypes();
547: if ($constantArray->isUnsealed()->yes() && $unsealed !== null) {
548: $narrowedUnsealedKey = TypeCombinator::intersect($unsealed[0], $keyType);
549: if (!$narrowedUnsealedKey instanceof NeverType) {
550: $builder->makeUnsealed($narrowedUnsealedKey, $valueType);
551: }
552: }
553:
554: return $builder->getArray();
555: }
556:
557: public function popArray(): Type
558: {
559: return $this;
560: }
561:
562: public function reverseArray(TrinaryLogic $preserveKeys): Type
563: {
564: return $this;
565: }
566:
567: public function searchArray(Type $needleType, ?TrinaryLogic $strict = null): Type
568: {
569: $strict ??= TrinaryLogic::createMaybe();
570: if ($strict->yes() && $this->getIterableValueType()->isSuperTypeOf($needleType)->no()) {
571: return new ConstantBooleanType(false);
572: }
573:
574: return TypeCombinator::union($this->getIterableKeyType(), new ConstantBooleanType(false));
575: }
576:
577: public function shiftArray(): Type
578: {
579: return $this;
580: }
581:
582: public function shuffleArray(): Type
583: {
584: return new IntersectionType([$this->withTypes(IntegerRangeType::createAllGreaterThanOrEqualTo(0), $this->itemType), new AccessoryArrayListType()]);
585: }
586:
587: public function sliceArray(Type $offsetType, Type $lengthType, TrinaryLogic $preserveKeys): Type
588: {
589: if ((new ConstantIntegerType(0))->isSuperTypeOf($lengthType)->yes()) {
590: return new ConstantArrayType([], []);
591: }
592:
593: if ($preserveKeys->no() && $this->keyType->isInteger()->yes()) {
594: return new IntersectionType([$this->withTypes(IntegerRangeType::createAllGreaterThanOrEqualTo(0), $this->itemType), new AccessoryArrayListType()]);
595: }
596:
597: return $this;
598: }
599:
600: public function spliceArray(Type $offsetType, Type $lengthType, Type $replacementType): Type
601: {
602: $replacementArrayType = $replacementType->toArray();
603: $replacementArrayTypeIsIterableAtLeastOnce = $replacementArrayType->isIterableAtLeastOnce();
604:
605: if ((new ConstantIntegerType(0))->isSuperTypeOf($offsetType)->yes() && $lengthType->isNull()->yes() && $replacementArrayTypeIsIterableAtLeastOnce->no()) {
606: return new ConstantArrayType([], []);
607: }
608:
609: $existingArrayKeyType = $this->getIterableKeyType();
610: $keyType = TypeTraverser::map($existingArrayKeyType, static function (Type $type, callable $traverse): Type {
611: if ($type instanceof UnionType) {
612: return $traverse($type);
613: }
614:
615: if ($type->isInteger()->yes()) {
616: return IntegerRangeType::createAllGreaterThanOrEqualTo(0);
617: }
618:
619: return $type;
620: });
621:
622: $arrayType = $this->withTypes(
623: TypeCombinator::union($keyType, $replacementArrayType->getKeysArray()->getIterableKeyType()),
624: TypeCombinator::union($this->getIterableValueType(), $replacementArrayType->getIterableValueType()),
625: );
626:
627: $accessories = [];
628: if ($replacementArrayTypeIsIterableAtLeastOnce->yes()) {
629: $accessories[] = new NonEmptyArrayType();
630: }
631: if ($existingArrayKeyType->isInteger()->yes()) {
632: $accessories[] = new AccessoryArrayListType();
633: }
634: if (count($accessories) > 0) {
635: $accessories[] = $arrayType;
636:
637: return new IntersectionType($accessories);
638: }
639:
640: return $arrayType;
641: }
642:
643: public function makeListMaybe(): Type
644: {
645: // `ArrayType` doesn't carry list-ness on its own — that's an
646: // `AccessoryArrayListType` in an enclosing `IntersectionType`.
647: return $this;
648: }
649:
650: public function truncateListToSize(Type $sizeType): Type
651: {
652: [$min, $max] = ConstantArrayType::extractTruncateListBounds($sizeType);
653:
654: // `isList()` is deliberately NOT checked here — see the matching
655: // note on `ConstantArrayType::truncateListToSize`. The call site
656: // has already established outer list-ness.
657: if (
658: $min === null
659: || $min >= ConstantArrayTypeBuilder::ARRAY_COUNT_LIMIT
660: || !$this->getKeyType()->isSuperTypeOf(IntegerRangeType::fromInterval(0, ($max ?? $min) - 1))->yes()
661: ) {
662: return TypeCombinator::intersect($this, new NonEmptyArrayType());
663: }
664:
665: if ($max !== null) {
666: // Bounded range — `ArrayType` doesn't carry per-offset types, so
667: // rebuild via the same CAT builder logic as `ConstantArrayType`.
668: // The values come from `$this->getOffsetValueType()` (which on a
669: // general `ArrayType` collapses to the iterable value type).
670: if ($max - $min > ConstantArrayTypeBuilder::ARRAY_COUNT_LIMIT) {
671: return TypeCombinator::intersect($this, new NonEmptyArrayType());
672: }
673:
674: $builder = ConstantArrayTypeBuilder::createEmpty();
675: for ($i = 0; $i < $min; $i++) {
676: $offsetType = new ConstantIntegerType($i);
677: $builder->setOffsetValueType($offsetType, $this->getOffsetValueType($offsetType), false);
678: }
679: for ($i = $min; $i < $max; $i++) {
680: $offsetType = new ConstantIntegerType($i);
681: $builder->setOffsetValueType($offsetType, $this->getOffsetValueType($offsetType), true);
682: }
683:
684: $builtArray = $builder->getArray();
685: if (!$builder->isList()) {
686: $constantArrays = $builtArray->getConstantArrays();
687: if (count($constantArrays) === 1) {
688: $builtArray = $constantArrays[0]->makeList();
689: }
690: }
691:
692: return $builtArray;
693: }
694:
695: // Unbounded max on a general `ArrayType` list: we can't enumerate the
696: // trailing entries, so anchor the lower bound with
697: // `HasOffsetValueType` accessories (skipping offset 0 — already
698: // implied by `NonEmptyArrayType`).
699: $intersection = [$this, new NonEmptyArrayType()];
700: $zero = new ConstantIntegerType(0);
701: $added = 0;
702: for ($i = 0; $i < $min; $i++) {
703: $offsetType = new ConstantIntegerType($i);
704: if ($zero->isSuperTypeOf($offsetType)->yes()) {
705: continue;
706: }
707: if ($added > self::TRUNCATE_ACCESSORIES_LIMIT) {
708: break;
709: }
710:
711: $intersection[] = new HasOffsetValueType($offsetType, $this->getOffsetValueType($offsetType));
712: $added++;
713: }
714:
715: return TypeCombinator::intersect(...$intersection);
716: }
717:
718: public function mapValueType(callable $cb): Type
719: {
720: return $this->withTypes($this->keyType, $cb($this->getItemType()));
721: }
722:
723: public function mapKeyType(callable $cb): Type
724: {
725: return $this->withTypes($cb($this->keyType), $this->getItemType());
726: }
727:
728: public function makeAllArrayKeysOptional(): Type
729: {
730: // `ArrayType` already models arbitrary key subsets.
731: return $this;
732: }
733:
734: public function changeKeyCaseArray(?int $case): Type
735: {
736: $newKeyType = TypeTraverser::map($this->keyType, static function (Type $type, callable $traverse) use ($case): Type {
737: if ($type instanceof UnionType) {
738: return $traverse($type);
739: }
740:
741: $constantStrings = $type->getConstantStrings();
742: if (count($constantStrings) > 0) {
743: return TypeCombinator::union(
744: ...array_map(
745: static fn (ConstantStringType $type): Type => self::foldConstantStringKeyCase($type, $case),
746: $constantStrings,
747: ),
748: );
749: }
750:
751: if ($type->isString()->yes()) {
752: $types = [new StringType()];
753: if ($type->isNonFalsyString()->yes()) {
754: $types[] = new AccessoryNonFalsyStringType();
755: } elseif ($type->isNonEmptyString()->yes()) {
756: $types[] = new AccessoryNonEmptyStringType();
757: }
758: if ($type->isNumericString()->yes()) {
759: $types[] = new AccessoryNumericStringType();
760: }
761: if ($case === CASE_LOWER) {
762: $types[] = new AccessoryLowercaseStringType();
763: } elseif ($case === CASE_UPPER) {
764: $types[] = new AccessoryUppercaseStringType();
765: }
766:
767: if (count($types) === 1) {
768: return $types[0];
769: }
770: return new IntersectionType($types);
771: }
772:
773: return $type;
774: });
775:
776: return $this->withTypes($newKeyType, $this->getItemType());
777: }
778:
779: public function filterArrayRemovingFalsey(): Type
780: {
781: $falseyTypes = StaticTypeFactory::falsey();
782: $valueType = TypeCombinator::remove($this->getItemType(), $falseyTypes);
783: if ($valueType instanceof NeverType) {
784: return new ConstantArrayType([], []);
785: }
786:
787: return $this->withTypes($this->keyType, $valueType);
788: }
789:
790: private static function foldConstantStringKeyCase(ConstantStringType $type, ?int $case): Type
791: {
792: if ($case === CASE_LOWER) {
793: return new ConstantStringType(strtolower($type->getValue()));
794: }
795: if ($case === CASE_UPPER) {
796: return new ConstantStringType(strtoupper($type->getValue()));
797: }
798:
799: return TypeCombinator::union(
800: new ConstantStringType(strtolower($type->getValue())),
801: new ConstantStringType(strtoupper($type->getValue())),
802: );
803: }
804:
805: public function isCallable(): TrinaryLogic
806: {
807: return TrinaryLogic::createMaybe()->and($this->itemType->isString());
808: }
809:
810: public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope): array
811: {
812: if ($this->isCallable()->no()) {
813: throw new ShouldNotHappenException();
814: }
815:
816: return [new TrivialParametersAcceptor()];
817: }
818:
819: public function toInteger(): Type
820: {
821: return new UnionType([
822: new ConstantIntegerType(0),
823: new ConstantIntegerType(1),
824: ]);
825: }
826:
827: public function toFloat(): Type
828: {
829: return new UnionType([
830: new ConstantFloatType(0.0),
831: new ConstantFloatType(1.0),
832: ]);
833: }
834:
835: public function inferTemplateTypes(Type $receivedType): TemplateTypeMap
836: {
837: if ($receivedType instanceof UnionType || $receivedType instanceof IntersectionType) {
838: return $receivedType->inferTemplateTypesOn($this);
839: }
840:
841: if ($receivedType->isArray()->yes()) {
842: $keyTypeMap = $this->getIterableKeyType()->inferTemplateTypes($receivedType->getIterableKeyType());
843: $itemTypeMap = $this->getItemType()->inferTemplateTypes($receivedType->getIterableValueType());
844:
845: return $keyTypeMap->union($itemTypeMap);
846: }
847:
848: return TemplateTypeMap::createEmpty();
849: }
850:
851: public function getReferencedTemplateTypes(TemplateTypeVariance $positionVariance): array
852: {
853: $variance = $positionVariance->compose(TemplateTypeVariance::createCovariant());
854:
855: return array_merge(
856: $this->getIterableKeyType()->getReferencedTemplateTypes($variance),
857: $this->getItemType()->getReferencedTemplateTypes($variance),
858: );
859: }
860:
861: public function traverse(callable $cb): Type
862: {
863: $keyType = $cb($this->keyType);
864: $itemType = $cb($this->itemType);
865:
866: if ($keyType !== $this->keyType || $itemType !== $this->itemType) {
867: if ($keyType instanceof NeverType && $itemType instanceof NeverType) {
868: return new ConstantArrayType([], []);
869: }
870:
871: return $this->withTypes($keyType, $itemType);
872: }
873:
874: return $this;
875: }
876:
877: public function toPhpDocNode(): TypeNode
878: {
879: $isMixedKeyType = $this->keyType instanceof MixedType && $this->keyType->describe(VerbosityLevel::precise()) === 'mixed' && !$this->keyType->isExplicitMixed();
880: $isMixedItemType = $this->itemType instanceof MixedType && $this->itemType->describe(VerbosityLevel::precise()) === 'mixed' && !$this->itemType->isExplicitMixed();
881:
882: if ($isMixedKeyType) {
883: if ($isMixedItemType) {
884: return new IdentifierTypeNode('array');
885: }
886:
887: return new GenericTypeNode(
888: new IdentifierTypeNode('array'),
889: [
890: $this->itemType->toPhpDocNode(),
891: ],
892: );
893: }
894:
895: return new GenericTypeNode(
896: new IdentifierTypeNode('array'),
897: [
898: $this->keyType->toPhpDocNode(),
899: $this->itemType->toPhpDocNode(),
900: ],
901: );
902: }
903:
904: public function traverseSimultaneously(Type $right, callable $cb): Type
905: {
906: $keyType = $cb($this->keyType, $right->getIterableKeyType());
907: $itemType = $cb($this->itemType, $right->getIterableValueType());
908:
909: if ($keyType !== $this->keyType || $itemType !== $this->itemType) {
910: if ($keyType instanceof NeverType && $itemType instanceof NeverType) {
911: return new ConstantArrayType([], []);
912: }
913:
914: return $this->withTypes($keyType, $itemType);
915: }
916:
917: return $this;
918: }
919:
920: public function tryRemove(Type $typeToRemove): ?Type
921: {
922: if ($typeToRemove->isSuperTypeOf(new ConstantArrayType([], []))->yes()) {
923: return TypeCombinator::intersect($this, new NonEmptyArrayType());
924: }
925:
926: if ($typeToRemove instanceof NonEmptyArrayType) {
927: return new ConstantArrayType([], []);
928: }
929:
930: if ($typeToRemove instanceof HasOffsetType) {
931: return $this->unsetOffset($typeToRemove->getOffsetType());
932: }
933:
934: if (
935: $typeToRemove instanceof HasOffsetValueType
936: && $typeToRemove->getValueType()->isSuperTypeOf($this->itemType)->yes()
937: ) {
938: return $this->unsetOffset($typeToRemove->getOffsetType());
939: }
940:
941: return null;
942: }
943:
944: public function getFiniteTypes(): array
945: {
946: return [];
947: }
948:
949: public function hasTemplateOrLateResolvableType(): bool
950: {
951: return $this->keyType->hasTemplateOrLateResolvableType() || $this->itemType->hasTemplateOrLateResolvableType();
952: }
953:
954: }
955: