1: <?php declare(strict_types = 1);
2:
3: namespace PHPStan\Type;
4:
5: use PHPStan\Php\PhpVersion;
6: use PHPStan\Reflection\ClassMemberAccessAnswerer;
7: use PHPStan\Reflection\ParametersAcceptor;
8: use PHPStan\Reflection\TrivialParametersAcceptor;
9: use PHPStan\ShouldNotHappenException;
10: use PHPStan\TrinaryLogic;
11: use PHPStan\Type\Accessory\AccessoryArrayListType;
12: use PHPStan\Type\Accessory\HasOffsetType;
13: use PHPStan\Type\Accessory\HasOffsetValueType;
14: use PHPStan\Type\Accessory\NonEmptyArrayType;
15: use PHPStan\Type\Constant\ConstantArrayType;
16: use PHPStan\Type\Constant\ConstantArrayTypeBuilder;
17: use PHPStan\Type\Constant\ConstantBooleanType;
18: use PHPStan\Type\Constant\ConstantFloatType;
19: use PHPStan\Type\Constant\ConstantIntegerType;
20: use PHPStan\Type\Constant\ConstantStringType;
21: use PHPStan\Type\Generic\TemplateMixedType;
22: use PHPStan\Type\Generic\TemplateType;
23: use PHPStan\Type\Generic\TemplateTypeMap;
24: use PHPStan\Type\Generic\TemplateTypeVariance;
25: use PHPStan\Type\Traits\MaybeCallableTypeTrait;
26: use PHPStan\Type\Traits\NonGeneralizableTypeTrait;
27: use PHPStan\Type\Traits\NonObjectTypeTrait;
28: use PHPStan\Type\Traits\UndecidedBooleanTypeTrait;
29: use PHPStan\Type\Traits\UndecidedComparisonTypeTrait;
30: use function array_merge;
31: use function count;
32: use function sprintf;
33:
34: /** @api */
35: class ArrayType implements Type
36: {
37:
38: use MaybeCallableTypeTrait;
39: use NonObjectTypeTrait;
40: use UndecidedBooleanTypeTrait;
41: use UndecidedComparisonTypeTrait;
42: use NonGeneralizableTypeTrait;
43:
44: private Type $keyType;
45:
46: /** @api */
47: public function __construct(Type $keyType, private Type $itemType)
48: {
49: if ($keyType->describe(VerbosityLevel::value()) === '(int|string)') {
50: $keyType = new MixedType();
51: }
52: $this->keyType = $keyType;
53: }
54:
55: public function getKeyType(): Type
56: {
57: return $this->keyType;
58: }
59:
60: public function getItemType(): Type
61: {
62: return $this->itemType;
63: }
64:
65: /**
66: * @return string[]
67: */
68: public function getReferencedClasses(): array
69: {
70: return array_merge(
71: $this->keyType->getReferencedClasses(),
72: $this->getItemType()->getReferencedClasses(),
73: );
74: }
75:
76: public function getObjectClassNames(): array
77: {
78: return [];
79: }
80:
81: public function getObjectClassReflections(): array
82: {
83: return [];
84: }
85:
86: public function getArrays(): array
87: {
88: return [$this];
89: }
90:
91: public function getConstantArrays(): array
92: {
93: return [];
94: }
95:
96: public function accepts(Type $type, bool $strictTypes): TrinaryLogic
97: {
98: return $this->acceptsWithReason($type, $strictTypes)->result;
99: }
100:
101: public function acceptsWithReason(Type $type, bool $strictTypes): AcceptsResult
102: {
103: if ($type instanceof CompoundType) {
104: return $type->isAcceptedWithReasonBy($this, $strictTypes);
105: }
106:
107: if ($type instanceof ConstantArrayType) {
108: $result = AcceptsResult::createYes();
109: $thisKeyType = $this->keyType;
110: $itemType = $this->getItemType();
111: foreach ($type->getKeyTypes() as $i => $keyType) {
112: $valueType = $type->getValueTypes()[$i];
113: $acceptsKey = $thisKeyType->acceptsWithReason($keyType, $strictTypes);
114: $acceptsValue = $itemType->acceptsWithReason($valueType, $strictTypes);
115: $result = $result->and($acceptsKey)->and($acceptsValue);
116: }
117:
118: return $result;
119: }
120:
121: if ($type instanceof ArrayType) {
122: return $this->getItemType()->acceptsWithReason($type->getItemType(), $strictTypes)
123: ->and($this->keyType->acceptsWithReason($type->keyType, $strictTypes));
124: }
125:
126: return AcceptsResult::createNo();
127: }
128:
129: public function isSuperTypeOf(Type $type): TrinaryLogic
130: {
131: if ($type instanceof self) {
132: return $this->getItemType()->isSuperTypeOf($type->getItemType())
133: ->and($this->keyType->isSuperTypeOf($type->keyType));
134: }
135:
136: if ($type instanceof CompoundType) {
137: return $type->isSubTypeOf($this);
138: }
139:
140: return TrinaryLogic::createNo();
141: }
142:
143: public function equals(Type $type): bool
144: {
145: return $type instanceof self
146: && $type->isConstantArray()->no()
147: && $this->getItemType()->equals($type->getIterableValueType())
148: && $this->keyType->equals($type->keyType);
149: }
150:
151: public function describe(VerbosityLevel $level): string
152: {
153: $isMixedKeyType = $this->keyType instanceof MixedType && $this->keyType->describe(VerbosityLevel::precise()) === 'mixed';
154: $isMixedItemType = $this->itemType instanceof MixedType && $this->itemType->describe(VerbosityLevel::precise()) === 'mixed';
155:
156: $valueHandler = function () use ($level, $isMixedKeyType, $isMixedItemType): string {
157: if ($isMixedKeyType || $this->keyType instanceof NeverType) {
158: if ($isMixedItemType || $this->itemType instanceof NeverType) {
159: return 'array';
160: }
161:
162: return sprintf('array<%s>', $this->itemType->describe($level));
163: }
164:
165: return sprintf('array<%s, %s>', $this->keyType->describe($level), $this->itemType->describe($level));
166: };
167:
168: return $level->handle(
169: $valueHandler,
170: $valueHandler,
171: function () use ($level, $isMixedKeyType, $isMixedItemType): string {
172: if ($isMixedKeyType) {
173: if ($isMixedItemType) {
174: return 'array';
175: }
176:
177: return sprintf('array<%s>', $this->itemType->describe($level));
178: }
179:
180: return sprintf('array<%s, %s>', $this->keyType->describe($level), $this->itemType->describe($level));
181: },
182: );
183: }
184:
185: /**
186: * @deprecated
187: */
188: public function generalizeKeys(): self
189: {
190: return new self($this->keyType->generalize(GeneralizePrecision::lessSpecific()), $this->itemType);
191: }
192:
193: public function generalizeValues(): self
194: {
195: return new self($this->keyType, $this->itemType->generalize(GeneralizePrecision::lessSpecific()));
196: }
197:
198: public function getKeysArray(): Type
199: {
200: return AccessoryArrayListType::intersectWith(new self(new IntegerType(), $this->getIterableKeyType()));
201: }
202:
203: public function getValuesArray(): Type
204: {
205: return AccessoryArrayListType::intersectWith(new self(new IntegerType(), $this->itemType));
206: }
207:
208: public function isIterable(): TrinaryLogic
209: {
210: return TrinaryLogic::createYes();
211: }
212:
213: public function isIterableAtLeastOnce(): TrinaryLogic
214: {
215: return TrinaryLogic::createMaybe();
216: }
217:
218: public function getArraySize(): Type
219: {
220: return IntegerRangeType::fromInterval(0, null);
221: }
222:
223: public function getIterableKeyType(): Type
224: {
225: $keyType = $this->keyType;
226: if ($keyType instanceof MixedType && !$keyType instanceof TemplateMixedType) {
227: return new BenevolentUnionType([new IntegerType(), new StringType()]);
228: }
229: if ($keyType instanceof StrictMixedType) {
230: return new BenevolentUnionType([new IntegerType(), new StringType()]);
231: }
232:
233: return $keyType;
234: }
235:
236: public function getFirstIterableKeyType(): Type
237: {
238: return $this->getIterableKeyType();
239: }
240:
241: public function getLastIterableKeyType(): Type
242: {
243: return $this->getIterableKeyType();
244: }
245:
246: public function getIterableValueType(): Type
247: {
248: return $this->getItemType();
249: }
250:
251: public function getFirstIterableValueType(): Type
252: {
253: return $this->getItemType();
254: }
255:
256: public function getLastIterableValueType(): Type
257: {
258: return $this->getItemType();
259: }
260:
261: public function isArray(): TrinaryLogic
262: {
263: return TrinaryLogic::createYes();
264: }
265:
266: public function isConstantArray(): TrinaryLogic
267: {
268: return TrinaryLogic::createNo();
269: }
270:
271: public function isOversizedArray(): TrinaryLogic
272: {
273: return TrinaryLogic::createMaybe();
274: }
275:
276: public function isList(): TrinaryLogic
277: {
278: if (IntegerRangeType::fromInterval(0, null)->isSuperTypeOf($this->getKeyType())->no()) {
279: return TrinaryLogic::createNo();
280: }
281:
282: return TrinaryLogic::createMaybe();
283: }
284:
285: public function isNull(): TrinaryLogic
286: {
287: return TrinaryLogic::createNo();
288: }
289:
290: public function isConstantValue(): TrinaryLogic
291: {
292: return TrinaryLogic::createNo();
293: }
294:
295: public function isConstantScalarValue(): TrinaryLogic
296: {
297: return TrinaryLogic::createNo();
298: }
299:
300: public function getConstantScalarTypes(): array
301: {
302: return [];
303: }
304:
305: public function getConstantScalarValues(): array
306: {
307: return [];
308: }
309:
310: public function isTrue(): TrinaryLogic
311: {
312: return TrinaryLogic::createNo();
313: }
314:
315: public function isFalse(): TrinaryLogic
316: {
317: return TrinaryLogic::createNo();
318: }
319:
320: public function isBoolean(): TrinaryLogic
321: {
322: return TrinaryLogic::createNo();
323: }
324:
325: public function isFloat(): TrinaryLogic
326: {
327: return TrinaryLogic::createNo();
328: }
329:
330: public function isInteger(): TrinaryLogic
331: {
332: return TrinaryLogic::createNo();
333: }
334:
335: public function isString(): TrinaryLogic
336: {
337: return TrinaryLogic::createNo();
338: }
339:
340: public function isNumericString(): TrinaryLogic
341: {
342: return TrinaryLogic::createNo();
343: }
344:
345: public function isNonEmptyString(): TrinaryLogic
346: {
347: return TrinaryLogic::createNo();
348: }
349:
350: public function isNonFalsyString(): TrinaryLogic
351: {
352: return TrinaryLogic::createNo();
353: }
354:
355: public function isLiteralString(): TrinaryLogic
356: {
357: return TrinaryLogic::createNo();
358: }
359:
360: public function isClassStringType(): TrinaryLogic
361: {
362: return TrinaryLogic::createNo();
363: }
364:
365: public function getClassStringObjectType(): Type
366: {
367: return new ErrorType();
368: }
369:
370: public function getObjectTypeOrClassStringObjectType(): Type
371: {
372: return new ErrorType();
373: }
374:
375: public function isVoid(): TrinaryLogic
376: {
377: return TrinaryLogic::createNo();
378: }
379:
380: public function isScalar(): TrinaryLogic
381: {
382: return TrinaryLogic::createNo();
383: }
384:
385: public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType
386: {
387: return new BooleanType();
388: }
389:
390: public function isOffsetAccessible(): TrinaryLogic
391: {
392: return TrinaryLogic::createYes();
393: }
394:
395: public function hasOffsetValueType(Type $offsetType): TrinaryLogic
396: {
397: $offsetType = $offsetType->toArrayKey();
398: if ($this->getKeyType()->isSuperTypeOf($offsetType)->no()) {
399: return TrinaryLogic::createNo();
400: }
401:
402: return TrinaryLogic::createMaybe();
403: }
404:
405: public function getOffsetValueType(Type $offsetType): Type
406: {
407: $offsetType = $offsetType->toArrayKey();
408: if ($this->getKeyType()->isSuperTypeOf($offsetType)->no()) {
409: return new ErrorType();
410: }
411:
412: $type = $this->getItemType();
413: if ($type instanceof ErrorType) {
414: return new MixedType();
415: }
416:
417: return $type;
418: }
419:
420: public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $unionValues = true): Type
421: {
422: if ($offsetType === null) {
423: $isKeyTypeInteger = $this->keyType->isInteger();
424: if ($isKeyTypeInteger->no()) {
425: $offsetType = new IntegerType();
426: } elseif ($isKeyTypeInteger->yes()) {
427: $offsetType = $this->keyType;
428: } else {
429: $integerTypes = [];
430: TypeTraverser::map($this->keyType, static function (Type $type, callable $traverse) use (&$integerTypes): Type {
431: if ($type instanceof UnionType) {
432: return $traverse($type);
433: }
434:
435: $isInteger = $type->isInteger();
436: if ($isInteger->yes()) {
437: $integerTypes[] = $type;
438: }
439:
440: return $type;
441: });
442: if (count($integerTypes) === 0) {
443: $offsetType = $this->keyType;
444: } else {
445: $offsetType = TypeCombinator::union(...$integerTypes);
446: }
447: }
448: } else {
449: $offsetType = $offsetType->toArrayKey();
450: }
451:
452: if ($offsetType instanceof ConstantStringType || $offsetType instanceof ConstantIntegerType) {
453: if ($offsetType->isSuperTypeOf($this->keyType)->yes()) {
454: $builder = ConstantArrayTypeBuilder::createEmpty();
455: $builder->setOffsetValueType($offsetType, $valueType);
456: return $builder->getArray();
457: }
458:
459: return TypeCombinator::intersect(
460: new self(
461: TypeCombinator::union($this->keyType, $offsetType),
462: TypeCombinator::union($this->itemType, $valueType),
463: ),
464: new HasOffsetValueType($offsetType, $valueType),
465: new NonEmptyArrayType(),
466: );
467: }
468:
469: return TypeCombinator::intersect(
470: new self(
471: TypeCombinator::union($this->keyType, $offsetType),
472: $unionValues ? TypeCombinator::union($this->itemType, $valueType) : $valueType,
473: ),
474: new NonEmptyArrayType(),
475: );
476: }
477:
478: public function unsetOffset(Type $offsetType): Type
479: {
480: $offsetType = $offsetType->toArrayKey();
481:
482: if (
483: ($offsetType instanceof ConstantIntegerType || $offsetType instanceof ConstantStringType)
484: && !$this->keyType->isSuperTypeOf($offsetType)->no()
485: ) {
486: $keyType = TypeCombinator::remove($this->keyType, $offsetType);
487: if ($keyType instanceof NeverType) {
488: return new ConstantArrayType([], []);
489: }
490:
491: return new self($keyType, $this->itemType);
492: }
493:
494: return $this;
495: }
496:
497: public function fillKeysArray(Type $valueType): Type
498: {
499: $itemType = $this->getItemType();
500: if ((new IntegerType())->isSuperTypeOf($itemType)->no()) {
501: $stringKeyType = $itemType->toString();
502: if ($stringKeyType instanceof ErrorType) {
503: return $stringKeyType;
504: }
505:
506: return new ArrayType($stringKeyType, $valueType);
507: }
508:
509: return new ArrayType($itemType, $valueType);
510: }
511:
512: public function flipArray(): Type
513: {
514: return new self($this->getIterableValueType()->toArrayKey(), $this->getIterableKeyType());
515: }
516:
517: public function intersectKeyArray(Type $otherArraysType): Type
518: {
519: $isKeySuperType = $otherArraysType->getIterableKeyType()->isSuperTypeOf($this->getIterableKeyType());
520: if ($isKeySuperType->no()) {
521: return ConstantArrayTypeBuilder::createEmpty()->getArray();
522: }
523:
524: if ($isKeySuperType->yes()) {
525: return $otherArraysType->isIterableAtLeastOnce()->yes()
526: ? TypeCombinator::intersect($this, new NonEmptyArrayType())
527: : $this;
528: }
529:
530: return new self($otherArraysType->getIterableKeyType(), $this->getIterableValueType());
531: }
532:
533: public function popArray(): Type
534: {
535: return $this;
536: }
537:
538: public function searchArray(Type $needleType): Type
539: {
540: return TypeCombinator::union($this->getIterableKeyType(), new ConstantBooleanType(false));
541: }
542:
543: public function shiftArray(): Type
544: {
545: return $this;
546: }
547:
548: public function shuffleArray(): Type
549: {
550: return AccessoryArrayListType::intersectWith(new self(new IntegerType(), $this->itemType));
551: }
552:
553: public function isCallable(): TrinaryLogic
554: {
555: return TrinaryLogic::createMaybe()->and($this->itemType->isString());
556: }
557:
558: /**
559: * @return ParametersAcceptor[]
560: */
561: public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope): array
562: {
563: if ($this->isCallable()->no()) {
564: throw new ShouldNotHappenException();
565: }
566:
567: return [new TrivialParametersAcceptor()];
568: }
569:
570: public function toNumber(): Type
571: {
572: return new ErrorType();
573: }
574:
575: public function toString(): Type
576: {
577: return new ErrorType();
578: }
579:
580: public function toInteger(): Type
581: {
582: return TypeCombinator::union(
583: new ConstantIntegerType(0),
584: new ConstantIntegerType(1),
585: );
586: }
587:
588: public function toFloat(): Type
589: {
590: return TypeCombinator::union(
591: new ConstantFloatType(0.0),
592: new ConstantFloatType(1.0),
593: );
594: }
595:
596: public function toArray(): Type
597: {
598: return $this;
599: }
600:
601: public function toArrayKey(): Type
602: {
603: return new ErrorType();
604: }
605:
606: /** @deprecated Use getArraySize() instead */
607: public function count(): Type
608: {
609: return $this->getArraySize();
610: }
611:
612: /** @deprecated Use $offsetType->toArrayKey() instead */
613: public static function castToArrayKeyType(Type $offsetType): Type
614: {
615: return $offsetType->toArrayKey();
616: }
617:
618: public function inferTemplateTypes(Type $receivedType): TemplateTypeMap
619: {
620: if ($receivedType instanceof UnionType || $receivedType instanceof IntersectionType) {
621: return $receivedType->inferTemplateTypesOn($this);
622: }
623:
624: if ($receivedType->isArray()->yes()) {
625: $keyTypeMap = $this->getKeyType()->inferTemplateTypes($receivedType->getIterableKeyType());
626: $itemTypeMap = $this->getItemType()->inferTemplateTypes($receivedType->getIterableValueType());
627:
628: return $keyTypeMap->union($itemTypeMap);
629: }
630:
631: return TemplateTypeMap::createEmpty();
632: }
633:
634: public function getReferencedTemplateTypes(TemplateTypeVariance $positionVariance): array
635: {
636: $keyVariance = $positionVariance;
637: $itemVariance = $positionVariance;
638:
639: if (!$positionVariance->contravariant()) {
640: $keyType = $this->getKeyType();
641: if ($keyType instanceof TemplateType) {
642: $keyVariance = $keyType->getVariance();
643: }
644:
645: $itemType = $this->getItemType();
646: if ($itemType instanceof TemplateType) {
647: $itemVariance = $itemType->getVariance();
648: }
649: }
650:
651: return array_merge(
652: $this->getKeyType()->getReferencedTemplateTypes($keyVariance),
653: $this->getItemType()->getReferencedTemplateTypes($itemVariance),
654: );
655: }
656:
657: public function traverse(callable $cb): Type
658: {
659: $keyType = $cb($this->keyType);
660: $itemType = $cb($this->itemType);
661:
662: if ($keyType !== $this->keyType || $itemType !== $this->itemType) {
663: if ($keyType instanceof NeverType && $itemType instanceof NeverType) {
664: return new ConstantArrayType([], []);
665: }
666:
667: return new self($keyType, $itemType);
668: }
669:
670: return $this;
671: }
672:
673: public function tryRemove(Type $typeToRemove): ?Type
674: {
675: if ($typeToRemove->isConstantArray()->yes() && $typeToRemove->isIterableAtLeastOnce()->no()) {
676: return TypeCombinator::intersect($this, new NonEmptyArrayType());
677: }
678:
679: if ($typeToRemove instanceof NonEmptyArrayType) {
680: return new ConstantArrayType([], []);
681: }
682:
683: if ($this->isConstantArray()->yes() && $typeToRemove instanceof HasOffsetType) {
684: return $this->unsetOffset($typeToRemove->getOffsetType());
685: }
686:
687: if ($this->isConstantArray()->yes() && $typeToRemove instanceof HasOffsetValueType) {
688: return $this->unsetOffset($typeToRemove->getOffsetType());
689: }
690:
691: return null;
692: }
693:
694: public function exponentiate(Type $exponent): Type
695: {
696: return new ErrorType();
697: }
698:
699: /**
700: * @param mixed[] $properties
701: */
702: public static function __set_state(array $properties): Type
703: {
704: return new self(
705: $properties['keyType'],
706: $properties['itemType'],
707: );
708: }
709:
710: }
711: