Skip to content

Commit 600b998

Browse files
ondrejmirtesclaude
andcommitted
Degrade chunkArray on unsealed sources to the general trait path
`array_chunk` with a constant length precisely enumerates the chunks for sealed CATs: each chunk is a known slice of the source. With real unsealed extras the source has an unknown number of trailing entries that could form additional partial or full chunks — the precise enumeration would lie about the chunk count. Fall back to `traitChunkArray`, which yields the general `non-empty-list<array{...source-shape...}>` form. Less precise but correct. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 625e1e2 commit 600b998

2 files changed

Lines changed: 29 additions & 0 deletions

File tree

src/Type/Constant/ConstantArrayType.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1436,6 +1436,15 @@ private static function isListAfterUnset(array $newKeyTypes, array $newOptionalK
14361436

14371437
public function chunkArray(Type $lengthType, TrinaryLogic $preserveKeys): Type
14381438
{
1439+
// With real unsealed extras, we can't precisely enumerate the
1440+
// chunks — the source has an unknown number of extras that
1441+
// could form additional partial or full chunks. Fall back to
1442+
// the general `list<chunk<sourceValues>>` shape produced by
1443+
// the trait, which is correct (just less precise).
1444+
if ($this->isUnsealed()->yes()) {
1445+
return $this->traitChunkArray($lengthType, $preserveKeys);
1446+
}
1447+
14391448
$biggerOne = IntegerRangeType::fromInterval(1, null);
14401449
$finiteTypes = $lengthType->getFiniteTypes();
14411450
if ($biggerOne->isSuperTypeOf($lengthType)->yes() && count($finiteTypes) < self::CHUNK_FINITE_TYPES_LIMIT) {

tests/PHPStan/Analyser/nsrt/unsealed-derivations.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -476,6 +476,26 @@ public function sliceBeyondExplicit(array $arr): void
476476

477477
}
478478

479+
class ChunkArray
480+
{
481+
482+
/**
483+
* @param array{a: 1, b: 2, c: 3, d: 4, ...<int, string>} $arr
484+
*/
485+
public function chunkPreservingKeys(array $arr): void
486+
{
487+
// With real unsealed extras the precise chunk count is unknown,
488+
// so the type falls back to a general "non-empty list of chunks
489+
// shaped like the source". Each chunk is described by the
490+
// source's own shape (preserveKeys=true).
491+
assertType(
492+
'non-empty-list<array{a: 1, b: 2, c: 3, d: 4, ...<int, string>}>',
493+
array_chunk($arr, 2, true),
494+
);
495+
}
496+
497+
}
498+
479499
class CountNarrowing
480500
{
481501

0 commit comments

Comments
 (0)