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: /**
102: * Adds unsealed extras on top of the ones the builder already collected
103: * instead of replacing them.
104: *
105: * Offsets the builder could not give a slot of their own (a non-constant
106: * key like `string`) end up in the unsealed extras, so overwriting them
107: * would drop their value type.
108: */
109: public function mergeUnsealed(Type $keyType, Type $valueType): void
110: {
111: if ($this->unsealed === null) {
112: $this->unsealed = [$keyType, $valueType];
113: return;
114: }
115:
116: [$existingKey, $existingValue] = $this->unsealed;
117: if ($existingKey instanceof NeverType && $existingKey->isExplicit()) {
118: $this->unsealed = [$keyType, $valueType];
119: return;
120: }
121:
122: $this->unsealed = [
123: TypeCombinator::union($existingKey, $keyType),
124: TypeCombinator::union($existingValue, $valueType),
125: ];
126: }
127:
128: public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $optional = false): void
129: {
130: if ($offsetType !== null) {
131: $offsetType = $offsetType->toArrayKey();
132: }
133:
134: if ($offsetType === null && count($this->nextAutoIndexes) === 0) {
135: return;
136: }
137:
138: if (!$optional) {
139: $this->isNonEmpty = TrinaryLogic::createYes();
140: }
141:
142: if (!$this->degradeToGeneralArray) {
143: if (
144: $valueType instanceof ClosureType
145: && $this->degradeClosures !== false
146: && !$this->disableArrayDegradation
147: ) {
148: $numClosures = 1;
149: foreach ($this->valueTypes as $innerType) {
150: if (!($innerType instanceof ClosureType)) {
151: continue;
152: }
153:
154: $numClosures++;
155: }
156:
157: if ($numClosures >= self::CLOSURES_COUNT_LIMIT) {
158: $this->degradeClosures = true;
159: $this->degradeToGeneralArray = true;
160: $this->oversized = true;
161: }
162: }
163:
164: if ($offsetType === null) {
165: $newAutoIndexes = $optional ? $this->nextAutoIndexes : [];
166: $hasOptional = false;
167: foreach ($this->keyTypes as $i => $keyType) {
168: if (!$keyType instanceof ConstantIntegerType) {
169: continue;
170: }
171:
172: if (!in_array($keyType->getValue(), $this->nextAutoIndexes, true)) {
173: continue;
174: }
175:
176: $this->valueTypes[$i] = TypeCombinator::union($this->valueTypes[$i], $valueType);
177:
178: if (!$hasOptional && !$optional) {
179: $this->optionalKeys = array_values(array_filter($this->optionalKeys, static fn (int $index): bool => $index !== $i));
180: }
181:
182: /** @var int|float $newAutoIndex */
183: $newAutoIndex = $keyType->getValue() + 1;
184: if (!is_float($newAutoIndex)) {
185: $newAutoIndexes[] = $newAutoIndex;
186: }
187:
188: $hasOptional = true;
189: }
190:
191: $max = max($this->nextAutoIndexes);
192:
193: $this->keyTypes[] = new ConstantIntegerType($max);
194: $this->valueTypes[] = $valueType;
195:
196: /** @var int|float $newAutoIndex */
197: $newAutoIndex = $max + 1;
198: if (!is_float($newAutoIndex)) {
199: $newAutoIndexes[] = $newAutoIndex;
200: }
201:
202: $this->nextAutoIndexes = array_values(array_unique($newAutoIndexes));
203:
204: if ($optional || $hasOptional) {
205: $this->optionalKeys[] = count($this->keyTypes) - 1;
206: }
207:
208: if (
209: !$this->disableArrayDegradation
210: && count($this->keyTypes) > self::ARRAY_COUNT_LIMIT
211: ) {
212: $this->degradeToGeneralArray = true;
213: $this->oversized = true;
214: }
215:
216: return;
217: }
218:
219: if ($offsetType instanceof ConstantIntegerType || $offsetType instanceof ConstantStringType) {
220: /** @var ConstantIntegerType|ConstantStringType $keyType */
221: foreach ($this->keyTypes as $i => $keyType) {
222: if ($keyType->getValue() !== $offsetType->getValue()) {
223: continue;
224: }
225:
226: if ($optional) {
227: $valueType = TypeCombinator::union($valueType, $this->valueTypes[$i]);
228: }
229:
230: $this->valueTypes[$i] = $valueType;
231:
232: if (!$optional) {
233: $this->optionalKeys = array_values(array_filter($this->optionalKeys, static fn (int $index): bool => $index !== $i));
234: if ($keyType instanceof ConstantIntegerType) {
235: $this->nextAutoIndexes = array_values(array_filter($this->nextAutoIndexes, static fn (int $index) => $index > $keyType->getValue()));
236: }
237: }
238: return;
239: }
240:
241: $this->keyTypes[] = $offsetType;
242: $this->valueTypes[] = $valueType;
243:
244: if ($offsetType instanceof ConstantIntegerType) {
245: if (count($this->nextAutoIndexes) > 0) {
246: $min = min($this->nextAutoIndexes);
247: $max = max($this->nextAutoIndexes);
248: $offsetValue = $offsetType->getValue();
249: if ($offsetValue >= 0) {
250: if ($offsetValue > $min) {
251: if ($offsetValue <= $max) {
252: $this->isList = $this->isList->and(TrinaryLogic::createMaybe());
253: } else {
254: $this->markNonListKey($optional);
255: }
256: }
257: } else {
258: $this->markNonListKey($optional);
259: }
260:
261: if ($offsetValue >= $max) {
262: /** @var int|float $newAutoIndex */
263: $newAutoIndex = $offsetValue + 1;
264: if (is_float($newAutoIndex)) {
265: if (!$optional) {
266: $this->nextAutoIndexes = [];
267: }
268: } elseif (!$optional) {
269: $this->nextAutoIndexes = [$newAutoIndex];
270: } else {
271: $this->nextAutoIndexes[] = $newAutoIndex;
272: }
273: }
274: } else {
275: $this->markNonListKey($optional);
276: }
277: } else {
278: $this->markNonListKey($optional);
279: }
280:
281: if ($optional) {
282: $this->optionalKeys[] = count($this->keyTypes) - 1;
283: }
284:
285: if (
286: !$this->disableArrayDegradation
287: && count($this->keyTypes) > self::ARRAY_COUNT_LIMIT
288: ) {
289: $this->degradeToGeneralArray = true;
290: $this->oversized = true;
291: }
292:
293: return;
294: }
295:
296: $scalarTypes = $offsetType->toArrayKey()->getConstantScalarTypes();
297: if (count($scalarTypes) === 0) {
298: $integerRanges = TypeUtils::getIntegerRanges($offsetType);
299: if (count($integerRanges) > 0) {
300: foreach ($integerRanges as $integerRange) {
301: $finiteTypes = $integerRange->getFiniteTypes();
302: if (count($finiteTypes) === 0) {
303: break;
304: }
305:
306: foreach ($finiteTypes as $finiteType) {
307: $scalarTypes[] = $finiteType;
308: }
309: }
310: }
311: }
312: if (count($scalarTypes) > 0 && count($scalarTypes) < self::ARRAY_COUNT_LIMIT) {
313: $valueTypes = $this->valueTypes;
314: $unmatchedScalars = [];
315: foreach ($scalarTypes as $scalarType) {
316: $offsetMatch = false;
317:
318: /** @var ConstantIntegerType|ConstantStringType $keyType */
319: foreach ($this->keyTypes as $i => $keyType) {
320: if ($keyType->getValue() !== $scalarType->getValue()) {
321: continue;
322: }
323:
324: if (!$optional && in_array($i, $this->optionalKeys, true)) {
325: $valueTypes[$i] = $valueType;
326: } else {
327: $valueTypes[$i] = TypeCombinator::union($valueTypes[$i], $valueType);
328: }
329: $offsetMatch = true;
330: }
331:
332: if ($offsetMatch) {
333: continue;
334: }
335:
336: $unmatchedScalars[] = $scalarType;
337: }
338:
339: $this->valueTypes = $valueTypes;
340:
341: if (count($unmatchedScalars) === 0) {
342: return;
343: }
344:
345: foreach ($unmatchedScalars as $scalarType) {
346: $this->keyTypes[] = $scalarType;
347: $this->valueTypes[] = $valueType;
348: $this->optionalKeys[] = count($this->keyTypes) - 1;
349:
350: if (!($scalarType instanceof ConstantIntegerType)) {
351: continue;
352: }
353:
354: if (count($this->nextAutoIndexes) === 0) {
355: continue;
356: }
357:
358: $max = max($this->nextAutoIndexes);
359: $offsetValue = $scalarType->getValue();
360: if ($offsetValue < $max) {
361: continue;
362: }
363:
364: /** @var int|float $newAutoIndex */
365: $newAutoIndex = $offsetValue + 1;
366: if (is_float($newAutoIndex)) {
367: continue;
368: }
369: $this->nextAutoIndexes[] = $newAutoIndex;
370: }
371:
372: $this->isList = TrinaryLogic::createNo();
373:
374: if (
375: !$this->disableArrayDegradation
376: && count($this->keyTypes) > self::ARRAY_COUNT_LIMIT
377: ) {
378: $this->degradeToGeneralArray = true;
379: $this->oversized = true;
380: }
381:
382: return;
383: }
384:
385: $this->isList = TrinaryLogic::createNo();
386:
387: // If the builder is already unsealed (e.g. fresh bleeding-edge
388: // builder, or a PHPDoc shape like `array{a: int, ...<int, T>}`),
389: // fold the unknown offset/value into the existing unsealed
390: // extras instead of dropping per-key precision by degrading to a
391: // general array. The actual decision between unsealed
392: // ConstantArrayType and general ArrayType is then made in
393: // getArray() based on whether any constant keys ended up
394: // alongside these extras.
395: if ($this->unsealed !== null) {
396: // Existing keys whose value the new offset could overwrite
397: // must widen to a union of (existing, new) — the assignment
398: // might or might not have hit them.
399: $residualOffset = $offsetType;
400: foreach ($this->keyTypes as $i => $keyType) {
401: if ($offsetType->isSuperTypeOf($keyType)->no()) {
402: continue;
403: }
404: $this->valueTypes[$i] = TypeCombinator::union($this->valueTypes[$i], $valueType);
405: $residualOffset = TypeCombinator::remove($residualOffset, $keyType);
406: }
407:
408: if ($residualOffset instanceof NeverType) {
409: return;
410: }
411:
412: [$existingKey, $existingValue] = $this->unsealed;
413: $isExplicitNever = $existingKey instanceof NeverType && $existingKey->isExplicit();
414: if ($isExplicitNever) {
415: $this->unsealed = [$residualOffset, $valueType];
416: } else {
417: $this->unsealed = [
418: TypeCombinator::union($existingKey, $residualOffset),
419: TypeCombinator::union($existingValue, $valueType),
420: ];
421: }
422: return;
423: }
424: }
425:
426: if ($offsetType === null) {
427: $offsetType = TypeCombinator::union(...array_map(static fn (int $index) => new ConstantIntegerType($index), $this->nextAutoIndexes));
428: } else {
429: $this->isList = TrinaryLogic::createNo();
430: }
431:
432: $this->keyTypes[] = $offsetType;
433: $this->valueTypes[] = $valueType;
434: if ($optional) {
435: $this->optionalKeys[] = count($this->keyTypes) - 1;
436: }
437: $this->degradeToGeneralArray = true;
438: }
439:
440: /**
441: * Record adding a key incompatible with list ordering. A required key breaks
442: * list-ness; an optional one only degrades Yes to Maybe (No stays No), since
443: * the array is still a list when the key is absent.
444: */
445: private function markNonListKey(bool $optional): void
446: {
447: $this->isList = $optional
448: ? $this->isList->and(TrinaryLogic::createMaybe())
449: : TrinaryLogic::createNo();
450: }
451:
452: public function degradeToGeneralArray(bool $oversized = false): void
453: {
454: if ($this->disableArrayDegradation) {
455: throw new ShouldNotHappenException();
456: }
457:
458: $this->degradeToGeneralArray = true;
459: $this->oversized = $this->oversized || $oversized;
460: }
461:
462: public function disableClosureDegradation(): void
463: {
464: $this->degradeClosures = false;
465: }
466:
467: public function disableArrayDegradation(): void
468: {
469: $this->degradeToGeneralArray = false;
470: $this->oversized = false;
471: $this->disableArrayDegradation = true;
472: }
473:
474: public function getArray(): Type
475: {
476: $keyTypesCount = count($this->keyTypes);
477: if ($keyTypesCount === 0) {
478: if ($this->unsealed !== null) {
479: [$unsealedKey, $unsealedValue] = $this->unsealed;
480: $isExplicitNever = $unsealedKey instanceof NeverType && $unsealedKey->isExplicit();
481: if (!$isExplicitNever) {
482: $arrayType = new ArrayType($unsealedKey, $unsealedValue);
483: if ($this->isNonEmpty->yes()) {
484: return TypeCombinator::intersect($arrayType, new NonEmptyArrayType());
485: }
486: return $arrayType;
487: }
488: }
489: return new ConstantArrayType([], [], unsealed: $this->unsealed);
490: }
491:
492: if (!$this->degradeToGeneralArray) {
493: /** @var list<ConstantIntegerType|ConstantStringType> $keyTypes */
494: $keyTypes = $this->keyTypes;
495: $array = new ConstantArrayType($keyTypes, $this->valueTypes, $this->nextAutoIndexes, $this->optionalKeys, $this->isList, $this->unsealed);
496: if ($this->isNonEmpty->yes() && !$array->isIterableAtLeastOnce()->yes()) {
497: return TypeCombinator::intersect($array, new NonEmptyArrayType());
498: }
499: return $array;
500: }
501:
502: if ($this->degradeClosures === true) {
503: $itemTypes = [];
504: $itemTypes[] = new CallableType();
505: foreach ($this->valueTypes as $valueType) {
506: if ($valueType instanceof ClosureType) {
507: continue;
508: }
509: $itemTypes[] = $valueType;
510: }
511: } else {
512: $itemTypes = $this->valueTypes;
513: }
514:
515: $keyTypesForArray = $this->keyTypes;
516: // Real unsealed extras describe additional key/value pairs that
517: // belong in the degraded `ArrayType`'s key/value unions too —
518: // otherwise the degraded type silently drops them.
519: if ($this->unsealed !== null) {
520: [$unsealedKey, $unsealedValue] = $this->unsealed;
521: $isExplicitNever = $unsealedKey instanceof NeverType && $unsealedKey->isExplicit();
522: if (!$isExplicitNever) {
523: $keyTypesForArray[] = $unsealedKey;
524: $itemTypes[] = $unsealedValue;
525: }
526: }
527:
528: $array = new ArrayType(
529: TypeCombinator::union(...$keyTypesForArray),
530: TypeCombinator::union(...$itemTypes),
531: );
532:
533: $types = [];
534: if ($this->isNonEmpty->yes() || count($this->optionalKeys) < $keyTypesCount) {
535: $types[] = new NonEmptyArrayType();
536: }
537:
538: if ($this->oversized) {
539: $types[] = new OversizedArrayType();
540: }
541:
542: if ($this->isList->yes()) {
543: $types[] = new AccessoryArrayListType();
544: }
545:
546: if (count($types) === 0) {
547: return $array;
548: }
549:
550: return new IntersectionType([$array, ...$types]);
551: }
552:
553: public function isList(): bool
554: {
555: return $this->isList->yes();
556: }
557:
558: }
559: