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