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