Thanks to visit codestin.com
Credit goes to apiref.phpstan.org

1: <?php declare(strict_types = 1);
2:
3: namespace PHPStan\Type\Constant;
4:
5: use Nette\Utils\Strings;
6: use PHPStan\Analyser\OutOfClassScope;
7: use PHPStan\DependencyInjection\BleedingEdgeToggle;
8: use PHPStan\Php\PhpVersion;
9: use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprIntegerNode;
10: use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprStringNode;
11: use PHPStan\PhpDocParser\Ast\Type\ArrayShapeItemNode;
12: use PHPStan\PhpDocParser\Ast\Type\ArrayShapeNode;
13: use PHPStan\PhpDocParser\Ast\Type\ArrayShapeUnsealedTypeNode;
14: use PHPStan\PhpDocParser\Ast\Type\ConstTypeNode;
15: use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
16: use PHPStan\PhpDocParser\Ast\Type\TypeNode;
17: use PHPStan\Reflection\Callables\FunctionCallableVariant;
18: use PHPStan\Reflection\ClassMemberAccessAnswerer;
19: use PHPStan\Reflection\InaccessibleMethod;
20: use PHPStan\Reflection\InitializerExprTypeResolver;
21: use PHPStan\Reflection\PhpVersionStaticAccessor;
22: use PHPStan\Reflection\TrivialParametersAcceptor;
23: use PHPStan\Rules\Arrays\AllowedArrayKeysTypes;
24: use PHPStan\ShouldNotHappenException;
25: use PHPStan\TrinaryLogic;
26: use PHPStan\Type\AcceptsResult;
27: use PHPStan\Type\Accessory\AccessoryArrayListType;
28: use PHPStan\Type\Accessory\AccessoryLowercaseStringType;
29: use PHPStan\Type\Accessory\AccessoryNonEmptyStringType;
30: use PHPStan\Type\Accessory\AccessoryNonFalsyStringType;
31: use PHPStan\Type\Accessory\AccessoryNumericStringType;
32: use PHPStan\Type\Accessory\AccessoryUppercaseStringType;
33: use PHPStan\Type\Accessory\HasOffsetType;
34: use PHPStan\Type\Accessory\HasOffsetValueType;
35: use PHPStan\Type\Accessory\NonEmptyArrayType;
36: use PHPStan\Type\ArrayType;
37: use PHPStan\Type\BenevolentUnionType;
38: use PHPStan\Type\BooleanType;
39: use PHPStan\Type\ClassStringType;
40: use PHPStan\Type\CompoundType;
41: use PHPStan\Type\ConstantScalarType;
42: use PHPStan\Type\ErrorType;
43: use PHPStan\Type\GeneralizePrecision;
44: use PHPStan\Type\Generic\TemplateMixedType;
45: use PHPStan\Type\Generic\TemplateStrictMixedType;
46: use PHPStan\Type\Generic\TemplateType;
47: use PHPStan\Type\Generic\TemplateTypeMap;
48: use PHPStan\Type\Generic\TemplateTypeVariance;
49: use PHPStan\Type\IntegerRangeType;
50: use PHPStan\Type\IntegerType;
51: use PHPStan\Type\IntersectionType;
52: use PHPStan\Type\IsSuperTypeOfResult;
53: use PHPStan\Type\MixedType;
54: use PHPStan\Type\NeverType;
55: use PHPStan\Type\NullType;
56: use PHPStan\Type\ObjectWithoutClassType;
57: use PHPStan\Type\RecursionGuard;
58: use PHPStan\Type\StaticTypeFactory;
59: use PHPStan\Type\StrictMixedType;
60: use PHPStan\Type\StringType;
61: use PHPStan\Type\Traits\ArrayTypeTrait;
62: use PHPStan\Type\Traits\NonObjectTypeTrait;
63: use PHPStan\Type\Traits\UndecidedComparisonTypeTrait;
64: use PHPStan\Type\Traverser\UnsafeArrayStringKeyCastingTraverser;
65: use PHPStan\Type\Type;
66: use PHPStan\Type\TypeCombinator;
67: use PHPStan\Type\UnionType;
68: use PHPStan\Type\VerbosityLevel;
69: use function array_key_exists;
70: use function array_keys;
71: use function array_map;
72: use function array_merge;
73: use function array_pop;
74: use function array_push;
75: use function array_slice;
76: use function array_unique;
77: use function array_values;
78: use function assert;
79: use function count;
80: use function implode;
81: use function in_array;
82: use function is_int;
83: use function is_string;
84: use function max;
85: use function min;
86: use function pow;
87: use function range;
88: use function sort;
89: use function sprintf;
90: use function str_contains;
91: use function strtolower;
92: use function strtoupper;
93: use function usort;
94: use const CASE_LOWER;
95: use const CASE_UPPER;
96:
97: /**
98: * @api
99: */
100: class ConstantArrayType implements Type
101: {
102:
103: use ArrayTypeTrait {
104: chunkArray as traitChunkArray;
105: }
106: use NonObjectTypeTrait;
107: use UndecidedComparisonTypeTrait;
108:
109: private const DESCRIBE_LIMIT = 8;
110: private const CHUNK_FINITE_TYPES_LIMIT = 5;
111:
112: private TrinaryLogic $isList;
113:
114: /** @var array{Type, Type}|null */
115: private ?array $unsealed; // phpcs:ignore
116:
117: /** @var self[]|null */
118: private ?array $allArrays = null;
119:
120: private ?Type $iterableKeyType = null;
121:
122: private ?Type $iterableValueType = null;
123:
124: private ?Type $keyTypesUnion = null;
125:
126: /** @var array<int|string, int>|null */
127: private ?array $keyIndexMap = null;
128:
129: /**
130: * @api
131: * @param list<ConstantIntegerType|ConstantStringType> $keyTypes
132: * @param array<int, Type> $valueTypes
133: * @param list<int> $nextAutoIndexes
134: * @param int[] $optionalKeys
135: * @param array{Type, Type}|null $unsealed
136: */
137: public function __construct(
138: private array $keyTypes,
139: private array $valueTypes,
140: private array $nextAutoIndexes = [0],
141: private array $optionalKeys = [],
142: ?TrinaryLogic $isList = null,
143: ?array $unsealed = null,
144: )
145: {
146: assert(count($keyTypes) === count($valueTypes));
147:
148: // Fill in `$isList` from the shape when the caller didn't pass one.
149: // For empty CATs the answer derives from the unsealed key type
150: // (no explicit keys to inspect); for non-empty ones the default
151: // is `No` and the caller is expected to assert list-ness via
152: // `makeList()` if appropriate.
153: if ($isList === null) {
154: if (count($this->keyTypes) === 0) {
155: if ($unsealed === null) {
156: $isList = TrinaryLogic::createYes();
157: } else {
158: [$unsealedKeyType] = $unsealed;
159: if ($unsealedKeyType instanceof NeverType && $unsealedKeyType->isExplicit()) {
160: $isList = TrinaryLogic::createYes();
161: } elseif ($unsealedKeyType->isInteger()->yes()) {
162: $isList = TrinaryLogic::createMaybe();
163: } else {
164: $isList = TrinaryLogic::createNo();
165: }
166: }
167: } else {
168: $isList = TrinaryLogic::createNo();
169: }
170: }
171: $this->isList = $isList;
172:
173: if ($unsealed !== null) {
174: if (in_array($unsealed[0]->describe(VerbosityLevel::value()), ['(int|string)', '(int|non-decimal-int-string)'], true)) {
175: $unsealed[0] = new MixedType();
176: }
177: if ($unsealed[0] instanceof StrictMixedType && !$unsealed[0] instanceof TemplateStrictMixedType) {
178: $unsealed[0] = (new UnionType([new StringType(), new IntegerType()]))->toArrayKey();
179: }
180: } elseif (BleedingEdgeToggle::isBleedingEdge()) {
181: $never = new NeverType(true);
182: $unsealed = [$never, $never];
183: }
184: $this->unsealed = $unsealed;
185: }
186:
187: public function isSealed(): TrinaryLogic
188: {
189: return $this->isUnsealed()->negate();
190: }
191:
192: public function isUnsealed(): TrinaryLogic
193: {
194: $unsealed = $this->unsealed;
195: if ($unsealed === null) {
196: return TrinaryLogic::createMaybe();
197: }
198:
199: [$keyType] = $unsealed;
200:
201: return TrinaryLogic::createFromBoolean(!$keyType instanceof NeverType || !$keyType->isExplicit());
202: }
203:
204: /**
205: * @phpstan-pure
206: * @return array{Type, Type}|null
207: */
208: public function getUnsealedTypes(): ?array
209: {
210: return $this->unsealed;
211: }
212:
213: /**
214: * @internal
215: */
216: public function dropUnsealedTypes(): self
217: {
218: return $this->recreate(
219: $this->keyTypes,
220: $this->valueTypes,
221: $this->nextAutoIndexes,
222: $this->optionalKeys,
223: $this->isList,
224: null,
225: );
226: }
227:
228: /**
229: * @param list<ConstantIntegerType|ConstantStringType> $keyTypes
230: * @param array<int, Type> $valueTypes
231: * @param list<int> $nextAutoIndexes
232: * @param int[] $optionalKeys
233: * @param array{Type, Type}|null $unsealed
234: */
235: protected function recreate(
236: array $keyTypes,
237: array $valueTypes,
238: array $nextAutoIndexes,
239: array $optionalKeys,
240: ?TrinaryLogic $isList,
241: ?array $unsealed,
242: ): self
243: {
244: return new self($keyTypes, $valueTypes, $nextAutoIndexes, $optionalKeys, $isList, $unsealed);
245: }
246:
247: public function getConstantArrays(): array
248: {
249: return [$this];
250: }
251:
252: public function getReferencedClasses(): array
253: {
254: $referencedClasses = [];
255: foreach ($this->getKeyTypes() as $keyType) {
256: foreach ($keyType->getReferencedClasses() as $referencedClass) {
257: $referencedClasses[] = $referencedClass;
258: }
259: }
260:
261: foreach ($this->getValueTypes() as $valueType) {
262: foreach ($valueType->getReferencedClasses() as $referencedClass) {
263: $referencedClasses[] = $referencedClass;
264: }
265: }
266:
267: if ($this->unsealed !== null) {
268: [$unsealedKeyType, $unsealedValueType] = $this->unsealed;
269: foreach ($unsealedKeyType->getReferencedClasses() as $referencedClass) {
270: $referencedClasses[] = $referencedClass;
271: }
272: foreach ($unsealedValueType->getReferencedClasses() as $referencedClass) {
273: $referencedClasses[] = $referencedClass;
274: }
275: }
276:
277: return $referencedClasses;
278: }
279:
280: public function getIterableKeyType(): Type
281: {
282: if ($this->iterableKeyType !== null) {
283: return $this->iterableKeyType;
284: }
285:
286: $keyTypesCount = count($this->keyTypes);
287: if ($keyTypesCount === 0) {
288: $keyType = new NeverType(true);
289: } elseif ($keyTypesCount === 1) {
290: $keyType = $this->keyTypes[0];
291: } else {
292: $keyType = new UnionType($this->keyTypes);
293: }
294:
295: if ($this->isUnsealed()->yes() && $this->unsealed !== null) {
296: $unsealedKeyType = $this->unsealed[0];
297: if ($unsealedKeyType instanceof MixedType && !$unsealedKeyType instanceof TemplateMixedType) {
298: $unsealedKeyType = (new BenevolentUnionType([new IntegerType(), new StringType()]))->toArrayKey();
299: } elseif ($unsealedKeyType instanceof StrictMixedType && !$unsealedKeyType instanceof TemplateStrictMixedType) {
300: $unsealedKeyType = (new BenevolentUnionType([new IntegerType(), new StringType()]))->toArrayKey();
301: }
302: $keyType = TypeCombinator::union($keyType, $unsealedKeyType);
303: }
304:
305: return $this->iterableKeyType = UnsafeArrayStringKeyCastingTraverser::castKeyType($keyType);
306: }
307:
308: public function getIterableValueType(): Type
309: {
310: if ($this->iterableValueType !== null) {
311: return $this->iterableValueType;
312: }
313:
314: $valueType = count($this->valueTypes) > 0 ? TypeCombinator::union(...$this->valueTypes) : new NeverType(true);
315: if ($this->isUnsealed()->yes() && $this->unsealed !== null) {
316: $valueType = TypeCombinator::union($valueType, $this->unsealed[1]);
317: }
318:
319: return $this->iterableValueType = $valueType;
320: }
321:
322: private function getKeyTypesUnion(): Type
323: {
324: return $this->keyTypesUnion ??= count($this->keyTypes) > 0
325: ? TypeCombinator::union(...$this->keyTypes)
326: : new NeverType();
327: }
328:
329: public function getKeyType(): Type
330: {
331: return $this->getIterableKeyType();
332: }
333:
334: public function getItemType(): Type
335: {
336: return $this->getIterableValueType();
337: }
338:
339: public function isConstantValue(): TrinaryLogic
340: {
341: if ($this->isUnsealed()->yes()) {
342: return TrinaryLogic::createNo();
343: }
344:
345: return TrinaryLogic::createYes();
346: }
347:
348: /**
349: * @return list<int>
350: */
351: public function getNextAutoIndexes(): array
352: {
353: return $this->nextAutoIndexes;
354: }
355:
356: /**
357: * @return int[]
358: */
359: public function getOptionalKeys(): array
360: {
361: return $this->optionalKeys;
362: }
363:
364: /**
365: * @return self[]
366: */
367: public function getAllArrays(): array
368: {
369: if ($this->allArrays !== null) {
370: return $this->allArrays;
371: }
372:
373: if (count($this->optionalKeys) <= 10) {
374: $optionalKeysCombinations = $this->powerSet($this->optionalKeys);
375: } else {
376: $optionalKeysCombinations = [
377: [],
378: array_slice($this->optionalKeys, 0, 1, true),
379: array_slice($this->optionalKeys, -1, 1, true),
380: $this->optionalKeys,
381: ];
382: }
383:
384: $requiredKeys = [];
385: foreach (array_keys($this->keyTypes) as $i) {
386: if (in_array($i, $this->optionalKeys, true)) {
387: continue;
388: }
389: $requiredKeys[] = $i;
390: }
391:
392: $arrays = [];
393: foreach ($optionalKeysCombinations as $combination) {
394: $keys = array_merge($requiredKeys, $combination);
395: sort($keys);
396:
397: if ($this->isList->yes() && array_keys($keys) !== $keys) {
398: continue;
399: }
400:
401: if (count($keys) === 0 && $this->isUnsealed()->yes() && $this->unsealed !== null) {
402: // Variant with no explicit keys but real unsealed extras: the
403: // builder's getArray() would degrade this to a general
404: // ArrayType. Construct the CAT directly so the variant keeps
405: // its extras for downstream consumers (e.g. flattenTypes).
406: $arrays[] = new ConstantArrayType([], [], unsealed: $this->unsealed);
407: continue;
408: }
409:
410: $builder = ConstantArrayTypeBuilder::createEmpty();
411: $builder->disableArrayDegradation();
412: foreach ($keys as $i) {
413: $builder->setOffsetValueType($this->keyTypes[$i], $this->valueTypes[$i]);
414: }
415: if ($this->isUnsealed()->yes() && $this->unsealed !== null) {
416: $builder->makeUnsealed($this->unsealed[0], $this->unsealed[1]);
417: }
418:
419: $array = $builder->getArray();
420: if (!$array instanceof self) {
421: throw new ShouldNotHappenException();
422: }
423:
424: $arrays[] = $array;
425: }
426:
427: return $this->allArrays = $arrays;
428: }
429:
430: /**
431: * @template T
432: * @param T[] $in
433: * @return T[][]
434: */
435: private function powerSet(array $in): array
436: {
437: $count = count($in);
438: $members = pow(2, $count);
439: $return = [];
440: for ($i = 0; $i < $members; $i++) {
441: $b = sprintf('%0' . $count . 'b', $i);
442: $out = [];
443: for ($j = 0; $j < $count; $j++) {
444: if ($b[$j] !== '1') {
445: continue;
446: }
447:
448: $out[] = $in[$j];
449: }
450: $return[] = $out;
451: }
452:
453: return $return;
454: }
455:
456: /**
457: * @return list<ConstantIntegerType|ConstantStringType>
458: */
459: public function getKeyTypes(): array
460: {
461: return $this->keyTypes;
462: }
463:
464: /**
465: * @return array<int, Type>
466: */
467: public function getValueTypes(): array
468: {
469: return $this->valueTypes;
470: }
471:
472: public function isOptionalKey(int $i): bool
473: {
474: return in_array($i, $this->optionalKeys, true);
475: }
476:
477: public function sortKeys(): self
478: {
479: $indices = array_keys($this->keyTypes);
480: usort($indices, fn (int $a, int $b): int => $this->keyTypes[$a]->getValue() <=> $this->keyTypes[$b]->getValue());
481:
482: $newKeyTypes = [];
483: $newValueTypes = [];
484: $indexMap = [];
485: foreach ($indices as $newIdx => $oldIdx) {
486: $newKeyTypes[] = $this->keyTypes[$oldIdx];
487: $newValueTypes[] = $this->valueTypes[$oldIdx];
488: $indexMap[$oldIdx] = $newIdx;
489: }
490:
491: $newOptionalKeys = [];
492: foreach ($this->optionalKeys as $oldIdx) {
493: $newOptionalKeys[] = $indexMap[$oldIdx];
494: }
495: sort($newOptionalKeys);
496:
497: return $this->recreate(
498: $newKeyTypes,
499: $newValueTypes,
500: $this->nextAutoIndexes,
501: $newOptionalKeys,
502: $this->isList,
503: $this->unsealed,
504: );
505: }
506:
507: public function accepts(Type $type, bool $strictTypes): AcceptsResult
508: {
509: if ($type instanceof CompoundType && !$type instanceof IntersectionType) {
510: return $type->isAcceptedBy($this, $strictTypes);
511: }
512:
513: $isUnsealed = $this->isUnsealed();
514: if (!$isUnsealed->yes()) {
515: if ($type instanceof self && count($this->keyTypes) === 0) {
516: return AcceptsResult::createFromBoolean(count($type->keyTypes) === 0);
517: }
518: }
519:
520: $result = $this->checkOurKeys($type, $strictTypes)->and(new AcceptsResult($type->isArray(), []));
521: if ($this->unsealed === null) {
522: if ($type->isOversizedArray()->yes()) {
523: if (!$result->no()) {
524: return AcceptsResult::createYes();
525: }
526: }
527:
528: return $result;
529: }
530:
531: if ($result->no()) {
532: return $result;
533: }
534:
535: [$unsealedKeyType, $unsealedValueType] = $this->unsealed;
536:
537: if ($isUnsealed->no()) {
538: if (!$type->isConstantArray()->yes()) {
539: return $result->and(AcceptsResult::createNo([
540: 'Sealed array shape can only accept a constant array. Extra keys are not allowed.',
541: ]));
542: }
543:
544: $constantArrays = $type->getConstantArrays();
545: if (count($constantArrays) !== 1) {
546: throw new ShouldNotHappenException('Type with more than one constant array occurred, should have been eliminated with `instanceof CompoundType` above.');
547: }
548:
549: $keys = [];
550: foreach ($constantArrays[0]->getKeyTypes() as $otherKeyType) {
551: $keys[$otherKeyType->getValue()] = $otherKeyType;
552: }
553:
554: foreach ($this->keyTypes as $keyType) {
555: unset($keys[$keyType->getValue()]);
556: }
557:
558: foreach ($keys as $extraKey) {
559: $result = $result->and(AcceptsResult::createNo([
560: sprintf('Sealed array shape does not accept array with extra key %s.', $extraKey->describe(VerbosityLevel::precise())),
561: ]));
562: }
563:
564: if (!$constantArrays[0]->isUnsealed()->no()) {
565: $result = $result->and(AcceptsResult::createNo([
566: 'Sealed array shape does not accept unsealed array shape.',
567: ]));
568: }
569:
570: return $result;
571: }
572:
573: if (!$type->isConstantArray()->yes()) {
574: return $result->and($unsealedKeyType->accepts($type->getIterableKeyType(), $strictTypes))
575: ->and($unsealedValueType->accepts($type->getIterableValueType(), $strictTypes));
576: }
577:
578: $constantArrays = $type->getConstantArrays();
579: if (count($constantArrays) !== 1) {
580: throw new ShouldNotHappenException('Type with more than one constant array occurred, should have been eliminated with `instanceof CompoundType` above.');
581: }
582:
583: $keys = [];
584: $constantArray = $constantArrays[0];
585: foreach ($constantArray->getKeyTypes() as $i => $otherKeyType) {
586: $keys[$otherKeyType->getValue()] = [$i, $otherKeyType];
587: }
588:
589: foreach ($this->keyTypes as $keyType) {
590: unset($keys[$keyType->getValue()]);
591: }
592:
593: foreach ($keys as [$i, $extraKeyType]) {
594: $acceptsKey = $unsealedKeyType->accepts($extraKeyType, $strictTypes)->decorateReasons(
595: static fn (string $reason) => sprintf(
596: 'Unsealed array key type %s does not accept extra key type %s: %s',
597: $unsealedKeyType->describe(VerbosityLevel::value()),
598: $extraKeyType->describe(VerbosityLevel::value()),
599: $reason,
600: ),
601: );
602: if (!$acceptsKey->yes() && count($acceptsKey->reasons) === 0) {
603: $acceptsKey = new AcceptsResult($acceptsKey->result, [
604: sprintf(
605: 'Unsealed array key type %s does not accept extra key type %s.',
606: $unsealedKeyType->describe(VerbosityLevel::value()),
607: $extraKeyType->describe(VerbosityLevel::value()),
608: ),
609: ]);
610: }
611: $result = $result->and($acceptsKey);
612:
613: $extraValueType = $constantArray->getValueTypes()[$i];
614: $acceptsValue = $unsealedValueType->accepts($extraValueType, $strictTypes)->decorateReasons(
615: static fn (string $reason) => sprintf(
616: 'Unsealed array value type %s does not accept extra offset %s with value type %s: %s',
617: $unsealedValueType->describe(VerbosityLevel::value()),
618: $extraKeyType->describe(VerbosityLevel::value()),
619: $extraValueType->describe(VerbosityLevel::value()),
620: $reason,
621: ),
622: );
623: if (!$acceptsValue->yes() && count($acceptsValue->reasons) === 0) {
624: $acceptsValue = new AcceptsResult($acceptsValue->result, [
625: sprintf(
626: 'Unsealed array value type %s does not accept extra offset %s with value type %s.',
627: $unsealedValueType->describe(VerbosityLevel::value()),
628: $extraKeyType->describe(VerbosityLevel::value()),
629: $extraValueType->describe(VerbosityLevel::value()),
630: ),
631: ]);
632: }
633: $result = $result->and($acceptsValue);
634: }
635:
636: $otherUnsealed = $constantArray->unsealed;
637: if ($otherUnsealed !== null && !$constantArray->isUnsealed()->no()) {
638: [$otherUnsealedKeyType, $otherUnsealedValueType] = $otherUnsealed;
639:
640: $acceptsUnsealedKey = $unsealedKeyType->accepts($otherUnsealedKeyType, $strictTypes)->decorateReasons(
641: static fn (string $reason) => sprintf(
642: 'Unsealed array key type %s does not accept unsealed array key type %s: %s',
643: $unsealedKeyType->describe(VerbosityLevel::value()),
644: $otherUnsealedKeyType->describe(VerbosityLevel::value()),
645: $reason,
646: ),
647: );
648: if (!$acceptsUnsealedKey->yes() && count($acceptsUnsealedKey->reasons) === 0) {
649: $acceptsUnsealedKey = new AcceptsResult($acceptsUnsealedKey->result, [
650: sprintf(
651: 'Unsealed array key type %s does not accept unsealed array key type %s.',
652: $unsealedKeyType->describe(VerbosityLevel::value()),
653: $otherUnsealedKeyType->describe(VerbosityLevel::value()),
654: ),
655: ]);
656: }
657: $result = $result->and($acceptsUnsealedKey);
658:
659: $acceptsUnsealedValue = $unsealedValueType->accepts($otherUnsealedValueType, $strictTypes)->decorateReasons(
660: static fn (string $reason) => sprintf(
661: 'Unsealed array value type %s does not accept unsealed array value type %s: %s',
662: $unsealedValueType->describe(VerbosityLevel::value()),
663: $otherUnsealedValueType->describe(VerbosityLevel::value()),
664: $reason,
665: ),
666: );
667: if (!$acceptsUnsealedValue->yes() && count($acceptsUnsealedValue->reasons) === 0) {
668: $acceptsUnsealedValue = new AcceptsResult($acceptsUnsealedValue->result, [
669: sprintf(
670: 'Unsealed array value type %s does not accept unsealed array value type %s.',
671: $unsealedValueType->describe(VerbosityLevel::value()),
672: $otherUnsealedValueType->describe(VerbosityLevel::value()),
673: ),
674: ]);
675: }
676: $result = $result->and($acceptsUnsealedValue);
677: }
678:
679: return $result;
680: }
681:
682: private function checkOurKeys(Type $type, bool $strictTypes): AcceptsResult
683: {
684: $result = AcceptsResult::createYes();
685: foreach ($this->keyTypes as $i => $keyType) {
686: $valueType = $this->valueTypes[$i];
687: $hasOffsetValueType = $type->hasOffsetValueType($keyType);
688: $hasOffset = new AcceptsResult(
689: $hasOffsetValueType,
690: $hasOffsetValueType->yes() || !$type->isConstantArray()->yes() ? [] : [sprintf('Array %s have offset %s.', $hasOffsetValueType->no() ? 'does not' : 'might not', $keyType->describe(VerbosityLevel::value()))],
691: );
692: if ($hasOffset->no()) {
693: if ($this->isOptionalKey($i)) {
694: continue;
695: }
696: return $hasOffset;
697: }
698: if ($hasOffset->maybe() && $this->isOptionalKey($i)) {
699: $hasOffset = AcceptsResult::createYes();
700: }
701:
702: $result = $result->and($hasOffset);
703: $otherValueType = $type->getOffsetValueType($keyType);
704: $verbosity = VerbosityLevel::getRecommendedLevelByType($valueType, $otherValueType);
705: $acceptsValue = $valueType->accepts($otherValueType, $strictTypes)->decorateReasons(
706: static fn (string $reason) => sprintf(
707: 'Offset %s (%s) does not accept type %s: %s',
708: $keyType->describe(VerbosityLevel::precise()),
709: $valueType->describe($verbosity),
710: $otherValueType->describe($verbosity),
711: $reason,
712: ),
713: );
714: if (!$acceptsValue->yes() && count($acceptsValue->reasons) === 0 && $type->isConstantArray()->yes()) {
715: $acceptsValue = new AcceptsResult($acceptsValue->result, [
716: sprintf(
717: 'Offset %s (%s) does not accept type %s.',
718: $keyType->describe(VerbosityLevel::precise()),
719: $valueType->describe($verbosity),
720: $otherValueType->describe($verbosity),
721: ),
722: ]);
723: }
724: if ($acceptsValue->no()) {
725: return $acceptsValue;
726: }
727: $result = $result->and($acceptsValue);
728: }
729:
730: return $result;
731: }
732:
733: public function isSuperTypeOf(Type $type): IsSuperTypeOfResult
734: {
735: if ($type instanceof self) {
736: $thisUnsealedness = $this->isUnsealed();
737: $typeUnsealedness = $type->isUnsealed();
738: $bothDefinite = $this->unsealed !== null && $type->unsealed !== null;
739:
740: if (count($this->keyTypes) === 0) {
741: if (!$bothDefinite) {
742: return new IsSuperTypeOfResult($type->isIterableAtLeastOnce()->negate(), []);
743: }
744: if ($thisUnsealedness->no()) {
745: return new IsSuperTypeOfResult($type->isIterableAtLeastOnce()->negate(), []);
746: }
747: // $this is unsealed with no known keys — fall through to extras/unsealed-part checks below
748: }
749:
750: $results = [];
751: foreach ($this->keyTypes as $i => $keyType) {
752: $hasOffset = $type->hasOffsetValueType($keyType);
753: if ($bothDefinite && $hasOffset->no() && $typeUnsealedness->yes()) {
754: [$typeUnsealedKey] = $type->unsealed;
755: if (!$typeUnsealedKey->isSuperTypeOf($keyType)->no()) {
756: $hasOffset = TrinaryLogic::createMaybe();
757: }
758: }
759: if ($hasOffset->no()) {
760: if (!$this->isOptionalKey($i)) {
761: return IsSuperTypeOfResult::createNo();
762: }
763:
764: $results[] = IsSuperTypeOfResult::createYes();
765: continue;
766: } elseif ($hasOffset->maybe() && !$this->isOptionalKey($i)) {
767: $results[] = IsSuperTypeOfResult::createMaybe();
768: }
769:
770: $otherValueType = $type->getOffsetValueType($keyType);
771: if ($otherValueType instanceof ErrorType && $bothDefinite && $typeUnsealedness->yes()) {
772: [, $typeUnsealedValue] = $type->unsealed;
773: $otherValueType = $typeUnsealedValue;
774: }
775: $isValueSuperType = $this->valueTypes[$i]->isSuperTypeOf($otherValueType);
776: if ($isValueSuperType->no()) {
777: return $isValueSuperType->decorateReasons(static fn (string $reason) => sprintf('Offset %s: %s', $keyType->describe(VerbosityLevel::value()), $reason));
778: }
779: $results[] = $isValueSuperType;
780: }
781:
782: if ($bothDefinite) {
783: $thisKeyValues = [];
784: foreach ($this->keyTypes as $thisKeyType) {
785: $thisKeyValues[$thisKeyType->getValue()] = true;
786: }
787:
788: foreach ($type->getKeyTypes() as $i => $typeKey) {
789: if (array_key_exists($typeKey->getValue(), $thisKeyValues)) {
790: continue;
791: }
792:
793: if ($thisUnsealedness->no()) {
794: if (!$type->isOptionalKey($i)) {
795: return IsSuperTypeOfResult::createNo();
796: }
797: $results[] = IsSuperTypeOfResult::createMaybe();
798: continue;
799: }
800:
801: [$thisUnsealedKey, $thisUnsealedValue] = $this->unsealed;
802: $keyCheck = $thisUnsealedKey->isSuperTypeOf($typeKey);
803: if ($keyCheck->no()) {
804: if ($type->isOptionalKey($i)) {
805: $results[] = IsSuperTypeOfResult::createMaybe();
806: continue;
807: }
808: return IsSuperTypeOfResult::createNo();
809: }
810: $valueCheck = $thisUnsealedValue->isSuperTypeOf($type->getValueTypes()[$i]);
811: if ($valueCheck->no()) {
812: if ($type->isOptionalKey($i)) {
813: $results[] = IsSuperTypeOfResult::createMaybe();
814: continue;
815: }
816: return IsSuperTypeOfResult::createNo();
817: }
818: $results[] = $keyCheck->and($valueCheck);
819: }
820:
821: if ($typeUnsealedness->yes()) {
822: if ($thisUnsealedness->no()) {
823: $results[] = IsSuperTypeOfResult::createMaybe();
824: } else {
825: [$thisUnsealedKey, $thisUnsealedValue] = $this->unsealed;
826: [$typeUnsealedKey, $typeUnsealedValue] = $type->unsealed;
827: $results[] = $thisUnsealedKey->isSuperTypeOf($typeUnsealedKey);
828: $results[] = $thisUnsealedValue->isSuperTypeOf($typeUnsealedValue);
829: }
830: }
831: }
832:
833: return IsSuperTypeOfResult::createYes()->and(...$results);
834: }
835:
836: if ($type instanceof ArrayType) {
837: $result = IsSuperTypeOfResult::createMaybe();
838: if (count($this->keyTypes) === 0) {
839: return $result;
840: }
841:
842: $isKeySuperType = $this->getKeyType()->isSuperTypeOf($type->getKeyType());
843: if ($isKeySuperType->no()) {
844: return $isKeySuperType;
845: }
846:
847: return $result->and($isKeySuperType, $this->getItemType()->isSuperTypeOf($type->getItemType()));
848: }
849:
850: if ($type instanceof CompoundType) {
851: return $type->isSubTypeOf($this);
852: }
853:
854: return IsSuperTypeOfResult::createNo();
855: }
856:
857: public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType
858: {
859: if ($type->isInteger()->yes()) {
860: return new ConstantBooleanType(false);
861: }
862:
863: if ($this->isIterableAtLeastOnce()->no()) {
864: if ($type->isIterableAtLeastOnce()->yes()) {
865: return new ConstantBooleanType(false);
866: }
867:
868: $constantScalarValues = $type->getConstantScalarValues();
869: if (count($constantScalarValues) > 0) {
870: $results = [];
871: foreach ($constantScalarValues as $constantScalarValue) {
872: // @phpstan-ignore equal.invalid, equal.notAllowed
873: $results[] = TrinaryLogic::createFromBoolean($constantScalarValue == []); // phpcs:ignore
874: }
875:
876: return TrinaryLogic::extremeIdentity(...$results)->toBooleanType();
877: }
878: }
879:
880: return new BooleanType();
881: }
882:
883: public function equals(Type $type): bool
884: {
885: if (!$type instanceof self) {
886: return false;
887: }
888:
889: if (count($this->keyTypes) !== count($type->keyTypes)) {
890: return false;
891: }
892:
893: foreach ($this->keyTypes as $i => $keyType) {
894: $valueType = $this->valueTypes[$i];
895: if (!$valueType->equals($type->valueTypes[$i])) {
896: return false;
897: }
898: if (!$keyType->equals($type->keyTypes[$i])) {
899: return false;
900: }
901: }
902:
903: if ($this->optionalKeys !== $type->optionalKeys) {
904: return false;
905: }
906:
907: // Both `unsealed === null` (legacy / pre-bleeding-edge, where
908: // `isUnsealed()` answers `Maybe`) and `unsealed === [explicitNever,
909: // explicitNever]` (the fresh bleeding-edge sealed marker, where
910: // `isUnsealed()` answers `No`) mean "no real extras". Treat them as
911: // equivalent here — use `!isUnsealed()->yes()` rather than
912: // `isUnsealed()->no()`, otherwise a legacy-null shape and a
913: // marker-sealed shape compare unequal. Only compare the actual
914: // extras when both sides genuinely have them.
915: $thisHasExtras = $this->isUnsealed()->yes();
916: $otherHasExtras = $type->isUnsealed()->yes();
917: if ($thisHasExtras !== $otherHasExtras) {
918: return false;
919: }
920:
921: if ($thisHasExtras && $this->unsealed !== null && $type->unsealed !== null) {
922: if (!$this->unsealed[0]->equals($type->unsealed[0])) {
923: return false;
924: }
925: if (!$this->unsealed[1]->equals($type->unsealed[1])) {
926: return false;
927: }
928: }
929:
930: return true;
931: }
932:
933: public function isCallable(): TrinaryLogic
934: {
935: $result = RecursionGuard::run($this, function (): TrinaryLogic {
936: $hasNonExistentMethod = false;
937: $typeAndMethods = $this->doFindTypeAndMethodNames($hasNonExistentMethod);
938: if ($typeAndMethods === []) {
939: return TrinaryLogic::createNo();
940: }
941:
942: $results = array_map(
943: static fn (ConstantArrayTypeAndMethod $typeAndMethod): TrinaryLogic => $typeAndMethod->getCertainty(),
944: $typeAndMethods,
945: );
946:
947: $result = TrinaryLogic::createYes()->and(...$results);
948:
949: if ($hasNonExistentMethod) {
950: $result = $result->and(TrinaryLogic::createMaybe());
951: }
952:
953: return $result;
954: });
955:
956: if ($result instanceof ErrorType) {
957: return TrinaryLogic::createNo();
958: }
959:
960: return $result;
961: }
962:
963: public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope): array
964: {
965: $typeAndMethodNames = $this->findTypeAndMethodNames();
966: if ($typeAndMethodNames === []) {
967: throw new ShouldNotHappenException();
968: }
969:
970: $acceptors = [];
971: foreach ($typeAndMethodNames as $typeAndMethodName) {
972: if ($typeAndMethodName->isUnknown() || !$typeAndMethodName->getCertainty()->yes()) {
973: $acceptors[] = new TrivialParametersAcceptor();
974: continue;
975: }
976:
977: $method = $typeAndMethodName->getType()
978: ->getMethod($typeAndMethodName->getMethod(), $scope);
979:
980: if (!$scope->canCallMethod($method)) {
981: $acceptors[] = new InaccessibleMethod($method);
982: continue;
983: }
984:
985: array_push($acceptors, ...FunctionCallableVariant::createFromVariants($method, $method->getVariants()));
986: }
987:
988: return $acceptors;
989: }
990:
991: /** @return ConstantArrayTypeAndMethod[] */
992: public function findTypeAndMethodNames(): array
993: {
994: return $this->doFindTypeAndMethodNames();
995: }
996:
997: /** @return ConstantArrayTypeAndMethod[] */
998: private function doFindTypeAndMethodNames(bool &$hasNonExistentMethod = false): array
999: {
1000: $isUnsealed = $this->isUnsealed()->yes();
1001:
1002: // Sealed: must have exactly the two callable slots, no more, no less.
1003: // Unsealed: explicit keys may cover 0, 1, both, or neither — but any
1004: // explicit key outside {0, 1} immediately disqualifies, because the
1005: // callable shape `[classOrObject, method]` has no room for other
1006: // keys.
1007: if (!$isUnsealed && count($this->keyTypes) !== 2) {
1008: return [];
1009: }
1010: if (count($this->keyTypes) > 2) {
1011: return [];
1012: }
1013:
1014: $classOrObject = null;
1015: $method = null;
1016: foreach ($this->keyTypes as $i => $keyType) {
1017: if ($keyType->isSuperTypeOf(new ConstantIntegerType(0))->yes()) {
1018: $classOrObject = $this->valueTypes[$i];
1019: continue;
1020: }
1021:
1022: if ($keyType->isSuperTypeOf(new ConstantIntegerType(1))->yes()) {
1023: $method = $this->valueTypes[$i];
1024: continue;
1025: }
1026:
1027: // Explicit key is something other than 0 or 1 — not callable.
1028: return [];
1029: }
1030:
1031: // Try to fill missing callable slots from the unsealed extras: an
1032: // unsealed array `array{0: object, ...<int, string>}` *might* turn
1033: // into a callable if the actual value carries a `1 => 'method'`
1034: // extra. Require that the unsealed key range covers the missing
1035: // slot and that the unsealed value type can overlap with the
1036: // type required for that slot (object|class-string for key 0,
1037: // non-falsy-string for key 1) — otherwise no concrete value of
1038: // this CAT can ever be callable.
1039: if ($isUnsealed && $this->unsealed !== null) {
1040: [$unsealedKey, $unsealedValue] = $this->unsealed;
1041:
1042: if ($classOrObject === null) {
1043: if ($unsealedKey->isSuperTypeOf(new ConstantIntegerType(0))->no()) {
1044: return [];
1045: }
1046: $expected = TypeCombinator::union(new ObjectWithoutClassType(), new ClassStringType());
1047: if ($expected->isSuperTypeOf($unsealedValue)->no()) {
1048: return [];
1049: }
1050: $classOrObject = $unsealedValue;
1051: }
1052:
1053: if ($method === null) {
1054: if ($unsealedKey->isSuperTypeOf(new ConstantIntegerType(1))->no()) {
1055: return [];
1056: }
1057: $expected = TypeCombinator::intersect(new StringType(), new AccessoryNonFalsyStringType());
1058: if ($expected->isSuperTypeOf($unsealedValue)->no()) {
1059: return [];
1060: }
1061: $method = $unsealedValue;
1062: }
1063: }
1064:
1065: if ($classOrObject === null || $method === null) {
1066: return [];
1067: }
1068:
1069: $callableArray = [$classOrObject, $method];
1070:
1071: [$classOrObject, $methods] = $callableArray;
1072: if (count($methods->getConstantStrings()) === 0) {
1073: return [ConstantArrayTypeAndMethod::createUnknown()];
1074: }
1075:
1076: $type = $classOrObject->getObjectTypeOrClassStringObjectType();
1077: if (!$type->isObject()->yes()) {
1078: return [ConstantArrayTypeAndMethod::createUnknown()];
1079: }
1080:
1081: $typeAndMethods = [];
1082: $phpVersion = PhpVersionStaticAccessor::getInstance();
1083: foreach ($methods->getConstantStrings() as $methodName) {
1084: $has = $type->hasMethod($methodName->getValue());
1085: if ($has->no()) {
1086: $hasNonExistentMethod = true;
1087: continue;
1088: }
1089:
1090: if (
1091: $has->yes()
1092: && !$phpVersion->supportsCallableInstanceMethods()
1093: ) {
1094: $isString = $classOrObject->isString();
1095: if ($isString->yes()) {
1096: $methodReflection = $type->getMethod($methodName->getValue(), new OutOfClassScope());
1097:
1098: if (!$methodReflection->isStatic()) {
1099: continue;
1100: }
1101: } elseif ($isString->maybe()) {
1102: $has = $has->and(TrinaryLogic::createMaybe());
1103: }
1104: }
1105:
1106: if ($this->isOptionalKey(0) || $this->isOptionalKey(1)) {
1107: $has = $has->and(TrinaryLogic::createMaybe());
1108: }
1109:
1110: // Unsealed: the actual value may carry extras beyond keys 0/1,
1111: // which would void the callable shape. The CAT itself describes
1112: // "zero or more extras", so callable-ness is uncertain.
1113: if ($isUnsealed) {
1114: $has = $has->and(TrinaryLogic::createMaybe());
1115: }
1116:
1117: $typeAndMethods[] = ConstantArrayTypeAndMethod::createConcrete($type, $methodName->getValue(), $has);
1118: }
1119:
1120: return $typeAndMethods;
1121: }
1122:
1123: public function hasOffsetValueType(Type $offsetType): TrinaryLogic
1124: {
1125: $offsetArrayKeyType = $offsetType->toArrayKey();
1126: if ($offsetArrayKeyType instanceof ErrorType) {
1127: $allowedArrayKeys = AllowedArrayKeysTypes::getType();
1128: $offsetArrayKeyType = TypeCombinator::intersect($allowedArrayKeys, $offsetType)->toArrayKey();
1129: if ($offsetArrayKeyType instanceof NeverType) {
1130: return TrinaryLogic::createNo();
1131: }
1132: }
1133:
1134: return $this->recursiveHasOffsetValueType($offsetArrayKeyType);
1135: }
1136:
1137: private function recursiveHasOffsetValueType(Type $offsetType): TrinaryLogic
1138: {
1139: if ($offsetType instanceof UnionType) {
1140: $results = [];
1141: foreach ($offsetType->getTypes() as $innerType) {
1142: $results[] = $this->recursiveHasOffsetValueType($innerType);
1143: }
1144:
1145: return TrinaryLogic::extremeIdentity(...$results);
1146: }
1147: if ($offsetType instanceof IntegerRangeType) {
1148: $finiteTypes = $offsetType->getFiniteTypes();
1149: if ($finiteTypes !== []) {
1150: $results = [];
1151: foreach ($finiteTypes as $innerType) {
1152: $results[] = $this->recursiveHasOffsetValueType($innerType);
1153: }
1154:
1155: return TrinaryLogic::extremeIdentity(...$results);
1156: }
1157: }
1158:
1159: $result = TrinaryLogic::createNo();
1160: foreach ($this->keyTypes as $i => $keyType) {
1161: // PHP coerces decimal-integer strings to int when used as array
1162: // keys ("123" → 123), so a non-constant string offset *could* hit
1163: // a constant-integer slot. Skip the upgrade when the offset is
1164: // definitely a non-decimal-integer string — those stay as strings
1165: // and can never collide with an int key.
1166: if (
1167: $keyType instanceof ConstantIntegerType
1168: && !$offsetType->isString()->no()
1169: && $offsetType->isConstantScalarValue()->no()
1170: && !$offsetType->isDecimalIntegerString()->no()
1171: ) {
1172: return TrinaryLogic::createMaybe();
1173: }
1174:
1175: $has = $keyType->isSuperTypeOf($offsetType);
1176: if ($has->yes()) {
1177: if ($this->isOptionalKey($i)) {
1178: return TrinaryLogic::createMaybe();
1179: }
1180: return TrinaryLogic::createYes();
1181: }
1182: if (!$has->maybe()) {
1183: continue;
1184: }
1185:
1186: $result = TrinaryLogic::createMaybe();
1187: }
1188:
1189: // Unsealed extras (zero-or-more additional entries) can never make a
1190: // hit definite — they're uncertain by construction. They only matter
1191: // when no explicit key matched ($result is No): if the unsealed key
1192: // range overlaps the offset, upgrade No → Maybe. Explicit keys take
1193: // precedence at any slot they cover (PHP keys are unique), so a
1194: // non-No $result already reflects the strongest answer the unsealed
1195: // extras could contribute.
1196: if ($result->no() && $this->isUnsealed()->yes() && $this->unsealed !== null) {
1197: [$unsealedKeyType] = $this->unsealed;
1198: if (!$unsealedKeyType->isSuperTypeOf($offsetType)->no()) {
1199: $result = TrinaryLogic::createMaybe();
1200: }
1201: }
1202:
1203: return $result;
1204: }
1205:
1206: public function getOffsetValueType(Type $offsetType): Type
1207: {
1208: if (count($this->keyTypes) === 0 && !$this->isUnsealed()->yes()) {
1209: return new ErrorType();
1210: }
1211:
1212: $offsetType = $offsetType->toArrayKey();
1213: $matchingValueTypes = [];
1214: $all = true;
1215: $maybeAll = true;
1216: foreach ($this->keyTypes as $i => $keyType) {
1217: if ($keyType->isSuperTypeOf($offsetType)->no()) {
1218: $all = false;
1219:
1220: if (
1221: $keyType instanceof ConstantIntegerType
1222: && !$offsetType->isString()->no()
1223: && $offsetType->isConstantScalarValue()->no()
1224: ) {
1225: continue;
1226: }
1227: $maybeAll = false;
1228: continue;
1229: }
1230:
1231: $matchingValueTypes[] = $this->valueTypes[$i];
1232: }
1233:
1234: // Unsealed extras describe entries at keys NOT in the explicit set —
1235: // PHP array keys are unique, so an explicit key fully owns its slot.
1236: // Only include the unsealed value when the offset has parts not
1237: // covered by any explicit key AND those parts overlap the unsealed
1238: // key range.
1239: if ($this->isUnsealed()->yes() && $this->unsealed !== null) {
1240: [$unsealedKeyType, $unsealedValueType] = $this->unsealed;
1241: if (!$this->getKeyTypesUnion()->isSuperTypeOf($offsetType)->yes() && !$unsealedKeyType->isSuperTypeOf($offsetType)->no()) {
1242: $matchingValueTypes[] = $unsealedValueType;
1243: }
1244: }
1245:
1246: if ($all && !$this->isUnsealed()->yes()) {
1247: return $this->getIterableValueType();
1248: }
1249:
1250: if (count($matchingValueTypes) > 0) {
1251: $type = TypeCombinator::union(...$matchingValueTypes);
1252: if ($type instanceof ErrorType) {
1253: return new MixedType();
1254: }
1255:
1256: return $type;
1257: }
1258:
1259: if ($maybeAll) {
1260: return $this->getIterableValueType();
1261: }
1262:
1263: return new ErrorType(); // undefined offset
1264: }
1265:
1266: public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $unionValues = true): Type
1267: {
1268: if ($offsetType === null && count($this->nextAutoIndexes) === 0) {
1269: return new ErrorType();
1270: }
1271:
1272: $builder = ConstantArrayTypeBuilder::createFromConstantArray($this);
1273: $builder->setOffsetValueType($offsetType, $valueType);
1274:
1275: return $builder->getArray();
1276: }
1277:
1278: public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type
1279: {
1280: $builder = ConstantArrayTypeBuilder::createFromConstantArray($this);
1281: $builder->setOffsetValueType($offsetType, $valueType);
1282:
1283: return $builder->getArray();
1284: }
1285:
1286: /**
1287: * Removes or marks as optional the key(s) matching the given offset type from this constant array.
1288: *
1289: * By default, the method assumes an actual `unset()` call was made, which actively modifies the
1290: * array and weakens its list certainty to "maybe". However, in some contexts, such as the else
1291: * branch of an array_key_exists() check, the key is statically known to be absent without any
1292: * modification, so list certainty should be preserved as-is.
1293: */
1294: public function unsetOffset(Type $offsetType, bool $preserveListCertainty = false): Type
1295: {
1296: $offsetType = $offsetType->toArrayKey();
1297: if ($offsetType instanceof ConstantIntegerType || $offsetType instanceof ConstantStringType) {
1298: foreach ($this->keyTypes as $i => $keyType) {
1299: if ($keyType->getValue() !== $offsetType->getValue()) {
1300: continue;
1301: }
1302:
1303: $keyTypes = $this->keyTypes;
1304: unset($keyTypes[$i]);
1305: $valueTypes = $this->valueTypes;
1306: unset($valueTypes[$i]);
1307:
1308: $newKeyTypes = [];
1309: $newValueTypes = [];
1310: $newOptionalKeys = [];
1311:
1312: $k = 0;
1313: foreach ($keyTypes as $j => $newKeyType) {
1314: $newKeyTypes[] = $newKeyType;
1315: $newValueTypes[] = $valueTypes[$j];
1316: if (in_array($j, $this->optionalKeys, true)) {
1317: $newOptionalKeys[] = $k;
1318: }
1319: $k++;
1320: }
1321:
1322: $newIsList = self::isListAfterUnset(
1323: $newKeyTypes,
1324: $newOptionalKeys,
1325: $this->isList,
1326: in_array($i, $this->optionalKeys, true),
1327: );
1328: if (!$preserveListCertainty) {
1329: $newIsList = $newIsList->and(TrinaryLogic::createMaybe());
1330: } elseif ($this->isList->yes() && $newIsList->no()) {
1331: return new NeverType();
1332: }
1333:
1334: return $this->recreate($newKeyTypes, $newValueTypes, $this->nextAutoIndexes, $newOptionalKeys, $newIsList, $this->unsealed);
1335: }
1336:
1337: return $this;
1338: }
1339:
1340: $constantScalars = $offsetType->getConstantScalarTypes();
1341: if (count($constantScalars) > 0) {
1342: $optionalKeys = $this->optionalKeys;
1343:
1344: $arrayHasChanged = false;
1345: foreach ($constantScalars as $constantScalar) {
1346: $constantScalar = $constantScalar->toArrayKey();
1347: if (!$constantScalar instanceof ConstantIntegerType && !$constantScalar instanceof ConstantStringType) {
1348: continue;
1349: }
1350:
1351: foreach ($this->keyTypes as $i => $keyType) {
1352: if ($keyType->getValue() !== $constantScalar->getValue()) {
1353: continue;
1354: }
1355:
1356: $arrayHasChanged = true;
1357: if (in_array($i, $optionalKeys, true)) {
1358: continue 2;
1359: }
1360:
1361: $optionalKeys[] = $i;
1362: }
1363: }
1364:
1365: if (!$arrayHasChanged) {
1366: return $this;
1367: }
1368:
1369: $newIsList = self::isListAfterUnset(
1370: $this->keyTypes,
1371: $optionalKeys,
1372: $this->isList,
1373: count($optionalKeys) === count($this->optionalKeys),
1374: );
1375: if (!$preserveListCertainty) {
1376: $newIsList = $newIsList->and(TrinaryLogic::createMaybe());
1377: }
1378:
1379: return $this->recreate($this->keyTypes, $this->valueTypes, $this->nextAutoIndexes, $optionalKeys, $newIsList, $this->unsealed);
1380: }
1381:
1382: $optionalKeys = $this->optionalKeys;
1383: $arrayHasChanged = false;
1384: foreach ($this->keyTypes as $i => $keyType) {
1385: if (!$offsetType->isSuperTypeOf($keyType)->yes()) {
1386: continue;
1387: }
1388: $arrayHasChanged = true;
1389: $optionalKeys[] = $i;
1390: }
1391: $optionalKeys = array_values(array_unique($optionalKeys));
1392:
1393: if (!$arrayHasChanged) {
1394: return $this;
1395: }
1396:
1397: $newIsList = self::isListAfterUnset(
1398: $this->keyTypes,
1399: $optionalKeys,
1400: $this->isList,
1401: count($optionalKeys) === count($this->optionalKeys),
1402: );
1403: if (!$preserveListCertainty) {
1404: $newIsList = $newIsList->and(TrinaryLogic::createMaybe());
1405: } elseif ($this->isList->yes() && $newIsList->no()) {
1406: return new NeverType();
1407: }
1408:
1409: return $this->recreate($this->keyTypes, $this->valueTypes, $this->nextAutoIndexes, $optionalKeys, $newIsList, $this->unsealed);
1410: }
1411:
1412: /**
1413: * When we're unsetting something not on the array, it will be untouched,
1414: * So the nextAutoIndexes won't change, and the array might still be a list even with PHPStan definition.
1415: *
1416: * @param list<ConstantIntegerType|ConstantStringType> $newKeyTypes
1417: * @param int[] $newOptionalKeys
1418: */
1419: private static function isListAfterUnset(array $newKeyTypes, array $newOptionalKeys, TrinaryLogic $arrayIsList, bool $unsetOptionalKey): TrinaryLogic
1420: {
1421: if (!$unsetOptionalKey || $arrayIsList->no()) {
1422: return TrinaryLogic::createNo();
1423: }
1424:
1425: $isListOnlyIfKeysAreOptional = false;
1426: foreach ($newKeyTypes as $k2 => $newKeyType2) {
1427: if (!$newKeyType2 instanceof ConstantIntegerType || $newKeyType2->getValue() !== $k2) {
1428: // We found a non-optional key that implies that the array is never a list.
1429: if (!in_array($k2, $newOptionalKeys, true)) {
1430: return TrinaryLogic::createNo();
1431: }
1432:
1433: // The array can still be a list if all the following keys are also optional.
1434: $isListOnlyIfKeysAreOptional = true;
1435: continue;
1436: }
1437:
1438: if ($isListOnlyIfKeysAreOptional && !in_array($k2, $newOptionalKeys, true)) {
1439: return TrinaryLogic::createNo();
1440: }
1441: }
1442:
1443: return $arrayIsList;
1444: }
1445:
1446: public function chunkArray(Type $lengthType, TrinaryLogic $preserveKeys): Type
1447: {
1448: // With real unsealed extras, we can't precisely enumerate the
1449: // chunks — the source has an unknown number of extras that
1450: // could form additional partial or full chunks. Fall back to
1451: // the general `list<chunk<sourceValues>>` shape produced by
1452: // the trait, which is correct (just less precise).
1453: if ($this->isUnsealed()->yes()) {
1454: return $this->traitChunkArray($lengthType, $preserveKeys);
1455: }
1456:
1457: $biggerOne = IntegerRangeType::fromInterval(1, null);
1458: $finiteTypes = $lengthType->getFiniteTypes();
1459: if ($biggerOne->isSuperTypeOf($lengthType)->yes() && count($finiteTypes) < self::CHUNK_FINITE_TYPES_LIMIT) {
1460: $results = [];
1461: foreach ($finiteTypes as $finiteType) {
1462: if (!$finiteType instanceof ConstantIntegerType || $finiteType->getValue() < 1) {
1463: return $this->traitChunkArray($lengthType, $preserveKeys);
1464: }
1465:
1466: $length = $finiteType->getValue();
1467:
1468: $builder = ConstantArrayTypeBuilder::createEmpty();
1469:
1470: $keyTypesCount = count($this->keyTypes);
1471: for ($i = 0; $i < $keyTypesCount; $i += $length) {
1472: $chunk = $this->sliceArray(new ConstantIntegerType($i), new ConstantIntegerType($length), TrinaryLogic::createYes());
1473: $builder->setOffsetValueType(null, $preserveKeys->yes() ? $chunk : $chunk->getValuesArray());
1474: }
1475:
1476: $results[] = $builder->getArray();
1477: }
1478:
1479: return TypeCombinator::union(...$results);
1480: }
1481:
1482: return $this->traitChunkArray($lengthType, $preserveKeys);
1483: }
1484:
1485: public function fillKeysArray(Type $valueType): Type
1486: {
1487: $builder = ConstantArrayTypeBuilder::createEmpty();
1488:
1489: foreach ($this->valueTypes as $i => $keyType) {
1490: if ($keyType->isInteger()->no()) {
1491: $stringKeyType = $keyType->toString();
1492: if ($stringKeyType instanceof ErrorType) {
1493: return $stringKeyType;
1494: }
1495:
1496: $builder->setOffsetValueType($stringKeyType, $valueType, $this->isOptionalKey($i));
1497: } else {
1498: $builder->setOffsetValueType($keyType, $valueType, $this->isOptionalKey($i));
1499: }
1500: }
1501:
1502: if ($this->isUnsealed()->yes() && $this->unsealed !== null) {
1503: [, $unsealedValue] = $this->unsealed;
1504: $builder->makeUnsealed($unsealedValue->toArrayKey(), $valueType);
1505: }
1506:
1507: return $builder->getArray();
1508: }
1509:
1510: public function flipArray(): Type
1511: {
1512: $builder = ConstantArrayTypeBuilder::createEmpty();
1513:
1514: foreach ($this->keyTypes as $i => $keyType) {
1515: $valueType = $this->valueTypes[$i];
1516: $builder->setOffsetValueType(
1517: $valueType->toArrayKey(),
1518: $keyType,
1519: $this->isOptionalKey($i),
1520: );
1521: }
1522:
1523: if ($this->isUnsealed()->yes() && $this->unsealed !== null) {
1524: [$unsealedKey, $unsealedValue] = $this->unsealed;
1525: $builder->makeUnsealed($unsealedValue->toArrayKey(), $unsealedKey);
1526: }
1527:
1528: return $builder->getArray();
1529: }
1530:
1531: public function intersectKeyArray(Type $otherArraysType): Type
1532: {
1533: $builder = ConstantArrayTypeBuilder::createEmpty();
1534:
1535: foreach ($this->keyTypes as $i => $keyType) {
1536: $valueType = $this->valueTypes[$i];
1537: $has = $otherArraysType->hasOffsetValueType($keyType);
1538: if ($has->no()) {
1539: continue;
1540: }
1541: $builder->setOffsetValueType($keyType, $valueType, $this->isOptionalKey($i) || !$has->yes());
1542: }
1543:
1544: if ($this->isUnsealed()->yes() && $this->unsealed !== null) {
1545: [$unsealedKey, $unsealedValue] = $this->unsealed;
1546: // An unsealed extra at key K survives only if `$other` can
1547: // also have key K. Narrow the unsealed key to the intersection
1548: // of our extras-range and `$other`'s key type. If they don't
1549: // overlap, the unsealed slot is dropped.
1550: $narrowedKey = TypeCombinator::intersect($unsealedKey, $otherArraysType->getIterableKeyType());
1551: if (!$narrowedKey instanceof NeverType) {
1552: $builder->makeUnsealed($narrowedKey, $unsealedValue);
1553: }
1554: }
1555:
1556: return $builder->getArray();
1557: }
1558:
1559: public function popArray(): Type
1560: {
1561: return $this->removeLastElements(1);
1562: }
1563:
1564: public function reverseArray(TrinaryLogic $preserveKeys): Type
1565: {
1566: $builder = ConstantArrayTypeBuilder::createEmpty();
1567:
1568: for ($i = count($this->keyTypes) - 1; $i >= 0; $i--) {
1569: $offsetType = $preserveKeys->yes() || $this->keyTypes[$i]->isInteger()->no()
1570: ? $this->keyTypes[$i]
1571: : null;
1572: $builder->setOffsetValueType($offsetType, $this->valueTypes[$i], $this->isOptionalKey($i));
1573: }
1574:
1575: if ($this->isUnsealed()->yes() && $this->unsealed !== null) {
1576: // `array_reverse` only permutes positions; the unsealed slot
1577: // is "zero or more extras at unspecified positions" both
1578: // before and after.
1579: [$unsealedKey, $unsealedValue] = $this->unsealed;
1580: $builder->makeUnsealed($unsealedKey, $unsealedValue);
1581: }
1582:
1583: return $builder->getArray();
1584: }
1585:
1586: public function searchArray(Type $needleType, ?TrinaryLogic $strict = null): Type
1587: {
1588: $strict ??= TrinaryLogic::createMaybe();
1589: $matches = [];
1590: $hasIdenticalValue = false;
1591:
1592: foreach ($this->valueTypes as $index => $valueType) {
1593: if ($strict->yes()) {
1594: $isNeedleSuperType = $valueType->isSuperTypeOf($needleType);
1595: if ($isNeedleSuperType->no()) {
1596: continue;
1597: }
1598: }
1599:
1600: if ($needleType instanceof ConstantScalarType && $valueType instanceof ConstantScalarType) {
1601: // @phpstan-ignore equal.notAllowed
1602: $isLooseEqual = $needleType->getValue() == $valueType->getValue(); // phpcs:ignore
1603: if (!$isLooseEqual) {
1604: continue;
1605: }
1606: if (
1607: ($strict->no() || $needleType->getValue() === $valueType->getValue())
1608: && !$this->isOptionalKey($index)
1609: ) {
1610: $hasIdenticalValue = true;
1611: }
1612: }
1613:
1614: $matches[] = $this->keyTypes[$index];
1615: }
1616:
1617: // Unsealed extras can host additional entries beyond the explicit
1618: // keys, so the search may also find the needle there. The unsealed
1619: // extras' presence is uncertain by definition (zero or more
1620: // entries), so they can never make the needle "definitely found"
1621: // (`hasIdenticalValue` stays false) — `false` always remains a
1622: // possible result.
1623: if ($this->isUnsealed()->yes() && $this->unsealed !== null) {
1624: [$unsealedKeyType, $unsealedValueType] = $this->unsealed;
1625: $considerUnsealed = true;
1626: if ($strict->yes()) {
1627: $considerUnsealed = !$unsealedValueType->isSuperTypeOf($needleType)->no();
1628: }
1629: if ($considerUnsealed) {
1630: $matches[] = $unsealedKeyType;
1631: }
1632: }
1633:
1634: if (count($matches) > 0) {
1635: if ($hasIdenticalValue) {
1636: return TypeCombinator::union(...$matches);
1637: }
1638:
1639: return TypeCombinator::union(new ConstantBooleanType(false), ...$matches);
1640: }
1641:
1642: return new ConstantBooleanType(false);
1643: }
1644:
1645: public function shiftArray(): Type
1646: {
1647: return $this->removeFirstElements(1);
1648: }
1649:
1650: public function shuffleArray(): Type
1651: {
1652: return $this->getValuesArray()->degradeToGeneralArray();
1653: }
1654:
1655: public function sliceArray(Type $offsetType, Type $lengthType, TrinaryLogic $preserveKeys): Type
1656: {
1657: $keyTypesCount = count($this->keyTypes);
1658: if ($keyTypesCount === 0) {
1659: return $this;
1660: }
1661:
1662: $offset = $offsetType instanceof ConstantIntegerType ? $offsetType->getValue() : null;
1663:
1664: if ($lengthType instanceof ConstantIntegerType) {
1665: $length = $lengthType->getValue();
1666: } elseif ($lengthType->isNull()->yes()) {
1667: $length = $keyTypesCount;
1668: } else {
1669: $length = null;
1670: }
1671:
1672: if ($offset === null || $length === null) {
1673: return $this->degradeToGeneralArray()
1674: ->sliceArray($offsetType, $lengthType, $preserveKeys);
1675: }
1676:
1677: if ($keyTypesCount + $offset <= 0) {
1678: // A negative offset cannot reach left outside the array twice
1679: $offset = 0;
1680: }
1681:
1682: if ($keyTypesCount + $length <= 0) {
1683: // A negative length cannot reach left outside the array twice
1684: $length = 0;
1685: }
1686:
1687: if ($length === 0 || ($offset < 0 && $length < 0 && $offset - $length >= 0)) {
1688: // 0 / 0, 3 / 0 or e.g. -3 / -3 or -3 / -4 and so on never extract anything
1689: return $this->recreate([], [], [0], [], null, [new NeverType(true), new NeverType(true)]);
1690: }
1691:
1692: if ($length < 0) {
1693: // Negative lengths prevent access to the most right n elements
1694: return $this->removeLastElements($length * -1)
1695: ->sliceArray($offsetType, new NullType(), $preserveKeys);
1696: }
1697:
1698: if ($offset < 0) {
1699: /*
1700: * Transforms the problem with the negative offset in one with a positive offset using array reversion.
1701: * The reason is below handling of optional keys which works only from left to right.
1702: *
1703: * e.g.
1704: * array{a: 0, b: 1, c: 2, d: 3, e: 4}
1705: * with offset -4 and length 2 (which would be sliced to array{b: 1, c: 2})
1706: *
1707: * is transformed via reversion to
1708: *
1709: * array{e: 4, d: 3, c: 2, b: 1, a: 0}
1710: * with offset 2 and length 2 (which will be sliced to array{c: 2, b: 1} and then reversed again)
1711: */
1712: $offset *= -1;
1713: $reversedLength = min($length, $offset);
1714: $reversedOffset = $offset - $reversedLength;
1715: return $this->reverseArray(TrinaryLogic::createYes())
1716: ->sliceArray(new ConstantIntegerType($reversedOffset), new ConstantIntegerType($reversedLength), $preserveKeys)
1717: ->reverseArray(TrinaryLogic::createYes());
1718: }
1719:
1720: if ($offset > 0) {
1721: return $this->removeFirstElements($offset, false)
1722: ->sliceArray(new ConstantIntegerType(0), $lengthType, $preserveKeys);
1723: }
1724:
1725: $builder = ConstantArrayTypeBuilder::createEmpty();
1726:
1727: $nonOptionalElementsCount = 0;
1728: $hasOptional = false;
1729: for ($i = 0; $nonOptionalElementsCount < $length && $i < $keyTypesCount; $i++) {
1730: $isOptional = $this->isOptionalKey($i);
1731: if (!$isOptional) {
1732: $nonOptionalElementsCount++;
1733: } else {
1734: $hasOptional = true;
1735: }
1736:
1737: $isLastElement = $nonOptionalElementsCount >= $length || $i + 1 >= $keyTypesCount;
1738: if ($isLastElement && $length < $keyTypesCount && $hasOptional) {
1739: // If the slice is not full yet, but has at least one optional key
1740: // the last non-optional element is going to be optional.
1741: // Otherwise, it would not fit into the slice if previous non-optional keys are there.
1742: $isOptional = true;
1743: }
1744:
1745: $offsetType = $preserveKeys->yes() || $this->keyTypes[$i]->isInteger()->no()
1746: ? $this->keyTypes[$i]
1747: : null;
1748:
1749: $builder->setOffsetValueType($offsetType, $this->valueTypes[$i], $isOptional);
1750: }
1751:
1752: // When the requested length runs past the explicit keys, the
1753: // missing trailing slots could be filled by the source's
1754: // unsealed extras (or be absent). Carry the unsealed slot
1755: // through so the result still describes those potential extras.
1756: if (
1757: $this->isUnsealed()->yes()
1758: && $this->unsealed !== null
1759: && $nonOptionalElementsCount < $length
1760: ) {
1761: [$unsealedKey, $unsealedValue] = $this->unsealed;
1762: $builder->makeUnsealed($unsealedKey, $unsealedValue);
1763: }
1764:
1765: return $builder->getArray();
1766: }
1767:
1768: public function spliceArray(Type $offsetType, Type $lengthType, Type $replacementType): Type
1769: {
1770: $keyTypesCount = count($this->keyTypes);
1771: if ($keyTypesCount === 0) {
1772: return $this;
1773: }
1774:
1775: $offset = $offsetType instanceof ConstantIntegerType ? $offsetType->getValue() : null;
1776:
1777: if ($lengthType instanceof ConstantIntegerType) {
1778: $length = $lengthType->getValue();
1779: } elseif ($lengthType->isNull()->yes()) {
1780: $length = $keyTypesCount;
1781: } else {
1782: $length = null;
1783: }
1784:
1785: if ($offset === null || $length === null) {
1786: return $this->degradeToGeneralArray()
1787: ->spliceArray($offsetType, $lengthType, $replacementType);
1788: }
1789:
1790: $allKeysInteger = $this->getIterableKeyType()->isInteger()->yes();
1791:
1792: if ($keyTypesCount + $offset <= 0) {
1793: // A negative offset cannot reach left outside the array twice
1794: $offset = 0;
1795: }
1796:
1797: if ($keyTypesCount + $length <= 0) {
1798: // A negative length cannot reach left outside the array twice
1799: $length = 0;
1800: }
1801:
1802: $offsetWasNegative = false;
1803: if ($offset < 0) {
1804: $offsetWasNegative = true;
1805: $offset = $keyTypesCount + $offset;
1806: }
1807:
1808: if ($length < 0) {
1809: $length = $keyTypesCount - $offset + $length;
1810: }
1811:
1812: $extractType = $this->sliceArray($offsetType, $lengthType, TrinaryLogic::createYes());
1813:
1814: $types = [];
1815: foreach ($replacementType->toArray()->getArrays() as $replacementArrayType) {
1816: $removeKeysCount = 0;
1817: $optionalKeysBeforeReplacement = 0;
1818:
1819: $builder = ConstantArrayTypeBuilder::createEmpty();
1820: for ($i = 0;; $i++) {
1821: $isOptional = $this->isOptionalKey($i);
1822:
1823: if (!$offsetWasNegative && $i < $offset && $isOptional) {
1824: $optionalKeysBeforeReplacement++;
1825: }
1826:
1827: if ($i === $offset + $optionalKeysBeforeReplacement) {
1828: // When the offset is reached we have to a) put the replacement array in and b) remove $length elements
1829: $removeKeysCount = $length;
1830:
1831: if ($replacementArrayType instanceof self) {
1832: $valuesArray = $replacementArrayType->getValuesArray();
1833: for ($j = 0, $jMax = count($valuesArray->keyTypes); $j < $jMax; $j++) {
1834: $builder->setOffsetValueType(null, $valuesArray->valueTypes[$j], $valuesArray->isOptionalKey($j));
1835: }
1836: } else {
1837: $builder->degradeToGeneralArray();
1838: $builder->setOffsetValueType($replacementArrayType->getValuesArray()->getIterableKeyType(), $replacementArrayType->getIterableValueType(), true);
1839: }
1840: }
1841:
1842: if (!isset($this->keyTypes[$i])) {
1843: break;
1844: }
1845:
1846: if ($removeKeysCount > 0) {
1847: $extractTypeHasOffsetValueType = $extractType->hasOffsetValueType($this->keyTypes[$i]);
1848:
1849: if (
1850: (!$isOptional && $extractTypeHasOffsetValueType->yes())
1851: || ($isOptional && $extractTypeHasOffsetValueType->maybe())
1852: ) {
1853: $removeKeysCount--;
1854: continue;
1855: }
1856: }
1857:
1858: if (!$isOptional && $extractType->hasOffsetValueType($this->keyTypes[$i])->maybe()) {
1859: $isOptional = true;
1860: }
1861:
1862: $builder->setOffsetValueType(
1863: $this->keyTypes[$i]->isInteger()->no() ? $this->keyTypes[$i] : null,
1864: $this->valueTypes[$i],
1865: $isOptional,
1866: );
1867: }
1868:
1869: // `array_splice` removes a slice at an explicit offset and
1870: // inserts a replacement there. Real unsealed extras live at
1871: // positions past the explicit keys, so they're unaffected
1872: // by the operation (re-indexing of int keys keeps the
1873: // `<int, V>` range intact). Carry the slot through.
1874: if ($this->isUnsealed()->yes() && $this->unsealed !== null) {
1875: [$unsealedKey, $unsealedValue] = $this->unsealed;
1876: $builder->makeUnsealed($unsealedKey, $unsealedValue);
1877: }
1878:
1879: $builtType = $builder->getArray();
1880: if ($allKeysInteger && !$builtType->isList()->yes()) {
1881: $builtType = TypeCombinator::intersect($builtType, new AccessoryArrayListType());
1882: }
1883: $types[] = $builtType;
1884: }
1885:
1886: return TypeCombinator::union(...$types);
1887: }
1888:
1889: public function truncateListToSize(Type $sizeType): Type
1890: {
1891: [$min, $max] = self::extractTruncateListBounds($sizeType);
1892:
1893: // `getMin() === null` ↔ unbounded below; the narrowing has no anchor
1894: // to start from. Also bail out when the required prefix would exceed
1895: // the array-shape limit — we can't enumerate that many keys.
1896: // `isList()` is intentionally NOT checked here: the call site
1897: // (`TypeSpecifier`) only invokes this when the *outer* aggregate is
1898: // already a list, but a CAT inside a `non-empty-list` intersection
1899: // may have its own `isList()` weakened to `Maybe`.
1900: if (
1901: $min === null
1902: || $min >= ConstantArrayTypeBuilder::ARRAY_COUNT_LIMIT
1903: || !$this->getKeyType()->isSuperTypeOf(IntegerRangeType::fromInterval(0, ($max ?? $min) - 1))->yes()
1904: ) {
1905: return TypeCombinator::intersect($this, new NonEmptyArrayType());
1906: }
1907:
1908: // Required prefix `[0, $min)`: every value definitely present.
1909: $builderData = [];
1910: for ($i = 0; $i < $min; $i++) {
1911: $offsetType = new ConstantIntegerType($i);
1912: $builderData[] = [$offsetType, $this->getOffsetValueType($offsetType), false];
1913: }
1914:
1915: if ($max !== null) {
1916: // Optional middle `[$min, $max)`.
1917: if ($max - $min > ConstantArrayTypeBuilder::ARRAY_COUNT_LIMIT) {
1918: return TypeCombinator::intersect($this, new NonEmptyArrayType());
1919: }
1920: for ($i = $min; $i < $max; $i++) {
1921: $offsetType = new ConstantIntegerType($i);
1922: $builderData[] = [$offsetType, $this->getOffsetValueType($offsetType), true];
1923: }
1924: } else {
1925: // Unbounded max: probe explicit keys from `$min` onward until
1926: // `hasOffsetValueType` answers `no`. Each probe contributes one
1927: // optional (or required, when `hasOffsetValueType` is `yes`) slot.
1928: $isUnsealed = $this->isUnsealed()->yes();
1929: for ($i = $min;; $i++) {
1930: $offsetType = new ConstantIntegerType($i);
1931: $hasOffset = $this->hasOffsetValueType($offsetType);
1932: if ($hasOffset->no()) {
1933: break;
1934: }
1935: // Real unsealed extras make `hasOffsetValueType` answer
1936: // `Maybe` for *any* in-range key, so the probe would
1937: // otherwise run until `ARRAY_COUNT_LIMIT` bails (slow +
1938: // lossy). Stop once the explicit keys are exhausted; the
1939: // unsealed slot attached below covers further entries.
1940: if ($isUnsealed && !$hasOffset->yes()) {
1941: break;
1942: }
1943: $builderData[] = [$offsetType, $this->getOffsetValueType($offsetType), !$hasOffset->yes()];
1944: }
1945: }
1946:
1947: if (count($builderData) > ConstantArrayTypeBuilder::ARRAY_COUNT_LIMIT) {
1948: return TypeCombinator::intersect($this, new NonEmptyArrayType());
1949: }
1950:
1951: $builder = ConstantArrayTypeBuilder::createEmpty();
1952: foreach ($builderData as [$offsetType, $valueType, $optional]) {
1953: $builder->setOffsetValueType($offsetType, $valueType, $optional);
1954: }
1955:
1956: // Carry the unsealed slot through only for the unbounded-max
1957: // branch — a bounded-max range caps the result size and the
1958: // unsealed extras can't fit.
1959: if ($max === null && $this->isUnsealed()->yes() && $this->unsealed !== null) {
1960: $builder->makeUnsealed($this->unsealed[0], $this->unsealed[1]);
1961: }
1962:
1963: $builtArray = $builder->getArray();
1964: // `setOffsetValueType` on a brand-new builder produces a list when
1965: // the resulting offsets are sequential ints — but it may not preserve
1966: // list-ness in every shape. Reattach it for the single-CAT case.
1967: if (!$builder->isList()) {
1968: $constantArrays = $builtArray->getConstantArrays();
1969: if (count($constantArrays) === 1) {
1970: $builtArray = $constantArrays[0]->makeList();
1971: }
1972: }
1973:
1974: return $builtArray;
1975: }
1976:
1977: /**
1978: * Extracts (min, max) bounds from a size type for `truncateListToSize`.
1979: * `ConstantIntegerType(N)` → `[N, N]`. `IntegerRangeType` →
1980: * `[$min, $max]`. Anything else returns `[null, null]` and the caller
1981: * falls back to the non-precise path.
1982: *
1983: * @return array{?int, ?int}
1984: */
1985: public static function extractTruncateListBounds(Type $sizeType): array
1986: {
1987: if ($sizeType instanceof ConstantIntegerType) {
1988: return [$sizeType->getValue(), $sizeType->getValue()];
1989: }
1990:
1991: if ($sizeType instanceof IntegerRangeType) {
1992: return [$sizeType->getMin(), $sizeType->getMax()];
1993: }
1994:
1995: return [null, null];
1996: }
1997:
1998: public function isIterableAtLeastOnce(): TrinaryLogic
1999: {
2000: $keysCount = count($this->keyTypes);
2001: if ($keysCount === 0) {
2002: if (!$this->isUnsealed()->yes()) {
2003: return TrinaryLogic::createNo();
2004: }
2005: return TrinaryLogic::createMaybe();
2006: }
2007:
2008: $optionalKeysCount = count($this->optionalKeys);
2009: if ($optionalKeysCount < $keysCount) {
2010: return TrinaryLogic::createYes();
2011: }
2012:
2013: return TrinaryLogic::createMaybe();
2014: }
2015:
2016: public function getArraySize(): Type
2017: {
2018: $optionalKeysCount = count($this->optionalKeys);
2019: $totalKeysCount = count($this->getKeyTypes());
2020: if (!$this->isUnsealed()->yes()) {
2021: if ($optionalKeysCount === 0) {
2022: return new ConstantIntegerType($totalKeysCount);
2023: }
2024: $max = $totalKeysCount;
2025: } else {
2026: $max = null;
2027: }
2028:
2029: return IntegerRangeType::fromInterval($totalKeysCount - $optionalKeysCount, $max);
2030: }
2031:
2032: public function getFirstIterableKeyType(): Type
2033: {
2034: $keyTypes = [];
2035: foreach ($this->keyTypes as $i => $keyType) {
2036: $keyTypes[] = $keyType;
2037: if (!$this->isOptionalKey($i)) {
2038: break;
2039: }
2040: }
2041:
2042: if ($this->isUnsealed()->yes() && $this->unsealed !== null) {
2043: $unsealedKeyType = $this->unsealed[0];
2044: if ($unsealedKeyType instanceof MixedType && !$unsealedKeyType instanceof TemplateMixedType) {
2045: $unsealedKeyType = (new BenevolentUnionType([new IntegerType(), new StringType()]))->toArrayKey();
2046: } elseif ($unsealedKeyType instanceof StrictMixedType && !$unsealedKeyType instanceof TemplateStrictMixedType) {
2047: $unsealedKeyType = (new BenevolentUnionType([new IntegerType(), new StringType()]))->toArrayKey();
2048: }
2049: $keyTypes[] = $unsealedKeyType;
2050: }
2051:
2052: return TypeCombinator::union(...$keyTypes);
2053: }
2054:
2055: public function getLastIterableKeyType(): Type
2056: {
2057: $keyTypes = [];
2058: for ($i = count($this->keyTypes) - 1; $i >= 0; $i--) {
2059: $keyTypes[] = $this->keyTypes[$i];
2060: if (!$this->isOptionalKey($i)) {
2061: break;
2062: }
2063: }
2064:
2065: if ($this->isUnsealed()->yes() && $this->unsealed !== null) {
2066: $unsealedKeyType = $this->unsealed[0];
2067: if ($unsealedKeyType instanceof MixedType && !$unsealedKeyType instanceof TemplateMixedType) {
2068: $unsealedKeyType = (new BenevolentUnionType([new IntegerType(), new StringType()]))->toArrayKey();
2069: } elseif ($unsealedKeyType instanceof StrictMixedType && !$unsealedKeyType instanceof TemplateStrictMixedType) {
2070: $unsealedKeyType = (new BenevolentUnionType([new IntegerType(), new StringType()]))->toArrayKey();
2071: }
2072: $keyTypes[] = $unsealedKeyType;
2073: }
2074:
2075: return TypeCombinator::union(...$keyTypes);
2076: }
2077:
2078: public function getFirstIterableValueType(): Type
2079: {
2080: $valueTypes = [];
2081: foreach ($this->valueTypes as $i => $valueType) {
2082: $valueTypes[] = $valueType;
2083: if (!$this->isOptionalKey($i)) {
2084: break;
2085: }
2086: }
2087:
2088: if ($this->isUnsealed()->yes() && $this->unsealed !== null) {
2089: $valueTypes[] = $this->unsealed[1];
2090: }
2091:
2092: return TypeCombinator::union(...$valueTypes);
2093: }
2094:
2095: public function getLastIterableValueType(): Type
2096: {
2097: $valueTypes = [];
2098: for ($i = count($this->keyTypes) - 1; $i >= 0; $i--) {
2099: $valueTypes[] = $this->valueTypes[$i];
2100: if (!$this->isOptionalKey($i)) {
2101: break;
2102: }
2103: }
2104:
2105: if ($this->isUnsealed()->yes() && $this->unsealed !== null) {
2106: $valueTypes[] = $this->unsealed[1];
2107: }
2108:
2109: return TypeCombinator::union(...$valueTypes);
2110: }
2111:
2112: public function isConstantArray(): TrinaryLogic
2113: {
2114: return TrinaryLogic::createYes();
2115: }
2116:
2117: public function isList(): TrinaryLogic
2118: {
2119: return $this->isList;
2120: }
2121:
2122: /** @param positive-int $length */
2123: private function removeLastElements(int $length): self
2124: {
2125: $keyTypesCount = count($this->keyTypes);
2126: if ($keyTypesCount === 0) {
2127: return $this;
2128: }
2129:
2130: // With real unsealed extras on the source, the elements being
2131: // "removed" might come from the unsealed range rather than from
2132: // the trailing explicit keys — the array might have zero extras
2133: // (so the trailing explicit keys are popped) or one+ extras (so
2134: // they're popped instead, leaving the explicit keys intact).
2135: // Encode this by marking the trailing keys as optional and
2136: // keeping the unsealed slot in place.
2137: if ($this->isUnsealed()->yes()) {
2138: $optionalKeys = $this->optionalKeys;
2139: $newLength = $keyTypesCount - $length;
2140: for ($i = $keyTypesCount - 1; $i >= max($newLength, 0); $i--) {
2141: if (in_array($i, $optionalKeys, true)) {
2142: continue;
2143: }
2144: $optionalKeys[] = $i;
2145: }
2146:
2147: return $this->recreate(
2148: $this->keyTypes,
2149: $this->valueTypes,
2150: $this->nextAutoIndexes,
2151: array_values($optionalKeys),
2152: $this->isList,
2153: $this->unsealed,
2154: );
2155: }
2156:
2157: $keyTypes = $this->keyTypes;
2158: $valueTypes = $this->valueTypes;
2159: $optionalKeys = $this->optionalKeys;
2160: $nextAutoindexes = $this->nextAutoIndexes;
2161:
2162: $optionalKeysRemoved = 0;
2163: $newLength = $keyTypesCount - $length;
2164: for ($i = $keyTypesCount - 1; $i >= 0; $i--) {
2165: $isOptional = $this->isOptionalKey($i);
2166:
2167: if ($i >= $newLength) {
2168: if ($isOptional) {
2169: $optionalKeysRemoved++;
2170: foreach ($optionalKeys as $key => $value) {
2171: if ($value === $i) {
2172: unset($optionalKeys[$key]);
2173: break;
2174: }
2175: }
2176: }
2177:
2178: $removedKeyType = array_pop($keyTypes);
2179: array_pop($valueTypes);
2180: $nextAutoindexes = $removedKeyType instanceof ConstantIntegerType
2181: ? [$removedKeyType->getValue()]
2182: : $this->nextAutoIndexes;
2183: continue;
2184: }
2185:
2186: if ($isOptional || $optionalKeysRemoved <= 0) {
2187: continue;
2188: }
2189:
2190: $optionalKeys[] = $i;
2191: $optionalKeysRemoved--;
2192: }
2193:
2194: return $this->recreate(
2195: $keyTypes,
2196: $valueTypes,
2197: $nextAutoindexes,
2198: array_values($optionalKeys),
2199: $this->isList,
2200: $this->unsealed,
2201: );
2202: }
2203:
2204: /** @param positive-int $length */
2205: private function removeFirstElements(int $length, bool $reindex = true): Type
2206: {
2207: $builder = ConstantArrayTypeBuilder::createEmpty();
2208:
2209: $optionalKeysIgnored = 0;
2210: foreach ($this->keyTypes as $i => $keyType) {
2211: $isOptional = $this->isOptionalKey($i);
2212: if ($i <= $length - 1) {
2213: if ($isOptional) {
2214: $optionalKeysIgnored++;
2215: }
2216: continue;
2217: }
2218:
2219: if (!$isOptional && $optionalKeysIgnored > 0) {
2220: $isOptional = true;
2221: $optionalKeysIgnored--;
2222: }
2223:
2224: $valueType = $this->valueTypes[$i];
2225: if ($reindex && $keyType instanceof ConstantIntegerType) {
2226: $keyType = null;
2227: }
2228:
2229: $builder->setOffsetValueType($keyType, $valueType, $isOptional);
2230: }
2231:
2232: if ($this->isUnsealed()->yes() && $this->unsealed !== null) {
2233: // `array_shift` removes the *first* element. The explicit
2234: // keys precede the unsealed extras in insertion order, so
2235: // the shift always lands on an explicit key (when there is
2236: // one); the unsealed slot is unaffected. Re-indexing of int
2237: // keys doesn't change the unsealed range — it stays `<int, V>`.
2238: [$unsealedKey, $unsealedValue] = $this->unsealed;
2239: $builder->makeUnsealed($unsealedKey, $unsealedValue);
2240: }
2241:
2242: return $builder->getArray();
2243: }
2244:
2245: public function toBoolean(): BooleanType
2246: {
2247: return $this->getArraySize()->toBoolean();
2248: }
2249:
2250: public function toInteger(): Type
2251: {
2252: return $this->toBoolean()->toInteger();
2253: }
2254:
2255: public function toFloat(): Type
2256: {
2257: return $this->toBoolean()->toFloat();
2258: }
2259:
2260: public function generalize(GeneralizePrecision $precision): Type
2261: {
2262: // No explicit keys and no real extras — actually empty, return as-is.
2263: if (count($this->keyTypes) === 0 && !$this->isUnsealed()->yes()) {
2264: return $this;
2265: }
2266:
2267: if ($precision->isTemplateArgument()) {
2268: return $this->traverse(static fn (Type $type) => $type->generalize($precision));
2269: }
2270:
2271: $arrayType = new ArrayType(
2272: $this->getIterableKeyType()->generalize($precision),
2273: $this->getIterableValueType()->generalize($precision),
2274: );
2275:
2276: $keyTypesCount = count($this->keyTypes);
2277: $optionalKeysCount = count($this->optionalKeys);
2278:
2279: $accessoryTypes = [];
2280: if ($precision->isMoreSpecific() && ($keyTypesCount - $optionalKeysCount) < 32) {
2281: foreach ($this->keyTypes as $i => $keyType) {
2282: if ($this->isOptionalKey($i)) {
2283: continue;
2284: }
2285:
2286: $accessoryTypes[] = new HasOffsetValueType($keyType, $this->valueTypes[$i]->generalize($precision));
2287: }
2288: } elseif ($this->isIterableAtLeastOnce()->yes()) {
2289: // Previously gated on `keyTypesCount > optionalKeysCount`,
2290: // which mishandles "no explicit keys + real unsealed
2291: // extras" (`isIterableAtLeastOnce()` answers `Maybe` —
2292: // extras might be empty — and correctly skips
2293: // `NonEmptyArrayType`). The new gate also covers the
2294: // usual sealed-with-required-keys case, so behaviour for
2295: // existing CAT shapes is unchanged.
2296: $accessoryTypes[] = new NonEmptyArrayType();
2297: }
2298:
2299: if ($this->isList()->yes()) {
2300: $arrayType = TypeCombinator::intersect($arrayType, new AccessoryArrayListType());
2301: }
2302:
2303: if (count($accessoryTypes) > 0) {
2304: return TypeCombinator::intersect($arrayType, ...$accessoryTypes);
2305: }
2306:
2307: return $arrayType;
2308: }
2309:
2310: public function generalizeValues(): self
2311: {
2312: $valueTypes = [];
2313: foreach ($this->valueTypes as $valueType) {
2314: $valueTypes[] = $valueType->generalize(GeneralizePrecision::lessSpecific());
2315: }
2316:
2317: $unsealed = $this->unsealed;
2318: if ($unsealed !== null) {
2319: [$unsealedKey, $unsealedValue] = $unsealed;
2320: $unsealed = [$unsealedKey, $unsealedValue->generalize(GeneralizePrecision::lessSpecific())];
2321: }
2322:
2323: return $this->recreate($this->keyTypes, $valueTypes, $this->nextAutoIndexes, $this->optionalKeys, $this->isList, $unsealed);
2324: }
2325:
2326: private function degradeToGeneralArray(): Type
2327: {
2328: $builder = ConstantArrayTypeBuilder::createFromConstantArray($this);
2329: $builder->degradeToGeneralArray();
2330:
2331: return $builder->getArray();
2332: }
2333:
2334: public function getKeysArrayFiltered(Type $filterValueType, TrinaryLogic $strict): Type
2335: {
2336: $keysArray = $this->getKeysOrValuesArray($this->keyTypes, $this->unsealed[0] ?? null);
2337:
2338: return new IntersectionType([
2339: new ArrayType(
2340: IntegerRangeType::createAllGreaterThanOrEqualTo(0),
2341: $keysArray->getIterableValueType(),
2342: ),
2343: new AccessoryArrayListType(),
2344: ]);
2345: }
2346:
2347: public function getKeysArray(): self
2348: {
2349: return $this->getKeysOrValuesArray($this->keyTypes, $this->unsealed[0] ?? null);
2350: }
2351:
2352: public function getValuesArray(): self
2353: {
2354: return $this->getKeysOrValuesArray($this->valueTypes, $this->unsealed[1] ?? null);
2355: }
2356:
2357: /**
2358: * @param array<int, Type> $types
2359: */
2360: private function getKeysOrValuesArray(array $types, ?Type $unsealedSourceType): self
2361: {
2362: $count = count($types);
2363: $autoIndexes = range($count - count($this->optionalKeys), $count);
2364:
2365: // The result is always a list — the source's keys/values are
2366: // numbered sequentially. The new unsealed slot (if the source
2367: // has real extras) describes "zero or more extras at int
2368: // positions >= 0 whose values are the source's unsealed
2369: // key/value type". `int<0, max>` is the conventional unsealed
2370: // key for list-shaped extras; it also enables the short-form
2371: // `<value>` describe.
2372: $resultUnsealed = null;
2373: if ($this->isUnsealed()->yes() && $unsealedSourceType !== null) {
2374: $resultUnsealed = [IntegerRangeType::createAllGreaterThanOrEqualTo(0), $unsealedSourceType];
2375: }
2376:
2377: if ($this->isList->yes()) {
2378: // Optimized version for lists: Assume that if a later key exists, then earlier keys also exist.
2379: $keyTypes = array_map(
2380: static fn (int $i): ConstantIntegerType => new ConstantIntegerType($i),
2381: array_keys($types),
2382: );
2383: return $this->recreate($keyTypes, $types, $autoIndexes, $this->optionalKeys, TrinaryLogic::createYes(), $resultUnsealed);
2384: }
2385:
2386: $keyTypes = [];
2387: $valueTypes = [];
2388: $optionalKeys = [];
2389: $maxIndex = 0;
2390:
2391: foreach ($types as $i => $type) {
2392: $keyTypes[] = new ConstantIntegerType($i);
2393:
2394: if ($this->isOptionalKey($maxIndex)) {
2395: // move $maxIndex to next non-optional key
2396: do {
2397: $maxIndex++;
2398: } while ($maxIndex < $count && $this->isOptionalKey($maxIndex));
2399: }
2400:
2401: if ($i === $maxIndex) {
2402: $valueTypes[] = $type;
2403: } else {
2404: $valueTypes[] = TypeCombinator::union(...array_slice($types, $i, $maxIndex - $i + 1));
2405: if ($maxIndex >= $count) {
2406: $optionalKeys[] = $i;
2407: }
2408: }
2409: $maxIndex++;
2410: }
2411:
2412: return $this->recreate($keyTypes, $valueTypes, $autoIndexes, $optionalKeys, TrinaryLogic::createYes(), $resultUnsealed);
2413: }
2414:
2415: public function describe(VerbosityLevel $level): string
2416: {
2417: $arrayName = $this->shouldBeDescribedAsAList() ? 'list' : 'array';
2418:
2419: $describeValue = function (bool $truncate) use ($level, $arrayName): string {
2420: $items = [];
2421: $values = [];
2422: $exportValuesOnly = true;
2423: foreach ($this->keyTypes as $i => $keyType) {
2424: $valueType = $this->valueTypes[$i];
2425: if ($keyType->getValue() !== $i) {
2426: $exportValuesOnly = false;
2427: }
2428:
2429: $isOptional = $this->isOptionalKey($i);
2430: if ($isOptional) {
2431: $exportValuesOnly = false;
2432: }
2433:
2434: $keyDescription = $keyType->getValue();
2435: if (is_string($keyDescription)) {
2436: if (str_contains($keyDescription, '"')) {
2437: $keyDescription = sprintf('\'%s\'', $keyDescription);
2438: } elseif (str_contains($keyDescription, '\'')) {
2439: $keyDescription = sprintf('"%s"', $keyDescription);
2440: } elseif (!self::isValidIdentifier($keyDescription)) {
2441: $keyDescription = sprintf('\'%s\'', $keyDescription);
2442: }
2443: }
2444:
2445: $valueTypeDescription = $valueType->describe($level);
2446: $items[] = sprintf('%s%s: %s', $keyDescription, $isOptional ? '?' : '', $valueTypeDescription);
2447: $values[] = $valueTypeDescription;
2448: }
2449:
2450: $append = '';
2451: if ($truncate && count($items) > self::DESCRIBE_LIMIT) {
2452: $items = array_slice($items, 0, self::DESCRIBE_LIMIT);
2453: $values = array_slice($values, 0, self::DESCRIBE_LIMIT);
2454: $append = ', ...';
2455: }
2456:
2457: if ($this->isUnsealed()->yes() && $this->unsealed !== null) {
2458: if (count($items) > 0) {
2459: $append .= ', ';
2460: }
2461: $append .= '...';
2462: $keyDescription = $this->unsealed[0]->describe(VerbosityLevel::precise());
2463: $isMixedKeyType = $this->unsealed[0] instanceof MixedType && $keyDescription === 'mixed' && !$this->unsealed[0]->isExplicitMixed();
2464: $isMixedItemType = $this->unsealed[1] instanceof MixedType && $this->unsealed[1]->describe(VerbosityLevel::precise()) === 'mixed' && !$this->unsealed[1]->isExplicitMixed();
2465: if ($isMixedKeyType || ($this->isList()->yes() && $keyDescription === 'int<0, max>')) {
2466: if (!$isMixedItemType) {
2467: $append .= sprintf('<%s>', $this->unsealed[1]->describe($level));
2468: }
2469: } else {
2470: $append .= sprintf('<%s, %s>', $this->unsealed[0]->describe($level), $this->unsealed[1]->describe($level));
2471: }
2472: }
2473:
2474: return sprintf(
2475: '%s{%s%s}',
2476: $arrayName,
2477: implode(', ', $exportValuesOnly ? $values : $items),
2478: $append,
2479: );
2480: };
2481: return $level->handle(
2482: fn (): string => $this->isIterableAtLeastOnce()->no() ? $arrayName : sprintf('%s<%s, %s>', $arrayName, $this->getIterableKeyType()->describe($level), $this->getIterableValueType()->describe($level)),
2483: static fn (): string => $describeValue(true),
2484: static fn (): string => $describeValue(false),
2485: );
2486: }
2487:
2488: private function shouldBeDescribedAsAList(): bool
2489: {
2490: if (!$this->isList->yes()) {
2491: return false;
2492: }
2493:
2494: if (count($this->optionalKeys) === 0) {
2495: return false;
2496: }
2497:
2498: if (count($this->optionalKeys) > 1) {
2499: return true;
2500: }
2501:
2502: return $this->optionalKeys[0] !== count($this->keyTypes) - 1;
2503: }
2504:
2505: public function inferTemplateTypes(Type $receivedType): TemplateTypeMap
2506: {
2507: if ($receivedType instanceof UnionType || $receivedType instanceof IntersectionType) {
2508: return $receivedType->inferTemplateTypesOn($this);
2509: }
2510:
2511: if ($receivedType instanceof self) {
2512: $typeMap = TemplateTypeMap::createEmpty();
2513: foreach ($this->keyTypes as $i => $keyType) {
2514: $valueType = $this->valueTypes[$i];
2515: if ($receivedType->hasOffsetValueType($keyType)->no()) {
2516: continue;
2517: }
2518: $receivedValueType = $receivedType->getOffsetValueType($keyType);
2519: $typeMap = $typeMap->union($valueType->inferTemplateTypes($receivedValueType));
2520: }
2521:
2522: $unsealed = $this->getUnsealedTypes();
2523: if ($unsealed !== null) {
2524: [$unsealedKeyType, $unsealedValueType] = $unsealed;
2525:
2526: // Received's explicit keys not in $this's explicit keys are
2527: // candidates for matching $this's unsealed extras pattern.
2528: // Only contribute when the key type matches; mismatched explicit
2529: // keys are extra entries the parameter wouldn't accept anyway,
2530: // surfaced by the regular argument-type check.
2531: $receivedKeyTypes = $receivedType->getKeyTypes();
2532: $receivedValueTypes = $receivedType->getValueTypes();
2533: foreach ($receivedKeyTypes as $j => $receivedKeyType) {
2534: if ($this->hasOffsetValueType($receivedKeyType)->yes()) {
2535: continue;
2536: }
2537: if (!$unsealedKeyType->isSuperTypeOf($receivedKeyType)->yes()) {
2538: continue;
2539: }
2540: $typeMap = $typeMap->union($unsealedKeyType->inferTemplateTypes($receivedKeyType));
2541: $typeMap = $typeMap->union($unsealedValueType->inferTemplateTypes($receivedValueTypes[$j]));
2542: }
2543:
2544: // Received's own unsealed extras describe "all the rest" — when
2545: // the key type doesn't fit $this's unsealed key pattern there
2546: // is no valid template assignment, so force NEVER.
2547: $receivedUnsealed = $receivedType->getUnsealedTypes();
2548: if ($receivedUnsealed !== null) {
2549: [$receivedUnsealedKey, $receivedUnsealedValue] = $receivedUnsealed;
2550: if ($unsealedKeyType->isSuperTypeOf($receivedUnsealedKey)->no()) {
2551: $typeMap = $typeMap->union($unsealedValueType->inferTemplateTypes(new NeverType()));
2552: } else {
2553: $typeMap = $typeMap->union($unsealedKeyType->inferTemplateTypes($receivedUnsealedKey));
2554: $typeMap = $typeMap->union($unsealedValueType->inferTemplateTypes($receivedUnsealedValue));
2555: }
2556: }
2557: }
2558:
2559: return $typeMap;
2560: }
2561:
2562: if ($receivedType->isArray()->yes()) {
2563: $keyTypeMap = $this->getIterableKeyType()->inferTemplateTypes($receivedType->getIterableKeyType());
2564: $itemTypeMap = $this->getIterableValueType()->inferTemplateTypes($receivedType->getIterableValueType());
2565:
2566: return $keyTypeMap->union($itemTypeMap);
2567: }
2568:
2569: return TemplateTypeMap::createEmpty();
2570: }
2571:
2572: public function getReferencedTemplateTypes(TemplateTypeVariance $positionVariance): array
2573: {
2574: $variance = $positionVariance->compose(TemplateTypeVariance::createCovariant());
2575: $references = [];
2576:
2577: foreach ($this->keyTypes as $type) {
2578: foreach ($type->getReferencedTemplateTypes($variance) as $reference) {
2579: $references[] = $reference;
2580: }
2581: }
2582:
2583: foreach ($this->valueTypes as $type) {
2584: foreach ($type->getReferencedTemplateTypes($variance) as $reference) {
2585: $references[] = $reference;
2586: }
2587: }
2588:
2589: if ($this->unsealed !== null) {
2590: [$unsealedKeyType, $unsealedValueType] = $this->unsealed;
2591: foreach ($unsealedKeyType->getReferencedTemplateTypes($variance) as $reference) {
2592: $references[] = $reference;
2593: }
2594: foreach ($unsealedValueType->getReferencedTemplateTypes($variance) as $reference) {
2595: $references[] = $reference;
2596: }
2597: }
2598:
2599: return $references;
2600: }
2601:
2602: public function tryRemove(Type $typeToRemove): ?Type
2603: {
2604: if ($typeToRemove->isConstantArray()->yes() && $typeToRemove->isIterableAtLeastOnce()->no()) {
2605: return TypeCombinator::intersect($this, new NonEmptyArrayType());
2606: }
2607:
2608: if ($typeToRemove instanceof NonEmptyArrayType) {
2609: return new ConstantArrayType([], []);
2610: }
2611:
2612: if ($typeToRemove instanceof HasOffsetType || $typeToRemove instanceof HasOffsetValueType) {
2613: $unsetResult = $this->unsetOffset($typeToRemove->getOffsetType(), true);
2614: // When the source was definitely a list but the post-unset shape
2615: // definitely isn't (e.g. unsetting a non-optional leading key
2616: // creates a hole), no value of $this could have lacked the
2617: // removed key — the subtraction yields the empty set.
2618: if ($this->isList->yes() && $unsetResult->isList()->no()) {
2619: return new NeverType();
2620: }
2621: return $unsetResult;
2622: }
2623:
2624: return null;
2625: }
2626:
2627: public function traverse(callable $cb): Type
2628: {
2629: $valueTypes = [];
2630:
2631: $stillOriginal = true;
2632: foreach ($this->valueTypes as $valueType) {
2633: $transformedValueType = $cb($valueType);
2634: if ($transformedValueType !== $valueType) {
2635: $stillOriginal = false;
2636: }
2637:
2638: $valueTypes[] = $transformedValueType;
2639: }
2640:
2641: $unsealed = $this->unsealed;
2642: if ($unsealed !== null) {
2643: [$unsealedKeyType, $unsealedValueType] = $unsealed;
2644: $transformedUnsealedValueType = $cb($unsealedValueType);
2645: if ($transformedUnsealedValueType !== $unsealedValueType) {
2646: $stillOriginal = false;
2647: $unsealed = [$unsealedKeyType, $transformedUnsealedValueType];
2648: }
2649: }
2650:
2651: if ($stillOriginal) {
2652: return $this;
2653: }
2654:
2655: return $this->recreate($this->keyTypes, $valueTypes, $this->nextAutoIndexes, $this->optionalKeys, $this->isList, $unsealed);
2656: }
2657:
2658: public function traverseSimultaneously(Type $right, callable $cb): Type
2659: {
2660: if (!$right->isArray()->yes()) {
2661: return $this;
2662: }
2663:
2664: $valueTypes = [];
2665:
2666: $stillOriginal = true;
2667: foreach ($this->valueTypes as $i => $valueType) {
2668: $keyType = $this->keyTypes[$i];
2669: $transformedValueType = $cb($valueType, $right->getOffsetValueType($keyType));
2670: if ($transformedValueType !== $valueType) {
2671: $stillOriginal = false;
2672: }
2673:
2674: $valueTypes[] = $transformedValueType;
2675: }
2676:
2677: $unsealed = $this->unsealed;
2678: if ($unsealed !== null) {
2679: [$unsealedKeyType, $unsealedValueType] = $unsealed;
2680: $transformedUnsealedValueType = $cb($unsealedValueType, $right->getIterableValueType());
2681: if ($transformedUnsealedValueType !== $unsealedValueType) {
2682: $stillOriginal = false;
2683: $unsealed = [$unsealedKeyType, $transformedUnsealedValueType];
2684: }
2685: }
2686:
2687: if ($stillOriginal) {
2688: return $this;
2689: }
2690:
2691: return $this->recreate($this->keyTypes, $valueTypes, $this->nextAutoIndexes, $this->optionalKeys, $this->isList, $unsealed);
2692: }
2693:
2694: public function isKeysSupersetOf(self $otherArray): bool
2695: {
2696: if ($this->unsealed === null || $otherArray->unsealed === null) {
2697: return $this->legacyIsKeysSupersetOf($otherArray);
2698: }
2699:
2700: [$thisUnsealedKey, $thisUnsealedValue] = $this->unsealed;
2701: [$otherUnsealedKey, $otherUnsealedValue] = $otherArray->unsealed;
2702: $thisHasExtras = $this->isUnsealed()->yes();
2703: $otherHasExtras = $otherArray->isUnsealed()->yes();
2704:
2705: $otherHasRequiredKeys = false;
2706: foreach ($otherArray->keyTypes as $j => $keyType) {
2707: if ($otherArray->isOptionalKey($j)) {
2708: continue;
2709: }
2710: $otherHasRequiredKeys = true;
2711: break;
2712: }
2713:
2714: // Sealed empty $other (no keys, no extras): absorbing it is lossless iff $this
2715: // already accepts []. i.e., all of $this's known keys are optional. Otherwise
2716: // merge would add [] as a new instance.
2717: if (!$otherHasRequiredKeys && !$otherHasExtras && count($otherArray->keyTypes) === 0) {
2718: foreach ($this->keyTypes as $i => $keyType) {
2719: if (!$this->isOptionalKey($i)) {
2720: return false;
2721: }
2722: }
2723: return true;
2724: }
2725:
2726: // With real unsealed extras on both sides that can absorb each other's
2727: // required keys, merging is acceptable regardless of which keys overlap.
2728: if ($thisHasExtras && $otherHasExtras) {
2729: return true;
2730: }
2731:
2732: // Asymmetric extras: one side has real extras that can absorb the other's keys.
2733: if ($thisHasExtras) {
2734: if ($this->legacyIsKeysSupersetOf($otherArray)) {
2735: return true;
2736: }
2737: foreach ($otherArray->keyTypes as $j => $keyType) {
2738: if ($otherArray->isOptionalKey($j)) {
2739: continue;
2740: }
2741: if ($thisUnsealedKey->isSuperTypeOf($keyType)->no()) {
2742: return false;
2743: }
2744: if ($thisUnsealedValue->isSuperTypeOf($otherArray->valueTypes[$j])->no()) {
2745: return false;
2746: }
2747: }
2748: return true;
2749: }
2750:
2751: if ($otherHasExtras) {
2752: if ($this->legacyIsKeysSupersetOf($otherArray)) {
2753: return true;
2754: }
2755: foreach ($this->keyTypes as $i => $keyType) {
2756: if ($this->isOptionalKey($i)) {
2757: continue;
2758: }
2759: if ($otherUnsealedKey->isSuperTypeOf($keyType)->no()) {
2760: return false;
2761: }
2762: if ($otherUnsealedValue->isSuperTypeOf($this->valueTypes[$i])->no()) {
2763: return false;
2764: }
2765: }
2766: return true;
2767: }
2768:
2769: // Both sealed: fall back to the legacy key/value shape check.
2770: return $this->legacyIsKeysSupersetOf($otherArray);
2771: }
2772:
2773: private function legacyIsKeysSupersetOf(self $otherArray): bool
2774: {
2775: $keyTypesCount = count($this->keyTypes);
2776: $otherKeyTypesCount = count($otherArray->keyTypes);
2777:
2778: if ($keyTypesCount < $otherKeyTypesCount) {
2779: return false;
2780: }
2781:
2782: if ($otherKeyTypesCount === 0) {
2783: return $keyTypesCount === 0;
2784: }
2785:
2786: $failOnDifferentValueType = $keyTypesCount !== $otherKeyTypesCount || $keyTypesCount < 2;
2787:
2788: $keyIndexMap = $this->getKeyIndexMap();
2789: $otherKeyValues = [];
2790:
2791: foreach ($otherArray->keyTypes as $j => $keyType) {
2792: $keyValue = $keyType->getValue();
2793: $i = $keyIndexMap[$keyValue] ?? null;
2794: if ($i === null) {
2795: return false;
2796: }
2797:
2798: $otherKeyValues[$keyValue] = true;
2799:
2800: $valueType = $this->valueTypes[$i];
2801: $otherValueType = $otherArray->valueTypes[$j];
2802: if (!$otherValueType->isSuperTypeOf($valueType)->no()) {
2803: continue;
2804: }
2805:
2806: if ($failOnDifferentValueType) {
2807: return false;
2808: }
2809: $failOnDifferentValueType = true;
2810: }
2811:
2812: $requiredKeyCount = 0;
2813: foreach ($this->keyTypes as $i => $keyType) {
2814: if (isset($otherKeyValues[$keyType->getValue()])) {
2815: continue;
2816: }
2817: if ($this->isOptionalKey($i)) {
2818: continue;
2819: }
2820:
2821: $requiredKeyCount++;
2822: if ($requiredKeyCount > 1) {
2823: return false;
2824: }
2825: }
2826:
2827: return true;
2828: }
2829:
2830: public function mergeWith(self $otherArray): self
2831: {
2832: // only call this after verifying isKeysSupersetOf, or if losing tagged unions is not an issue
2833: if ($this->unsealed === null || $otherArray->unsealed === null) {
2834: return $this->legacyMergeWith($otherArray);
2835: }
2836:
2837: [$thisUnsealedKey, $thisUnsealedValue] = $this->unsealed;
2838: [$otherUnsealedKey, $otherUnsealedValue] = $otherArray->unsealed;
2839:
2840: $mergedUnsealedKey = TypeCombinator::union($thisUnsealedKey, $otherUnsealedKey);
2841: $mergedUnsealedValue = TypeCombinator::union($thisUnsealedValue, $otherUnsealedValue);
2842:
2843: $absorbIntoExtras = static function (Type $keyType, Type $valueType) use (&$mergedUnsealedKey, &$mergedUnsealedValue): void {
2844: $mergedUnsealedKey = TypeCombinator::union($mergedUnsealedKey, $keyType);
2845: $mergedUnsealedValue = TypeCombinator::union($mergedUnsealedValue, $valueType);
2846: };
2847:
2848: $canAbsorb = static function (self $side, Type $keyType, Type $valueType): bool {
2849: if (!$side->isUnsealed()->yes()) {
2850: return false;
2851: }
2852: if ($side->unsealed === null) {
2853: return false;
2854: }
2855: [$sideUnsealedKey, $sideUnsealedValue] = $side->unsealed;
2856: if ($sideUnsealedKey->isSuperTypeOf($keyType)->no()) {
2857: return false;
2858: }
2859: if ($sideUnsealedValue->isSuperTypeOf($valueType)->no()) {
2860: return false;
2861: }
2862: return true;
2863: };
2864:
2865: $keyTypes = [];
2866: $valueTypes = [];
2867: $optionalKeys = [];
2868: $nextAutoIndexes = [0];
2869:
2870: $otherKeyIndexMap = $otherArray->getKeyIndexMap();
2871: $processed = [];
2872:
2873: foreach ($this->keyTypes as $i => $keyType) {
2874: $keyValue = $keyType->getValue();
2875: $processed[$keyValue] = true;
2876: $valueType = $this->valueTypes[$i];
2877:
2878: if (array_key_exists($keyValue, $otherKeyIndexMap)) {
2879: $j = $otherKeyIndexMap[$keyValue];
2880: $otherValueType = $otherArray->valueTypes[$j];
2881: $mergedValue = TypeCombinator::union($valueType, $otherValueType);
2882: $optional = $this->isOptionalKey($i) || $otherArray->isOptionalKey($j);
2883:
2884: $keyTypes[] = $keyType;
2885: $valueTypes[] = $mergedValue;
2886: if ($optional) {
2887: $optionalKeys[] = count($keyTypes) - 1;
2888: }
2889: continue;
2890: }
2891:
2892: if ($canAbsorb($otherArray, $keyType, $valueType)) {
2893: $absorbIntoExtras($keyType, $valueType);
2894: continue;
2895: }
2896:
2897: $keyTypes[] = $keyType;
2898: $valueTypes[] = $valueType;
2899: $optionalKeys[] = count($keyTypes) - 1;
2900: }
2901:
2902: foreach ($otherArray->keyTypes as $j => $keyType) {
2903: $keyValue = $keyType->getValue();
2904: if (array_key_exists($keyValue, $processed)) {
2905: continue;
2906: }
2907: $valueType = $otherArray->valueTypes[$j];
2908:
2909: if ($canAbsorb($this, $keyType, $valueType)) {
2910: $absorbIntoExtras($keyType, $valueType);
2911: continue;
2912: }
2913:
2914: $keyTypes[] = $keyType;
2915: $valueTypes[] = $valueType;
2916: $optionalKeys[] = count($keyTypes) - 1;
2917: }
2918:
2919: $resultUnsealed = [$mergedUnsealedKey, $mergedUnsealedValue];
2920:
2921: $nextAutoIndexes = array_values(array_unique(array_merge($this->nextAutoIndexes, $otherArray->nextAutoIndexes)));
2922: sort($nextAutoIndexes);
2923:
2924: $optionalKeys = array_values(array_unique($optionalKeys));
2925:
2926: /** @var list<ConstantIntegerType|ConstantStringType> $keyTypes */
2927: $keyTypes = $keyTypes;
2928:
2929: return $this->recreate(
2930: $keyTypes,
2931: $valueTypes,
2932: $nextAutoIndexes,
2933: $optionalKeys,
2934: $this->isList->and($otherArray->isList),
2935: $resultUnsealed,
2936: );
2937: }
2938:
2939: private function legacyMergeWith(self $otherArray): self
2940: {
2941: $valueTypes = $this->valueTypes;
2942: $optionalKeys = $this->optionalKeys;
2943: foreach ($this->keyTypes as $i => $keyType) {
2944: $otherIndex = $otherArray->getKeyIndex($keyType);
2945: if ($otherIndex === null) {
2946: $optionalKeys[] = $i;
2947: continue;
2948: }
2949: if ($otherArray->isOptionalKey($otherIndex)) {
2950: $optionalKeys[] = $i;
2951: }
2952: $otherValueType = $otherArray->valueTypes[$otherIndex];
2953: $valueTypes[$i] = TypeCombinator::union($valueTypes[$i], $otherValueType);
2954: }
2955:
2956: $optionalKeys = array_values(array_unique($optionalKeys));
2957:
2958: $nextAutoIndexes = array_values(array_unique(array_merge($this->nextAutoIndexes, $otherArray->nextAutoIndexes)));
2959: sort($nextAutoIndexes);
2960:
2961: return $this->recreate($this->keyTypes, $valueTypes, $nextAutoIndexes, $optionalKeys, $this->isList->and($otherArray->isList), $this->unsealed);
2962: }
2963:
2964: /**
2965: * @return array<int|string, int>
2966: */
2967: private function getKeyIndexMap(): array
2968: {
2969: if ($this->keyIndexMap !== null) {
2970: return $this->keyIndexMap;
2971: }
2972:
2973: $map = [];
2974: foreach ($this->keyTypes as $i => $keyType) {
2975: $map[$keyType->getValue()] = $i;
2976: }
2977:
2978: return $this->keyIndexMap = $map;
2979: }
2980:
2981: /**
2982: * @param ConstantIntegerType|ConstantStringType $otherKeyType
2983: */
2984: private function getKeyIndex($otherKeyType): ?int
2985: {
2986: return $this->getKeyIndexMap()[$otherKeyType->getValue()] ?? null;
2987: }
2988:
2989: public function makeOffsetRequired(Type $offsetType): self
2990: {
2991: $offsetType = $offsetType->toArrayKey();
2992: $optionalKeys = $this->optionalKeys;
2993: $isList = $this->isList->yes();
2994: foreach ($this->keyTypes as $i => $keyType) {
2995: if (!$keyType->equals($offsetType)) {
2996: continue;
2997: }
2998:
2999: $keyValue = $keyType->getValue();
3000: foreach ($optionalKeys as $j => $key) {
3001: if (
3002: $i !== $key
3003: && (
3004: !$isList
3005: || !is_int($keyValue)
3006: || !is_int($this->keyTypes[$key]->getValue())
3007: || $this->keyTypes[$key]->getValue() >= $keyValue
3008: )
3009: ) {
3010: continue;
3011: }
3012:
3013: unset($optionalKeys[$j]);
3014: }
3015:
3016: if (count($this->optionalKeys) !== count($optionalKeys)) {
3017: return $this->recreate($this->keyTypes, $this->valueTypes, $this->nextAutoIndexes, array_values($optionalKeys), $this->isList, $this->unsealed);
3018: }
3019:
3020: return $this;
3021: }
3022:
3023: // Offset isn't in the explicit set. If the unsealed extras' key range
3024: // covers it (e.g. `array{a: int, ...<string, float>}` narrowing on
3025: // `array_key_exists('b', $arr)`), promote it into the explicit set as
3026: // a required slot with the unsealed value type. The unsealed extras
3027: // stay around — additional entries at other matching keys are still
3028: // possible.
3029: if (
3030: $this->isUnsealed()->yes()
3031: && $this->unsealed !== null
3032: && ($offsetType instanceof ConstantIntegerType || $offsetType instanceof ConstantStringType)
3033: ) {
3034: [$unsealedKeyType, $unsealedValueType] = $this->unsealed;
3035: if (!$unsealedKeyType->isSuperTypeOf($offsetType)->no()) {
3036: $keyTypes = $this->keyTypes;
3037: $valueTypes = $this->valueTypes;
3038: $keyTypes[] = $offsetType;
3039: $valueTypes[] = $unsealedValueType;
3040:
3041: return $this->recreate(
3042: $keyTypes,
3043: $valueTypes,
3044: $this->nextAutoIndexes,
3045: $this->optionalKeys,
3046: TrinaryLogic::createNo(),
3047: $this->unsealed,
3048: );
3049: }
3050: }
3051:
3052: return $this;
3053: }
3054:
3055: public function makeList(): Type
3056: {
3057: if ($this->isList->yes()) {
3058: return $this;
3059: }
3060:
3061: if ($this->isList->no()) {
3062: return new NeverType();
3063: }
3064:
3065: return $this->recreate($this->keyTypes, $this->valueTypes, $this->nextAutoIndexes, $this->optionalKeys, TrinaryLogic::createYes(), $this->unsealed);
3066: }
3067:
3068: public function makeListMaybe(): Type
3069: {
3070: if (!$this->isList->yes()) {
3071: return $this;
3072: }
3073:
3074: return $this->recreate(
3075: $this->keyTypes,
3076: $this->valueTypes,
3077: $this->nextAutoIndexes,
3078: $this->optionalKeys,
3079: TrinaryLogic::createMaybe(),
3080: $this->unsealed,
3081: );
3082: }
3083:
3084: public function mapValueType(callable $cb): Type
3085: {
3086: $newValueTypes = [];
3087: foreach ($this->valueTypes as $valueType) {
3088: $newValueTypes[] = $cb($valueType);
3089: }
3090:
3091: $newUnsealed = $this->unsealed === null
3092: ? null
3093: : [$this->unsealed[0], $cb($this->unsealed[1])];
3094:
3095: return $this->recreate(
3096: $this->keyTypes,
3097: $newValueTypes,
3098: $this->nextAutoIndexes,
3099: $this->optionalKeys,
3100: $this->isList,
3101: $newUnsealed,
3102: );
3103: }
3104:
3105: public function mapKeyType(callable $cb): Type
3106: {
3107: // Constant array shapes already encode precise per-slot keys; a
3108: // blanket key-type rewrite (the prior `TypeTraverser`-based pattern
3109: // in `NodeScopeResolver`) would coerce constants into a broader
3110: // type and lose precision. Pass through unchanged.
3111: return $this;
3112: }
3113:
3114: public function makeAllArrayKeysOptional(): Type
3115: {
3116: $keyCount = count($this->keyTypes);
3117: if ($keyCount === 0) {
3118: return $this;
3119: }
3120:
3121: return $this->recreate(
3122: $this->keyTypes,
3123: $this->valueTypes,
3124: $this->nextAutoIndexes,
3125: range(0, $keyCount - 1),
3126: $this->isList,
3127: $this->unsealed,
3128: );
3129: }
3130:
3131: public function changeKeyCaseArray(?int $case): Type
3132: {
3133: $builder = ConstantArrayTypeBuilder::createEmpty();
3134: foreach ($this->keyTypes as $i => $keyType) {
3135: if ($keyType instanceof ConstantStringType) {
3136: $newKeyType = self::foldConstantStringKeyCase($keyType, $case);
3137: } else {
3138: $newKeyType = $keyType;
3139: }
3140: $builder->setOffsetValueType($newKeyType, $this->valueTypes[$i], $this->isOptionalKey($i));
3141: }
3142:
3143: if ($this->unsealed !== null) {
3144: $builder->makeUnsealed(self::foldUnsealedKeyCase($this->unsealed[0], $case), $this->unsealed[1]);
3145: }
3146:
3147: $result = $builder->getArray();
3148: if ($this->isList()->yes()) {
3149: $result = TypeCombinator::intersect($result, new AccessoryArrayListType());
3150: }
3151: return $result;
3152: }
3153:
3154: public function filterArrayRemovingFalsey(): Type
3155: {
3156: $falseyTypes = StaticTypeFactory::falsey();
3157: $builder = ConstantArrayTypeBuilder::createEmpty();
3158: foreach ($this->keyTypes as $i => $keyType) {
3159: $value = $this->valueTypes[$i];
3160: $isFalsey = $falseyTypes->isSuperTypeOf($value);
3161: if ($isFalsey->yes()) {
3162: continue;
3163: }
3164: if ($isFalsey->maybe()) {
3165: $builder->setOffsetValueType($keyType, TypeCombinator::remove($value, $falseyTypes), true);
3166: continue;
3167: }
3168: $builder->setOffsetValueType($keyType, $value, $this->isOptionalKey($i));
3169: }
3170:
3171: if ($this->unsealed !== null) {
3172: $unsealedValue = TypeCombinator::remove($this->unsealed[1], $falseyTypes);
3173: if (!$unsealedValue instanceof NeverType) {
3174: $builder->makeUnsealed($this->unsealed[0], $unsealedValue);
3175: }
3176: }
3177:
3178: return $builder->getArray();
3179: }
3180:
3181: private static function foldConstantStringKeyCase(ConstantStringType $type, ?int $case): Type
3182: {
3183: if ($case === CASE_LOWER) {
3184: return new ConstantStringType(strtolower($type->getValue()));
3185: }
3186: if ($case === CASE_UPPER) {
3187: return new ConstantStringType(strtoupper($type->getValue()));
3188: }
3189:
3190: return TypeCombinator::union(
3191: new ConstantStringType(strtolower($type->getValue())),
3192: new ConstantStringType(strtoupper($type->getValue())),
3193: );
3194: }
3195:
3196: private static function foldUnsealedKeyCase(Type $key, ?int $case): Type
3197: {
3198: if ($key instanceof ConstantStringType) {
3199: return self::foldConstantStringKeyCase($key, $case);
3200: }
3201:
3202: if ($key instanceof UnionType) {
3203: $folded = [];
3204: foreach ($key->getTypes() as $innerKey) {
3205: $folded[] = self::foldUnsealedKeyCase($innerKey, $case);
3206: }
3207:
3208: return TypeCombinator::union(...$folded);
3209: }
3210:
3211: // `array_change_key_case` only folds string keys — int keys
3212: // (e.g. `...<int, ...>`) pass through unchanged.
3213: if (!$key->isString()->yes()) {
3214: return $key;
3215: }
3216:
3217: // Rebuild from a clean `string` plus the non-case accessories that
3218: // case-folding preserves (length is unchanged, so numeric / non-
3219: // falsy / non-empty all survive). Any prior lowercase/uppercase
3220: // accessory is dropped — matches the `ArrayType::changeKeyCaseArray`
3221: // behavior where `strtoupper(lowercase-string)` reads as
3222: // `uppercase-string`, not the contradictory intersection.
3223: $preserved = [new StringType()];
3224: if ($key->isNumericString()->yes()) {
3225: $preserved[] = new AccessoryNumericStringType();
3226: } elseif ($key->isNonFalsyString()->yes()) {
3227: $preserved[] = new AccessoryNonFalsyStringType();
3228: } elseif ($key->isNonEmptyString()->yes()) {
3229: $preserved[] = new AccessoryNonEmptyStringType();
3230: }
3231:
3232: if ($case === CASE_LOWER) {
3233: return new IntersectionType([...$preserved, new AccessoryLowercaseStringType()]);
3234: }
3235: if ($case === CASE_UPPER) {
3236: return new IntersectionType([...$preserved, new AccessoryUppercaseStringType()]);
3237: }
3238:
3239: // `null` (PHP <8.4 / unspecified) yields lower- or upper-case
3240: // keys; record both as a union.
3241: return TypeCombinator::union(
3242: new IntersectionType([...$preserved, new AccessoryLowercaseStringType()]),
3243: new IntersectionType([...$preserved, new AccessoryUppercaseStringType()]),
3244: );
3245: }
3246:
3247: public function toPhpDocNode(): TypeNode
3248: {
3249: $items = [];
3250: $values = [];
3251: $exportValuesOnly = true;
3252: foreach ($this->keyTypes as $i => $keyType) {
3253: if ($keyType->getValue() !== $i) {
3254: $exportValuesOnly = false;
3255: }
3256: $keyPhpDocNode = $keyType->toPhpDocNode();
3257: if (!$keyPhpDocNode instanceof ConstTypeNode) {
3258: continue;
3259: }
3260: $valueType = $this->valueTypes[$i];
3261:
3262: /** @var ConstExprStringNode|ConstExprIntegerNode $keyNode */
3263: $keyNode = $keyPhpDocNode->constExpr;
3264: if ($keyNode instanceof ConstExprStringNode) {
3265: $value = $keyNode->value;
3266: if (self::isValidIdentifier($value)) {
3267: $keyNode = new IdentifierTypeNode($value);
3268: }
3269: }
3270:
3271: $isOptional = $this->isOptionalKey($i);
3272: if ($isOptional) {
3273: $exportValuesOnly = false;
3274: }
3275: $items[] = new ArrayShapeItemNode(
3276: $keyNode,
3277: $isOptional,
3278: $valueType->toPhpDocNode(),
3279: );
3280: $values[] = new ArrayShapeItemNode(
3281: null,
3282: $isOptional,
3283: $valueType->toPhpDocNode(),
3284: );
3285: }
3286:
3287: if ($this->isUnsealed()->yes() && $this->unsealed !== null) {
3288: $unsealedKeyTypeDescription = $this->unsealed[0]->describe(VerbosityLevel::precise());
3289: $isMixedUnsealedKeyType = $this->unsealed[0] instanceof MixedType && $unsealedKeyTypeDescription === 'mixed' && !$this->unsealed[0]->isExplicitMixed();
3290: $isMixedUnsealedItemType = $this->unsealed[1] instanceof MixedType && $this->unsealed[1]->describe(VerbosityLevel::precise()) === 'mixed' && !$this->unsealed[1]->isExplicitMixed();
3291: if ($isMixedUnsealedKeyType || ($this->isList()->yes() && $unsealedKeyTypeDescription === 'int<0, max>')) {
3292: if ($isMixedUnsealedItemType) {
3293: return ArrayShapeNode::createUnsealed(
3294: $exportValuesOnly ? $values : $items,
3295: null,
3296: $this->shouldBeDescribedAsAList() ? ArrayShapeNode::KIND_LIST : ArrayShapeNode::KIND_ARRAY,
3297: );
3298: }
3299:
3300: return ArrayShapeNode::createUnsealed(
3301: $exportValuesOnly ? $values : $items,
3302: new ArrayShapeUnsealedTypeNode($this->unsealed[1]->toPhpDocNode(), null),
3303: $this->shouldBeDescribedAsAList() ? ArrayShapeNode::KIND_LIST : ArrayShapeNode::KIND_ARRAY,
3304: );
3305: }
3306:
3307: return ArrayShapeNode::createUnsealed(
3308: $exportValuesOnly ? $values : $items,
3309: new ArrayShapeUnsealedTypeNode($this->unsealed[1]->toPhpDocNode(), $this->unsealed[0]->toPhpDocNode()),
3310: ArrayShapeNode::KIND_ARRAY,
3311: );
3312: }
3313:
3314: return ArrayShapeNode::createSealed(
3315: $exportValuesOnly ? $values : $items,
3316: $this->shouldBeDescribedAsAList() ? ArrayShapeNode::KIND_LIST : ArrayShapeNode::KIND_ARRAY,
3317: );
3318: }
3319:
3320: public static function isValidIdentifier(string $value): bool
3321: {
3322: $result = Strings::match($value, '~^(?:[\\\\]?+[a-z_\\x80-\\xFF][0-9a-z_\\x80-\\xFF-]*+)++$~si');
3323:
3324: return $result !== null;
3325: }
3326:
3327: public function getFiniteTypes(): array
3328: {
3329: if ($this->isUnsealed()->yes()) {
3330: return [];
3331: }
3332:
3333: $limit = InitializerExprTypeResolver::CALCULATE_SCALARS_LIMIT;
3334:
3335: // Build finite array types incrementally, processing one key at a time.
3336: // For optional keys, fork each partial result into with/without variants.
3337: // This avoids generating 2^N ConstantArrayType objects via getAllArrays().
3338: /** @var list<ConstantArrayTypeBuilder> $partials */
3339: $partials = [ConstantArrayTypeBuilder::createEmpty()];
3340:
3341: foreach ($this->keyTypes as $i => $keyType) {
3342: $finiteValueTypes = $this->valueTypes[$i]->getFiniteTypes();
3343: if ($finiteValueTypes === []) {
3344: return [];
3345: }
3346:
3347: $isOptional = $this->isOptionalKey($i);
3348: $newPartials = [];
3349:
3350: foreach ($partials as $partial) {
3351: if ($isOptional) {
3352: $newPartials[] = clone $partial;
3353: }
3354: foreach ($finiteValueTypes as $finiteValueType) {
3355: $newPartial = clone $partial;
3356: $newPartial->setOffsetValueType($keyType, $finiteValueType);
3357: $newPartials[] = $newPartial;
3358: }
3359: }
3360:
3361: $partials = $newPartials;
3362: if (count($partials) > $limit) {
3363: return [];
3364: }
3365: }
3366:
3367: $finiteTypes = [];
3368: foreach ($partials as $partial) {
3369: $finiteTypes[] = $partial->getArray();
3370: }
3371:
3372: return $finiteTypes;
3373: }
3374:
3375: public function hasTemplateOrLateResolvableType(): bool
3376: {
3377: foreach ($this->valueTypes as $valueType) {
3378: if (!$valueType->hasTemplateOrLateResolvableType()) {
3379: continue;
3380: }
3381:
3382: return true;
3383: }
3384:
3385: foreach ($this->keyTypes as $keyType) {
3386: if (!$keyType instanceof TemplateType) {
3387: continue;
3388: }
3389:
3390: return true;
3391: }
3392:
3393: if ($this->unsealed !== null) {
3394: if ($this->unsealed[0]->hasTemplateOrLateResolvableType()) {
3395: return true;
3396: }
3397: if ($this->unsealed[1]->hasTemplateOrLateResolvableType()) {
3398: return true;
3399: }
3400: }
3401:
3402: return false;
3403: }
3404:
3405: }
3406: