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