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->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: $valueTypes = $this->valueTypes;
262: foreach ($scalarTypes as $scalarType) {
263: $scalarOffsetType = $scalarType->toArrayKey();
264: if (!$scalarOffsetType instanceof ConstantIntegerType && !$scalarOffsetType instanceof ConstantStringType) {
265: throw new ShouldNotHappenException();
266: }
267: $offsetMatch = false;
268:
269: /** @var ConstantIntegerType|ConstantStringType $keyType */
270: foreach ($this->keyTypes as $i => $keyType) {
271: if ($keyType->getValue() !== $scalarOffsetType->getValue()) {
272: continue;
273: }
274:
275: $valueTypes[$i] = TypeCombinator::union($valueTypes[$i], $valueType);
276: $offsetMatch = true;
277: }
278:
279: if ($offsetMatch) {
280: continue;
281: }
282:
283: $match = false;
284: }
285:
286: if ($match) {
287: $this->valueTypes = $valueTypes;
288: return;
289: }
290: }
291:
292: $this->isList = TrinaryLogic::createNo();
293: }
294:
295: if ($offsetType === null) {
296: $offsetType = TypeCombinator::union(...array_map(static fn (int $index) => new ConstantIntegerType($index), $this->nextAutoIndexes));
297: } else {
298: $this->isList = TrinaryLogic::createNo();
299: }
300:
301: $this->keyTypes[] = $offsetType;
302: $this->valueTypes[] = $valueType;
303: if ($optional) {
304: $this->optionalKeys[] = count($this->keyTypes) - 1;
305: }
306: $this->degradeToGeneralArray = true;
307: }
308:
309: public function degradeToGeneralArray(bool $oversized = false): void
310: {
311: if ($this->disableArrayDegradation) {
312: throw new ShouldNotHappenException();
313: }
314:
315: $this->degradeToGeneralArray = true;
316: $this->oversized = $this->oversized || $oversized;
317: }
318:
319: public function disableClosureDegradation(): void
320: {
321: $this->degradeClosures = false;
322: }
323:
324: public function disableArrayDegradation(): void
325: {
326: $this->degradeToGeneralArray = false;
327: $this->oversized = false;
328: $this->disableArrayDegradation = true;
329: }
330:
331: public function getArray(): Type
332: {
333: $keyTypesCount = count($this->keyTypes);
334: if ($keyTypesCount === 0) {
335: return new ConstantArrayType([], []);
336: }
337:
338: if (!$this->degradeToGeneralArray) {
339: /** @var list<ConstantIntegerType|ConstantStringType> $keyTypes */
340: $keyTypes = $this->keyTypes;
341: return new ConstantArrayType($keyTypes, $this->valueTypes, $this->nextAutoIndexes, $this->optionalKeys, $this->isList);
342: }
343:
344: if ($this->degradeClosures === true) {
345: $itemTypes = [];
346: $itemTypes[] = new CallableType();
347: foreach ($this->valueTypes as $valueType) {
348: if ($valueType instanceof ClosureType) {
349: continue;
350: }
351: $itemTypes[] = $valueType;
352: }
353: } else {
354: $itemTypes = $this->valueTypes;
355: }
356:
357: $array = new ArrayType(
358: TypeCombinator::union(...$this->keyTypes),
359: TypeCombinator::union(...$itemTypes),
360: );
361:
362: $types = [];
363: if (count($this->optionalKeys) < $keyTypesCount) {
364: $types[] = new NonEmptyArrayType();
365: }
366:
367: if ($this->oversized) {
368: $types[] = new OversizedArrayType();
369: }
370:
371: if ($this->isList->yes()) {
372: $types[] = new AccessoryArrayListType();
373: }
374:
375: if (count($types) === 0) {
376: return $array;
377: }
378:
379: return new IntersectionType([$array, ...$types]);
380: }
381:
382: public function isList(): bool
383: {
384: return $this->isList->yes();
385: }
386:
387: }
388: