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