1: <?php declare(strict_types = 1);
2:
3: namespace PHPStan\Type\Constant;
4:
5: use PHPStan\ShouldNotHappenException;
6: use PHPStan\Type\Accessory\AccessoryArrayListType;
7: use PHPStan\Type\Accessory\NonEmptyArrayType;
8: use PHPStan\Type\ArrayType;
9: use PHPStan\Type\Type;
10: use PHPStan\Type\TypeCombinator;
11: use PHPStan\Type\TypeUtils;
12: use function array_filter;
13: use function array_map;
14: use function array_unique;
15: use function array_values;
16: use function count;
17: use function in_array;
18: use function is_float;
19: use function max;
20: use function range;
21:
22: /** @api */
23: class ConstantArrayTypeBuilder
24: {
25:
26: public const ARRAY_COUNT_LIMIT = 256;
27:
28: private bool $degradeToGeneralArray = false;
29:
30: /**
31: * @param array<int, Type> $keyTypes
32: * @param array<int, Type> $valueTypes
33: * @param non-empty-list<int> $nextAutoIndexes
34: * @param array<int> $optionalKeys
35: */
36: private function __construct(
37: private array $keyTypes,
38: private array $valueTypes,
39: private array $nextAutoIndexes,
40: private array $optionalKeys,
41: private bool $isList,
42: )
43: {
44: }
45:
46: public static function createEmpty(): self
47: {
48: return new self([], [], [0], [], true);
49: }
50:
51: public static function createFromConstantArray(ConstantArrayType $startArrayType): self
52: {
53: $builder = new self(
54: $startArrayType->getKeyTypes(),
55: $startArrayType->getValueTypes(),
56: $startArrayType->getNextAutoIndexes(),
57: $startArrayType->getOptionalKeys(),
58: $startArrayType->isList()->yes(),
59: );
60:
61: if (count($startArrayType->getKeyTypes()) > self::ARRAY_COUNT_LIMIT) {
62: $builder->degradeToGeneralArray();
63: }
64:
65: return $builder;
66: }
67:
68: public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $optional = false): void
69: {
70: if ($offsetType !== null) {
71: $offsetType = $offsetType->toArrayKey();
72: }
73:
74: if (!$this->degradeToGeneralArray) {
75: if ($offsetType === null) {
76: $newAutoIndexes = $optional ? $this->nextAutoIndexes : [];
77: $hasOptional = false;
78: foreach ($this->keyTypes as $i => $keyType) {
79: if (!$keyType instanceof ConstantIntegerType) {
80: continue;
81: }
82:
83: if (!in_array($keyType->getValue(), $this->nextAutoIndexes, true)) {
84: continue;
85: }
86:
87: $this->valueTypes[$i] = TypeCombinator::union($this->valueTypes[$i], $valueType);
88:
89: if (!$hasOptional && !$optional) {
90: $this->optionalKeys = array_values(array_filter($this->optionalKeys, static fn (int $index): bool => $index !== $i));
91: }
92:
93: /** @var int|float $newAutoIndex */
94: $newAutoIndex = $keyType->getValue() + 1;
95: if (is_float($newAutoIndex)) {
96: $newAutoIndex = $keyType->getValue();
97: }
98:
99: $newAutoIndexes[] = $newAutoIndex;
100: $hasOptional = true;
101: }
102:
103: $max = max($this->nextAutoIndexes);
104:
105: $this->keyTypes[] = new ConstantIntegerType($max);
106: $this->valueTypes[] = $valueType;
107:
108: /** @var int|float $newAutoIndex */
109: $newAutoIndex = $max + 1;
110: if (is_float($newAutoIndex)) {
111: $newAutoIndex = $max;
112: }
113:
114: $newAutoIndexes[] = $newAutoIndex;
115: $this->nextAutoIndexes = array_values(array_unique($newAutoIndexes));
116:
117: if ($optional || $hasOptional) {
118: $this->optionalKeys[] = count($this->keyTypes) - 1;
119: }
120:
121: if (count($this->keyTypes) > self::ARRAY_COUNT_LIMIT) {
122: $this->degradeToGeneralArray = true;
123: }
124:
125: return;
126: }
127:
128: if ($offsetType instanceof ConstantIntegerType || $offsetType instanceof ConstantStringType) {
129: /** @var ConstantIntegerType|ConstantStringType $keyType */
130: foreach ($this->keyTypes as $i => $keyType) {
131: if ($keyType->getValue() !== $offsetType->getValue()) {
132: continue;
133: }
134:
135: if ($optional) {
136: $valueType = TypeCombinator::union($valueType, $this->valueTypes[$i]);
137: }
138:
139: $this->valueTypes[$i] = $valueType;
140:
141: if (!$optional) {
142: $this->optionalKeys = array_values(array_filter($this->optionalKeys, static fn (int $index): bool => $index !== $i));
143: if ($keyType instanceof ConstantIntegerType) {
144: $nextAutoIndexes = array_values(array_filter($this->nextAutoIndexes, static fn (int $index) => $index > $keyType->getValue()));
145: if (count($nextAutoIndexes) === 0) {
146: throw new ShouldNotHappenException();
147: }
148: $this->nextAutoIndexes = $nextAutoIndexes;
149: }
150: }
151: return;
152: }
153:
154: $this->keyTypes[] = $offsetType;
155: $this->valueTypes[] = $valueType;
156:
157: if ($offsetType instanceof ConstantIntegerType) {
158: $max = max($this->nextAutoIndexes);
159: if ($offsetType->getValue() !== $max) {
160: $this->isList = false;
161: }
162: if ($offsetType->getValue() >= $max) {
163: /** @var int|float $newAutoIndex */
164: $newAutoIndex = $offsetType->getValue() + 1;
165: if (is_float($newAutoIndex)) {
166: $newAutoIndex = $max;
167: }
168: if (!$optional) {
169: $this->nextAutoIndexes = [$newAutoIndex];
170: } else {
171: $this->nextAutoIndexes[] = $newAutoIndex;
172: }
173: }
174: } else {
175: $this->isList = false;
176: }
177:
178: if ($optional) {
179: $this->optionalKeys[] = count($this->keyTypes) - 1;
180: }
181:
182: if (count($this->keyTypes) > self::ARRAY_COUNT_LIMIT) {
183: $this->degradeToGeneralArray = true;
184: }
185:
186: return;
187: }
188:
189: $this->isList = false;
190:
191: $scalarTypes = TypeUtils::getConstantScalars($offsetType);
192: if (count($scalarTypes) === 0) {
193: $integerRanges = TypeUtils::getIntegerRanges($offsetType);
194: if (count($integerRanges) > 0) {
195: foreach ($integerRanges as $integerRange) {
196: if ($integerRange->getMin() === null) {
197: break;
198: }
199: if ($integerRange->getMax() === null) {
200: break;
201: }
202:
203: $rangeLength = $integerRange->getMax() - $integerRange->getMin();
204: if ($rangeLength >= self::ARRAY_COUNT_LIMIT) {
205: $scalarTypes = [];
206: break;
207: }
208:
209: foreach (range($integerRange->getMin(), $integerRange->getMax()) as $rangeValue) {
210: $scalarTypes[] = new ConstantIntegerType($rangeValue);
211: }
212: }
213: }
214: }
215: if (count($scalarTypes) > 0 && count($scalarTypes) < self::ARRAY_COUNT_LIMIT) {
216: $match = true;
217: $valueTypes = $this->valueTypes;
218: foreach ($scalarTypes as $scalarType) {
219: $scalarOffsetType = $scalarType->toArrayKey();
220: if (!$scalarOffsetType instanceof ConstantIntegerType && !$scalarOffsetType instanceof ConstantStringType) {
221: throw new ShouldNotHappenException();
222: }
223: $offsetMatch = false;
224:
225: /** @var ConstantIntegerType|ConstantStringType $keyType */
226: foreach ($this->keyTypes as $i => $keyType) {
227: if ($keyType->getValue() !== $scalarOffsetType->getValue()) {
228: continue;
229: }
230:
231: $valueTypes[$i] = TypeCombinator::union($valueTypes[$i], $valueType);
232: $offsetMatch = true;
233: }
234:
235: if ($offsetMatch) {
236: continue;
237: }
238:
239: $match = false;
240: }
241:
242: if ($match) {
243: $this->valueTypes = $valueTypes;
244: return;
245: }
246: }
247: }
248:
249: if ($offsetType === null) {
250: $offsetType = TypeCombinator::union(...array_map(static fn (int $index) => new ConstantIntegerType($index), $this->nextAutoIndexes));
251: } else {
252: $this->isList = false;
253: }
254:
255: $this->keyTypes[] = $offsetType;
256: $this->valueTypes[] = $valueType;
257: if ($optional) {
258: $this->optionalKeys[] = count($this->keyTypes) - 1;
259: }
260: $this->degradeToGeneralArray = true;
261: }
262:
263: public function degradeToGeneralArray(): void
264: {
265: $this->degradeToGeneralArray = true;
266: }
267:
268: public function getArray(): Type
269: {
270: $keyTypesCount = count($this->keyTypes);
271: if ($keyTypesCount === 0) {
272: return new ConstantArrayType([], []);
273: }
274:
275: if (!$this->degradeToGeneralArray) {
276: /** @var array<int, ConstantIntegerType|ConstantStringType> $keyTypes */
277: $keyTypes = $this->keyTypes;
278: return new ConstantArrayType($keyTypes, $this->valueTypes, $this->nextAutoIndexes, $this->optionalKeys, $this->isList);
279: }
280:
281: $array = new ArrayType(
282: TypeCombinator::union(...$this->keyTypes),
283: TypeCombinator::union(...$this->valueTypes),
284: );
285:
286: if (count($this->optionalKeys) < $keyTypesCount) {
287: $array = TypeCombinator::intersect($array, new NonEmptyArrayType());
288: }
289: if ($this->isList) {
290: $array = AccessoryArrayListType::intersectWith($array);
291: }
292:
293: return $array;
294: }
295:
296: public function isList(): bool
297: {
298: return $this->isList;
299: }
300:
301: }
302: