| 1: | <?php declare(strict_types = 1); | 
| 2: |  | 
| 3: | namespace PHPStan\Type; | 
| 4: |  | 
| 5: | use PHPStan\PhpDocParser\Ast\Type\ConditionalTypeNode; | 
| 6: | use PHPStan\PhpDocParser\Ast\Type\TypeNode; | 
| 7: | use PHPStan\TrinaryLogic; | 
| 8: | use PHPStan\Type\Generic\TemplateTypeVariance; | 
| 9: | use PHPStan\Type\Traits\LateResolvableTypeTrait; | 
| 10: | use PHPStan\Type\Traits\NonGeneralizableTypeTrait; | 
| 11: | use function array_merge; | 
| 12: | use function sprintf; | 
| 13: |  | 
| 14: |  | 
| 15: | final class ConditionalType implements CompoundType, LateResolvableType | 
| 16: | { | 
| 17: |  | 
| 18: | use LateResolvableTypeTrait; | 
| 19: | use NonGeneralizableTypeTrait; | 
| 20: |  | 
| 21: | private ?Type $normalizedIf = null; | 
| 22: |  | 
| 23: | private ?Type $normalizedElse = null; | 
| 24: |  | 
| 25: | private ?Type $subjectWithTargetIntersectedType = null; | 
| 26: |  | 
| 27: | private ?Type $subjectWithTargetRemovedType = null; | 
| 28: |  | 
| 29: | public function __construct( | 
| 30: | private Type $subject, | 
| 31: | private Type $target, | 
| 32: | private Type $if, | 
| 33: | private Type $else, | 
| 34: | private bool $negated, | 
| 35: | ) | 
| 36: | { | 
| 37: | } | 
| 38: |  | 
| 39: | public function getSubject(): Type | 
| 40: | { | 
| 41: | return $this->subject; | 
| 42: | } | 
| 43: |  | 
| 44: | public function getTarget(): Type | 
| 45: | { | 
| 46: | return $this->target; | 
| 47: | } | 
| 48: |  | 
| 49: | public function getIf(): Type | 
| 50: | { | 
| 51: | return $this->if; | 
| 52: | } | 
| 53: |  | 
| 54: | public function getElse(): Type | 
| 55: | { | 
| 56: | return $this->else; | 
| 57: | } | 
| 58: |  | 
| 59: | public function isNegated(): bool | 
| 60: | { | 
| 61: | return $this->negated; | 
| 62: | } | 
| 63: |  | 
| 64: | public function isSuperTypeOf(Type $type): TrinaryLogic | 
| 65: | { | 
| 66: | if ($type instanceof self) { | 
| 67: | return $this->if->isSuperTypeOf($type->if) | 
| 68: | ->and($this->else->isSuperTypeOf($type->else)); | 
| 69: | } | 
| 70: |  | 
| 71: | return $this->isSuperTypeOfDefault($type); | 
| 72: | } | 
| 73: |  | 
| 74: | public function getReferencedClasses(): array | 
| 75: | { | 
| 76: | return array_merge( | 
| 77: | $this->subject->getReferencedClasses(), | 
| 78: | $this->target->getReferencedClasses(), | 
| 79: | $this->if->getReferencedClasses(), | 
| 80: | $this->else->getReferencedClasses(), | 
| 81: | ); | 
| 82: | } | 
| 83: |  | 
| 84: | public function getReferencedTemplateTypes(TemplateTypeVariance $positionVariance): array | 
| 85: | { | 
| 86: | return array_merge( | 
| 87: | $this->subject->getReferencedTemplateTypes($positionVariance), | 
| 88: | $this->target->getReferencedTemplateTypes($positionVariance), | 
| 89: | $this->if->getReferencedTemplateTypes($positionVariance), | 
| 90: | $this->else->getReferencedTemplateTypes($positionVariance), | 
| 91: | ); | 
| 92: | } | 
| 93: |  | 
| 94: | public function equals(Type $type): bool | 
| 95: | { | 
| 96: | return $type instanceof self | 
| 97: | && $this->subject->equals($type->subject) | 
| 98: | && $this->target->equals($type->target) | 
| 99: | && $this->if->equals($type->if) | 
| 100: | && $this->else->equals($type->else); | 
| 101: | } | 
| 102: |  | 
| 103: | public function describe(VerbosityLevel $level): string | 
| 104: | { | 
| 105: | return sprintf( | 
| 106: | '(%s %s %s ? %s : %s)', | 
| 107: | $this->subject->describe($level), | 
| 108: | $this->negated ? 'is not' : 'is', | 
| 109: | $this->target->describe($level), | 
| 110: | $this->if->describe($level), | 
| 111: | $this->else->describe($level), | 
| 112: | ); | 
| 113: | } | 
| 114: |  | 
| 115: | public function isResolvable(): bool | 
| 116: | { | 
| 117: | return !TypeUtils::containsTemplateType($this->subject) && !TypeUtils::containsTemplateType($this->target); | 
| 118: | } | 
| 119: |  | 
| 120: | protected function getResult(): Type | 
| 121: | { | 
| 122: | $isSuperType = $this->target->isSuperTypeOf($this->subject); | 
| 123: |  | 
| 124: | if ($isSuperType->yes()) { | 
| 125: | return !$this->negated ? $this->getNormalizedIf() : $this->getNormalizedElse(); | 
| 126: | } | 
| 127: |  | 
| 128: | if ($isSuperType->no()) { | 
| 129: | return !$this->negated ? $this->getNormalizedElse() : $this->getNormalizedIf(); | 
| 130: | } | 
| 131: |  | 
| 132: | return TypeCombinator::union( | 
| 133: | $this->getNormalizedIf(), | 
| 134: | $this->getNormalizedElse(), | 
| 135: | ); | 
| 136: | } | 
| 137: |  | 
| 138: | public function traverse(callable $cb): Type | 
| 139: | { | 
| 140: | $subject = $cb($this->subject); | 
| 141: | $target = $cb($this->target); | 
| 142: | $if = $cb($this->getNormalizedIf()); | 
| 143: | $else = $cb($this->getNormalizedElse()); | 
| 144: |  | 
| 145: | if ( | 
| 146: | $this->subject === $subject | 
| 147: | && $this->target === $target | 
| 148: | && $this->getNormalizedIf() === $if | 
| 149: | && $this->getNormalizedElse() === $else | 
| 150: | ) { | 
| 151: | return $this; | 
| 152: | } | 
| 153: |  | 
| 154: | return new self($subject, $target, $if, $else, $this->negated); | 
| 155: | } | 
| 156: |  | 
| 157: | public function traverseSimultaneously(Type $right, callable $cb): Type | 
| 158: | { | 
| 159: | if (!$right instanceof self) { | 
| 160: | return $this; | 
| 161: | } | 
| 162: |  | 
| 163: | $subject = $cb($this->subject, $right->subject); | 
| 164: | $target = $cb($this->target, $right->target); | 
| 165: | $if = $cb($this->getNormalizedIf(), $right->getNormalizedIf()); | 
| 166: | $else = $cb($this->getNormalizedElse(), $right->getNormalizedElse()); | 
| 167: |  | 
| 168: | if ( | 
| 169: | $this->subject === $subject | 
| 170: | && $this->target === $target | 
| 171: | && $this->getNormalizedIf() === $if | 
| 172: | && $this->getNormalizedElse() === $else | 
| 173: | ) { | 
| 174: | return $this; | 
| 175: | } | 
| 176: |  | 
| 177: | return new self($subject, $target, $if, $else, $this->negated); | 
| 178: | } | 
| 179: |  | 
| 180: | public function toPhpDocNode(): TypeNode | 
| 181: | { | 
| 182: | return new ConditionalTypeNode( | 
| 183: | $this->subject->toPhpDocNode(), | 
| 184: | $this->target->toPhpDocNode(), | 
| 185: | $this->if->toPhpDocNode(), | 
| 186: | $this->else->toPhpDocNode(), | 
| 187: | $this->negated, | 
| 188: | ); | 
| 189: | } | 
| 190: |  | 
| 191: |  | 
| 192: |  | 
| 193: |  | 
| 194: | public static function __set_state(array $properties): Type | 
| 195: | { | 
| 196: | return new self( | 
| 197: | $properties['subject'], | 
| 198: | $properties['target'], | 
| 199: | $properties['if'], | 
| 200: | $properties['else'], | 
| 201: | $properties['negated'], | 
| 202: | ); | 
| 203: | } | 
| 204: |  | 
| 205: | private function getNormalizedIf(): Type | 
| 206: | { | 
| 207: | return $this->normalizedIf ??= TypeTraverser::map( | 
| 208: | $this->if, | 
| 209: | fn (Type $type, callable $traverse) => $type === $this->subject | 
| 210: | ? (!$this->negated ? $this->getSubjectWithTargetIntersectedType() : $this->getSubjectWithTargetRemovedType()) | 
| 211: | : $traverse($type), | 
| 212: | ); | 
| 213: | } | 
| 214: |  | 
| 215: | private function getNormalizedElse(): Type | 
| 216: | { | 
| 217: | return $this->normalizedElse ??= TypeTraverser::map( | 
| 218: | $this->else, | 
| 219: | fn (Type $type, callable $traverse) => $type === $this->subject | 
| 220: | ? (!$this->negated ? $this->getSubjectWithTargetRemovedType() : $this->getSubjectWithTargetIntersectedType()) | 
| 221: | : $traverse($type), | 
| 222: | ); | 
| 223: | } | 
| 224: |  | 
| 225: | private function getSubjectWithTargetIntersectedType(): Type | 
| 226: | { | 
| 227: | return $this->subjectWithTargetIntersectedType ??= TypeCombinator::intersect($this->subject, $this->target); | 
| 228: | } | 
| 229: |  | 
| 230: | private function getSubjectWithTargetRemovedType(): Type | 
| 231: | { | 
| 232: | return $this->subjectWithTargetRemovedType ??= TypeCombinator::remove($this->subject, $this->target); | 
| 233: | } | 
| 234: |  | 
| 235: | } | 
| 236: |  |