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