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