1: <?php declare(strict_types = 1);
2:
3: namespace PHPStan\Type;
4:
5: use PHPStan\Reflection\ClassMemberAccessAnswerer;
6: use PHPStan\Reflection\ConstantReflection;
7: use PHPStan\Reflection\MethodReflection;
8: use PHPStan\Reflection\ParametersAcceptor;
9: use PHPStan\Reflection\PropertyReflection;
10: use PHPStan\Reflection\TrivialParametersAcceptor;
11: use PHPStan\Reflection\Type\IntersectionTypeUnresolvedMethodPrototypeReflection;
12: use PHPStan\Reflection\Type\IntersectionTypeUnresolvedPropertyPrototypeReflection;
13: use PHPStan\Reflection\Type\UnresolvedMethodPrototypeReflection;
14: use PHPStan\Reflection\Type\UnresolvedPropertyPrototypeReflection;
15: use PHPStan\ShouldNotHappenException;
16: use PHPStan\TrinaryLogic;
17: use PHPStan\Type\Accessory\AccessoryLiteralStringType;
18: use PHPStan\Type\Accessory\AccessoryNonEmptyStringType;
19: use PHPStan\Type\Accessory\AccessoryNonFalsyStringType;
20: use PHPStan\Type\Accessory\AccessoryNumericStringType;
21: use PHPStan\Type\Accessory\AccessoryType;
22: use PHPStan\Type\Accessory\NonEmptyArrayType;
23: use PHPStan\Type\Generic\TemplateType;
24: use PHPStan\Type\Generic\TemplateTypeMap;
25: use PHPStan\Type\Generic\TemplateTypeVariance;
26: use PHPStan\Type\Traits\NonGeneralizableTypeTrait;
27: use PHPStan\Type\Traits\NonRemoveableTypeTrait;
28: use function array_map;
29: use function count;
30: use function implode;
31: use function in_array;
32: use function sprintf;
33: use function strlen;
34: use function substr;
35:
36: /** @api */
37: class IntersectionType implements CompoundType
38: {
39:
40: use NonRemoveableTypeTrait;
41: use NonGeneralizableTypeTrait;
42:
43: private bool $sortedTypes = false;
44:
45: /**
46: * @api
47: * @param Type[] $types
48: */
49: public function __construct(private array $types)
50: {
51: if (count($types) < 2) {
52: throw new ShouldNotHappenException(sprintf(
53: 'Cannot create %s with: %s',
54: self::class,
55: implode(', ', array_map(static fn (Type $type): string => $type->describe(VerbosityLevel::value()), $types)),
56: ));
57: }
58: }
59:
60: /**
61: * @return Type[]
62: */
63: public function getTypes(): array
64: {
65: return $this->types;
66: }
67:
68: /**
69: * @return Type[]
70: */
71: private function getSortedTypes(): array
72: {
73: if ($this->sortedTypes) {
74: return $this->types;
75: }
76:
77: $this->types = UnionTypeHelper::sortTypes($this->types);
78: $this->sortedTypes = true;
79:
80: return $this->types;
81: }
82:
83: public function inferTemplateTypesOn(Type $templateType): TemplateTypeMap
84: {
85: $types = TemplateTypeMap::createEmpty();
86:
87: foreach ($this->types as $type) {
88: $types = $types->intersect($templateType->inferTemplateTypes($type));
89: }
90:
91: return $types;
92: }
93:
94: /**
95: * @return string[]
96: */
97: public function getReferencedClasses(): array
98: {
99: return UnionTypeHelper::getReferencedClasses($this->types);
100: }
101:
102: public function accepts(Type $otherType, bool $strictTypes): TrinaryLogic
103: {
104: foreach ($this->types as $type) {
105: if (!$type->accepts($otherType, $strictTypes)->yes()) {
106: return TrinaryLogic::createNo();
107: }
108: }
109:
110: return TrinaryLogic::createYes();
111: }
112:
113: public function isSuperTypeOf(Type $otherType): TrinaryLogic
114: {
115: if ($otherType instanceof IntersectionType && $this->equals($otherType)) {
116: return TrinaryLogic::createYes();
117: }
118:
119: if ($otherType instanceof NeverType) {
120: return TrinaryLogic::createYes();
121: }
122:
123: return TrinaryLogic::createYes()->lazyAnd($this->getTypes(), static fn (Type $innerType) => $innerType->isSuperTypeOf($otherType));
124: }
125:
126: public function isSubTypeOf(Type $otherType): TrinaryLogic
127: {
128: if (($otherType instanceof self || $otherType instanceof UnionType) && !$otherType instanceof TemplateType) {
129: return $otherType->isSuperTypeOf($this);
130: }
131:
132: return TrinaryLogic::lazyMaxMin($this->getTypes(), static fn (Type $innerType) => $otherType->isSuperTypeOf($innerType));
133: }
134:
135: public function isAcceptedBy(Type $acceptingType, bool $strictTypes): TrinaryLogic
136: {
137: return TrinaryLogic::lazyMaxMin($this->getTypes(), static fn (Type $innerType) => $acceptingType->accepts($innerType, $strictTypes));
138: }
139:
140: public function equals(Type $type): bool
141: {
142: if (!$type instanceof static) {
143: return false;
144: }
145:
146: if (count($this->types) !== count($type->types)) {
147: return false;
148: }
149:
150: $otherTypes = $type->types;
151: foreach ($this->types as $innerType) {
152: $match = false;
153: foreach ($otherTypes as $i => $otherType) {
154: if (!$innerType->equals($otherType)) {
155: continue;
156: }
157:
158: $match = true;
159: unset($otherTypes[$i]);
160: break;
161: }
162:
163: if (!$match) {
164: return false;
165: }
166: }
167:
168: return count($otherTypes) === 0;
169: }
170:
171: public function describe(VerbosityLevel $level): string
172: {
173: return $level->handle(
174: function () use ($level): string {
175: $typeNames = [];
176: foreach ($this->getSortedTypes() as $type) {
177: if ($type instanceof AccessoryType) {
178: continue;
179: }
180: $typeNames[] = $type->generalize(GeneralizePrecision::lessSpecific())->describe($level);
181: }
182:
183: return implode('&', $typeNames);
184: },
185: fn (): string => $this->describeItself($level, true),
186: fn (): string => $this->describeItself($level, false),
187: );
188: }
189:
190: private function describeItself(VerbosityLevel $level, bool $skipAccessoryTypes): string
191: {
192: $typesToDescribe = [];
193: $skipTypeNames = [];
194:
195: $nonEmptyStr = false;
196: $nonFalsyStr = false;
197: foreach ($this->getSortedTypes() as $type) {
198: if ($type instanceof AccessoryNonEmptyStringType
199: || $type instanceof AccessoryLiteralStringType
200: || $type instanceof AccessoryNumericStringType
201: || $type instanceof AccessoryNonFalsyStringType
202: ) {
203: if ($type instanceof AccessoryNonFalsyStringType) {
204: $nonFalsyStr = true;
205: }
206: if ($type instanceof AccessoryNonEmptyStringType) {
207: $nonEmptyStr = true;
208: }
209: if ($nonEmptyStr && $nonFalsyStr) {
210: // prevent redundant 'non-empty-string&non-falsy-string'
211: foreach ($typesToDescribe as $key => $typeToDescribe) {
212: if (!($typeToDescribe instanceof AccessoryNonEmptyStringType)) {
213: continue;
214: }
215:
216: unset($typesToDescribe[$key]);
217: }
218: }
219:
220: $typesToDescribe[] = $type;
221: $skipTypeNames[] = 'string';
222: continue;
223: }
224: if ($type instanceof NonEmptyArrayType) {
225: $typesToDescribe[] = $type;
226: $skipTypeNames[] = 'array';
227: continue;
228: }
229:
230: if ($skipAccessoryTypes) {
231: continue;
232: }
233:
234: if (!$type instanceof AccessoryType) {
235: continue;
236: }
237:
238: $typesToDescribe[] = $type;
239: }
240:
241: $describedTypes = [];
242: foreach ($this->getSortedTypes() as $type) {
243: if ($type instanceof AccessoryType) {
244: continue;
245: }
246: $typeDescription = $type->describe($level);
247: if (
248: substr($typeDescription, 0, strlen('array<')) === 'array<'
249: && in_array('array', $skipTypeNames, true)
250: ) {
251: foreach ($typesToDescribe as $j => $typeToDescribe) {
252: if (!$typeToDescribe instanceof NonEmptyArrayType) {
253: continue;
254: }
255:
256: unset($typesToDescribe[$j]);
257: }
258:
259: $describedTypes[] = 'non-empty-array<' . substr($typeDescription, strlen('array<'));
260: continue;
261: }
262:
263: if (in_array($typeDescription, $skipTypeNames, true)) {
264: continue;
265: }
266:
267: $describedTypes[] = $type->describe($level);
268: }
269:
270: foreach ($typesToDescribe as $typeToDescribe) {
271: $describedTypes[] = $typeToDescribe->describe($level);
272: }
273:
274: return implode('&', $describedTypes);
275: }
276:
277: public function canAccessProperties(): TrinaryLogic
278: {
279: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->canAccessProperties());
280: }
281:
282: public function hasProperty(string $propertyName): TrinaryLogic
283: {
284: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->hasProperty($propertyName));
285: }
286:
287: public function getProperty(string $propertyName, ClassMemberAccessAnswerer $scope): PropertyReflection
288: {
289: return $this->getUnresolvedPropertyPrototype($propertyName, $scope)->getTransformedProperty();
290: }
291:
292: public function getUnresolvedPropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection
293: {
294: $propertyPrototypes = [];
295: foreach ($this->types as $type) {
296: if (!$type->hasProperty($propertyName)->yes()) {
297: continue;
298: }
299:
300: $propertyPrototypes[] = $type->getUnresolvedPropertyPrototype($propertyName, $scope)->withFechedOnType($this);
301: }
302:
303: $propertiesCount = count($propertyPrototypes);
304: if ($propertiesCount === 0) {
305: throw new ShouldNotHappenException();
306: }
307:
308: if ($propertiesCount === 1) {
309: return $propertyPrototypes[0];
310: }
311:
312: return new IntersectionTypeUnresolvedPropertyPrototypeReflection($propertyName, $propertyPrototypes);
313: }
314:
315: public function canCallMethods(): TrinaryLogic
316: {
317: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->canCallMethods());
318: }
319:
320: public function hasMethod(string $methodName): TrinaryLogic
321: {
322: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->hasMethod($methodName));
323: }
324:
325: public function getMethod(string $methodName, ClassMemberAccessAnswerer $scope): MethodReflection
326: {
327: return $this->getUnresolvedMethodPrototype($methodName, $scope)->getTransformedMethod();
328: }
329:
330: public function getUnresolvedMethodPrototype(string $methodName, ClassMemberAccessAnswerer $scope): UnresolvedMethodPrototypeReflection
331: {
332: $methodPrototypes = [];
333: foreach ($this->types as $type) {
334: if (!$type->hasMethod($methodName)->yes()) {
335: continue;
336: }
337:
338: $methodPrototypes[] = $type->getUnresolvedMethodPrototype($methodName, $scope)->withCalledOnType($this);
339: }
340:
341: $methodsCount = count($methodPrototypes);
342: if ($methodsCount === 0) {
343: throw new ShouldNotHappenException();
344: }
345:
346: if ($methodsCount === 1) {
347: return $methodPrototypes[0];
348: }
349:
350: return new IntersectionTypeUnresolvedMethodPrototypeReflection($methodName, $methodPrototypes);
351: }
352:
353: public function canAccessConstants(): TrinaryLogic
354: {
355: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->canAccessConstants());
356: }
357:
358: public function hasConstant(string $constantName): TrinaryLogic
359: {
360: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->hasConstant($constantName));
361: }
362:
363: public function getConstant(string $constantName): ConstantReflection
364: {
365: foreach ($this->types as $type) {
366: if ($type->hasConstant($constantName)->yes()) {
367: return $type->getConstant($constantName);
368: }
369: }
370:
371: throw new ShouldNotHappenException();
372: }
373:
374: public function isIterable(): TrinaryLogic
375: {
376: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isIterable());
377: }
378:
379: public function isIterableAtLeastOnce(): TrinaryLogic
380: {
381: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isIterableAtLeastOnce());
382: }
383:
384: public function getIterableKeyType(): Type
385: {
386: return $this->intersectTypes(static fn (Type $type): Type => $type->getIterableKeyType());
387: }
388:
389: public function getIterableValueType(): Type
390: {
391: return $this->intersectTypes(static fn (Type $type): Type => $type->getIterableValueType());
392: }
393:
394: public function isArray(): TrinaryLogic
395: {
396: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isArray());
397: }
398:
399: public function isOversizedArray(): TrinaryLogic
400: {
401: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isOversizedArray());
402: }
403:
404: public function isString(): TrinaryLogic
405: {
406: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isString());
407: }
408:
409: public function isNumericString(): TrinaryLogic
410: {
411: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isNumericString());
412: }
413:
414: public function isNonEmptyString(): TrinaryLogic
415: {
416: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isNonEmptyString());
417: }
418:
419: public function isNonFalsyString(): TrinaryLogic
420: {
421: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isNonFalsyString());
422: }
423:
424: public function isLiteralString(): TrinaryLogic
425: {
426: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isLiteralString());
427: }
428:
429: public function isOffsetAccessible(): TrinaryLogic
430: {
431: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isOffsetAccessible());
432: }
433:
434: public function hasOffsetValueType(Type $offsetType): TrinaryLogic
435: {
436: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->hasOffsetValueType($offsetType));
437: }
438:
439: public function getOffsetValueType(Type $offsetType): Type
440: {
441: return $this->intersectTypes(static fn (Type $type): Type => $type->getOffsetValueType($offsetType));
442: }
443:
444: public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $unionValues = true): Type
445: {
446: return $this->intersectTypes(static fn (Type $type): Type => $type->setOffsetValueType($offsetType, $valueType, $unionValues));
447: }
448:
449: public function unsetOffset(Type $offsetType): Type
450: {
451: return $this->intersectTypes(static fn (Type $type): Type => $type->unsetOffset($offsetType));
452: }
453:
454: public function isCallable(): TrinaryLogic
455: {
456: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isCallable());
457: }
458:
459: /**
460: * @return ParametersAcceptor[]
461: */
462: public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope): array
463: {
464: if ($this->isCallable()->no()) {
465: throw new ShouldNotHappenException();
466: }
467:
468: return [new TrivialParametersAcceptor()];
469: }
470:
471: public function isCloneable(): TrinaryLogic
472: {
473: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isCloneable());
474: }
475:
476: public function isSmallerThan(Type $otherType): TrinaryLogic
477: {
478: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isSmallerThan($otherType));
479: }
480:
481: public function isSmallerThanOrEqual(Type $otherType): TrinaryLogic
482: {
483: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isSmallerThanOrEqual($otherType));
484: }
485:
486: public function isGreaterThan(Type $otherType): TrinaryLogic
487: {
488: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $otherType->isSmallerThan($type));
489: }
490:
491: public function isGreaterThanOrEqual(Type $otherType): TrinaryLogic
492: {
493: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $otherType->isSmallerThanOrEqual($type));
494: }
495:
496: public function getSmallerType(): Type
497: {
498: return $this->intersectTypes(static fn (Type $type): Type => $type->getSmallerType());
499: }
500:
501: public function getSmallerOrEqualType(): Type
502: {
503: return $this->intersectTypes(static fn (Type $type): Type => $type->getSmallerOrEqualType());
504: }
505:
506: public function getGreaterType(): Type
507: {
508: return $this->intersectTypes(static fn (Type $type): Type => $type->getGreaterType());
509: }
510:
511: public function getGreaterOrEqualType(): Type
512: {
513: return $this->intersectTypes(static fn (Type $type): Type => $type->getGreaterOrEqualType());
514: }
515:
516: public function toBoolean(): BooleanType
517: {
518: $type = $this->intersectTypes(static fn (Type $type): BooleanType => $type->toBoolean());
519:
520: if (!$type instanceof BooleanType) {
521: return new BooleanType();
522: }
523:
524: return $type;
525: }
526:
527: public function toNumber(): Type
528: {
529: $type = $this->intersectTypes(static fn (Type $type): Type => $type->toNumber());
530:
531: return $type;
532: }
533:
534: public function toString(): Type
535: {
536: $type = $this->intersectTypes(static fn (Type $type): Type => $type->toString());
537:
538: return $type;
539: }
540:
541: public function toInteger(): Type
542: {
543: $type = $this->intersectTypes(static fn (Type $type): Type => $type->toInteger());
544:
545: return $type;
546: }
547:
548: public function toFloat(): Type
549: {
550: $type = $this->intersectTypes(static fn (Type $type): Type => $type->toFloat());
551:
552: return $type;
553: }
554:
555: public function toArray(): Type
556: {
557: $type = $this->intersectTypes(static fn (Type $type): Type => $type->toArray());
558:
559: return $type;
560: }
561:
562: public function inferTemplateTypes(Type $receivedType): TemplateTypeMap
563: {
564: $types = TemplateTypeMap::createEmpty();
565:
566: foreach ($this->types as $type) {
567: $types = $types->intersect($type->inferTemplateTypes($receivedType));
568: }
569:
570: return $types;
571: }
572:
573: public function getReferencedTemplateTypes(TemplateTypeVariance $positionVariance): array
574: {
575: $references = [];
576:
577: foreach ($this->types as $type) {
578: foreach ($type->getReferencedTemplateTypes($positionVariance) as $reference) {
579: $references[] = $reference;
580: }
581: }
582:
583: return $references;
584: }
585:
586: public function traverse(callable $cb): Type
587: {
588: $types = [];
589: $changed = false;
590:
591: foreach ($this->types as $type) {
592: $newType = $cb($type);
593: if ($type !== $newType) {
594: $changed = true;
595: }
596: $types[] = $newType;
597: }
598:
599: if ($changed) {
600: return TypeCombinator::intersect(...$types);
601: }
602:
603: return $this;
604: }
605:
606: public function tryRemove(Type $typeToRemove): ?Type
607: {
608: return $this->intersectTypes(static fn (Type $type): Type => TypeCombinator::remove($type, $typeToRemove));
609: }
610:
611: /**
612: * @param mixed[] $properties
613: */
614: public static function __set_state(array $properties): Type
615: {
616: return new self($properties['types']);
617: }
618:
619: /**
620: * @param callable(Type $type): TrinaryLogic $getResult
621: */
622: private function intersectResults(callable $getResult): TrinaryLogic
623: {
624: return TrinaryLogic::lazyMaxMin($this->types, $getResult);
625: }
626:
627: /**
628: * @param callable(Type $type): Type $getType
629: */
630: private function intersectTypes(callable $getType): Type
631: {
632: $operands = array_map($getType, $this->types);
633: return TypeCombinator::intersect(...$operands);
634: }
635:
636: }
637: