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