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