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