@@ -287,9 +287,8 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $opt
287287 }
288288 }
289289 if (count ($ scalarTypes ) > 0 && count ($ scalarTypes ) < self ::ARRAY_COUNT_LIMIT ) {
290- $ match = true ;
291- $ hasMatch = false ;
292290 $ valueTypes = $ this ->valueTypes ;
291+ $ unmatchedScalars = [];
293292 foreach ($ scalarTypes as $ scalarType ) {
294293 $ offsetMatch = false ;
295294
@@ -308,61 +307,97 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $opt
308307 }
309308
310309 if ($ offsetMatch ) {
311- $ hasMatch = true ;
312310 continue ;
313311 }
314312
315- $ match = false ;
313+ $ unmatchedScalars [] = $ scalarType ;
316314 }
317315
318- if ($ match ) {
319- $ this ->valueTypes = $ valueTypes ;
316+ $ this ->valueTypes = $ valueTypes ;
317+
318+ if (count ($ unmatchedScalars ) === 0 ) {
320319 return ;
321320 }
322321
323- if (!$ hasMatch ) {
324- foreach ($ scalarTypes as $ scalarType ) {
325- $ this ->keyTypes [] = $ scalarType ;
326- $ this ->valueTypes [] = $ valueType ;
327- $ this ->optionalKeys [] = count ($ this ->keyTypes ) - 1 ;
322+ foreach ($ unmatchedScalars as $ scalarType ) {
323+ $ this ->keyTypes [] = $ scalarType ;
324+ $ this ->valueTypes [] = $ valueType ;
325+ $ this ->optionalKeys [] = count ($ this ->keyTypes ) - 1 ;
328326
329- if (!($ scalarType instanceof ConstantIntegerType)) {
330- continue ;
331- }
327+ if (!($ scalarType instanceof ConstantIntegerType)) {
328+ continue ;
329+ }
332330
333- if (count ($ this ->nextAutoIndexes ) === 0 ) {
334- continue ;
335- }
331+ if (count ($ this ->nextAutoIndexes ) === 0 ) {
332+ continue ;
333+ }
336334
337- $ max = max ($ this ->nextAutoIndexes );
338- $ offsetValue = $ scalarType ->getValue ();
339- if ($ offsetValue < $ max ) {
340- continue ;
341- }
335+ $ max = max ($ this ->nextAutoIndexes );
336+ $ offsetValue = $ scalarType ->getValue ();
337+ if ($ offsetValue < $ max ) {
338+ continue ;
339+ }
342340
343- /** @var int|float $newAutoIndex */
344- $ newAutoIndex = $ offsetValue + 1 ;
345- if (is_float ($ newAutoIndex )) {
346- continue ;
347- }
348- $ this ->nextAutoIndexes [] = $ newAutoIndex ;
341+ /** @var int|float $newAutoIndex */
342+ $ newAutoIndex = $ offsetValue + 1 ;
343+ if (is_float ($ newAutoIndex )) {
344+ continue ;
349345 }
346+ $ this ->nextAutoIndexes [] = $ newAutoIndex ;
347+ }
350348
351- $ this ->isList = TrinaryLogic::createNo ();
349+ $ this ->isList = TrinaryLogic::createNo ();
350+
351+ if (
352+ !$ this ->disableArrayDegradation
353+ && count ($ this ->keyTypes ) > self ::ARRAY_COUNT_LIMIT
354+ ) {
355+ $ this ->degradeToGeneralArray = true ;
356+ $ this ->oversized = true ;
357+ }
352358
353- if (
354- !$ this ->disableArrayDegradation
355- && count ($ this ->keyTypes ) > self ::ARRAY_COUNT_LIMIT
356- ) {
357- $ this ->degradeToGeneralArray = true ;
358- $ this ->oversized = true ;
359+ return ;
360+ }
361+
362+ $ this ->isList = TrinaryLogic::createNo ();
363+
364+ // If the builder is already unsealed (e.g. fresh bleeding-edge
365+ // builder, or a PHPDoc shape like `array{a: int, ...<int, T>}`),
366+ // fold the unknown offset/value into the existing unsealed
367+ // extras instead of dropping per-key precision by degrading to a
368+ // general array. The actual decision between unsealed
369+ // ConstantArrayType and general ArrayType is then made in
370+ // getArray() based on whether any constant keys ended up
371+ // alongside these extras.
372+ if ($ this ->unsealed !== null ) {
373+ // Existing keys whose value the new offset could overwrite
374+ // must widen to a union of (existing, new) — the assignment
375+ // might or might not have hit them.
376+ $ residualOffset = $ offsetType ;
377+ foreach ($ this ->keyTypes as $ i => $ keyType ) {
378+ if ($ offsetType ->isSuperTypeOf ($ keyType )->no ()) {
379+ continue ;
359380 }
381+ $ this ->valueTypes [$ i ] = TypeCombinator::union ($ this ->valueTypes [$ i ], $ valueType );
382+ $ residualOffset = TypeCombinator::remove ($ residualOffset , $ keyType );
383+ }
360384
385+ if ($ residualOffset instanceof NeverType) {
361386 return ;
362387 }
363- }
364388
365- $ this ->isList = TrinaryLogic::createNo ();
389+ [$ existingKey , $ existingValue ] = $ this ->unsealed ;
390+ $ isExplicitNever = $ existingKey instanceof NeverType && $ existingKey ->isExplicit ();
391+ if ($ isExplicitNever ) {
392+ $ this ->unsealed = [$ residualOffset , $ valueType ];
393+ } else {
394+ $ this ->unsealed = [
395+ TypeCombinator::union ($ existingKey , $ residualOffset ),
396+ TypeCombinator::union ($ existingValue , $ valueType ),
397+ ];
398+ }
399+ return ;
400+ }
366401 }
367402
368403 if ($ offsetType === null ) {
@@ -409,7 +444,11 @@ public function getArray(): Type
409444 [$ unsealedKey , $ unsealedValue ] = $ this ->unsealed ;
410445 $ isExplicitNever = $ unsealedKey instanceof NeverType && $ unsealedKey ->isExplicit ();
411446 if (!$ isExplicitNever ) {
412- return new ArrayType ($ unsealedKey , $ unsealedValue );
447+ $ arrayType = new ArrayType ($ unsealedKey , $ unsealedValue );
448+ if ($ this ->isNonEmpty ->yes ()) {
449+ return TypeCombinator::intersect ($ arrayType , new NonEmptyArrayType ());
450+ }
451+ return $ arrayType ;
413452 }
414453 }
415454 return new ConstantArrayType ([], [], unsealed: $ this ->unsealed );
@@ -418,7 +457,7 @@ public function getArray(): Type
418457 if (!$ this ->degradeToGeneralArray ) {
419458 /** @var list<ConstantIntegerType|ConstantStringType> $keyTypes */
420459 $ keyTypes = $ this ->keyTypes ;
421- $ array = new ConstantArrayType ($ keyTypes , $ this ->valueTypes , $ this ->nextAutoIndexes , $ this ->optionalKeys , $ this ->isList , , $ this ->unsealed );
460+ $ array = new ConstantArrayType ($ keyTypes , $ this ->valueTypes , $ this ->nextAutoIndexes , $ this ->optionalKeys , $ this ->isList , $ this ->unsealed );
422461 if ($ this ->isNonEmpty ->yes () && !$ array ->isIterableAtLeastOnce ()->yes ()) {
423462 return TypeCombinator::intersect ($ array , new NonEmptyArrayType ());
424463 }
0 commit comments