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