1: <?php declare(strict_types = 1);
2:
3: namespace PHPStan\Type;
4:
5: use PHPStan\TrinaryLogic;
6: use PHPStan\Type\Constant\ConstantArrayType;
7: use PHPStan\Type\Generic\GenericObjectType;
8: use PHPStan\Type\Generic\TemplateMixedType;
9: use PHPStan\Type\Generic\TemplateType;
10: use PHPStan\Type\Generic\TemplateTypeMap;
11: use PHPStan\Type\Generic\TemplateTypeVariance;
12: use PHPStan\Type\Traits\MaybeCallableTypeTrait;
13: use PHPStan\Type\Traits\MaybeObjectTypeTrait;
14: use PHPStan\Type\Traits\MaybeOffsetAccessibleTypeTrait;
15: use PHPStan\Type\Traits\NonGeneralizableTypeTrait;
16: use PHPStan\Type\Traits\UndecidedBooleanTypeTrait;
17: use PHPStan\Type\Traits\UndecidedComparisonCompoundTypeTrait;
18: use Traversable;
19: use function array_merge;
20: use function sprintf;
21:
22: /** @api */
23: class IterableType implements CompoundType
24: {
25:
26: use MaybeCallableTypeTrait;
27: use MaybeObjectTypeTrait;
28: use MaybeOffsetAccessibleTypeTrait;
29: use UndecidedBooleanTypeTrait;
30: use UndecidedComparisonCompoundTypeTrait;
31: use NonGeneralizableTypeTrait;
32:
33: /** @api */
34: public function __construct(
35: private Type $keyType,
36: private Type $itemType,
37: )
38: {
39: }
40:
41: public function getKeyType(): Type
42: {
43: return $this->keyType;
44: }
45:
46: public function getItemType(): Type
47: {
48: return $this->itemType;
49: }
50:
51: /**
52: * @return string[]
53: */
54: public function getReferencedClasses(): array
55: {
56: return array_merge(
57: $this->keyType->getReferencedClasses(),
58: $this->getItemType()->getReferencedClasses(),
59: );
60: }
61:
62: public function accepts(Type $type, bool $strictTypes): TrinaryLogic
63: {
64: if ($type instanceof ConstantArrayType && $type->isEmpty()) {
65: return TrinaryLogic::createYes();
66: }
67: if ($type->isIterable()->yes()) {
68: return $this->getIterableValueType()->accepts($type->getIterableValueType(), $strictTypes)
69: ->and($this->getIterableKeyType()->accepts($type->getIterableKeyType(), $strictTypes));
70: }
71:
72: if ($type instanceof CompoundType) {
73: return $type->isAcceptedBy($this, $strictTypes);
74: }
75:
76: return TrinaryLogic::createNo();
77: }
78:
79: public function isSuperTypeOf(Type $type): TrinaryLogic
80: {
81: if ($type instanceof CompoundType) {
82: return $type->isSubTypeOf($this);
83: }
84:
85: return $type->isIterable()
86: ->and($this->getIterableValueType()->isSuperTypeOf($type->getIterableValueType()))
87: ->and($this->getIterableKeyType()->isSuperTypeOf($type->getIterableKeyType()));
88: }
89:
90: public function isSuperTypeOfMixed(Type $type): TrinaryLogic
91: {
92: return $type->isIterable()
93: ->and($this->isNestedTypeSuperTypeOf($this->getIterableValueType(), $type->getIterableValueType()))
94: ->and($this->isNestedTypeSuperTypeOf($this->getIterableKeyType(), $type->getIterableKeyType()));
95: }
96:
97: private function isNestedTypeSuperTypeOf(Type $a, Type $b): TrinaryLogic
98: {
99: if (!$a instanceof MixedType || !$b instanceof MixedType) {
100: return $a->isSuperTypeOf($b);
101: }
102:
103: if ($a instanceof TemplateMixedType || $b instanceof TemplateMixedType) {
104: return $a->isSuperTypeOf($b);
105: }
106:
107: if ($a->isExplicitMixed()) {
108: if ($b->isExplicitMixed()) {
109: return TrinaryLogic::createYes();
110: }
111:
112: return TrinaryLogic::createMaybe();
113: }
114:
115: return TrinaryLogic::createYes();
116: }
117:
118: public function isSubTypeOf(Type $otherType): TrinaryLogic
119: {
120: if ($otherType instanceof IntersectionType || $otherType instanceof UnionType) {
121: return $otherType->isSuperTypeOf(new UnionType([
122: new ArrayType($this->keyType, $this->itemType),
123: new IntersectionType([
124: new ObjectType(Traversable::class),
125: $this,
126: ]),
127: ]));
128: }
129:
130: if ($otherType instanceof self) {
131: $limit = TrinaryLogic::createYes();
132: } else {
133: $limit = TrinaryLogic::createMaybe();
134: }
135:
136: if ($otherType instanceof ConstantArrayType && $otherType->isEmpty()) {
137: return TrinaryLogic::createMaybe();
138: }
139:
140: return $limit->and(
141: $otherType->isIterable(),
142: $otherType->getIterableValueType()->isSuperTypeOf($this->itemType),
143: $otherType->getIterableKeyType()->isSuperTypeOf($this->keyType),
144: );
145: }
146:
147: public function isAcceptedBy(Type $acceptingType, bool $strictTypes): TrinaryLogic
148: {
149: return $this->isSubTypeOf($acceptingType);
150: }
151:
152: public function equals(Type $type): bool
153: {
154: if (!$type instanceof self) {
155: return false;
156: }
157:
158: return $this->keyType->equals($type->keyType)
159: && $this->itemType->equals($type->itemType);
160: }
161:
162: public function describe(VerbosityLevel $level): string
163: {
164: $isMixedKeyType = $this->keyType instanceof MixedType && !$this->keyType instanceof TemplateType;
165: $isMixedItemType = $this->itemType instanceof MixedType && !$this->itemType instanceof TemplateType;
166:
167: if ($isMixedKeyType) {
168: if ($isMixedItemType) {
169: return 'iterable';
170: }
171:
172: return sprintf('iterable<%s>', $this->itemType->describe($level));
173: }
174:
175: return sprintf('iterable<%s, %s>', $this->keyType->describe($level), $this->itemType->describe($level));
176: }
177:
178: public function hasOffsetValueType(Type $offsetType): TrinaryLogic
179: {
180: if ($this->getIterableKeyType()->isSuperTypeOf($offsetType)->no()) {
181: return TrinaryLogic::createNo();
182: }
183:
184: return TrinaryLogic::createMaybe();
185: }
186:
187: public function toNumber(): Type
188: {
189: return new ErrorType();
190: }
191:
192: public function toString(): Type
193: {
194: return new ErrorType();
195: }
196:
197: public function toInteger(): Type
198: {
199: return new ErrorType();
200: }
201:
202: public function toFloat(): Type
203: {
204: return new ErrorType();
205: }
206:
207: public function toArray(): Type
208: {
209: return new ArrayType($this->keyType, $this->getItemType());
210: }
211:
212: public function isIterable(): TrinaryLogic
213: {
214: return TrinaryLogic::createYes();
215: }
216:
217: public function isIterableAtLeastOnce(): TrinaryLogic
218: {
219: return TrinaryLogic::createMaybe();
220: }
221:
222: public function getIterableKeyType(): Type
223: {
224: return $this->keyType;
225: }
226:
227: public function getIterableValueType(): Type
228: {
229: return $this->getItemType();
230: }
231:
232: public function isArray(): TrinaryLogic
233: {
234: return TrinaryLogic::createMaybe();
235: }
236:
237: public function isOversizedArray(): TrinaryLogic
238: {
239: return TrinaryLogic::createMaybe();
240: }
241:
242: public function isString(): TrinaryLogic
243: {
244: return TrinaryLogic::createNo();
245: }
246:
247: public function isNumericString(): TrinaryLogic
248: {
249: return TrinaryLogic::createNo();
250: }
251:
252: public function isNonEmptyString(): TrinaryLogic
253: {
254: return TrinaryLogic::createNo();
255: }
256:
257: public function isNonFalsyString(): TrinaryLogic
258: {
259: return TrinaryLogic::createNo();
260: }
261:
262: public function isLiteralString(): TrinaryLogic
263: {
264: return TrinaryLogic::createNo();
265: }
266:
267: public function inferTemplateTypes(Type $receivedType): TemplateTypeMap
268: {
269: if ($receivedType instanceof UnionType || $receivedType instanceof IntersectionType) {
270: return $receivedType->inferTemplateTypesOn($this);
271: }
272:
273: if (!$receivedType->isIterable()->yes()) {
274: return TemplateTypeMap::createEmpty();
275: }
276:
277: $keyTypeMap = $this->getIterableKeyType()->inferTemplateTypes($receivedType->getIterableKeyType());
278: $valueTypeMap = $this->getIterableValueType()->inferTemplateTypes($receivedType->getIterableValueType());
279:
280: return $keyTypeMap->union($valueTypeMap);
281: }
282:
283: public function getReferencedTemplateTypes(TemplateTypeVariance $positionVariance): array
284: {
285: return array_merge(
286: $this->getIterableKeyType()->getReferencedTemplateTypes(TemplateTypeVariance::createCovariant()),
287: $this->getIterableValueType()->getReferencedTemplateTypes(TemplateTypeVariance::createCovariant()),
288: );
289: }
290:
291: public function traverse(callable $cb): Type
292: {
293: $keyType = $cb($this->keyType);
294: $itemType = $cb($this->itemType);
295:
296: if ($keyType !== $this->keyType || $itemType !== $this->itemType) {
297: return new self($keyType, $itemType);
298: }
299:
300: return $this;
301: }
302:
303: public function tryRemove(Type $typeToRemove): ?Type
304: {
305: $arrayType = new ArrayType(new MixedType(), new MixedType());
306: if ($typeToRemove->isSuperTypeOf($arrayType)->yes()) {
307: return new GenericObjectType(Traversable::class, [
308: $this->getIterableKeyType(),
309: $this->getIterableValueType(),
310: ]);
311: }
312:
313: $traversableType = new ObjectType(Traversable::class);
314: if ($typeToRemove->isSuperTypeOf($traversableType)->yes()) {
315: return new ArrayType($this->getIterableKeyType(), $this->getIterableValueType());
316: }
317:
318: return null;
319: }
320:
321: /**
322: * @param mixed[] $properties
323: */
324: public static function __set_state(array $properties): Type
325: {
326: return new self($properties['keyType'], $properties['itemType']);
327: }
328:
329: }
330: