From f81e0d2abd878074abae13a4de72ca1cc08f5fa8 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Thu, 22 May 2025 13:50:20 +0200 Subject: [PATCH 1/3] Improve mb_convert_encoding return type --- ...ertEncodingFunctionReturnTypeExtension.php | 45 +++++++++++++++++++ .../Analyser/NodeScopeResolverTest.php | 6 +++ .../mb-convert-encoding-php7.php} | 2 +- .../data/mb-convert-encoding-php8.php | 45 +++++++++++++++++++ 4 files changed, 97 insertions(+), 1 deletion(-) rename tests/PHPStan/Analyser/{nsrt/mb_convert_encoding.php => data/mb-convert-encoding-php7.php} (97%) create mode 100644 tests/PHPStan/Analyser/data/mb-convert-encoding-php8.php diff --git a/src/Type/Php/MbConvertEncodingFunctionReturnTypeExtension.php b/src/Type/Php/MbConvertEncodingFunctionReturnTypeExtension.php index a09f0a823a..b5fc1f1ce8 100644 --- a/src/Type/Php/MbConvertEncodingFunctionReturnTypeExtension.php +++ b/src/Type/Php/MbConvertEncodingFunctionReturnTypeExtension.php @@ -4,6 +4,7 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\Php\PhpVersion; use PHPStan\Reflection\FunctionReflection; use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Type\Accessory\AccessoryArrayListType; @@ -17,10 +18,15 @@ use PHPStan\Type\TypeCombinator; use PHPStan\Type\UnionType; use function count; +use function str_contains; final class MbConvertEncodingFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { + public function __construct(private PhpVersion $phpVersion) + { + } + public function isFunctionSupported(FunctionReflection $functionReflection): bool { return $functionReflection->getName() === 'mb_convert_encoding'; @@ -49,6 +55,45 @@ public function getTypeFromFunctionCall( return null; } + if ($this->phpVersion->throwsValueErrorForInternalFunctions()) { + if (!isset($functionCall->getArgs()[2])) { + return $result; + } + $fromEncodingArgType = $scope->getType($functionCall->getArgs()[2]->value); + + $mayNotDetectEncoding = false; + if (!$fromEncodingArgType->isArray()->no()) { + $constantArrays = $fromEncodingArgType->getConstantArrays(); + if (count($constantArrays) > 0) { + foreach ($constantArrays as $constantArray) { + if (count($constantArray->getValueTypes()) > 1) { + $mayNotDetectEncoding = true; + break; + } + } + } else { + $mayNotDetectEncoding = true; + } + } + if (!$mayNotDetectEncoding && !$fromEncodingArgType->isString()->no()) { + $constantStrings = $fromEncodingArgType->getConstantStrings(); + if (count($constantStrings) > 0) { + foreach ($constantStrings as $constantString) { + if (str_contains($constantString->getValue(), ',')) { + $mayNotDetectEncoding = true; + break; + } + } + } else { + $mayNotDetectEncoding = true; + } + } + + if (!$mayNotDetectEncoding) { + return $result; + } + } + return TypeCombinator::union($result, new ConstantBooleanType(false)); } diff --git a/tests/PHPStan/Analyser/NodeScopeResolverTest.php b/tests/PHPStan/Analyser/NodeScopeResolverTest.php index 1e3afb4e70..f3fb58a50c 100644 --- a/tests/PHPStan/Analyser/NodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/NodeScopeResolverTest.php @@ -57,6 +57,12 @@ private static function findTestFiles(): iterable yield __DIR__ . '/data/explode-php80.php'; } + if (PHP_VERSION_ID < 80000) { + yield __DIR__ . '/data/mb-convert-encoding-php7.php'; + } else { + yield __DIR__ . '/data/mb-convert-encoding-php8.php'; + } + if (PHP_VERSION_ID >= 80000) { yield __DIR__ . '/../Reflection/data/unionTypes.php'; yield __DIR__ . '/../Reflection/data/mixedType.php'; diff --git a/tests/PHPStan/Analyser/nsrt/mb_convert_encoding.php b/tests/PHPStan/Analyser/data/mb-convert-encoding-php7.php similarity index 97% rename from tests/PHPStan/Analyser/nsrt/mb_convert_encoding.php rename to tests/PHPStan/Analyser/data/mb-convert-encoding-php7.php index f5f524d44f..36b3dc5f2b 100644 --- a/tests/PHPStan/Analyser/nsrt/mb_convert_encoding.php +++ b/tests/PHPStan/Analyser/data/mb-convert-encoding-php7.php @@ -1,6 +1,6 @@ $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', mb_convert_encoding($constantString, 'UTF-8')); + \PHPStan\Testing\assertType('string', mb_convert_encoding($string, 'UTF-8')); + \PHPStan\Testing\assertType('array', mb_convert_encoding($mixedArray, 'UTF-8')); + \PHPStan\Testing\assertType('array{foo: string, bar: int, baz: string}', mb_convert_encoding($structuredArray, 'UTF-8')); + \PHPStan\Testing\assertType('list', mb_convert_encoding($stringList, 'UTF-8')); + \PHPStan\Testing\assertType('list', 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')); + + \PHPStan\Testing\assertType('string', mb_convert_encoding($string, 'UTF-8', 'FOO')); + \PHPStan\Testing\assertType('string|false', mb_convert_encoding($string, 'UTF-8', $string)); + \PHPStan\Testing\assertType('string|false', mb_convert_encoding($string, 'UTF-8', 'FOO,BAR')); + \PHPStan\Testing\assertType('string', mb_convert_encoding($string, 'UTF-8', ['FOO'])); + \PHPStan\Testing\assertType('string|false', mb_convert_encoding($string, 'UTF-8', ['FOO', 'BAR'])); + \PHPStan\Testing\assertType('string', mb_convert_encoding($string, 'UTF-8', ['FOO,BAR'])); + \PHPStan\Testing\assertType('list', mb_convert_encoding($stringList, 'UTF-8', 'FOO')); + \PHPStan\Testing\assertType('list|false', mb_convert_encoding($stringList, 'UTF-8', $string)); + \PHPStan\Testing\assertType('list|false', mb_convert_encoding($stringList, 'UTF-8', 'FOO,BAR')); + \PHPStan\Testing\assertType('list', mb_convert_encoding($stringList, 'UTF-8', ['FOO'])); + \PHPStan\Testing\assertType('list|false', mb_convert_encoding($stringList, 'UTF-8', ['FOO', 'BAR'])); + \PHPStan\Testing\assertType('list', mb_convert_encoding($stringList, 'UTF-8', ['FOO,BAR'])); +}; From ab9ecd39ce59d04e0e6309ca90d4d1c93d80785f Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Thu, 22 May 2025 16:47:49 +0200 Subject: [PATCH 2/3] Rework --- ...bConvertEncodingFunctionReturnTypeExtension.php | 14 +++++++------- tests/PHPStan/Analyser/NodeScopeResolverTest.php | 6 ------ .../{data => nsrt}/mb-convert-encoding-php7.php | 2 +- .../{data => nsrt}/mb-convert-encoding-php8.php | 2 +- 4 files changed, 9 insertions(+), 15 deletions(-) rename tests/PHPStan/Analyser/{data => nsrt}/mb-convert-encoding-php7.php (98%) rename tests/PHPStan/Analyser/{data => nsrt}/mb-convert-encoding-php8.php (99%) diff --git a/src/Type/Php/MbConvertEncodingFunctionReturnTypeExtension.php b/src/Type/Php/MbConvertEncodingFunctionReturnTypeExtension.php index b5fc1f1ce8..17f0211010 100644 --- a/src/Type/Php/MbConvertEncodingFunctionReturnTypeExtension.php +++ b/src/Type/Php/MbConvertEncodingFunctionReturnTypeExtension.php @@ -61,35 +61,35 @@ public function getTypeFromFunctionCall( } $fromEncodingArgType = $scope->getType($functionCall->getArgs()[2]->value); - $mayNotDetectEncoding = false; + $returnFalseIfCannotDetectEncoding = false; if (!$fromEncodingArgType->isArray()->no()) { $constantArrays = $fromEncodingArgType->getConstantArrays(); if (count($constantArrays) > 0) { foreach ($constantArrays as $constantArray) { if (count($constantArray->getValueTypes()) > 1) { - $mayNotDetectEncoding = true; + $returnFalseIfCannotDetectEncoding = true; break; } } } else { - $mayNotDetectEncoding = true; + $returnFalseIfCannotDetectEncoding = true; } } - if (!$mayNotDetectEncoding && !$fromEncodingArgType->isString()->no()) { + if (!$returnFalseIfCannotDetectEncoding && !$fromEncodingArgType->isString()->no()) { $constantStrings = $fromEncodingArgType->getConstantStrings(); if (count($constantStrings) > 0) { foreach ($constantStrings as $constantString) { if (str_contains($constantString->getValue(), ',')) { - $mayNotDetectEncoding = true; + $returnFalseIfCannotDetectEncoding = true; break; } } } else { - $mayNotDetectEncoding = true; + $returnFalseIfCannotDetectEncoding = true; } } - if (!$mayNotDetectEncoding) { + if (!$returnFalseIfCannotDetectEncoding) { return $result; } } diff --git a/tests/PHPStan/Analyser/NodeScopeResolverTest.php b/tests/PHPStan/Analyser/NodeScopeResolverTest.php index f3fb58a50c..1e3afb4e70 100644 --- a/tests/PHPStan/Analyser/NodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/NodeScopeResolverTest.php @@ -57,12 +57,6 @@ private static function findTestFiles(): iterable yield __DIR__ . '/data/explode-php80.php'; } - if (PHP_VERSION_ID < 80000) { - yield __DIR__ . '/data/mb-convert-encoding-php7.php'; - } else { - yield __DIR__ . '/data/mb-convert-encoding-php8.php'; - } - if (PHP_VERSION_ID >= 80000) { yield __DIR__ . '/../Reflection/data/unionTypes.php'; yield __DIR__ . '/../Reflection/data/mixedType.php'; diff --git a/tests/PHPStan/Analyser/data/mb-convert-encoding-php7.php b/tests/PHPStan/Analyser/nsrt/mb-convert-encoding-php7.php similarity index 98% rename from tests/PHPStan/Analyser/data/mb-convert-encoding-php7.php rename to tests/PHPStan/Analyser/nsrt/mb-convert-encoding-php7.php index 36b3dc5f2b..d1de6c7c24 100644 --- a/tests/PHPStan/Analyser/data/mb-convert-encoding-php7.php +++ b/tests/PHPStan/Analyser/nsrt/mb-convert-encoding-php7.php @@ -1,4 +1,4 @@ -= 8.0 namespace MbConvertEncodingPHP8; From 49f7925b47532e7e618b17dba6893ddb5f8115a8 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Thu, 22 May 2025 17:10:03 +0200 Subject: [PATCH 3/3] Try --- .../Php/MbConvertEncodingFunctionReturnTypeExtension.php | 6 +++--- tests/PHPStan/Analyser/nsrt/mb-convert-encoding-php8.php | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Type/Php/MbConvertEncodingFunctionReturnTypeExtension.php b/src/Type/Php/MbConvertEncodingFunctionReturnTypeExtension.php index 17f0211010..7878ba704e 100644 --- a/src/Type/Php/MbConvertEncodingFunctionReturnTypeExtension.php +++ b/src/Type/Php/MbConvertEncodingFunctionReturnTypeExtension.php @@ -52,12 +52,12 @@ public function getTypeFromFunctionCall( $result = TypeCombinator::intersect($initialReturnType, $this->generalizeStringType($argType)); if ($result instanceof NeverType) { - return null; + $result = $initialReturnType; } if ($this->phpVersion->throwsValueErrorForInternalFunctions()) { if (!isset($functionCall->getArgs()[2])) { - return $result; + return TypeCombinator::remove($result, new ConstantBooleanType(false)); } $fromEncodingArgType = $scope->getType($functionCall->getArgs()[2]->value); @@ -90,7 +90,7 @@ public function getTypeFromFunctionCall( } if (!$returnFalseIfCannotDetectEncoding) { - return $result; + return TypeCombinator::remove($result, new ConstantBooleanType(false)); } } diff --git a/tests/PHPStan/Analyser/nsrt/mb-convert-encoding-php8.php b/tests/PHPStan/Analyser/nsrt/mb-convert-encoding-php8.php index 24b219d66e..a4d4121e18 100644 --- a/tests/PHPStan/Analyser/nsrt/mb-convert-encoding-php8.php +++ b/tests/PHPStan/Analyser/nsrt/mb-convert-encoding-php8.php @@ -20,15 +20,15 @@ function test_mb_convert_encoding( string|array|bool $union, int $int, ): void { - \PHPStan\Testing\assertType('array|string|false', mb_convert_encoding($mixed, 'UTF-8')); + \PHPStan\Testing\assertType('array|string', mb_convert_encoding($mixed, 'UTF-8')); \PHPStan\Testing\assertType('string', mb_convert_encoding($constantString, 'UTF-8')); \PHPStan\Testing\assertType('string', mb_convert_encoding($string, 'UTF-8')); \PHPStan\Testing\assertType('array', mb_convert_encoding($mixedArray, 'UTF-8')); \PHPStan\Testing\assertType('array{foo: string, bar: int, baz: string}', mb_convert_encoding($structuredArray, 'UTF-8')); \PHPStan\Testing\assertType('list', mb_convert_encoding($stringList, 'UTF-8')); \PHPStan\Testing\assertType('list', 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')); + \PHPStan\Testing\assertType('array{foo: string, bar: int, baz: string}|string', mb_convert_encoding($union, 'UTF-8')); + \PHPStan\Testing\assertType('array|string', mb_convert_encoding($int, 'UTF-8')); \PHPStan\Testing\assertType('string', mb_convert_encoding($string, 'UTF-8', 'FOO')); \PHPStan\Testing\assertType('string|false', mb_convert_encoding($string, 'UTF-8', $string));