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