From 349280df706b1dc2884f1794e95a0d0253c0410f Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Mon, 3 Nov 2025 19:01:53 +0100 Subject: [PATCH 1/4] Preserve constant array when setting a union of constant scalar keys --- src/Type/Constant/ConstantArrayType.php | 27 +++++++++++++++++++ tests/PHPStan/Analyser/nsrt/bug-11716.php | 2 +- .../Analyser/nsrt/constant-array-type-set.php | 2 +- ...onstant-union-offset-on-constant-array.php | 20 ++++++++++++++ 4 files changed, 49 insertions(+), 2 deletions(-) create mode 100644 tests/PHPStan/Analyser/nsrt/set-constant-union-offset-on-constant-array.php diff --git a/src/Type/Constant/ConstantArrayType.php b/src/Type/Constant/ConstantArrayType.php index 4cec2bf409..99c5d7b7d3 100644 --- a/src/Type/Constant/ConstantArrayType.php +++ b/src/Type/Constant/ConstantArrayType.php @@ -693,6 +693,20 @@ public function getOffsetValueType(Type $offsetType): Type public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $unionValues = true): Type { + if ($offsetType !== null) { + $constantScalars = $offsetType->getConstantScalarTypes(); + $constantScalarsCount = count($constantScalars); + if ($constantScalarsCount > 1 && $constantScalarsCount < ConstantArrayTypeBuilder::ARRAY_COUNT_LIMIT) { + $arrays = []; + foreach ($constantScalars as $constantScalar) { + $builder = ConstantArrayTypeBuilder::createFromConstantArray($this); + $builder->setOffsetValueType($constantScalar, $valueType); + $arrays[] = $builder->getArray(); + } + + return TypeCombinator::union(...$arrays); + } + } $builder = ConstantArrayTypeBuilder::createFromConstantArray($this); $builder->setOffsetValueType($offsetType, $valueType); @@ -701,6 +715,19 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type { + $constantScalars = $offsetType->getConstantScalarTypes(); + $constantScalarsCount = count($constantScalars); + if ($constantScalarsCount > 1 && $constantScalarsCount < ConstantArrayTypeBuilder::ARRAY_COUNT_LIMIT) { + $arrays = []; + foreach ($constantScalars as $constantScalar) { + $builder = ConstantArrayTypeBuilder::createFromConstantArray($this); + $builder->setOffsetValueType($constantScalar, $valueType); + $arrays[] = $builder->getArray(); + } + + return TypeCombinator::union(...$arrays); + } + $builder = ConstantArrayTypeBuilder::createFromConstantArray($this); $builder->setOffsetValueType($offsetType, $valueType); diff --git a/tests/PHPStan/Analyser/nsrt/bug-11716.php b/tests/PHPStan/Analyser/nsrt/bug-11716.php index 3dced2a08d..66a2bd0108 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-11716.php +++ b/tests/PHPStan/Analyser/nsrt/bug-11716.php @@ -22,7 +22,7 @@ public function parse(string $glue): string $seenGlues[$glue] = true; assertType("'&'|'|'", $glue); - assertType("array{'|': bool, '&': bool}", $seenGlues); + assertType("array{'|': false, '&': true}|array{'|': true, '&': false}", $seenGlues); } else { assertType("''", $glue); } diff --git a/tests/PHPStan/Analyser/nsrt/constant-array-type-set.php b/tests/PHPStan/Analyser/nsrt/constant-array-type-set.php index 9ae0b88828..90e13929f0 100644 --- a/tests/PHPStan/Analyser/nsrt/constant-array-type-set.php +++ b/tests/PHPStan/Analyser/nsrt/constant-array-type-set.php @@ -33,7 +33,7 @@ public function doFoo(int $i) /** @var 0|1|2|3 $offset3 */ $offset3 = doFoo(); $e[$offset3] = true; - assertType('non-empty-array<0|1|2|3, bool>', $e); + assertType('array{0: bool, 1: bool, 2: bool, 3?: true}', $e); $f = [false, false, false]; /** @var 0|1 $offset4 */ diff --git a/tests/PHPStan/Analyser/nsrt/set-constant-union-offset-on-constant-array.php b/tests/PHPStan/Analyser/nsrt/set-constant-union-offset-on-constant-array.php new file mode 100644 index 0000000000..4b862c6750 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/set-constant-union-offset-on-constant-array.php @@ -0,0 +1,20 @@ + Date: Thu, 6 Nov 2025 08:56:13 +0100 Subject: [PATCH 2/4] Prevent explosion of types which turns into over-generalization --- src/Type/Constant/ConstantArrayType.php | 24 ++++++++++--------- .../Analyser/nsrt/constant-array-type-set.php | 8 +++---- 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/src/Type/Constant/ConstantArrayType.php b/src/Type/Constant/ConstantArrayType.php index 99c5d7b7d3..4b7030a356 100644 --- a/src/Type/Constant/ConstantArrayType.php +++ b/src/Type/Constant/ConstantArrayType.php @@ -693,7 +693,7 @@ public function getOffsetValueType(Type $offsetType): Type public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $unionValues = true): Type { - if ($offsetType !== null) { + if ($offsetType !== null && $valueType->isConstantScalarValue()->yes()) { $constantScalars = $offsetType->getConstantScalarTypes(); $constantScalarsCount = count($constantScalars); if ($constantScalarsCount > 1 && $constantScalarsCount < ConstantArrayTypeBuilder::ARRAY_COUNT_LIMIT) { @@ -715,17 +715,19 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type { - $constantScalars = $offsetType->getConstantScalarTypes(); - $constantScalarsCount = count($constantScalars); - if ($constantScalarsCount > 1 && $constantScalarsCount < ConstantArrayTypeBuilder::ARRAY_COUNT_LIMIT) { - $arrays = []; - foreach ($constantScalars as $constantScalar) { - $builder = ConstantArrayTypeBuilder::createFromConstantArray($this); - $builder->setOffsetValueType($constantScalar, $valueType); - $arrays[] = $builder->getArray(); - } + if ($valueType->isConstantScalarValue()->yes()) { + $constantScalars = $offsetType->getConstantScalarTypes(); + $constantScalarsCount = count($constantScalars); + if ($constantScalarsCount > 1 && $constantScalarsCount < ConstantArrayTypeBuilder::ARRAY_COUNT_LIMIT) { + $arrays = []; + foreach ($constantScalars as $constantScalar) { + $builder = ConstantArrayTypeBuilder::createFromConstantArray($this); + $builder->setOffsetValueType($constantScalar, $valueType); + $arrays[] = $builder->getArray(); + } - return TypeCombinator::union(...$arrays); + return TypeCombinator::union(...$arrays); + } } $builder = ConstantArrayTypeBuilder::createFromConstantArray($this); diff --git a/tests/PHPStan/Analyser/nsrt/constant-array-type-set.php b/tests/PHPStan/Analyser/nsrt/constant-array-type-set.php index 90e13929f0..b1dca88b42 100644 --- a/tests/PHPStan/Analyser/nsrt/constant-array-type-set.php +++ b/tests/PHPStan/Analyser/nsrt/constant-array-type-set.php @@ -21,7 +21,7 @@ public function doFoo(int $i) /** @var 0|1|2 $offset */ $offset = doFoo(); $c[$offset] = true; - assertType('array{bool, bool, bool}', $c); + assertType('array{false, false, true}|array{false, true, false}|array{true, false, false}', $c); $d = [false, false, false]; /** @var int<0, 2> $offset2 */ @@ -33,13 +33,13 @@ public function doFoo(int $i) /** @var 0|1|2|3 $offset3 */ $offset3 = doFoo(); $e[$offset3] = true; - assertType('array{0: bool, 1: bool, 2: bool, 3?: true}', $e); + assertType('array{false, false, false, true}|array{false, false, true}|array{false, true, false}|array{true, false, false}', $e); $f = [false, false, false]; /** @var 0|1 $offset4 */ $offset4 = doFoo(); $f[$offset4] = true; - assertType('array{bool, bool, false}', $f); + assertType('array{false, true, false}|array{true, false, false}', $f); } /** @@ -101,7 +101,7 @@ public function doBar6(bool $offset): void { $a = [false, false, false]; $a[$offset] = true; - assertType('array{bool, bool, false}', $a); + assertType('array{false, true, false}|array{true, false, false}', $a); } /** From 3a9225dded6d78c7c33bc765018faaa84e14967a Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Thu, 6 Nov 2025 09:10:02 +0100 Subject: [PATCH 3/4] Preserve constant array when setting a integer-range key --- src/Type/Constant/ConstantArrayType.php | 14 ++++++++++++-- .../Analyser/nsrt/constant-array-type-set.php | 6 +++--- ...set-constant-union-offset-on-constant-array.php | 10 ++++++++++ 3 files changed, 25 insertions(+), 5 deletions(-) diff --git a/src/Type/Constant/ConstantArrayType.php b/src/Type/Constant/ConstantArrayType.php index 4b7030a356..280fae3c3d 100644 --- a/src/Type/Constant/ConstantArrayType.php +++ b/src/Type/Constant/ConstantArrayType.php @@ -694,7 +694,12 @@ public function getOffsetValueType(Type $offsetType): Type public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $unionValues = true): Type { if ($offsetType !== null && $valueType->isConstantScalarValue()->yes()) { - $constantScalars = $offsetType->getConstantScalarTypes(); + if ($offsetType instanceof IntegerRangeType) { + $constantScalars = $offsetType->getFiniteTypes(); + } else { + $constantScalars = $offsetType->getConstantScalarTypes(); + } + $constantScalarsCount = count($constantScalars); if ($constantScalarsCount > 1 && $constantScalarsCount < ConstantArrayTypeBuilder::ARRAY_COUNT_LIMIT) { $arrays = []; @@ -716,7 +721,12 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type { if ($valueType->isConstantScalarValue()->yes()) { - $constantScalars = $offsetType->getConstantScalarTypes(); + if ($offsetType instanceof IntegerRangeType) { + $constantScalars = $offsetType->getFiniteTypes(); + } else { + $constantScalars = $offsetType->getConstantScalarTypes(); + } + $constantScalarsCount = count($constantScalars); if ($constantScalarsCount > 1 && $constantScalarsCount < ConstantArrayTypeBuilder::ARRAY_COUNT_LIMIT) { $arrays = []; diff --git a/tests/PHPStan/Analyser/nsrt/constant-array-type-set.php b/tests/PHPStan/Analyser/nsrt/constant-array-type-set.php index b1dca88b42..6fadee3c05 100644 --- a/tests/PHPStan/Analyser/nsrt/constant-array-type-set.php +++ b/tests/PHPStan/Analyser/nsrt/constant-array-type-set.php @@ -27,7 +27,7 @@ public function doFoo(int $i) /** @var int<0, 2> $offset2 */ $offset2 = doFoo(); $d[$offset2] = true; - assertType('array{bool, bool, bool}', $d); + assertType('array{false, false, true}|array{false, true, false}|array{true, false, false}', $d); $e = [false, false, false]; /** @var 0|1|2|3 $offset3 */ @@ -50,7 +50,7 @@ public function doBar(int $offset): void { $a = [false, false, false]; $a[$offset] = true; - assertType('array{bool, bool, false}', $a); + assertType('array{false, true, false}|array{true, false, false}', $a); } /** @@ -94,7 +94,7 @@ public function doBar5(int $offset): void { $a = [false, false, false]; $a[$offset] = true; - assertType('non-empty-array, bool>', $a); + assertType('array{0: false, 1: false, 2: false, 4: true}|array{false, false, false, true}|array{false, false, true}|array{false, true, false}|array{true, false, false}', $a); } public function doBar6(bool $offset): void diff --git a/tests/PHPStan/Analyser/nsrt/set-constant-union-offset-on-constant-array.php b/tests/PHPStan/Analyser/nsrt/set-constant-union-offset-on-constant-array.php index 4b862c6750..830c299a5e 100644 --- a/tests/PHPStan/Analyser/nsrt/set-constant-union-offset-on-constant-array.php +++ b/tests/PHPStan/Analyser/nsrt/set-constant-union-offset-on-constant-array.php @@ -17,4 +17,14 @@ public function doFoo(array $a): void assertType('array{foo: int, a: 256}|array{foo: int, b: 256}', $a); } + /** + * @param array{foo: int} $a + * @param int<1,5> $intRange + */ + public function doBar(array $a, $intRange): void + { + $a[$intRange] = 256; + assertType('array{foo: int, 1: 256}|array{foo: int, 2: 256}|array{foo: int, 3: 256}|array{foo: int, 4: 256}|array{foo: int, 5: 256}', $a); + } + } From 7964b98a6595aa6b635dfb6f5bd0f1d063328fe9 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Thu, 6 Nov 2025 18:03:56 +0100 Subject: [PATCH 4/4] partly revert --- src/Type/Constant/ConstantArrayType.php | 32 ++++++++++++------------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/src/Type/Constant/ConstantArrayType.php b/src/Type/Constant/ConstantArrayType.php index 280fae3c3d..51a317681f 100644 --- a/src/Type/Constant/ConstantArrayType.php +++ b/src/Type/Constant/ConstantArrayType.php @@ -693,7 +693,7 @@ public function getOffsetValueType(Type $offsetType): Type public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $unionValues = true): Type { - if ($offsetType !== null && $valueType->isConstantScalarValue()->yes()) { + if ($offsetType !== null) { if ($offsetType instanceof IntegerRangeType) { $constantScalars = $offsetType->getFiniteTypes(); } else { @@ -720,24 +720,22 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type { - if ($valueType->isConstantScalarValue()->yes()) { - if ($offsetType instanceof IntegerRangeType) { - $constantScalars = $offsetType->getFiniteTypes(); - } else { - $constantScalars = $offsetType->getConstantScalarTypes(); - } - - $constantScalarsCount = count($constantScalars); - if ($constantScalarsCount > 1 && $constantScalarsCount < ConstantArrayTypeBuilder::ARRAY_COUNT_LIMIT) { - $arrays = []; - foreach ($constantScalars as $constantScalar) { - $builder = ConstantArrayTypeBuilder::createFromConstantArray($this); - $builder->setOffsetValueType($constantScalar, $valueType); - $arrays[] = $builder->getArray(); - } + if ($offsetType instanceof IntegerRangeType) { + $constantScalars = $offsetType->getFiniteTypes(); + } else { + $constantScalars = $offsetType->getConstantScalarTypes(); + } - return TypeCombinator::union(...$arrays); + $constantScalarsCount = count($constantScalars); + if ($constantScalarsCount > 1 && $constantScalarsCount < ConstantArrayTypeBuilder::ARRAY_COUNT_LIMIT) { + $arrays = []; + foreach ($constantScalars as $constantScalar) { + $builder = ConstantArrayTypeBuilder::createFromConstantArray($this); + $builder->setOffsetValueType($constantScalar, $valueType); + $arrays[] = $builder->getArray(); } + + return TypeCombinator::union(...$arrays); } $builder = ConstantArrayTypeBuilder::createFromConstantArray($this);