From dc67ec4a76846e5515809b9b34e9922e5ff8c878 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 4 Oct 2025 09:46:27 +0200 Subject: [PATCH 1/8] Get rid of `instanceof *Type` checks --- phpstan-baseline.neon | 6 ----- ...rrayCombineFunctionReturnTypeExtension.php | 22 ++++++++++++------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 1ec2cd321b..ef44d82964 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1521,12 +1521,6 @@ parameters: count: 2 path: src/Type/Php/ArrayCombineFunctionReturnTypeExtension.php - - - rawMessage: 'Doing instanceof PHPStan\Type\Constant\ConstantStringType is error-prone and deprecated. Use Type::getConstantStrings() instead.' - identifier: phpstanApi.instanceofType - count: 1 - path: src/Type/Php/ArrayCombineFunctionReturnTypeExtension.php - - rawMessage: 'Doing instanceof PHPStan\Type\Constant\ConstantStringType is error-prone and deprecated. Use Type::getConstantStrings() instead.' identifier: phpstanApi.instanceofType diff --git a/src/Type/Php/ArrayCombineFunctionReturnTypeExtension.php b/src/Type/Php/ArrayCombineFunctionReturnTypeExtension.php index 663859a069..1bd7c15ef9 100644 --- a/src/Type/Php/ArrayCombineFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArrayCombineFunctionReturnTypeExtension.php @@ -13,8 +13,7 @@ use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantArrayTypeBuilder; use PHPStan\Type\Constant\ConstantBooleanType; -use PHPStan\Type\Constant\ConstantIntegerType; -use PHPStan\Type\Constant\ConstantStringType; +use PHPStan\Type\ConstantScalarType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\ErrorType; use PHPStan\Type\MixedType; @@ -23,6 +22,8 @@ use PHPStan\Type\TypeCombinator; use PHPStan\Type\UnionType; use function count; +use function is_int; +use function is_string; #[AutowiredService] final class ArrayCombineFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension @@ -114,7 +115,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, /** * @param array $types * - * @return array|null + * @return list|null */ private function sanitizeConstantArrayKeyTypes(array $types): ?array { @@ -125,14 +126,19 @@ private function sanitizeConstantArrayKeyTypes(array $types): ?array $type = $type->toString(); } - if ( - !$type instanceof ConstantIntegerType - && !$type instanceof ConstantStringType - ) { + $scalars = $type->getConstantScalarTypes(); + if (count($scalars) === 0) { return null; } - $sanitizedTypes[] = $type; + foreach ($scalars as $scalar) { + $value = $scalar->getValue(); + if (!is_int($value) && !is_string($value)) { + return null; + } + + $sanitizedTypes[] = $scalar; + } } return $sanitizedTypes; From 37638171d7137dae1da5e88560a6e76e73bea571 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 4 Oct 2025 12:00:33 +0200 Subject: [PATCH 2/8] Support union of constant arrays --- ...rrayCombineFunctionReturnTypeExtension.php | 40 +++++++++++++------ .../Analyser/nsrt/array-combine-php8.php | 13 ++++++ 2 files changed, 41 insertions(+), 12 deletions(-) diff --git a/src/Type/Php/ArrayCombineFunctionReturnTypeExtension.php b/src/Type/Php/ArrayCombineFunctionReturnTypeExtension.php index 1bd7c15ef9..8e5341d29d 100644 --- a/src/Type/Php/ArrayCombineFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArrayCombineFunctionReturnTypeExtension.php @@ -10,7 +10,6 @@ use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\Accessory\NonEmptyArrayType; use PHPStan\Type\ArrayType; -use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantArrayTypeBuilder; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\ConstantScalarType; @@ -50,29 +49,46 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $keysParamType = $scope->getType($firstArg); $valuesParamType = $scope->getType($secondArg); - if ( - $keysParamType instanceof ConstantArrayType - && $valuesParamType instanceof ConstantArrayType - ) { - $keyTypes = $keysParamType->getValueTypes(); - $valueTypes = $valuesParamType->getValueTypes(); - - if (count($keyTypes) !== count($valueTypes)) { + $constantKeysArrays = $keysParamType->getConstantArrays(); + $constantValuesArrays = $valuesParamType->getConstantArrays(); + if ($constantKeysArrays !== [] && $constantValuesArrays !== []) { + if (count($constantKeysArrays) !== count($constantValuesArrays)) { if ($this->phpVersion->throwsTypeErrorForInternalFunctions()) { return new NeverType(); } return new ConstantBooleanType(false); } - $keyTypes = $this->sanitizeConstantArrayKeyTypes($keyTypes); - if ($keyTypes !== null) { + $results = []; + foreach ($constantKeysArrays as $k => $constantKeysArray) { + $constantValueArrays = $constantValuesArrays[$k]; + + $keyTypes = $constantKeysArray->getValueTypes(); + $valueTypes = $constantValueArrays->getValueTypes(); + + if (count($keyTypes) !== count($valueTypes)) { + if ($this->phpVersion->throwsTypeErrorForInternalFunctions()) { + return new NeverType(); + } + return new ConstantBooleanType(false); + } + + $keyTypes = $this->sanitizeConstantArrayKeyTypes($keyTypes); + if ($keyTypes === null) { + continue; + } + $builder = ConstantArrayTypeBuilder::createEmpty(); foreach ($keyTypes as $i => $keyType) { $valueType = $valueTypes[$i]; $builder->setOffsetValueType($keyType, $valueType); } - return $builder->getArray(); + $results[] = $builder->getArray(); + } + + if ($results !== []) { + return TypeCombinator::union(...$results); } } diff --git a/tests/PHPStan/Analyser/nsrt/array-combine-php8.php b/tests/PHPStan/Analyser/nsrt/array-combine-php8.php index 0f415089bd..efb1d8df32 100644 --- a/tests/PHPStan/Analyser/nsrt/array-combine-php8.php +++ b/tests/PHPStan/Analyser/nsrt/array-combine-php8.php @@ -70,6 +70,19 @@ function withObjectKey() : void assertType("*NEVER*", array_combine([new Baz, 'red', 'yellow'], $b)); } +function withUnionConstArrays(): void +{ + if (rand(0, 1)) { + $a = [1]; + $b = ['avocado']; + } else { + $a = ["2", "3"]; + $b = ['apple', 'banana']; + } + + assertType("array{1: 'avocado'}|array{2: 'apple', 3: 'banana'}", array_combine($a, $b)); +} + /** * @param non-empty-array $a * @param non-empty-array $b From cf5587e2b42033bdf49cf0cea57275da66921f24 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 4 Oct 2025 12:01:01 +0200 Subject: [PATCH 3/8] cleanup baseline --- phpstan-baseline.neon | 6 ------ 1 file changed, 6 deletions(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index ef44d82964..2db90a78a2 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1515,12 +1515,6 @@ parameters: count: 1 path: src/Type/PHPStan/ClassNameUsageLocationCreateIdentifierDynamicReturnTypeExtension.php - - - rawMessage: 'Doing instanceof PHPStan\Type\Constant\ConstantArrayType is error-prone and deprecated. Use Type::getConstantArrays() instead.' - identifier: phpstanApi.instanceofType - count: 2 - path: src/Type/Php/ArrayCombineFunctionReturnTypeExtension.php - - rawMessage: 'Doing instanceof PHPStan\Type\Constant\ConstantStringType is error-prone and deprecated. Use Type::getConstantStrings() instead.' identifier: phpstanApi.instanceofType From 6ed62e8f7a06760d0f07630a1dc41c7a0d68db6e Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 4 Oct 2025 12:03:25 +0200 Subject: [PATCH 4/8] Update array-combine-php8.php --- tests/PHPStan/Analyser/nsrt/array-combine-php8.php | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/PHPStan/Analyser/nsrt/array-combine-php8.php b/tests/PHPStan/Analyser/nsrt/array-combine-php8.php index efb1d8df32..54eaacf082 100644 --- a/tests/PHPStan/Analyser/nsrt/array-combine-php8.php +++ b/tests/PHPStan/Analyser/nsrt/array-combine-php8.php @@ -83,6 +83,19 @@ function withUnionConstArrays(): void assertType("array{1: 'avocado'}|array{2: 'apple', 3: 'banana'}", array_combine($a, $b)); } +function withUnionConstArraysDifferentKeyValueCount(): void +{ + if (rand(0, 1)) { + $a = [1]; + $b = []; + } else { + $a = ["2", "3"]; + $b = ['apple', 'banana']; + } + + assertType("*NEVER*", array_combine($a, $b)); +} + /** * @param non-empty-array $a * @param non-empty-array $b From b69054c1f7b27da40e1e391540e7e4f28ff5fc31 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 4 Oct 2025 12:07:54 +0200 Subject: [PATCH 5/8] fix different constant arrays counts --- .../ArrayCombineFunctionReturnTypeExtension.php | 13 +++++-------- .../Analyser/nsrt/array-combine-php8.php | 17 +++++++++++++++++ 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/src/Type/Php/ArrayCombineFunctionReturnTypeExtension.php b/src/Type/Php/ArrayCombineFunctionReturnTypeExtension.php index 8e5341d29d..4a705422a8 100644 --- a/src/Type/Php/ArrayCombineFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArrayCombineFunctionReturnTypeExtension.php @@ -51,14 +51,11 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $constantKeysArrays = $keysParamType->getConstantArrays(); $constantValuesArrays = $valuesParamType->getConstantArrays(); - if ($constantKeysArrays !== [] && $constantValuesArrays !== []) { - if (count($constantKeysArrays) !== count($constantValuesArrays)) { - if ($this->phpVersion->throwsTypeErrorForInternalFunctions()) { - return new NeverType(); - } - return new ConstantBooleanType(false); - } - + if ( + $constantKeysArrays !== [] + && $constantValuesArrays !== [] + && count($constantKeysArrays) === count($constantValuesArrays) + ) { $results = []; foreach ($constantKeysArrays as $k => $constantKeysArray) { $constantValueArrays = $constantValuesArrays[$k]; diff --git a/tests/PHPStan/Analyser/nsrt/array-combine-php8.php b/tests/PHPStan/Analyser/nsrt/array-combine-php8.php index 54eaacf082..6634be78d7 100644 --- a/tests/PHPStan/Analyser/nsrt/array-combine-php8.php +++ b/tests/PHPStan/Analyser/nsrt/array-combine-php8.php @@ -96,6 +96,23 @@ function withUnionConstArraysDifferentKeyValueCount(): void assertType("*NEVER*", array_combine($a, $b)); } +function withUnionConstArraysDifferentArraysCount(): void +{ + if (rand(0, 1)) { + $a = [1]; + $b = ['avocado']; + } else { + $a = ["2", "3"]; + if (rand(0, 1)) { + $b = ['apple', 'banana']; + } else { + $b = ['banana', 'pear']; + } + } + + assertType("non-empty-array<1|'2'|'3', 'apple'|'avocado'|'banana'|'pear'>", array_combine($a, $b)); +} + /** * @param non-empty-array $a * @param non-empty-array $b From fff9f2ef9b68929122be470a57e232de98167ddc Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 4 Oct 2025 12:56:14 +0200 Subject: [PATCH 6/8] prevent crash --- .../Php/ArrayCombineFunctionReturnTypeExtension.php | 5 +++++ tests/PHPStan/Analyser/nsrt/array-combine-php8.php | 13 +++++++++++++ 2 files changed, 18 insertions(+) diff --git a/src/Type/Php/ArrayCombineFunctionReturnTypeExtension.php b/src/Type/Php/ArrayCombineFunctionReturnTypeExtension.php index 4a705422a8..a9b9c0e042 100644 --- a/src/Type/Php/ArrayCombineFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArrayCombineFunctionReturnTypeExtension.php @@ -20,6 +20,7 @@ use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use PHPStan\Type\UnionType; +use function array_key_exists; use function count; use function is_int; use function is_string; @@ -77,6 +78,10 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $builder = ConstantArrayTypeBuilder::createEmpty(); foreach ($keyTypes as $i => $keyType) { + if (!array_key_exists($i, $valueTypes)) { + $results = []; + break 2; + } $valueType = $valueTypes[$i]; $builder->setOffsetValueType($keyType, $valueType); } diff --git a/tests/PHPStan/Analyser/nsrt/array-combine-php8.php b/tests/PHPStan/Analyser/nsrt/array-combine-php8.php index 6634be78d7..842137f33e 100644 --- a/tests/PHPStan/Analyser/nsrt/array-combine-php8.php +++ b/tests/PHPStan/Analyser/nsrt/array-combine-php8.php @@ -113,6 +113,19 @@ function withUnionConstArraysDifferentArraysCount(): void assertType("non-empty-array<1|'2'|'3', 'apple'|'avocado'|'banana'|'pear'>", array_combine($a, $b)); } +function withUnionConstArraysAndInvalidKeys(bool $bool): void +{ + if (rand(0, 1)) { + $a = [$bool]; + $b = ['avocado']; + } else { + $a = ["2", $bool]; + $b = ['apple', 'banana']; + } + + assertType("non-empty-array<''|'1'|'2', 'apple'|'avocado'|'banana'>", array_combine($a, $b)); +} + /** * @param non-empty-array $a * @param non-empty-array $b From 18e7813afbc553840550f0d01378913a7219f8ee Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 4 Oct 2025 13:01:11 +0200 Subject: [PATCH 7/8] better test name --- tests/PHPStan/Analyser/nsrt/array-combine-php8.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/PHPStan/Analyser/nsrt/array-combine-php8.php b/tests/PHPStan/Analyser/nsrt/array-combine-php8.php index 842137f33e..fe7b1988ec 100644 --- a/tests/PHPStan/Analyser/nsrt/array-combine-php8.php +++ b/tests/PHPStan/Analyser/nsrt/array-combine-php8.php @@ -113,7 +113,7 @@ function withUnionConstArraysDifferentArraysCount(): void assertType("non-empty-array<1|'2'|'3', 'apple'|'avocado'|'banana'|'pear'>", array_combine($a, $b)); } -function withUnionConstArraysAndInvalidKeys(bool $bool): void +function withUnionConstArraysAndDifferentFiniteKeysCount(bool $bool): void { if (rand(0, 1)) { $a = [$bool]; From 19c82b11c513cf07c337875bc50b22f9ff91a177 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 4 Oct 2025 13:02:29 +0200 Subject: [PATCH 8/8] Update array-combine-php8.php --- tests/PHPStan/Analyser/nsrt/array-combine-php8.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/PHPStan/Analyser/nsrt/array-combine-php8.php b/tests/PHPStan/Analyser/nsrt/array-combine-php8.php index fe7b1988ec..b3a74723d7 100644 --- a/tests/PHPStan/Analyser/nsrt/array-combine-php8.php +++ b/tests/PHPStan/Analyser/nsrt/array-combine-php8.php @@ -116,10 +116,10 @@ function withUnionConstArraysDifferentArraysCount(): void function withUnionConstArraysAndDifferentFiniteKeysCount(bool $bool): void { if (rand(0, 1)) { - $a = [$bool]; + $a = [$bool]; // translates to [true, false] $b = ['avocado']; } else { - $a = ["2", $bool]; + $a = ["2", $bool]; // translates to ["2", true, false] $b = ['apple', 'banana']; }