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