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