1: <?php declare(strict_types = 1);
2:
3: namespace PHPStan\Type\Constant;
4:
5: use PHPStan\DependencyInjection\BleedingEdgeToggle;
6: use PHPStan\ShouldNotHappenException;
7: use PHPStan\TrinaryLogic;
8: use PHPStan\Type\Accessory\AccessoryArrayListType;
9: use PHPStan\Type\Accessory\NonEmptyArrayType;
10: use PHPStan\Type\Accessory\OversizedArrayType;
11: use PHPStan\Type\ArrayType;
12: use PHPStan\Type\CallableType;
13: use PHPStan\Type\ClosureType;
14: use PHPStan\Type\IntersectionType;
15: use PHPStan\Type\NeverType;
16: use PHPStan\Type\Type;
17: use PHPStan\Type\TypeCombinator;
18: use PHPStan\Type\TypeUtils;
19: use function array_filter;
20: use function array_map;
21: use function array_unique;
22: use function array_values;
23: use function count;
24: use function in_array;
25: use function is_float;
26: use function max;
27: use function min;
28:
29: /**
30: * @api
31: */
32: final class ConstantArrayTypeBuilder
33: {
34:
35: public const ARRAY_COUNT_LIMIT = 256;
36: private const CLOSURES_COUNT_LIMIT = 32;
37:
38: private bool $degradeToGeneralArray = false;
39:
40: private bool $disableArrayDegradation = false;
41:
42: private ?bool $degradeClosures = null;
43:
44: private bool $oversized = false;
45:
46: private TrinaryLogic $isNonEmpty;
47:
48: /**
49: * @param list<Type> $keyTypes
50: * @param array<int, Type> $valueTypes
51: * @param list<int> $nextAutoIndexes
52: * @param array<int> $optionalKeys
53: * @param array{Type, Type}|null $unsealed
54: */
55: private function __construct(
56: private array $keyTypes,
57: private array $valueTypes,
58: private array $nextAutoIndexes,
59: private array $optionalKeys,
60: private TrinaryLogic $isList,
61: private ?array $unsealed,
62: )
63: {
64: $this->isNonEmpty = TrinaryLogic::createNo();
65: }
66:
67: public static function createEmpty(): self
68: {
69: $unsealed = null;
70: if (BleedingEdgeToggle::isBleedingEdge()) {
71: $never = new NeverType(true);
72: $unsealed = [$never, $never];
73: }
74: return new self([], [], [0], [], TrinaryLogic::createYes(), $unsealed);
75: }
76:
77: public static function createFromConstantArray(ConstantArrayType $startArrayType): self
78: {
79: $builder = new self(
80: $startArrayType->getKeyTypes(),
81: $startArrayType->getValueTypes(),
82: $startArrayType->getNextAutoIndexes(),
83: $startArrayType->getOptionalKeys(),
84: $startArrayType->isList(),
85: $startArrayType->getUnsealedTypes(),
86: );
87: $builder->isNonEmpty = $startArrayType->isIterableAtLeastOnce();
88:
89: if (count($startArrayType->getKeyTypes()) > self::ARRAY_COUNT_LIMIT) {
90: $builder->degradeToGeneralArray(true);
91: }
92:
93: return $builder;
94: }
95:
96: public function makeUnsealed(Type $keyType, Type $valueType): void
97: {
98: $this->unsealed = [$keyType, $valueType];
99: }
100:
101: public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $optional = false): void
102: {
103: if ($offsetType !== null) {
104: $offsetType = $offsetType->toArrayKey();
105: }
106:
107: if ($offsetType === null && count($this->nextAutoIndexes) === 0) {
108: return;
109: }
110:
111: if (!$optional) {
112: $this->isNonEmpty = TrinaryLogic::createYes();
113: }
114:
115: if (!$this->degradeToGeneralArray) {
116: if (
117: $valueType instanceof ClosureType
118: && $this->degradeClosures !== false
119: && !$this->disableArrayDegradation
120: ) {
121: $numClosures = 1;
122: foreach ($this->valueTypes as $innerType) {
123: if (!($innerType instanceof ClosureType)) {
124: continue;
125: }
126:
127: $numClosures++;
128: }
129:
130: if ($numClosures >= self::CLOSURES_COUNT_LIMIT) {
131: $this->degradeClosures = true;
132: $this->degradeToGeneralArray = true;
133: $this->oversized = true;
134: }
135: }
136:
137: if ($offsetType === null) {
138: $newAutoIndexes = $optional ? $this->nextAutoIndexes : [];
139: $hasOptional = false;
140: foreach ($this->keyTypes as $i => $keyType) {
141: if (!$keyType instanceof ConstantIntegerType) {
142: continue;
143: }
144:
145: if (!in_array($keyType->getValue(), $this->nextAutoIndexes, true)) {
146: continue;
147: }
148:
149: $this->valueTypes[$i] = TypeCombinator::union($this->valueTypes[$i], $valueType);
150:
151: if (!$hasOptional && !$optional) {
152: $this->optionalKeys = array_values(array_filter($this->optionalKeys, static fn (int $index): bool => $index !== $i));
153: }
154:
155: /** @var int|float $newAutoIndex */
156: $newAutoIndex = $keyType->getValue() + 1;
157: if (!is_float($newAutoIndex)) {
158: $newAutoIndexes[] = $newAutoIndex;
159: }
160:
161: $hasOptional = true;
162: }
163:
164: $max = max($this->nextAutoIndexes);
165:
166: $this->keyTypes[] = new ConstantIntegerType($max);
167: $this->valueTypes[] = $valueType;
168:
169: /** @var int|float $newAutoIndex */
170: $newAutoIndex = $max + 1;
171: if (!is_float($newAutoIndex)) {
172: $newAutoIndexes[] = $newAutoIndex;
173: }
174:
175: $this->nextAutoIndexes = array_values(array_unique($newAutoIndexes));
176:
177: if ($optional || $hasOptional) {
178: $this->optionalKeys[] = count($this->keyTypes) - 1;
179: }
180:
181: if (
182: !$this->disableArrayDegradation
183: && count($this->keyTypes) > self::ARRAY_COUNT_LIMIT
184: ) {
185: $this->degradeToGeneralArray = true;
186: $this->oversized = true;
187: }
188:
189: return;
190: }
191:
192: if ($offsetType instanceof ConstantIntegerType || $offsetType instanceof ConstantStringType) {
193: /** @var ConstantIntegerType|ConstantStringType $keyType */
194: foreach ($this->keyTypes as $i => $keyType) {
195: if ($keyType->getValue() !== $offsetType->getValue()) {
196: continue;
197: }
198:
199: if ($optional) {
200: $valueType = TypeCombinator::union($valueType, $this->valueTypes[$i]);
201: }
202:
203: $this->valueTypes[$i] = $valueType;
204:
205: if (!$optional) {
206: $this->optionalKeys = array_values(array_filter($this->optionalKeys, static fn (int $index): bool => $index !== $i));
207: if ($keyType instanceof ConstantIntegerType) {
208: $this->nextAutoIndexes = array_values(array_filter($this->nextAutoIndexes, static fn (int $index) => $index > $keyType->getValue()));
209: }
210: }
211: return;
212: }
213:
214: $this->keyTypes[] = $offsetType;
215: $this->valueTypes[] = $valueType;
216:
217: if ($offsetType instanceof ConstantIntegerType) {
218: if (count($this->nextAutoIndexes) > 0) {
219: $min = min($this->nextAutoIndexes);
220: $max = max($this->nextAutoIndexes);
221: $offsetValue = $offsetType->getValue();
222: if ($offsetValue >= 0) {
223: if ($offsetValue > $min) {
224: if ($offsetValue <= $max) {
225: $this->isList = $this->isList->and(TrinaryLogic::createMaybe());
226: } else {
227: $this->isList = TrinaryLogic::createNo();
228: }
229: }
230: } else {
231: $this->isList = TrinaryLogic::createNo();
232: }
233:
234: if ($offsetValue >= $max) {
235: /** @var int|float $newAutoIndex */
236: $newAutoIndex = $offsetValue + 1;
237: if (is_float($newAutoIndex)) {
238: if (!$optional) {
239: $this->nextAutoIndexes = [];
240: }
241: } elseif (!$optional) {
242: $this->nextAutoIndexes = [$newAutoIndex];
243: } else {
244: $this->nextAutoIndexes[] = $newAutoIndex;
245: }
246: }
247: } else {
248: $this->isList = TrinaryLogic::createNo();
249: }
250: } else {
251: $this->isList = TrinaryLogic::createNo();
252: }
253:
254: if ($optional) {
255: $this->optionalKeys[] = count($this->keyTypes) - 1;
256: }
257:
258: if (
259: !$this->disableArrayDegradation
260: && count($this->keyTypes) > self::ARRAY_COUNT_LIMIT
261: ) {
262: $this->degradeToGeneralArray = true;
263: $this->oversized = true;
264: }
265:
266: return;
267: }
268:
269: $scalarTypes = $offsetType->toArrayKey()->getConstantScalarTypes();
270: if (count($scalarTypes) === 0) {
271: $integerRanges = TypeUtils::getIntegerRanges($offsetType);
272: if (count($integerRanges) > 0) {
273: foreach ($integerRanges as $integerRange) {
274: $finiteTypes = $integerRange->getFiniteTypes();
275: if (count($finiteTypes) === 0) {
276: break;
277: }
278:
279: foreach ($finiteTypes as $finiteType) {
280: $scalarTypes[] = $finiteType;
281: }
282: }
283: }
284: }
285: if (count($scalarTypes) > 0 && count($scalarTypes) < self::ARRAY_COUNT_LIMIT) {
286: $valueTypes = $this->valueTypes;
287: $unmatchedScalars = [];
288: foreach ($scalarTypes as $scalarType) {
289: $offsetMatch = false;
290:
291: /** @var ConstantIntegerType|ConstantStringType $keyType */
292: foreach ($this->keyTypes as $i => $keyType) {
293: if ($keyType->getValue() !== $scalarType->getValue()) {
294: continue;
295: }
296:
297: if (!$optional && in_array($i, $this->optionalKeys, true)) {
298: $valueTypes[$i] = $valueType;
299: } else {
300: $valueTypes[$i] = TypeCombinator::union($valueTypes[$i], $valueType);
301: }
302: $offsetMatch = true;
303: }
304:
305: if ($offsetMatch) {
306: continue;
307: }
308:
309: $unmatchedScalars[] = $scalarType;
310: }
311:
312: $this->valueTypes = $valueTypes;
313:
314: if (count($unmatchedScalars) === 0) {
315: return;
316: }
317:
318: foreach ($unmatchedScalars as $scalarType) {
319: $this->keyTypes[] = $scalarType;
320: $this->valueTypes[] = $valueType;
321: $this->optionalKeys[] = count($this->keyTypes) - 1;
322:
323: if (!($scalarType instanceof ConstantIntegerType)) {
324: continue;
325: }
326:
327: if (count($this->nextAutoIndexes) === 0) {
328: continue;
329: }
330:
331: $max = max($this->nextAutoIndexes);
332: $offsetValue = $scalarType->getValue();
333: if ($offsetValue < $max) {
334: continue;
335: }
336:
337: /** @var int|float $newAutoIndex */
338: $newAutoIndex = $offsetValue + 1;
339: if (is_float($newAutoIndex)) {
340: continue;
341: }
342: $this->nextAutoIndexes[] = $newAutoIndex;
343: }
344:
345: $this->isList = TrinaryLogic::createNo();
346:
347: if (
348: !$this->disableArrayDegradation
349: && count($this->keyTypes) > self::ARRAY_COUNT_LIMIT
350: ) {
351: $this->degradeToGeneralArray = true;
352: $this->oversized = true;
353: }
354:
355: return;
356: }
357:
358: $this->isList = TrinaryLogic::createNo();
359:
360: // If the builder is already unsealed (e.g. fresh bleeding-edge
361: // builder, or a PHPDoc shape like `array{a: int, ...<int, T>}`),
362: // fold the unknown offset/value into the existing unsealed
363: // extras instead of dropping per-key precision by degrading to a
364: // general array. The actual decision between unsealed
365: // ConstantArrayType and general ArrayType is then made in
366: // getArray() based on whether any constant keys ended up
367: // alongside these extras.
368: if ($this->unsealed !== null) {
369: // Existing keys whose value the new offset could overwrite
370: // must widen to a union of (existing, new) — the assignment
371: // might or might not have hit them.
372: $residualOffset = $offsetType;
373: foreach ($this->keyTypes as $i => $keyType) {
374: if ($offsetType->isSuperTypeOf($keyType)->no()) {
375: continue;
376: }
377: $this->valueTypes[$i] = TypeCombinator::union($this->valueTypes[$i], $valueType);
378: $residualOffset = TypeCombinator::remove($residualOffset, $keyType);
379: }
380:
381: if ($residualOffset instanceof NeverType) {
382: return;
383: }
384:
385: [$existingKey, $existingValue] = $this->unsealed;
386: $isExplicitNever = $existingKey instanceof NeverType && $existingKey->isExplicit();
387: if ($isExplicitNever) {
388: $this->unsealed = [$residualOffset, $valueType];
389: } else {
390: $this->unsealed = [
391: TypeCombinator::union($existingKey, $residualOffset),
392: TypeCombinator::union($existingValue, $valueType),
393: ];
394: }
395: return;
396: }
397: }
398:
399: if ($offsetType === null) {
400: $offsetType = TypeCombinator::union(...array_map(static fn (int $index) => new ConstantIntegerType($index), $this->nextAutoIndexes));
401: } else {
402: $this->isList = TrinaryLogic::createNo();
403: }
404:
405: $this->keyTypes[] = $offsetType;
406: $this->valueTypes[] = $valueType;
407: if ($optional) {
408: $this->optionalKeys[] = count($this->keyTypes) - 1;
409: }
410: $this->degradeToGeneralArray = true;
411: }
412:
413: public function degradeToGeneralArray(bool $oversized = false): void
414: {
415: if ($this->disableArrayDegradation) {
416: throw new ShouldNotHappenException();
417: }
418:
419: $this->degradeToGeneralArray = true;
420: $this->oversized = $this->oversized || $oversized;
421: }
422:
423: public function disableClosureDegradation(): void
424: {
425: $this->degradeClosures = false;
426: }
427:
428: public function disableArrayDegradation(): void
429: {
430: $this->degradeToGeneralArray = false;
431: $this->oversized = false;
432: $this->disableArrayDegradation = true;
433: }
434:
435: public function getArray(): Type
436: {
437: $keyTypesCount = count($this->keyTypes);
438: if ($keyTypesCount === 0) {
439: if ($this->unsealed !== null) {
440: [$unsealedKey, $unsealedValue] = $this->unsealed;
441: $isExplicitNever = $unsealedKey instanceof NeverType && $unsealedKey->isExplicit();
442: if (!$isExplicitNever) {
443: $arrayType = new ArrayType($unsealedKey, $unsealedValue);
444: if ($this->isNonEmpty->yes()) {
445: return TypeCombinator::intersect($arrayType, new NonEmptyArrayType());
446: }
447: return $arrayType;
448: }
449: }
450: return new ConstantArrayType([], [], unsealed: $this->unsealed);
451: }
452:
453: if (!$this->degradeToGeneralArray) {
454: /** @var list<ConstantIntegerType|ConstantStringType> $keyTypes */
455: $keyTypes = $this->keyTypes;
456: $array = new ConstantArrayType($keyTypes, $this->valueTypes, $this->nextAutoIndexes, $this->optionalKeys, $this->isList, $this->unsealed);
457: if ($this->isNonEmpty->yes() && !$array->isIterableAtLeastOnce()->yes()) {
458: return TypeCombinator::intersect($array, new NonEmptyArrayType());
459: }
460: return $array;
461: }
462:
463: if ($this->degradeClosures === true) {
464: $itemTypes = [];
465: $itemTypes[] = new CallableType();
466: foreach ($this->valueTypes as $valueType) {
467: if ($valueType instanceof ClosureType) {
468: continue;
469: }
470: $itemTypes[] = $valueType;
471: }
472: } else {
473: $itemTypes = $this->valueTypes;
474: }
475:
476: $keyTypesForArray = $this->keyTypes;
477: // Real unsealed extras describe additional key/value pairs that
478: // belong in the degraded `ArrayType`'s key/value unions too —
479: // otherwise the degraded type silently drops them.
480: if ($this->unsealed !== null) {
481: [$unsealedKey, $unsealedValue] = $this->unsealed;
482: $isExplicitNever = $unsealedKey instanceof NeverType && $unsealedKey->isExplicit();
483: if (!$isExplicitNever) {
484: $keyTypesForArray[] = $unsealedKey;
485: $itemTypes[] = $unsealedValue;
486: }
487: }
488:
489: $array = new ArrayType(
490: TypeCombinator::union(...$keyTypesForArray),
491: TypeCombinator::union(...$itemTypes),
492: );
493:
494: $types = [];
495: if ($this->isNonEmpty->yes() || count($this->optionalKeys) < $keyTypesCount) {
496: $types[] = new NonEmptyArrayType();
497: }
498:
499: if ($this->oversized) {
500: $types[] = new OversizedArrayType();
501: }
502:
503: if ($this->isList->yes()) {
504: $types[] = new AccessoryArrayListType();
505: }
506:
507: if (count($types) === 0) {
508: return $array;
509: }
510:
511: return new IntersectionType([$array, ...$types]);
512: }
513:
514: public function isList(): bool
515: {
516: return $this->isList->yes();
517: }
518:
519: }
520: