From d49d8945aa0e7ef3d9716e1e4925a96d9d6c062b Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Mon, 31 Mar 2025 23:05:17 +0200 Subject: [PATCH] Fix mb_convert_encoding signature --- resources/functionMap.php | 2 +- ...ertEncodingFunctionReturnTypeExtension.php | 61 ++++++++++++++++--- tests/PHPStan/Analyser/nsrt/bug-3336.php | 8 +-- .../Analyser/nsrt/mb_convert_encoding.php | 32 ++++++++++ 4 files changed, 90 insertions(+), 13 deletions(-) create mode 100644 tests/PHPStan/Analyser/nsrt/mb_convert_encoding.php diff --git a/resources/functionMap.php b/resources/functionMap.php index c772596bcc..f5d9a82715 100644 --- a/resources/functionMap.php +++ b/resources/functionMap.php @@ -6312,7 +6312,7 @@ 'mb_check_encoding' => ['bool', 'var='=>'string|array', 'encoding='=>'string'], 'mb_chr' => ['string|false', 'cp'=>'int', 'encoding='=>'string'], 'mb_convert_case' => ['string', 'sourcestring'=>'string', 'mode'=>'int', 'encoding='=>'string'], -'mb_convert_encoding' => ['string|array|false', 'val'=>'string|array', 'to_encoding'=>'string', 'from_encoding='=>'mixed'], +'mb_convert_encoding' => ['string|array|false', 'val'=>'string|array', 'to_encoding'=>'string', 'from_encoding='=>'mixed'], 'mb_convert_kana' => ['string', 'str'=>'string', 'option='=>'string', 'encoding='=>'string'], 'mb_convert_variables' => ['string|false', 'to_encoding'=>'string', 'from_encoding'=>'array|string', '&rw_vars'=>'string|array|object', '&...rw_vars='=>'string|array|object'], 'mb_decode_mimeheader' => ['string', 'string'=>'string'], diff --git a/src/Type/Php/MbConvertEncodingFunctionReturnTypeExtension.php b/src/Type/Php/MbConvertEncodingFunctionReturnTypeExtension.php index dd46ed4c2a..a09f0a823a 100644 --- a/src/Type/Php/MbConvertEncodingFunctionReturnTypeExtension.php +++ b/src/Type/Php/MbConvertEncodingFunctionReturnTypeExtension.php @@ -5,11 +5,18 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; +use PHPStan\Reflection\ParametersAcceptorSelector; +use PHPStan\Type\Accessory\AccessoryArrayListType; +use PHPStan\Type\Accessory\NonEmptyArrayType; use PHPStan\Type\ArrayType; +use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; -use PHPStan\Type\IntegerType; +use PHPStan\Type\NeverType; use PHPStan\Type\StringType; use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; +use PHPStan\Type\UnionType; +use function count; final class MbConvertEncodingFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { @@ -30,16 +37,54 @@ public function getTypeFromFunctionCall( } $argType = $scope->getType($functionCall->getArgs()[0]->value); - $isString = $argType->isString(); - $isArray = $argType->isArray(); - $compare = $isString->compareTo($isArray); - if ($compare === $isString) { + + $initialReturnType = ParametersAcceptorSelector::selectFromArgs( + $scope, + $functionCall->getArgs(), + $functionReflection->getVariants(), + )->getReturnType(); + + $result = TypeCombinator::intersect($initialReturnType, $this->generalizeStringType($argType)); + if ($result instanceof NeverType) { + return null; + } + + return TypeCombinator::union($result, new ConstantBooleanType(false)); + } + + public function generalizeStringType(Type $type): Type + { + if ($type instanceof UnionType) { + return $type->traverse([$this, 'generalizeStringType']); + } + + if ($type->isString()->yes()) { return new StringType(); - } elseif ($compare === $isArray) { - return new ArrayType(new IntegerType(), new StringType()); } - return null; + $constantArrays = $type->getConstantArrays(); + if (count($constantArrays) > 0) { + $types = []; + foreach ($constantArrays as $constantArray) { + $types[] = $constantArray->traverse([$this, 'generalizeStringType']); + } + + return TypeCombinator::union(...$types); + } + + if ($type->isArray()->yes()) { + $newArrayType = new ArrayType($type->getIterableKeyType(), $this->generalizeStringType($type->getIterableValueType())); + if ($type->isIterableAtLeastOnce()->yes()) { + $newArrayType = TypeCombinator::intersect($newArrayType, new NonEmptyArrayType()); + } + if ($type->isList()->yes()) { + $newArrayType = TypeCombinator::intersect($newArrayType, new AccessoryArrayListType()); + } + + return $newArrayType; + } + + return $type; } } diff --git a/tests/PHPStan/Analyser/nsrt/bug-3336.php b/tests/PHPStan/Analyser/nsrt/bug-3336.php index a6712e6f69..b0707c61aa 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-3336.php +++ b/tests/PHPStan/Analyser/nsrt/bug-3336.php @@ -3,8 +3,8 @@ namespace Bug3336; function (array $arr, string $str, $mixed): void { - \PHPStan\Testing\assertType('array', mb_convert_encoding($arr)); - \PHPStan\Testing\assertType('string', mb_convert_encoding($str)); - \PHPStan\Testing\assertType('array|string|false', mb_convert_encoding($mixed)); - \PHPStan\Testing\assertType('array|string|false', mb_convert_encoding()); + \PHPStan\Testing\assertType('array|false', mb_convert_encoding($arr)); + \PHPStan\Testing\assertType('string|false', mb_convert_encoding($str)); + \PHPStan\Testing\assertType('array|string|false', mb_convert_encoding($mixed)); + \PHPStan\Testing\assertType('array|string|false', mb_convert_encoding()); }; diff --git a/tests/PHPStan/Analyser/nsrt/mb_convert_encoding.php b/tests/PHPStan/Analyser/nsrt/mb_convert_encoding.php new file mode 100644 index 0000000000..f5f524d44f --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/mb_convert_encoding.php @@ -0,0 +1,32 @@ + $stringList + * @param list $intList + * @param 'foo'|'bar'|array{foo: string, bar: int, baz: 'foo'}|bool $union + */ +function test_mb_convert_encoding( + mixed $mixed, + string $constantString, + string $string, + array $mixedArray, + array $structuredArray, + array $stringList, + array $intList, + string|array|bool $union, + int $int, +): void { + \PHPStan\Testing\assertType('array|string|false', mb_convert_encoding($mixed, 'UTF-8')); + \PHPStan\Testing\assertType('string|false', mb_convert_encoding($constantString, 'UTF-8')); + \PHPStan\Testing\assertType('string|false', mb_convert_encoding($string, 'UTF-8')); + \PHPStan\Testing\assertType('array|false', mb_convert_encoding($mixedArray, 'UTF-8')); + \PHPStan\Testing\assertType('array{foo: string, bar: int, baz: string}|false', mb_convert_encoding($structuredArray, 'UTF-8')); + \PHPStan\Testing\assertType('list|false', mb_convert_encoding($stringList, 'UTF-8')); + \PHPStan\Testing\assertType('list|false', mb_convert_encoding($intList, 'UTF-8')); + \PHPStan\Testing\assertType('array{foo: string, bar: int, baz: string}|string|false', mb_convert_encoding($union, 'UTF-8')); + \PHPStan\Testing\assertType('array|string|false', mb_convert_encoding($int, 'UTF-8')); +};