1: <?php declare(strict_types = 1);
2:
3: namespace PHPStan\Type\Generic;
4:
5: use PHPStan\Type\NeverType;
6: use PHPStan\Type\Type;
7: use PHPStan\Type\TypeCombinator;
8: use PHPStan\Type\TypeTraverser;
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: final class TemplateTypeMap
37: {
38:
39: private static ?TemplateTypeMap $empty = null;
40:
41: private ?TemplateTypeMap $resolvedToBounds = null;
42:
43: /**
44: * @api
45: * @param array<string, Type> $types Concrete types for each template parameter (upper bounds)
46: * @param array<string, Type> $lowerBoundTypes Types inferred from contravariant positions
47: */
48: public function __construct(private array $types, private array $lowerBoundTypes = [])
49: {
50: }
51:
52: public function convertToLowerBoundTypes(): self
53: {
54: $lowerBoundTypes = $this->types;
55: foreach ($this->lowerBoundTypes as $name => $type) {
56: if (isset($lowerBoundTypes[$name])) {
57: $intersection = TypeCombinator::intersect($lowerBoundTypes[$name], $type);
58: if ($intersection instanceof NeverType) {
59: continue;
60: }
61: $lowerBoundTypes[$name] = $intersection;
62: } else {
63: $lowerBoundTypes[$name] = $type;
64: }
65: }
66:
67: return new self([], $lowerBoundTypes);
68: }
69:
70: public static function createEmpty(): self
71: {
72: $empty = self::$empty;
73:
74: if ($empty !== null) {
75: return $empty;
76: }
77:
78: $empty = new self([], []);
79: self::$empty = $empty;
80:
81: return $empty;
82: }
83:
84: public function isEmpty(): bool
85: {
86: return $this->count() === 0;
87: }
88:
89: public function count(): int
90: {
91: return count($this->types + $this->lowerBoundTypes);
92: }
93:
94: /** @return array<string, Type> */
95: public function getTypes(): array
96: {
97: $types = $this->types;
98: foreach ($this->lowerBoundTypes as $name => $type) {
99: if (array_key_exists($name, $types)) {
100: continue;
101: }
102:
103: $types[$name] = $type;
104: }
105:
106: return $types;
107: }
108:
109: public function hasType(string $name): bool
110: {
111: return array_key_exists($name, $this->getTypes());
112: }
113:
114: public function getType(string $name): ?Type
115: {
116: return $this->getTypes()[$name] ?? null;
117: }
118:
119: public function unsetType(string $name): self
120: {
121: if (!$this->hasType($name)) {
122: return $this;
123: }
124:
125: $types = $this->types;
126: $lowerBoundTypes = $this->lowerBoundTypes;
127:
128: unset($types[$name]);
129: unset($lowerBoundTypes[$name]);
130:
131: if (count($types) === 0 && count($lowerBoundTypes) === 0) {
132: return self::createEmpty();
133: }
134:
135: return new self($types, $lowerBoundTypes);
136: }
137:
138: public function union(self $other): self
139: {
140: $result = $this->types;
141:
142: foreach ($other->types as $name => $type) {
143: if (isset($result[$name])) {
144: $result[$name] = TypeCombinator::union($result[$name], $type);
145: } else {
146: $result[$name] = $type;
147: }
148: }
149:
150: $resultLowerBoundTypes = $this->lowerBoundTypes;
151: foreach ($other->lowerBoundTypes as $name => $type) {
152: if (isset($resultLowerBoundTypes[$name])) {
153: $intersection = TypeCombinator::intersect($resultLowerBoundTypes[$name], $type);
154: if ($intersection instanceof NeverType) {
155: continue;
156: }
157: $resultLowerBoundTypes[$name] = $intersection;
158: } else {
159: $resultLowerBoundTypes[$name] = $type;
160: }
161: }
162:
163: return new self($result, $resultLowerBoundTypes);
164: }
165:
166: public function benevolentUnion(self $other): self
167: {
168: $result = $this->types;
169:
170: foreach ($other->types as $name => $type) {
171: if (isset($result[$name])) {
172: $result[$name] = TypeUtils::toBenevolentUnion(TypeCombinator::union($result[$name], $type));
173: } else {
174: $result[$name] = $type;
175: }
176: }
177:
178: $resultLowerBoundTypes = $this->lowerBoundTypes;
179: foreach ($other->lowerBoundTypes as $name => $type) {
180: if (isset($resultLowerBoundTypes[$name])) {
181: $intersection = TypeCombinator::intersect($resultLowerBoundTypes[$name], $type);
182: if ($intersection instanceof NeverType) {
183: continue;
184: }
185: $resultLowerBoundTypes[$name] = $intersection;
186: } else {
187: $resultLowerBoundTypes[$name] = $type;
188: }
189: }
190:
191: return new self($result, $resultLowerBoundTypes);
192: }
193:
194: public function intersect(self $other): self
195: {
196: $result = $this->types;
197:
198: foreach ($other->types as $name => $type) {
199: if (isset($result[$name])) {
200: $result[$name] = TypeCombinator::intersect($result[$name], $type);
201: } else {
202: $result[$name] = $type;
203: }
204: }
205:
206: $resultLowerBoundTypes = $this->lowerBoundTypes;
207: foreach ($other->lowerBoundTypes as $name => $type) {
208: if (isset($resultLowerBoundTypes[$name])) {
209: $resultLowerBoundTypes[$name] = TypeCombinator::union($resultLowerBoundTypes[$name], $type);
210: } else {
211: $resultLowerBoundTypes[$name] = $type;
212: }
213: }
214:
215: return new self($result, $resultLowerBoundTypes);
216: }
217:
218: /** @param callable(string,Type):Type $cb */
219: public function map(callable $cb): self
220: {
221: $types = [];
222: foreach ($this->getTypes() as $name => $type) {
223: $types[$name] = $cb($name, $type);
224: }
225:
226: return new self($types);
227: }
228:
229: /**
230: * Replaces unresolved TemplateType values with their declared bounds (or defaults).
231: */
232: public function resolveToBounds(): self
233: {
234: if ($this->resolvedToBounds !== null) {
235: return $this->resolvedToBounds;
236: }
237: return $this->resolvedToBounds = $this->map(static fn (string $name, Type $type): Type => TypeTraverser::map(
238: $type,
239: static fn (Type $type, callable $traverse): Type => $type instanceof TemplateType ? $traverse($type->getDefault() ?? $type->getBound()) : $traverse($type),
240: ));
241: }
242:
243: }
244: