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