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