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\BenevolentUnionType;
9: use PHPStan\Type\IsSuperTypeOfResult;
10: use PHPStan\Type\MixedType;
11: use PHPStan\Type\NeverType;
12: use PHPStan\Type\Type;
13: use function sprintf;
14:
15: /**
16: * Represents the variance of a template type parameter.
17: *
18: * Variance describes how subtyping of a generic type relates to subtyping of its
19: * type arguments. For a class `Box<T>`:
20: *
21: * - **Invariant** (default): `Box<Cat>` is NOT a subtype of `Box<Animal>`, even though
22: * Cat extends Animal. The type argument must match exactly. Declared with `@template T`.
23: * - **Covariant**: `Box<Cat>` IS a subtype of `Box<Animal>`. Safe when T only appears
24: * in "output" positions (return types). Declared with `@template-covariant T`.
25: * - **Contravariant**: `Box<Animal>` IS a subtype of `Box<Cat>`. Safe when T only
26: * appears in "input" positions (parameter types). Declared with `@template-contravariant T`.
27: * - **Bivariant**: The type argument is ignored for subtyping purposes. Rarely used.
28: * - **Static**: Special variance for `static` return type in template context.
29: *
30: * Variance composition follows standard rules — e.g. covariant composed with
31: * contravariant yields contravariant. This is used when template types appear
32: * inside nested generic types.
33: *
34: * @api
35: */
36: final class TemplateTypeVariance
37: {
38:
39: private const INVARIANT = 1;
40: private const COVARIANT = 2;
41: private const CONTRAVARIANT = 3;
42: private const STATIC = 4;
43: private const BIVARIANT = 5;
44:
45: /** @var self[] */
46: private static array $registry;
47:
48: private function __construct(private int $value)
49: {
50: }
51:
52: private static function create(int $value): self
53: {
54: self::$registry[$value] ??= new self($value);
55: return self::$registry[$value];
56: }
57:
58: /** Type argument must match exactly. This is the default for @template T. */
59: public static function createInvariant(): self
60: {
61: return self::create(self::INVARIANT);
62: }
63:
64: /** Subtyping flows with the type argument: Cat <: Animal ⟹ Box<Cat> <: Box<Animal>. */
65: public static function createCovariant(): self
66: {
67: return self::create(self::COVARIANT);
68: }
69:
70: /** Subtyping flows against the type argument: Cat <: Animal ⟹ Box<Animal> <: Box<Cat>. */
71: public static function createContravariant(): self
72: {
73: return self::create(self::CONTRAVARIANT);
74: }
75:
76: /** Special variance for static return type in template context. */
77: public static function createStatic(): self
78: {
79: return self::create(self::STATIC);
80: }
81:
82: /** Type argument is ignored for subtyping — all types are compatible. */
83: public static function createBivariant(): self
84: {
85: return self::create(self::BIVARIANT);
86: }
87:
88: public function invariant(): bool
89: {
90: return $this->value === self::INVARIANT;
91: }
92:
93: public function covariant(): bool
94: {
95: return $this->value === self::COVARIANT;
96: }
97:
98: public function contravariant(): bool
99: {
100: return $this->value === self::CONTRAVARIANT;
101: }
102:
103: public function static(): bool
104: {
105: return $this->value === self::STATIC;
106: }
107:
108: public function bivariant(): bool
109: {
110: return $this->value === self::BIVARIANT;
111: }
112:
113: public function compose(self $other): self
114: {
115: if ($this->contravariant()) {
116: if ($other->contravariant()) {
117: return self::createCovariant();
118: }
119: if ($other->covariant()) {
120: return self::createContravariant();
121: }
122: if ($other->bivariant()) {
123: return self::createBivariant();
124: }
125: return self::createInvariant();
126: }
127:
128: if ($this->covariant()) {
129: if ($other->contravariant()) {
130: return self::createContravariant();
131: }
132: if ($other->covariant()) {
133: return self::createCovariant();
134: }
135: if ($other->bivariant()) {
136: return self::createBivariant();
137: }
138: return self::createInvariant();
139: }
140:
141: if ($this->invariant()) {
142: return self::createInvariant();
143: }
144:
145: if ($this->bivariant()) {
146: return self::createBivariant();
147: }
148:
149: return $other;
150: }
151:
152: public function isValidVariance(TemplateType $templateType, Type $a, Type $b): IsSuperTypeOfResult
153: {
154: if ($b instanceof NeverType) {
155: return IsSuperTypeOfResult::createYes();
156: }
157:
158: if ($a instanceof MixedType && !$a instanceof TemplateType) {
159: return IsSuperTypeOfResult::createYes();
160: }
161:
162: if ($a instanceof BenevolentUnionType) {
163: if (!$a->isSuperTypeOf($b)->no()) {
164: return IsSuperTypeOfResult::createYes();
165: }
166: }
167:
168: if ($b instanceof BenevolentUnionType) {
169: if (!$b->isSuperTypeOf($a)->no()) {
170: return IsSuperTypeOfResult::createYes();
171: }
172: }
173:
174: if ($b instanceof MixedType && !$b instanceof TemplateType) {
175: return IsSuperTypeOfResult::createYes();
176: }
177:
178: if ($this->invariant()) {
179: $result = $a->equals($b);
180: $reasons = [];
181: if (!$result) {
182: if (
183: $templateType->getScope()->getClassName() !== null
184: && $a->isSuperTypeOf($b)->yes()
185: ) {
186: $reasons[] = sprintf(
187: 'Template type %s on class %s is not covariant. Learn more: <fg=cyan>https://phpstan.org/blog/whats-up-with-template-covariant</>',
188: $templateType->getName(),
189: $templateType->getScope()->getClassName(),
190: );
191: }
192: }
193:
194: return new IsSuperTypeOfResult(TrinaryLogic::createFromBoolean($result), $reasons);
195: }
196:
197: if ($this->covariant()) {
198: return $a->isSuperTypeOf($b);
199: }
200:
201: if ($this->contravariant()) {
202: return $b->isSuperTypeOf($a);
203: }
204:
205: if ($this->bivariant()) {
206: return IsSuperTypeOfResult::createYes();
207: }
208:
209: throw new ShouldNotHappenException();
210: }
211:
212: public function equals(self $other): bool
213: {
214: return $other->value === $this->value;
215: }
216:
217: public function validPosition(self $other): bool
218: {
219: return $other->value === $this->value
220: || $other->invariant()
221: || $this->bivariant()
222: || $this->static();
223: }
224:
225: public function describe(): string
226: {
227: switch ($this->value) {
228: case self::INVARIANT:
229: return 'invariant';
230: case self::COVARIANT:
231: return 'covariant';
232: case self::CONTRAVARIANT:
233: return 'contravariant';
234: case self::STATIC:
235: return 'static';
236: case self::BIVARIANT:
237: return 'bivariant';
238: }
239:
240: throw new ShouldNotHappenException();
241: }
242:
243: /**
244: * @return GenericTypeNode::VARIANCE_*
245: */
246: public function toPhpDocNodeVariance(): string
247: {
248: switch ($this->value) {
249: case self::INVARIANT:
250: return GenericTypeNode::VARIANCE_INVARIANT;
251: case self::COVARIANT:
252: return GenericTypeNode::VARIANCE_COVARIANT;
253: case self::CONTRAVARIANT:
254: return GenericTypeNode::VARIANCE_CONTRAVARIANT;
255: case self::BIVARIANT:
256: return GenericTypeNode::VARIANCE_BIVARIANT;
257: }
258:
259: throw new ShouldNotHappenException();
260: }
261:
262: }
263: