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