1: <?php declare(strict_types = 1);
2:
3: namespace PHPStan\Type\Generic;
4:
5: use PHPStan\ShouldNotHappenException;
6: use PHPStan\TrinaryLogic;
7: use PHPStan\Type\AcceptsResult;
8: use PHPStan\Type\BenevolentUnionType;
9: use PHPStan\Type\MixedType;
10: use PHPStan\Type\NeverType;
11: use PHPStan\Type\Type;
12: use function sprintf;
13:
14: /** @api */
15: class TemplateTypeVariance
16: {
17:
18: private const INVARIANT = 1;
19: private const COVARIANT = 2;
20: private const CONTRAVARIANT = 3;
21: private const STATIC = 4;
22:
23: /** @var self[] */
24: private static array $registry;
25:
26: private static bool $invarianceCompositionEnabled = false;
27:
28: private function __construct(private int $value)
29: {
30: }
31:
32: private static function create(int $value): self
33: {
34: self::$registry[$value] ??= new self($value);
35: return self::$registry[$value];
36: }
37:
38: public static function createInvariant(): self
39: {
40: return self::create(self::INVARIANT);
41: }
42:
43: public static function createCovariant(): self
44: {
45: return self::create(self::COVARIANT);
46: }
47:
48: public static function createContravariant(): self
49: {
50: return self::create(self::CONTRAVARIANT);
51: }
52:
53: public static function createStatic(): self
54: {
55: return self::create(self::STATIC);
56: }
57:
58: public function invariant(): bool
59: {
60: return $this->value === self::INVARIANT;
61: }
62:
63: public function covariant(): bool
64: {
65: return $this->value === self::COVARIANT;
66: }
67:
68: public function contravariant(): bool
69: {
70: return $this->value === self::CONTRAVARIANT;
71: }
72:
73: public function static(): bool
74: {
75: return $this->value === self::STATIC;
76: }
77:
78: public function compose(self $other): self
79: {
80: if ($this->contravariant()) {
81: if ($other->contravariant()) {
82: return self::createCovariant();
83: }
84: if ($other->covariant()) {
85: return self::createContravariant();
86: }
87: return self::createInvariant();
88: }
89:
90: if ($this->covariant()) {
91: if ($other->contravariant()) {
92: return self::createContravariant();
93: }
94: if ($other->covariant()) {
95: return self::createCovariant();
96: }
97: return self::createInvariant();
98: }
99:
100: if (self::$invarianceCompositionEnabled && $this->invariant()) {
101: return self::createInvariant();
102: }
103:
104: return $other;
105: }
106:
107: public function isValidVariance(Type $a, Type $b): TrinaryLogic
108: {
109: return $this->isValidVarianceWithReason(null, $a, $b)->result;
110: }
111:
112: public function isValidVarianceWithReason(?TemplateType $templateType, Type $a, Type $b): AcceptsResult
113: {
114: if ($b instanceof NeverType) {
115: return AcceptsResult::createYes();
116: }
117:
118: if ($a instanceof MixedType && !$a instanceof TemplateType) {
119: return AcceptsResult::createYes();
120: }
121:
122: if ($a instanceof BenevolentUnionType) {
123: if (!$a->isSuperTypeOf($b)->no()) {
124: return AcceptsResult::createYes();
125: }
126: }
127:
128: if ($b instanceof BenevolentUnionType) {
129: if (!$b->isSuperTypeOf($a)->no()) {
130: return AcceptsResult::createYes();
131: }
132: }
133:
134: if ($b instanceof MixedType && !$b instanceof TemplateType) {
135: return AcceptsResult::createYes();
136: }
137:
138: if ($this->invariant()) {
139: $result = $a->equals($b);
140: $reasons = [];
141: if (!$result) {
142: if (
143: $templateType !== null
144: && $templateType->getScope()->getClassName() !== null
145: && $a->isSuperTypeOf($b)->yes()
146: ) {
147: $reasons[] = sprintf(
148: 'Template type %s on class %s is not covariant. Learn more: <fg=cyan>https://phpstan.org/blog/whats-up-with-template-covariant</>',
149: $templateType->getName(),
150: $templateType->getScope()->getClassName(),
151: );
152: }
153: }
154:
155: return new AcceptsResult(TrinaryLogic::createFromBoolean($result), $reasons);
156: }
157:
158: if ($this->covariant()) {
159: return new AcceptsResult($a->isSuperTypeOf($b), []);
160: }
161:
162: if ($this->contravariant()) {
163: return new AcceptsResult($b->isSuperTypeOf($a), []);
164: }
165:
166: throw new ShouldNotHappenException();
167: }
168:
169: public function equals(self $other): bool
170: {
171: return $other->value === $this->value;
172: }
173:
174: public function validPosition(self $other): bool
175: {
176: return $other->value === $this->value
177: || $other->invariant()
178: || $this->static();
179: }
180:
181: public function describe(): string
182: {
183: switch ($this->value) {
184: case self::INVARIANT:
185: return 'invariant';
186: case self::COVARIANT:
187: return 'covariant';
188: case self::CONTRAVARIANT:
189: return 'contravariant';
190: case self::STATIC:
191: return 'static';
192: }
193:
194: throw new ShouldNotHappenException();
195: }
196:
197: /**
198: * @param array{value: int} $properties
199: */
200: public static function __set_state(array $properties): self
201: {
202: return new self($properties['value']);
203: }
204:
205: public static function setInvarianceCompositionEnabled(bool $enabled): void
206: {
207: self::$invarianceCompositionEnabled = $enabled;
208: }
209:
210: }
211: