From 1b462085e7c073d73b7e1580182e77a6a33196b8 Mon Sep 17 00:00:00 2001 From: Martin Herndl Date: Wed, 4 Dec 2024 16:59:12 +0100 Subject: [PATCH] Narrow explode return to constant arrays for small positive limits --- ...lodeFunctionDynamicReturnTypeExtension.php | 25 +++++++++++--- tests/PHPStan/Analyser/nsrt/bug-3961-php8.php | 4 +-- tests/PHPStan/Analyser/nsrt/bug-3961.php | 4 +-- tests/PHPStan/Analyser/nsrt/explode-php7.php | 34 +++++++++++++++++++ tests/PHPStan/Analyser/nsrt/explode-php8.php | 34 +++++++++++++++++++ 5 files changed, 92 insertions(+), 9 deletions(-) create mode 100755 tests/PHPStan/Analyser/nsrt/explode-php7.php create mode 100755 tests/PHPStan/Analyser/nsrt/explode-php8.php diff --git a/src/Type/Php/ExplodeFunctionDynamicReturnTypeExtension.php b/src/Type/Php/ExplodeFunctionDynamicReturnTypeExtension.php index d675eee45c..a3ef3dbf3b 100644 --- a/src/Type/Php/ExplodeFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/ExplodeFunctionDynamicReturnTypeExtension.php @@ -11,7 +11,9 @@ use PHPStan\Type\Accessory\AccessoryUppercaseStringType; use PHPStan\Type\Accessory\NonEmptyArrayType; use PHPStan\Type\ArrayType; +use PHPStan\Type\Constant\ConstantArrayTypeBuilder; use PHPStan\Type\Constant\ConstantBooleanType; +use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\IntegerRangeType; @@ -24,10 +26,13 @@ use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypeUtils; use function count; +use const PHP_INT_MAX; final class ExplodeFunctionDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { + private const CONST_ARRAY_LIMIT = 8; + public function __construct(private PhpVersion $phpVersion) { } @@ -74,11 +79,21 @@ public function getTypeFromFunctionCall( $returnType = AccessoryArrayListType::intersectWith(new ArrayType(new IntegerType(), $returnValueType)); - if ( - !isset($args[2]) - || IntegerRangeType::fromInterval(0, null)->isSuperTypeOf($scope->getType($args[2]->value))->yes() - ) { - $returnType = TypeCombinator::intersect($returnType, new NonEmptyArrayType()); + $limitType = isset($args[2]) ? $scope->getType($args[2]->value) : new ConstantIntegerType(PHP_INT_MAX); + if (IntegerRangeType::fromInterval(0, null)->isSuperTypeOf($limitType)->yes()) { + $constantScalarTypes = $limitType->getConstantScalarTypes(); + if (count($constantScalarTypes) === 1 && IntegerRangeType::fromInterval(0, self::CONST_ARRAY_LIMIT)->isSuperTypeOf($limitType)->yes()) { + $limit = (int) $constantScalarTypes[0]->getValue() ?: 1; // 0 is treated as 1 + + $builder = ConstantArrayTypeBuilder::createEmpty(); + for ($i = 0; $i < $limit; $i++) { + $builder->setOffsetValueType(null, $returnValueType, $i !== 0); + } + + $returnType = $builder->getArray(); + } else { + $returnType = TypeCombinator::intersect($returnType, new NonEmptyArrayType()); + } } if (!$this->phpVersion->throwsValueErrorForInternalFunctions() && $isEmptyString->maybe()) { diff --git a/tests/PHPStan/Analyser/nsrt/bug-3961-php8.php b/tests/PHPStan/Analyser/nsrt/bug-3961-php8.php index 9eaeff4a72..dc0ad9f383 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-3961-php8.php +++ b/tests/PHPStan/Analyser/nsrt/bug-3961-php8.php @@ -12,8 +12,8 @@ public function doFoo(string $v, string $d, $m): void assertType('non-empty-list', explode('.', $v)); assertType('*NEVER*', explode('', $v)); assertType('list', explode('.', $v, -2)); - assertType('non-empty-list', explode('.', $v, 0)); - assertType('non-empty-list', explode('.', $v, 1)); + assertType('array{string}', explode('.', $v, 0)); + assertType('array{string}', explode('.', $v, 1)); assertType('non-empty-list', explode($d, $v)); assertType('non-empty-list', explode($m, $v)); } diff --git a/tests/PHPStan/Analyser/nsrt/bug-3961.php b/tests/PHPStan/Analyser/nsrt/bug-3961.php index b4725ec070..da961cb9fa 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-3961.php +++ b/tests/PHPStan/Analyser/nsrt/bug-3961.php @@ -12,8 +12,8 @@ public function doFoo(string $v, string $d, $m): void assertType('non-empty-list', explode('.', $v)); assertType('false', explode('', $v)); assertType('list', explode('.', $v, -2)); - assertType('non-empty-list', explode('.', $v, 0)); - assertType('non-empty-list', explode('.', $v, 1)); + assertType('array{string}', explode('.', $v, 0)); + assertType('array{string}', explode('.', $v, 1)); assertType('non-empty-list|false', explode($d, $v)); assertType('(non-empty-list|false)', explode($m, $v)); } diff --git a/tests/PHPStan/Analyser/nsrt/explode-php7.php b/tests/PHPStan/Analyser/nsrt/explode-php7.php new file mode 100755 index 0000000000..9c7d902e94 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/explode-php7.php @@ -0,0 +1,34 @@ +', $strings); + + $strings = explode(',', $nonEmptyString, 2); + assertType('array{0: string, 1?: string}', $strings); + + $strings = explode(',', $nonEmptyString, 16); + assertType('non-empty-list', $strings); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/explode-php8.php b/tests/PHPStan/Analyser/nsrt/explode-php8.php new file mode 100755 index 0000000000..cc33512b49 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/explode-php8.php @@ -0,0 +1,34 @@ += 8.0 + +namespace ExplodePhp8; + +use function PHPStan\Testing\assertType; + +class Foo +{ + + /** + * @param non-empty-string $nonEmptyString + */ + public function constantArrays(string $string, string $nonEmptyString): void + { + $strings = explode(',', $string, 0); + assertType('array{string}', $strings); + + $strings = explode(',', $string, 2); + assertType('array{0: string, 1?: string}', $strings); + + $strings = explode(rand(0, 1) ? '' : ',', $string, 2); + assertType('array{0: string, 1?: string}', $strings); + + $strings = explode(',', $string, 16); + assertType('non-empty-list', $strings); + + $strings = explode(',', $nonEmptyString, 2); + assertType('array{0: string, 1?: string}', $strings); + + $strings = explode(',', $nonEmptyString, 16); + assertType('non-empty-list', $strings); + } + +}