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