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