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: * @api
15: */
16: final class TemplateTypeMap
17: {
18:
19: private static ?TemplateTypeMap $empty = null;
20:
21: private ?TemplateTypeMap $resolvedToBounds = null;
22:
23: /**
24: * @api
25: * @param array<string, Type> $types
26: * @param array<string, Type> $lowerBoundTypes
27: */
28: public function __construct(private array $types, private array $lowerBoundTypes = [])
29: {
30: }
31:
32: public function convertToLowerBoundTypes(): self
33: {
34: $lowerBoundTypes = $this->types;
35: foreach ($this->lowerBoundTypes as $name => $type) {
36: if (isset($lowerBoundTypes[$name])) {
37: $intersection = TypeCombinator::intersect($lowerBoundTypes[$name], $type);
38: if ($intersection instanceof NeverType) {
39: continue;
40: }
41: $lowerBoundTypes[$name] = $intersection;
42: } else {
43: $lowerBoundTypes[$name] = $type;
44: }
45: }
46:
47: return new self([], $lowerBoundTypes);
48: }
49:
50: public static function createEmpty(): self
51: {
52: $empty = self::$empty;
53:
54: if ($empty !== null) {
55: return $empty;
56: }
57:
58: $empty = new self([], []);
59: self::$empty = $empty;
60:
61: return $empty;
62: }
63:
64: public function isEmpty(): bool
65: {
66: return $this->count() === 0;
67: }
68:
69: public function count(): int
70: {
71: return count($this->types + $this->lowerBoundTypes);
72: }
73:
74: /** @return array<string, Type> */
75: public function getTypes(): array
76: {
77: $types = $this->types;
78: foreach ($this->lowerBoundTypes as $name => $type) {
79: if (array_key_exists($name, $types)) {
80: continue;
81: }
82:
83: $types[$name] = $type;
84: }
85:
86: return $types;
87: }
88:
89: public function hasType(string $name): bool
90: {
91: return array_key_exists($name, $this->getTypes());
92: }
93:
94: public function getType(string $name): ?Type
95: {
96: return $this->getTypes()[$name] ?? null;
97: }
98:
99: public function unsetType(string $name): self
100: {
101: if (!$this->hasType($name)) {
102: return $this;
103: }
104:
105: $types = $this->types;
106: $lowerBoundTypes = $this->lowerBoundTypes;
107:
108: unset($types[$name]);
109: unset($lowerBoundTypes[$name]);
110:
111: if (count($types) === 0 && count($lowerBoundTypes) === 0) {
112: return self::createEmpty();
113: }
114:
115: return new self($types, $lowerBoundTypes);
116: }
117:
118: public function union(self $other): self
119: {
120: $result = $this->types;
121:
122: foreach ($other->types as $name => $type) {
123: if (isset($result[$name])) {
124: $result[$name] = TypeCombinator::union($result[$name], $type);
125: } else {
126: $result[$name] = $type;
127: }
128: }
129:
130: $resultLowerBoundTypes = $this->lowerBoundTypes;
131: foreach ($other->lowerBoundTypes as $name => $type) {
132: if (isset($resultLowerBoundTypes[$name])) {
133: $intersection = TypeCombinator::intersect($resultLowerBoundTypes[$name], $type);
134: if ($intersection instanceof NeverType) {
135: continue;
136: }
137: $resultLowerBoundTypes[$name] = $intersection;
138: } else {
139: $resultLowerBoundTypes[$name] = $type;
140: }
141: }
142:
143: return new self($result, $resultLowerBoundTypes);
144: }
145:
146: public function benevolentUnion(self $other): self
147: {
148: $result = $this->types;
149:
150: foreach ($other->types as $name => $type) {
151: if (isset($result[$name])) {
152: $result[$name] = TypeUtils::toBenevolentUnion(TypeCombinator::union($result[$name], $type));
153: } else {
154: $result[$name] = $type;
155: }
156: }
157:
158: $resultLowerBoundTypes = $this->lowerBoundTypes;
159: foreach ($other->lowerBoundTypes as $name => $type) {
160: if (isset($resultLowerBoundTypes[$name])) {
161: $intersection = TypeCombinator::intersect($resultLowerBoundTypes[$name], $type);
162: if ($intersection instanceof NeverType) {
163: continue;
164: }
165: $resultLowerBoundTypes[$name] = $intersection;
166: } else {
167: $resultLowerBoundTypes[$name] = $type;
168: }
169: }
170:
171: return new self($result, $resultLowerBoundTypes);
172: }
173:
174: public function intersect(self $other): self
175: {
176: $result = $this->types;
177:
178: foreach ($other->types as $name => $type) {
179: if (isset($result[$name])) {
180: $result[$name] = TypeCombinator::intersect($result[$name], $type);
181: } else {
182: $result[$name] = $type;
183: }
184: }
185:
186: $resultLowerBoundTypes = $this->lowerBoundTypes;
187: foreach ($other->lowerBoundTypes as $name => $type) {
188: if (isset($resultLowerBoundTypes[$name])) {
189: $resultLowerBoundTypes[$name] = TypeCombinator::union($resultLowerBoundTypes[$name], $type);
190: } else {
191: $resultLowerBoundTypes[$name] = $type;
192: }
193: }
194:
195: return new self($result, $resultLowerBoundTypes);
196: }
197:
198: /** @param callable(string,Type):Type $cb */
199: public function map(callable $cb): self
200: {
201: $types = [];
202: foreach ($this->getTypes() as $name => $type) {
203: $types[$name] = $cb($name, $type);
204: }
205:
206: return new self($types);
207: }
208:
209: public function resolveToBounds(): self
210: {
211: if ($this->resolvedToBounds !== null) {
212: return $this->resolvedToBounds;
213: }
214: return $this->resolvedToBounds = $this->map(static fn (string $name, Type $type): Type => TypeTraverser::map(
215: $type,
216: static fn (Type $type, callable $traverse): Type => $type instanceof TemplateType ? $traverse($type->getDefault() ?? $type->getBound()) : $traverse($type),
217: ));
218: }
219:
220: }
221: