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