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