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