1: <?php declare(strict_types = 1);
2:
3: namespace PHPStan\Type;
4:
5: use PHPStan\Php\PhpVersion;
6: use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode;
7: use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
8: use PHPStan\PhpDocParser\Ast\Type\TypeNode;
9: use PHPStan\TrinaryLogic;
10: use PHPStan\Type\Generic\GenericObjectType;
11: use PHPStan\Type\Generic\TemplateMixedType;
12: use PHPStan\Type\Generic\TemplateTypeMap;
13: use PHPStan\Type\Generic\TemplateTypeVariance;
14: use PHPStan\Type\Traits\MaybeArrayTypeTrait;
15: use PHPStan\Type\Traits\MaybeCallableTypeTrait;
16: use PHPStan\Type\Traits\MaybeObjectTypeTrait;
17: use PHPStan\Type\Traits\MaybeOffsetAccessibleTypeTrait;
18: use PHPStan\Type\Traits\NonGeneralizableTypeTrait;
19: use PHPStan\Type\Traits\UndecidedBooleanTypeTrait;
20: use PHPStan\Type\Traits\UndecidedComparisonCompoundTypeTrait;
21: use Traversable;
22: use function array_merge;
23: use function sprintf;
24:
25: /** @api */
26: class IterableType implements CompoundType
27: {
28:
29: use MaybeArrayTypeTrait;
30: use MaybeCallableTypeTrait;
31: use MaybeObjectTypeTrait;
32: use MaybeOffsetAccessibleTypeTrait;
33: use UndecidedBooleanTypeTrait;
34: use UndecidedComparisonCompoundTypeTrait;
35: use NonGeneralizableTypeTrait;
36:
37: /** @api */
38: public function __construct(
39: private Type $keyType,
40: private Type $itemType,
41: )
42: {
43: }
44:
45: public function getKeyType(): Type
46: {
47: return $this->keyType;
48: }
49:
50: public function getItemType(): Type
51: {
52: return $this->itemType;
53: }
54:
55: public function getReferencedClasses(): array
56: {
57: return array_merge(
58: $this->keyType->getReferencedClasses(),
59: $this->getItemType()->getReferencedClasses(),
60: );
61: }
62:
63: public function getObjectClassNames(): array
64: {
65: return [];
66: }
67:
68: public function getObjectClassReflections(): array
69: {
70: return [];
71: }
72:
73: public function getConstantStrings(): array
74: {
75: return [];
76: }
77:
78: public function accepts(Type $type, bool $strictTypes): AcceptsResult
79: {
80: if ($type->isConstantArray()->yes() && $type->isIterableAtLeastOnce()->no()) {
81: return AcceptsResult::createYes();
82: }
83: if ($type->isIterable()->yes()) {
84: return $this->getIterableValueType()->accepts($type->getIterableValueType(), $strictTypes)
85: ->and($this->getIterableKeyType()->accepts($type->getIterableKeyType(), $strictTypes));
86: }
87:
88: if ($type instanceof CompoundType) {
89: return $type->isAcceptedBy($this, $strictTypes);
90: }
91:
92: return AcceptsResult::createNo();
93: }
94:
95: public function isSuperTypeOf(Type $type): IsSuperTypeOfResult
96: {
97: if ($type instanceof CompoundType) {
98: return $type->isSubTypeOf($this);
99: }
100:
101: return (new IsSuperTypeOfResult($type->isIterable(), []))
102: ->and($this->getIterableValueType()->isSuperTypeOf($type->getIterableValueType()))
103: ->and($this->getIterableKeyType()->isSuperTypeOf($type->getIterableKeyType()));
104: }
105:
106: public function isSuperTypeOfMixed(Type $type): IsSuperTypeOfResult
107: {
108: return (new IsSuperTypeOfResult($type->isIterable(), []))
109: ->and($this->isNestedTypeSuperTypeOf($this->getIterableValueType(), $type->getIterableValueType()))
110: ->and($this->isNestedTypeSuperTypeOf($this->getIterableKeyType(), $type->getIterableKeyType()));
111: }
112:
113: private function isNestedTypeSuperTypeOf(Type $a, Type $b): IsSuperTypeOfResult
114: {
115: if (!$a instanceof MixedType || !$b instanceof MixedType) {
116: return $a->isSuperTypeOf($b);
117: }
118:
119: if ($a instanceof TemplateMixedType || $b instanceof TemplateMixedType) {
120: return $a->isSuperTypeOf($b);
121: }
122:
123: if ($a->isExplicitMixed()) {
124: if ($b->isExplicitMixed()) {
125: return IsSuperTypeOfResult::createYes();
126: }
127:
128: return IsSuperTypeOfResult::createMaybe();
129: }
130:
131: return IsSuperTypeOfResult::createYes();
132: }
133:
134: public function isSubTypeOf(Type $otherType): IsSuperTypeOfResult
135: {
136: if ($otherType instanceof IntersectionType || $otherType instanceof UnionType) {
137: return $otherType->isSuperTypeOf(new UnionType([
138: new ArrayType($this->keyType, $this->itemType),
139: new IntersectionType([
140: new ObjectType(Traversable::class),
141: $this,
142: ]),
143: ]));
144: }
145:
146: if ($otherType instanceof self) {
147: $limit = IsSuperTypeOfResult::createYes();
148: } else {
149: $limit = IsSuperTypeOfResult::createMaybe();
150: }
151:
152: if ($otherType->isConstantArray()->yes() && $otherType->isIterableAtLeastOnce()->no()) {
153: return IsSuperTypeOfResult::createMaybe();
154: }
155:
156: return $limit->and(
157: new IsSuperTypeOfResult($otherType->isIterable(), []),
158: $otherType->getIterableValueType()->isSuperTypeOf($this->itemType),
159: $otherType->getIterableKeyType()->isSuperTypeOf($this->keyType),
160: );
161: }
162:
163: public function isAcceptedBy(Type $acceptingType, bool $strictTypes): AcceptsResult
164: {
165: return $this->isSubTypeOf($acceptingType)->toAcceptsResult();
166: }
167:
168: public function equals(Type $type): bool
169: {
170: if (!$type instanceof self) {
171: return false;
172: }
173:
174: return $this->keyType->equals($type->keyType)
175: && $this->itemType->equals($type->itemType);
176: }
177:
178: public function describe(VerbosityLevel $level): string
179: {
180: $isMixedKeyType = $this->keyType instanceof MixedType && $this->keyType->describe(VerbosityLevel::precise()) === 'mixed';
181: $isMixedItemType = $this->itemType instanceof MixedType && $this->itemType->describe(VerbosityLevel::precise()) === 'mixed';
182: if ($isMixedKeyType) {
183: if ($isMixedItemType) {
184: return 'iterable';
185: }
186:
187: return sprintf('iterable<%s>', $this->itemType->describe($level));
188: }
189:
190: return sprintf('iterable<%s, %s>', $this->keyType->describe($level), $this->itemType->describe($level));
191: }
192:
193: public function hasOffsetValueType(Type $offsetType): TrinaryLogic
194: {
195: if ($this->getIterableKeyType()->isSuperTypeOf($offsetType)->no()) {
196: return TrinaryLogic::createNo();
197: }
198:
199: return TrinaryLogic::createMaybe();
200: }
201:
202: public function toNumber(): Type
203: {
204: return new ErrorType();
205: }
206:
207: public function toAbsoluteNumber(): Type
208: {
209: return new ErrorType();
210: }
211:
212: public function toString(): Type
213: {
214: return new ErrorType();
215: }
216:
217: public function toInteger(): Type
218: {
219: return new ErrorType();
220: }
221:
222: public function toFloat(): Type
223: {
224: return new ErrorType();
225: }
226:
227: public function toArray(): Type
228: {
229: return new ArrayType($this->keyType, $this->getItemType());
230: }
231:
232: public function toArrayKey(): Type
233: {
234: return new ErrorType();
235: }
236:
237: public function isOffsetAccessLegal(): TrinaryLogic
238: {
239: return TrinaryLogic::createMaybe();
240: }
241:
242: public function isIterable(): TrinaryLogic
243: {
244: return TrinaryLogic::createYes();
245: }
246:
247: public function isIterableAtLeastOnce(): TrinaryLogic
248: {
249: return TrinaryLogic::createMaybe();
250: }
251:
252: public function getArraySize(): Type
253: {
254: return IntegerRangeType::fromInterval(0, null);
255: }
256:
257: public function getIterableKeyType(): Type
258: {
259: return $this->keyType;
260: }
261:
262: public function getFirstIterableKeyType(): Type
263: {
264: return $this->keyType;
265: }
266:
267: public function getLastIterableKeyType(): Type
268: {
269: return $this->keyType;
270: }
271:
272: public function getIterableValueType(): Type
273: {
274: return $this->getItemType();
275: }
276:
277: public function getFirstIterableValueType(): Type
278: {
279: return $this->getItemType();
280: }
281:
282: public function getLastIterableValueType(): Type
283: {
284: return $this->getItemType();
285: }
286:
287: public function isNull(): TrinaryLogic
288: {
289: return TrinaryLogic::createNo();
290: }
291:
292: public function isConstantValue(): TrinaryLogic
293: {
294: return TrinaryLogic::createNo();
295: }
296:
297: public function isConstantScalarValue(): TrinaryLogic
298: {
299: return TrinaryLogic::createNo();
300: }
301:
302: public function getConstantScalarTypes(): array
303: {
304: return [];
305: }
306:
307: public function getConstantScalarValues(): array
308: {
309: return [];
310: }
311:
312: public function isTrue(): TrinaryLogic
313: {
314: return TrinaryLogic::createNo();
315: }
316:
317: public function isFalse(): TrinaryLogic
318: {
319: return TrinaryLogic::createNo();
320: }
321:
322: public function isBoolean(): TrinaryLogic
323: {
324: return TrinaryLogic::createNo();
325: }
326:
327: public function isFloat(): TrinaryLogic
328: {
329: return TrinaryLogic::createNo();
330: }
331:
332: public function isInteger(): TrinaryLogic
333: {
334: return TrinaryLogic::createNo();
335: }
336:
337: public function isString(): TrinaryLogic
338: {
339: return TrinaryLogic::createNo();
340: }
341:
342: public function isNumericString(): TrinaryLogic
343: {
344: return TrinaryLogic::createNo();
345: }
346:
347: public function isNonEmptyString(): TrinaryLogic
348: {
349: return TrinaryLogic::createNo();
350: }
351:
352: public function isNonFalsyString(): TrinaryLogic
353: {
354: return TrinaryLogic::createNo();
355: }
356:
357: public function isLiteralString(): TrinaryLogic
358: {
359: return TrinaryLogic::createNo();
360: }
361:
362: public function isLowercaseString(): TrinaryLogic
363: {
364: return TrinaryLogic::createNo();
365: }
366:
367: public function isClassString(): TrinaryLogic
368: {
369: return TrinaryLogic::createNo();
370: }
371:
372: public function isUppercaseString(): TrinaryLogic
373: {
374: return TrinaryLogic::createNo();
375: }
376:
377: public function getClassStringObjectType(): Type
378: {
379: return new ErrorType();
380: }
381:
382: public function getObjectTypeOrClassStringObjectType(): Type
383: {
384: return new ObjectWithoutClassType();
385: }
386:
387: public function isVoid(): TrinaryLogic
388: {
389: return TrinaryLogic::createNo();
390: }
391:
392: public function isScalar(): TrinaryLogic
393: {
394: return TrinaryLogic::createNo();
395: }
396:
397: public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType
398: {
399: return new BooleanType();
400: }
401:
402: public function getEnumCases(): array
403: {
404: return [];
405: }
406:
407: public function inferTemplateTypes(Type $receivedType): TemplateTypeMap
408: {
409: if ($receivedType instanceof UnionType || $receivedType instanceof IntersectionType) {
410: return $receivedType->inferTemplateTypesOn($this);
411: }
412:
413: if (!$receivedType->isIterable()->yes()) {
414: return TemplateTypeMap::createEmpty();
415: }
416:
417: $keyTypeMap = $this->getIterableKeyType()->inferTemplateTypes($receivedType->getIterableKeyType());
418: $valueTypeMap = $this->getIterableValueType()->inferTemplateTypes($receivedType->getIterableValueType());
419:
420: return $keyTypeMap->union($valueTypeMap);
421: }
422:
423: public function getReferencedTemplateTypes(TemplateTypeVariance $positionVariance): array
424: {
425: $variance = $positionVariance->compose(TemplateTypeVariance::createCovariant());
426:
427: return array_merge(
428: $this->getIterableKeyType()->getReferencedTemplateTypes($variance),
429: $this->getIterableValueType()->getReferencedTemplateTypes($variance),
430: );
431: }
432:
433: public function traverse(callable $cb): Type
434: {
435: $keyType = $cb($this->keyType);
436: $itemType = $cb($this->itemType);
437:
438: if ($keyType !== $this->keyType || $itemType !== $this->itemType) {
439: return new self($keyType, $itemType);
440: }
441:
442: return $this;
443: }
444:
445: public function traverseSimultaneously(Type $right, callable $cb): Type
446: {
447: $keyType = $cb($this->keyType, $right->getIterableKeyType());
448: $itemType = $cb($this->itemType, $right->getIterableValueType());
449:
450: if ($keyType !== $this->keyType || $itemType !== $this->itemType) {
451: return new self($keyType, $itemType);
452: }
453:
454: return $this;
455: }
456:
457: public function tryRemove(Type $typeToRemove): ?Type
458: {
459: $arrayType = new ArrayType(new MixedType(), new MixedType());
460: if ($typeToRemove->isSuperTypeOf($arrayType)->yes()) {
461: return new GenericObjectType(Traversable::class, [
462: $this->getIterableKeyType(),
463: $this->getIterableValueType(),
464: ]);
465: }
466:
467: $traversableType = new ObjectType(Traversable::class);
468: if ($typeToRemove->isSuperTypeOf($traversableType)->yes()) {
469: return new ArrayType($this->getIterableKeyType(), $this->getIterableValueType());
470: }
471:
472: return null;
473: }
474:
475: public function exponentiate(Type $exponent): Type
476: {
477: return new ErrorType();
478: }
479:
480: public function getFiniteTypes(): array
481: {
482: return [];
483: }
484:
485: public function toPhpDocNode(): TypeNode
486: {
487: $isMixedKeyType = $this->keyType instanceof MixedType && $this->keyType->describe(VerbosityLevel::precise()) === 'mixed';
488: $isMixedItemType = $this->itemType instanceof MixedType && $this->itemType->describe(VerbosityLevel::precise()) === 'mixed';
489:
490: if ($isMixedKeyType) {
491: if ($isMixedItemType) {
492: return new IdentifierTypeNode('iterable');
493: }
494:
495: return new GenericTypeNode(
496: new IdentifierTypeNode('iterable'),
497: [
498: $this->itemType->toPhpDocNode(),
499: ],
500: );
501: }
502:
503: return new GenericTypeNode(
504: new IdentifierTypeNode('iterable'),
505: [
506: $this->keyType->toPhpDocNode(),
507: $this->itemType->toPhpDocNode(),
508: ],
509: );
510: }
511:
512: }
513: