Thanks to visit codestin.com
Credit goes to apiref.phpstan.org

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