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 toBitwiseNotType(): Type
204: {
205: return new ErrorType();
206: }
207:
208: public function toAbsoluteNumber(): Type
209: {
210: return new ErrorType();
211: }
212:
213: public function toString(): Type
214: {
215: return new ErrorType();
216: }
217:
218: public function toInteger(): Type
219: {
220: return new ErrorType();
221: }
222:
223: public function toFloat(): Type
224: {
225: return new ErrorType();
226: }
227:
228: public function toArray(): Type
229: {
230: return new ArrayType($this->keyType, $this->getItemType());
231: }
232:
233: public function toArrayOrTraversable(): Type
234: {
235: return new UnionType([
236: new ArrayType($this->keyType, $this->itemType),
237: new GenericObjectType(Traversable::class, [
238: $this->keyType,
239: $this->itemType,
240: ]),
241: ]);
242: }
243:
244: public function toArrayKey(): Type
245: {
246: return new ErrorType();
247: }
248:
249: public function toCoercedArgumentType(bool $strictTypes): Type
250: {
251: return TypeCombinator::union(
252: $this,
253: new ArrayType(
254: TypeCombinator::intersect(
255: $this->keyType->toArrayKey(),
256: new UnionType([
257: new IntegerType(),
258: new StringType(),
259: ]),
260: ),
261: $this->itemType,
262: ),
263: new GenericObjectType(Traversable::class, [
264: $this->keyType,
265: $this->itemType,
266: ]),
267: );
268: }
269:
270: public function isIterable(): TrinaryLogic
271: {
272: return TrinaryLogic::createYes();
273: }
274:
275: public function isIterableAtLeastOnce(): TrinaryLogic
276: {
277: return TrinaryLogic::createMaybe();
278: }
279:
280: public function getArraySize(): Type
281: {
282: return IntegerRangeType::fromInterval(0, null);
283: }
284:
285: public function getIterableKeyType(): Type
286: {
287: return $this->keyType;
288: }
289:
290: public function getFirstIterableKeyType(): Type
291: {
292: return $this->keyType;
293: }
294:
295: public function getLastIterableKeyType(): Type
296: {
297: return $this->keyType;
298: }
299:
300: public function getIterableValueType(): Type
301: {
302: return $this->getItemType();
303: }
304:
305: public function getFirstIterableValueType(): Type
306: {
307: return $this->getItemType();
308: }
309:
310: public function getLastIterableValueType(): Type
311: {
312: return $this->getItemType();
313: }
314:
315: public function isNull(): TrinaryLogic
316: {
317: return TrinaryLogic::createNo();
318: }
319:
320: public function isConstantValue(): TrinaryLogic
321: {
322: return TrinaryLogic::createNo();
323: }
324:
325: public function isConstantScalarValue(): TrinaryLogic
326: {
327: return TrinaryLogic::createNo();
328: }
329:
330: public function getConstantScalarTypes(): array
331: {
332: return [];
333: }
334:
335: public function getConstantScalarValues(): array
336: {
337: return [];
338: }
339:
340: public function isTrue(): TrinaryLogic
341: {
342: return TrinaryLogic::createNo();
343: }
344:
345: public function isFalse(): TrinaryLogic
346: {
347: return TrinaryLogic::createNo();
348: }
349:
350: public function isBoolean(): TrinaryLogic
351: {
352: return TrinaryLogic::createNo();
353: }
354:
355: public function isFloat(): TrinaryLogic
356: {
357: return TrinaryLogic::createNo();
358: }
359:
360: public function isInteger(): TrinaryLogic
361: {
362: return TrinaryLogic::createNo();
363: }
364:
365: public function isString(): TrinaryLogic
366: {
367: return TrinaryLogic::createNo();
368: }
369:
370: public function isNumericString(): TrinaryLogic
371: {
372: return TrinaryLogic::createNo();
373: }
374:
375: public function isDecimalIntegerString(): TrinaryLogic
376: {
377: return TrinaryLogic::createNo();
378: }
379:
380: public function isNonEmptyString(): TrinaryLogic
381: {
382: return TrinaryLogic::createNo();
383: }
384:
385: public function isNonFalsyString(): TrinaryLogic
386: {
387: return TrinaryLogic::createNo();
388: }
389:
390: public function isLiteralString(): TrinaryLogic
391: {
392: return TrinaryLogic::createNo();
393: }
394:
395: public function isLowercaseString(): TrinaryLogic
396: {
397: return TrinaryLogic::createNo();
398: }
399:
400: public function isClassString(): TrinaryLogic
401: {
402: return TrinaryLogic::createNo();
403: }
404:
405: public function isUppercaseString(): TrinaryLogic
406: {
407: return TrinaryLogic::createNo();
408: }
409:
410: public function getClassStringObjectType(): Type
411: {
412: return new ErrorType();
413: }
414:
415: public function getObjectTypeOrClassStringObjectType(): Type
416: {
417: return new ObjectWithoutClassType();
418: }
419:
420: public function isVoid(): TrinaryLogic
421: {
422: return TrinaryLogic::createNo();
423: }
424:
425: public function isScalar(): TrinaryLogic
426: {
427: return TrinaryLogic::createNo();
428: }
429:
430: public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType
431: {
432: return new BooleanType();
433: }
434:
435: public function getEnumCases(): array
436: {
437: return [];
438: }
439:
440: public function getEnumCaseObject(): ?EnumCaseObjectType
441: {
442: return null;
443: }
444:
445: public function inferTemplateTypes(Type $receivedType): TemplateTypeMap
446: {
447: if ($receivedType instanceof UnionType || $receivedType instanceof IntersectionType) {
448: return $receivedType->inferTemplateTypesOn($this);
449: }
450:
451: if (!$receivedType->isIterable()->yes()) {
452: return TemplateTypeMap::createEmpty();
453: }
454:
455: $keyTypeMap = $this->getIterableKeyType()->inferTemplateTypes($receivedType->getIterableKeyType());
456: $valueTypeMap = $this->getIterableValueType()->inferTemplateTypes($receivedType->getIterableValueType());
457:
458: return $keyTypeMap->union($valueTypeMap);
459: }
460:
461: public function getReferencedTemplateTypes(TemplateTypeVariance $positionVariance): array
462: {
463: $variance = $positionVariance->compose(TemplateTypeVariance::createCovariant());
464:
465: return array_merge(
466: $this->getIterableKeyType()->getReferencedTemplateTypes($variance),
467: $this->getIterableValueType()->getReferencedTemplateTypes($variance),
468: );
469: }
470:
471: public function traverse(callable $cb): Type
472: {
473: $keyType = $cb($this->keyType);
474: $itemType = $cb($this->itemType);
475:
476: if ($keyType !== $this->keyType || $itemType !== $this->itemType) {
477: return new self($keyType, $itemType);
478: }
479:
480: return $this;
481: }
482:
483: public function traverseSimultaneously(Type $right, callable $cb): Type
484: {
485: $keyType = $cb($this->keyType, $right->getIterableKeyType());
486: $itemType = $cb($this->itemType, $right->getIterableValueType());
487:
488: if ($keyType !== $this->keyType || $itemType !== $this->itemType) {
489: return new self($keyType, $itemType);
490: }
491:
492: return $this;
493: }
494:
495: public function tryRemove(Type $typeToRemove): ?Type
496: {
497: $arrayType = new ArrayType(new MixedType(), new MixedType());
498: if ($typeToRemove->isSuperTypeOf($arrayType)->yes()) {
499: return new GenericObjectType(Traversable::class, [
500: $this->getIterableKeyType(),
501: $this->getIterableValueType(),
502: ]);
503: }
504:
505: $traversableType = new ObjectType(Traversable::class);
506: if ($typeToRemove->isSuperTypeOf($traversableType)->yes()) {
507: return new ArrayType($this->getIterableKeyType(), $this->getIterableValueType());
508: }
509:
510: return null;
511: }
512:
513: public function exponentiate(Type $exponent): Type
514: {
515: return new ErrorType();
516: }
517:
518: public function getFiniteTypes(): array
519: {
520: return [];
521: }
522:
523: public function toPhpDocNode(): TypeNode
524: {
525: $isMixedKeyType = $this->keyType instanceof MixedType && $this->keyType->describe(VerbosityLevel::precise()) === 'mixed';
526: $isMixedItemType = $this->itemType instanceof MixedType && $this->itemType->describe(VerbosityLevel::precise()) === 'mixed';
527:
528: if ($isMixedKeyType) {
529: if ($isMixedItemType) {
530: return new IdentifierTypeNode('iterable');
531: }
532:
533: return new GenericTypeNode(
534: new IdentifierTypeNode('iterable'),
535: [
536: $this->itemType->toPhpDocNode(),
537: ],
538: );
539: }
540:
541: return new GenericTypeNode(
542: new IdentifierTypeNode('iterable'),
543: [
544: $this->keyType->toPhpDocNode(),
545: $this->itemType->toPhpDocNode(),
546: ],
547: );
548: }
549:
550: public function hasTemplateOrLateResolvableType(): bool
551: {
552: return $this->keyType->hasTemplateOrLateResolvableType() || $this->itemType->hasTemplateOrLateResolvableType();
553: }
554:
555: }
556: