1: <?php declare(strict_types = 1);
2:
3: namespace PHPStan\Type\Generic;
4:
5: use PHPStan\Turbo\ShadowedByTurboExtension;
6: use PHPStan\Type\NeverType;
7: use PHPStan\Type\Type;
8: use PHPStan\Type\TypeCombinator;
9: use PHPStan\Type\TypeUtils;
10: use function array_key_exists;
11: use function count;
12:
13: /**
14: * Maps template type parameter names to their resolved types.
15: *
16: * This is the core data structure for PHPStan's generics support. When a class declares
17: * `@template T`, `@template U of object`, etc., the TemplateTypeMap tracks what concrete
18: * types T and U resolve to in a particular context.
19: *
20: * Two kinds of type bindings are tracked:
21: * - **types** (upper bounds): The concrete type inferred or declared for each template.
22: * For `@template T of Countable`, if T is inferred as `array`, types maps T → array.
23: * - **lowerBoundTypes**: Types inferred from contravariant positions (e.g. parameter types).
24: * Used during type inference to narrow template types from below.
25: *
26: * TemplateTypeMap supports set operations (union, intersect, benevolentUnion) that combine
27: * maps from different code paths, and resolveToBounds() which replaces unresolved template
28: * types with their declared bounds.
29: *
30: * Common usage: ParametersAcceptor::getTemplateTypeMap() returns the template declarations,
31: * and ParametersAcceptor::getResolvedTemplateTypeMap() returns inferred concrete types.
32: * Type::inferTemplateTypes() produces a TemplateTypeMap from a concrete type.
33: *
34: * @api
35: */
36: #[ShadowedByTurboExtension(implementation: __DIR__ . '/../../../turbo-ext/src/TemplateTypeMap.cpp')]
37: final class TemplateTypeMap
38: {
39:
40: private static ?TemplateTypeMap $empty = null;
41:
42: private ?TemplateTypeMap $resolvedToBounds = null;
43:
44: /**
45: * @api
46: * @param array<string, Type> $types Concrete types for each template parameter (upper bounds)
47: * @param array<string, Type> $lowerBoundTypes Types inferred from contravariant positions
48: */
49: public function __construct(private array $types, private array $lowerBoundTypes = [])
50: {
51: }
52:
53: public function convertToLowerBoundTypes(): self
54: {
55: $lowerBoundTypes = $this->types;
56: foreach ($this->lowerBoundTypes as $name => $type) {
57: if (isset($lowerBoundTypes[$name])) {
58: $intersection = TypeCombinator::intersect($lowerBoundTypes[$name], $type);
59: if ($intersection instanceof NeverType) {
60: continue;
61: }
62: $lowerBoundTypes[$name] = $intersection;
63: } else {
64: $lowerBoundTypes[$name] = $type;
65: }
66: }
67:
68: return new self([], $lowerBoundTypes);
69: }
70:
71: public static function createEmpty(): self
72: {
73: $empty = self::$empty;
74:
75: if ($empty !== null) {
76: return $empty;
77: }
78:
79: $empty = new self([], []);
80: self::$empty = $empty;
81:
82: return $empty;
83: }
84:
85: public function isEmpty(): bool
86: {
87: return $this->count() === 0;
88: }
89:
90: public function count(): int
91: {
92: return count($this->types + $this->lowerBoundTypes);
93: }
94:
95: /** @return array<string, Type> */
96: public function getTypes(): array
97: {
98: $types = $this->types;
99: foreach ($this->lowerBoundTypes as $name => $type) {
100: if (array_key_exists($name, $types)) {
101: continue;
102: }
103:
104: $types[$name] = $type;
105: }
106:
107: return $types;
108: }
109:
110: public function hasType(string $name): bool
111: {
112: return array_key_exists($name, $this->getTypes());
113: }
114:
115: public function getType(string $name): ?Type
116: {
117: return $this->getTypes()[$name] ?? null;
118: }
119:
120: public function unsetType(string $name): self
121: {
122: if (!$this->hasType($name)) {
123: return $this;
124: }
125:
126: $types = $this->types;
127: $lowerBoundTypes = $this->lowerBoundTypes;
128:
129: unset($types[$name]);
130: unset($lowerBoundTypes[$name]);
131:
132: if (count($types) === 0 && count($lowerBoundTypes) === 0) {
133: return self::createEmpty();
134: }
135:
136: return new self($types, $lowerBoundTypes);
137: }
138:
139: public function union(self $other): self
140: {
141: $result = $this->types;
142:
143: foreach ($other->types as $name => $type) {
144: if (isset($result[$name])) {
145: $result[$name] = self::combine($result[$name], $type, static fn (Type $a, Type $b): Type => TypeCombinator::union($a, $b));
146: } else {
147: $result[$name] = $type;
148: }
149: }
150:
151: $resultLowerBoundTypes = $this->lowerBoundTypes;
152: foreach ($other->lowerBoundTypes as $name => $type) {
153: if (isset($resultLowerBoundTypes[$name])) {
154: $intersection = TypeCombinator::intersect($resultLowerBoundTypes[$name], $type);
155: if ($intersection instanceof NeverType) {
156: continue;
157: }
158: $resultLowerBoundTypes[$name] = $intersection;
159: } else {
160: $resultLowerBoundTypes[$name] = $type;
161: }
162: }
163:
164: return new self($result, $resultLowerBoundTypes);
165: }
166:
167: public function benevolentUnion(self $other): self
168: {
169: $result = $this->types;
170:
171: foreach ($other->types as $name => $type) {
172: if (isset($result[$name])) {
173: $result[$name] = self::combine($result[$name], $type, static fn (Type $a, Type $b): Type => TypeUtils::toBenevolentUnion(TypeCombinator::union($a, $b)));
174: } else {
175: $result[$name] = $type;
176: }
177: }
178:
179: $resultLowerBoundTypes = $this->lowerBoundTypes;
180: foreach ($other->lowerBoundTypes as $name => $type) {
181: if (isset($resultLowerBoundTypes[$name])) {
182: $intersection = TypeCombinator::intersect($resultLowerBoundTypes[$name], $type);
183: if ($intersection instanceof NeverType) {
184: continue;
185: }
186: $resultLowerBoundTypes[$name] = $intersection;
187: } else {
188: $resultLowerBoundTypes[$name] = $type;
189: }
190: }
191:
192: return new self($result, $resultLowerBoundTypes);
193: }
194:
195: /**
196: * A template left unresolved on purpose by one occurrence of the parameter type carries
197: * no type at all, so any type another occurrence did infer wins over it outright.
198: *
199: * @param callable(Type, Type): Type $cb
200: */
201: private static function combine(Type $a, Type $b, callable $cb): Type
202: {
203: if ($a instanceof AbsorbedTemplateArgumentType) {
204: return $b;
205: }
206: if ($b instanceof AbsorbedTemplateArgumentType) {
207: return $a;
208: }
209:
210: return $cb($a, $b);
211: }
212:
213: public function intersect(self $other): self
214: {
215: $result = $this->types;
216:
217: foreach ($other->types as $name => $type) {
218: if (isset($result[$name])) {
219: $result[$name] = TypeCombinator::intersect($result[$name], $type);
220: } else {
221: $result[$name] = $type;
222: }
223: }
224:
225: $resultLowerBoundTypes = $this->lowerBoundTypes;
226: foreach ($other->lowerBoundTypes as $name => $type) {
227: if (isset($resultLowerBoundTypes[$name])) {
228: $resultLowerBoundTypes[$name] = TypeCombinator::union($resultLowerBoundTypes[$name], $type);
229: } else {
230: $resultLowerBoundTypes[$name] = $type;
231: }
232: }
233:
234: return new self($result, $resultLowerBoundTypes);
235: }
236:
237: /** @param callable(string,Type):Type $cb */
238: public function map(callable $cb): self
239: {
240: $types = [];
241: foreach ($this->getTypes() as $name => $type) {
242: $types[$name] = $cb($name, $type);
243: }
244:
245: return new self($types);
246: }
247:
248: /**
249: * Replaces unresolved TemplateType values with their declared bounds (or defaults).
250: */
251: public function resolveToBounds(): self
252: {
253: if ($this->resolvedToBounds !== null) {
254: return $this->resolvedToBounds;
255: }
256: return $this->resolvedToBounds = $this->map(static fn (string $name, Type $type): Type => TemplateTypeHelper::resolveToDefaults($type));
257: }
258:
259: }
260: