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