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\HasOffsetType;
11: use PHPStan\Type\Accessory\HasOffsetValueType;
12: use PHPStan\Type\Accessory\NonEmptyArrayType;
13: use PHPStan\Type\Constant\ConstantArrayType;
14: use PHPStan\Type\Constant\ConstantArrayTypeBuilder;
15: use PHPStan\Type\Constant\ConstantFloatType;
16: use PHPStan\Type\Constant\ConstantIntegerType;
17: use PHPStan\Type\Constant\ConstantStringType;
18: use PHPStan\Type\Generic\TemplateMixedType;
19: use PHPStan\Type\Generic\TemplateType;
20: use PHPStan\Type\Generic\TemplateTypeMap;
21: use PHPStan\Type\Generic\TemplateTypeVariance;
22: use PHPStan\Type\Traits\MaybeCallableTypeTrait;
23: use PHPStan\Type\Traits\NonGeneralizableTypeTrait;
24: use PHPStan\Type\Traits\NonObjectTypeTrait;
25: use PHPStan\Type\Traits\UndecidedBooleanTypeTrait;
26: use PHPStan\Type\Traits\UndecidedComparisonTypeTrait;
27: use function array_merge;
28: use function is_float;
29: use function is_int;
30: use function key;
31: use function sprintf;
32:
33: /** @api */
34: class ArrayType implements Type
35: {
36:
37: use MaybeCallableTypeTrait;
38: use NonObjectTypeTrait;
39: use UndecidedBooleanTypeTrait;
40: use UndecidedComparisonTypeTrait;
41: use NonGeneralizableTypeTrait;
42:
43: private Type $keyType;
44:
45: /** @api */
46: public function __construct(Type $keyType, private Type $itemType)
47: {
48: if ($keyType->describe(VerbosityLevel::value()) === '(int|string)') {
49: $keyType = new MixedType();
50: }
51: $this->keyType = $keyType;
52: }
53:
54: public function getKeyType(): Type
55: {
56: return $this->keyType;
57: }
58:
59: public function getItemType(): Type
60: {
61: return $this->itemType;
62: }
63:
64: /**
65: * @return string[]
66: */
67: public function getReferencedClasses(): array
68: {
69: return array_merge(
70: $this->keyType->getReferencedClasses(),
71: $this->getItemType()->getReferencedClasses(),
72: );
73: }
74:
75: public function accepts(Type $type, bool $strictTypes): TrinaryLogic
76: {
77: if ($type instanceof CompoundType) {
78: return $type->isAcceptedBy($this, $strictTypes);
79: }
80:
81: if ($type instanceof ConstantArrayType) {
82: $result = TrinaryLogic::createYes();
83: $thisKeyType = $this->keyType;
84: $itemType = $this->getItemType();
85: foreach ($type->getKeyTypes() as $i => $keyType) {
86: $valueType = $type->getValueTypes()[$i];
87: $result = $result->and($thisKeyType->accepts($keyType, $strictTypes))->and($itemType->accepts($valueType, $strictTypes));
88: }
89:
90: return $result;
91: }
92:
93: if ($type instanceof ArrayType) {
94: return $this->getItemType()->accepts($type->getItemType(), $strictTypes)
95: ->and($this->keyType->accepts($type->keyType, $strictTypes));
96: }
97:
98: return TrinaryLogic::createNo();
99: }
100:
101: public function isSuperTypeOf(Type $type): TrinaryLogic
102: {
103: if ($type instanceof self) {
104: return $this->getItemType()->isSuperTypeOf($type->getItemType())
105: ->and($this->keyType->isSuperTypeOf($type->keyType));
106: }
107:
108: if ($type instanceof CompoundType) {
109: return $type->isSubTypeOf($this);
110: }
111:
112: return TrinaryLogic::createNo();
113: }
114:
115: public function equals(Type $type): bool
116: {
117: return $type instanceof self
118: && !$type instanceof ConstantArrayType
119: && $this->getItemType()->equals($type->getItemType())
120: && $this->keyType->equals($type->keyType);
121: }
122:
123: public function describe(VerbosityLevel $level): string
124: {
125: $isMixedKeyType = $this->keyType instanceof MixedType && $this->keyType->describe(VerbosityLevel::precise()) === 'mixed';
126: $isMixedItemType = $this->itemType instanceof MixedType && $this->itemType->describe(VerbosityLevel::precise()) === 'mixed';
127:
128: $valueHandler = function () use ($level, $isMixedKeyType, $isMixedItemType): string {
129: if ($isMixedKeyType || $this->keyType instanceof NeverType) {
130: if ($isMixedItemType || $this->itemType instanceof NeverType) {
131: return 'array';
132: }
133:
134: return sprintf('array<%s>', $this->itemType->describe($level));
135: }
136:
137: return sprintf('array<%s, %s>', $this->keyType->describe($level), $this->itemType->describe($level));
138: };
139:
140: return $level->handle(
141: $valueHandler,
142: $valueHandler,
143: function () use ($level, $isMixedKeyType, $isMixedItemType): string {
144: if ($isMixedKeyType) {
145: if ($isMixedItemType) {
146: return 'array';
147: }
148:
149: return sprintf('array<%s>', $this->itemType->describe($level));
150: }
151:
152: return sprintf('array<%s, %s>', $this->keyType->describe($level), $this->itemType->describe($level));
153: },
154: );
155: }
156:
157: public function generalizeKeys(): self
158: {
159: return new self($this->keyType->generalize(GeneralizePrecision::lessSpecific()), $this->itemType);
160: }
161:
162: public function generalizeValues(): self
163: {
164: return new self($this->keyType, $this->itemType->generalize(GeneralizePrecision::lessSpecific()));
165: }
166:
167: public function getKeysArray(): self
168: {
169: return new self(new IntegerType(), $this->keyType);
170: }
171:
172: public function getValuesArray(): self
173: {
174: return new self(new IntegerType(), $this->itemType);
175: }
176:
177: public function isIterable(): TrinaryLogic
178: {
179: return TrinaryLogic::createYes();
180: }
181:
182: public function isIterableAtLeastOnce(): TrinaryLogic
183: {
184: return TrinaryLogic::createMaybe();
185: }
186:
187: public function getIterableKeyType(): Type
188: {
189: $keyType = $this->keyType;
190: if ($keyType instanceof MixedType && !$keyType instanceof TemplateMixedType) {
191: return new BenevolentUnionType([new IntegerType(), new StringType()]);
192: }
193: if ($keyType instanceof StrictMixedType) {
194: return new BenevolentUnionType([new IntegerType(), new StringType()]);
195: }
196:
197: return $keyType;
198: }
199:
200: public function getIterableValueType(): Type
201: {
202: return $this->getItemType();
203: }
204:
205: public function isArray(): TrinaryLogic
206: {
207: return TrinaryLogic::createYes();
208: }
209:
210: public function isOversizedArray(): TrinaryLogic
211: {
212: return TrinaryLogic::createMaybe();
213: }
214:
215: public function isString(): TrinaryLogic
216: {
217: return TrinaryLogic::createNo();
218: }
219:
220: public function isNumericString(): TrinaryLogic
221: {
222: return TrinaryLogic::createNo();
223: }
224:
225: public function isNonEmptyString(): TrinaryLogic
226: {
227: return TrinaryLogic::createNo();
228: }
229:
230: public function isNonFalsyString(): TrinaryLogic
231: {
232: return TrinaryLogic::createNo();
233: }
234:
235: public function isLiteralString(): TrinaryLogic
236: {
237: return TrinaryLogic::createNo();
238: }
239:
240: public function isOffsetAccessible(): TrinaryLogic
241: {
242: return TrinaryLogic::createYes();
243: }
244:
245: public function hasOffsetValueType(Type $offsetType): TrinaryLogic
246: {
247: $offsetType = self::castToArrayKeyType($offsetType);
248: if ($this->getKeyType()->isSuperTypeOf($offsetType)->no()) {
249: return TrinaryLogic::createNo();
250: }
251:
252: return TrinaryLogic::createMaybe();
253: }
254:
255: public function getOffsetValueType(Type $offsetType): Type
256: {
257: $offsetType = self::castToArrayKeyType($offsetType);
258: if ($this->getKeyType()->isSuperTypeOf($offsetType)->no()) {
259: return new ErrorType();
260: }
261:
262: $type = $this->getItemType();
263: if ($type instanceof ErrorType) {
264: return new MixedType();
265: }
266:
267: return $type;
268: }
269:
270: public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $unionValues = true): Type
271: {
272: if ($offsetType === null) {
273: $offsetType = new IntegerType();
274: } else {
275: $offsetType = self::castToArrayKeyType($offsetType);
276: }
277:
278: if (
279: ($offsetType instanceof ConstantStringType || $offsetType instanceof ConstantIntegerType)
280: && $offsetType->isSuperTypeOf($this->keyType)->yes()
281: ) {
282: $builder = ConstantArrayTypeBuilder::createEmpty();
283: $builder->setOffsetValueType($offsetType, $valueType);
284: return $builder->getArray();
285: }
286:
287: $array = new self(
288: TypeCombinator::union($this->keyType, $offsetType),
289: $unionValues ? TypeCombinator::union($this->itemType, $valueType) : $valueType,
290: );
291: if ($offsetType instanceof ConstantIntegerType || $offsetType instanceof ConstantStringType) {
292: return TypeCombinator::intersect($array, new HasOffsetValueType($offsetType, $valueType), new NonEmptyArrayType());
293: }
294:
295: return TypeCombinator::intersect($array, new NonEmptyArrayType());
296: }
297:
298: public function unsetOffset(Type $offsetType): Type
299: {
300: $offsetType = self::castToArrayKeyType($offsetType);
301:
302: if (
303: ($offsetType instanceof ConstantIntegerType || $offsetType instanceof ConstantStringType)
304: && !$this->keyType->isSuperTypeOf($offsetType)->no()
305: ) {
306: $keyType = TypeCombinator::remove($this->keyType, $offsetType);
307: if ($keyType instanceof NeverType) {
308: return new ConstantArrayType([], []);
309: }
310:
311: return new self($keyType, $this->itemType);
312: }
313:
314: return $this;
315: }
316:
317: public function isCallable(): TrinaryLogic
318: {
319: return TrinaryLogic::createMaybe()->and($this->itemType->isString());
320: }
321:
322: /**
323: * @return ParametersAcceptor[]
324: */
325: public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope): array
326: {
327: if ($this->isCallable()->no()) {
328: throw new ShouldNotHappenException();
329: }
330:
331: return [new TrivialParametersAcceptor()];
332: }
333:
334: public function toNumber(): Type
335: {
336: return new ErrorType();
337: }
338:
339: public function toString(): Type
340: {
341: return new ErrorType();
342: }
343:
344: public function toInteger(): Type
345: {
346: return TypeCombinator::union(
347: new ConstantIntegerType(0),
348: new ConstantIntegerType(1),
349: );
350: }
351:
352: public function toFloat(): Type
353: {
354: return TypeCombinator::union(
355: new ConstantFloatType(0.0),
356: new ConstantFloatType(1.0),
357: );
358: }
359:
360: public function toArray(): Type
361: {
362: return $this;
363: }
364:
365: public function count(): Type
366: {
367: return IntegerRangeType::fromInterval(0, null);
368: }
369:
370: public static function castToArrayKeyType(Type $offsetType): Type
371: {
372: return TypeTraverser::map($offsetType, static function (Type $offsetType, callable $traverse): Type {
373: if ($offsetType instanceof TemplateType) {
374: return $offsetType;
375: }
376:
377: if ($offsetType instanceof ConstantScalarType) {
378: $keyValue = $offsetType->getValue();
379: if (is_float($keyValue)) {
380: $keyValue = (int) $keyValue;
381: }
382: /** @var int|string $offsetValue */
383: $offsetValue = key([$keyValue => null]);
384: return is_int($offsetValue) ? new ConstantIntegerType($offsetValue) : new ConstantStringType($offsetValue);
385: }
386:
387: if ($offsetType instanceof IntegerType) {
388: return $offsetType;
389: }
390:
391: if ($offsetType instanceof BooleanType) {
392: return new UnionType([new ConstantIntegerType(0), new ConstantIntegerType(1)]);
393: }
394:
395: if ($offsetType instanceof UnionType) {
396: return $traverse($offsetType);
397: }
398:
399: if ($offsetType instanceof FloatType || $offsetType->isNumericString()->yes()) {
400: return new IntegerType();
401: }
402:
403: if ($offsetType->isString()->yes()) {
404: return $offsetType;
405: }
406:
407: if ($offsetType instanceof IntersectionType) {
408: return $traverse($offsetType);
409: }
410:
411: return new UnionType([new IntegerType(), new StringType()]);
412: });
413: }
414:
415: public function inferTemplateTypes(Type $receivedType): TemplateTypeMap
416: {
417: if ($receivedType instanceof UnionType || $receivedType instanceof IntersectionType) {
418: return $receivedType->inferTemplateTypesOn($this);
419: }
420:
421: if ($receivedType->isArray()->yes()) {
422: $keyTypeMap = $this->getKeyType()->inferTemplateTypes($receivedType->getIterableKeyType());
423: $itemTypeMap = $this->getItemType()->inferTemplateTypes($receivedType->getIterableValueType());
424:
425: return $keyTypeMap->union($itemTypeMap);
426: }
427:
428: return TemplateTypeMap::createEmpty();
429: }
430:
431: public function getReferencedTemplateTypes(TemplateTypeVariance $positionVariance): array
432: {
433: $keyVariance = $positionVariance;
434: $itemVariance = $positionVariance;
435:
436: if (!$positionVariance->contravariant()) {
437: $keyType = $this->getKeyType();
438: if ($keyType instanceof TemplateType) {
439: $keyVariance = $keyType->getVariance();
440: }
441:
442: $itemType = $this->getItemType();
443: if ($itemType instanceof TemplateType) {
444: $itemVariance = $itemType->getVariance();
445: }
446: }
447:
448: return array_merge(
449: $this->getKeyType()->getReferencedTemplateTypes($keyVariance),
450: $this->getItemType()->getReferencedTemplateTypes($itemVariance),
451: );
452: }
453:
454: public function traverse(callable $cb): Type
455: {
456: $keyType = $cb($this->keyType);
457: $itemType = $cb($this->itemType);
458:
459: if ($keyType !== $this->keyType || $itemType !== $this->itemType) {
460: if ($keyType instanceof NeverType && $itemType instanceof NeverType) {
461: return new ConstantArrayType([], []);
462: }
463:
464: return new self($keyType, $itemType);
465: }
466:
467: return $this;
468: }
469:
470: public function tryRemove(Type $typeToRemove): ?Type
471: {
472: if ($typeToRemove instanceof ConstantArrayType && $typeToRemove->isIterableAtLeastOnce()->no()) {
473: return TypeCombinator::intersect($this, new NonEmptyArrayType());
474: }
475:
476: if ($typeToRemove instanceof NonEmptyArrayType) {
477: return new ConstantArrayType([], []);
478: }
479:
480: if ($this instanceof ConstantArrayType && $typeToRemove instanceof HasOffsetType) {
481: return $this->unsetOffset($typeToRemove->getOffsetType());
482: }
483:
484: if ($this instanceof ConstantArrayType && $typeToRemove instanceof HasOffsetValueType) {
485: return $this->unsetOffset($typeToRemove->getOffsetType());
486: }
487:
488: return null;
489: }
490:
491: /**
492: * @param mixed[] $properties
493: */
494: public static function __set_state(array $properties): Type
495: {
496: return new self(
497: $properties['keyType'],
498: $properties['itemType'],
499: );
500: }
501:
502: }
503: