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