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