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