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