diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 1ec2cd321b..2db90a78a2 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1515,18 +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 - 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..a9b9c0e042 100644 --- a/src/Type/Php/ArrayCombineFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArrayCombineFunctionReturnTypeExtension.php @@ -10,11 +10,9 @@ 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\Constant\ConstantIntegerType; -use PHPStan\Type\Constant\ConstantStringType; +use PHPStan\Type\ConstantScalarType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\ErrorType; use PHPStan\Type\MixedType; @@ -22,7 +20,10 @@ 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; #[AutowiredService] final class ArrayCombineFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension @@ -49,29 +50,47 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $keysParamType = $scope->getType($firstArg); $valuesParamType = $scope->getType($secondArg); + $constantKeysArrays = $keysParamType->getConstantArrays(); + $constantValuesArrays = $valuesParamType->getConstantArrays(); if ( - $keysParamType instanceof ConstantArrayType - && $valuesParamType instanceof ConstantArrayType + $constantKeysArrays !== [] + && $constantValuesArrays !== [] + && count($constantKeysArrays) === count($constantValuesArrays) ) { - $keyTypes = $keysParamType->getValueTypes(); - $valueTypes = $valuesParamType->getValueTypes(); + $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); + } - if (count($keyTypes) !== count($valueTypes)) { - if ($this->phpVersion->throwsTypeErrorForInternalFunctions()) { - return new NeverType(); + $keyTypes = $this->sanitizeConstantArrayKeyTypes($keyTypes); + if ($keyTypes === null) { + continue; } - return new ConstantBooleanType(false); - } - $keyTypes = $this->sanitizeConstantArrayKeyTypes($keyTypes); - if ($keyTypes !== null) { $builder = ConstantArrayTypeBuilder::createEmpty(); foreach ($keyTypes as $i => $keyType) { + if (!array_key_exists($i, $valueTypes)) { + $results = []; + break 2; + } $valueType = $valueTypes[$i]; $builder->setOffsetValueType($keyType, $valueType); } - return $builder->getArray(); + $results[] = $builder->getArray(); + } + + if ($results !== []) { + return TypeCombinator::union(...$results); } } @@ -114,7 +133,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, /** * @param array $types * - * @return array|null + * @return list|null */ private function sanitizeConstantArrayKeyTypes(array $types): ?array { @@ -125,14 +144,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; diff --git a/tests/PHPStan/Analyser/nsrt/array-combine-php8.php b/tests/PHPStan/Analyser/nsrt/array-combine-php8.php index 0f415089bd..b3a74723d7 100644 --- a/tests/PHPStan/Analyser/nsrt/array-combine-php8.php +++ b/tests/PHPStan/Analyser/nsrt/array-combine-php8.php @@ -70,6 +70,62 @@ 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)); +} + +function withUnionConstArraysDifferentKeyValueCount(): void +{ + if (rand(0, 1)) { + $a = [1]; + $b = []; + } else { + $a = ["2", "3"]; + $b = ['apple', 'banana']; + } + + 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)); +} + +function withUnionConstArraysAndDifferentFiniteKeysCount(bool $bool): void +{ + if (rand(0, 1)) { + $a = [$bool]; // translates to [true, false] + $b = ['avocado']; + } else { + $a = ["2", $bool]; // translates to ["2", true, false] + $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