From 8764aad737db84c1e74eca7aab24715af7737bbe Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Sun, 18 May 2025 21:57:29 +0200 Subject: [PATCH] Improve StrSplit returnType --- .../StrSplitFunctionReturnTypeExtension.php | 50 ++++++++++++------- .../Analyser/NodeScopeResolverTest.php | 6 +++ .../PHPStan/Analyser/data/str-split-php82.php | 46 +++++++++++++++++ tests/PHPStan/Analyser/data/str-split.php | 46 +++++++++++++++++ 4 files changed, 130 insertions(+), 18 deletions(-) create mode 100644 tests/PHPStan/Analyser/data/str-split-php82.php create mode 100644 tests/PHPStan/Analyser/data/str-split.php diff --git a/src/Type/Php/StrSplitFunctionReturnTypeExtension.php b/src/Type/Php/StrSplitFunctionReturnTypeExtension.php index 34d58152dc..224c2f5a2b 100644 --- a/src/Type/Php/StrSplitFunctionReturnTypeExtension.php +++ b/src/Type/Php/StrSplitFunctionReturnTypeExtension.php @@ -9,6 +9,9 @@ use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; use PHPStan\Type\Accessory\AccessoryArrayListType; +use PHPStan\Type\Accessory\AccessoryLowercaseStringType; +use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; +use PHPStan\Type\Accessory\AccessoryUppercaseStringType; use PHPStan\Type\Accessory\NonEmptyArrayType; use PHPStan\Type\ArrayType; use PHPStan\Type\Constant\ConstantArrayType; @@ -80,32 +83,43 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, } } - if (!isset($splitLength)) { - return null; - } - $stringType = $scope->getType($functionCall->getArgs()[0]->value); - - $constantStrings = $stringType->getConstantStrings(); - if (count($constantStrings) > 0) { - $results = []; - foreach ($constantStrings as $constantString) { - $items = $encoding === null - ? str_split($constantString->getValue(), $splitLength) - : @mb_str_split($constantString->getValue(), $splitLength, $encoding); - if ($items === false) { - throw new ShouldNotHappenException(); + if (isset($splitLength)) { + $constantStrings = $stringType->getConstantStrings(); + if (count($constantStrings) > 0) { + $results = []; + foreach ($constantStrings as $constantString) { + $items = $encoding === null + ? str_split($constantString->getValue(), $splitLength) + : @mb_str_split($constantString->getValue(), $splitLength, $encoding); + if ($items === false) { + throw new ShouldNotHappenException(); + } + + $results[] = self::createConstantArrayFrom($items, $scope); } - $results[] = self::createConstantArrayFrom($items, $scope); + return TypeCombinator::union(...$results); } + } + + $isInputNonEmptyString = $stringType->isNonEmptyString()->yes(); - return TypeCombinator::union(...$results); + $valueTypes = [new StringType()]; + if ($isInputNonEmptyString || $this->phpVersion->strSplitReturnsEmptyArray()) { + $valueTypes[] = new AccessoryNonEmptyStringType(); + } + if ($stringType->isLowercaseString()->yes()) { + $valueTypes[] = new AccessoryLowercaseStringType(); + } + if ($stringType->isUppercaseString()->yes()) { + $valueTypes[] = new AccessoryUppercaseStringType(); } + $returnValueType = TypeCombinator::intersect(new StringType(), ...$valueTypes); - $returnType = AccessoryArrayListType::intersectWith(new ArrayType(new IntegerType(), new StringType())); + $returnType = AccessoryArrayListType::intersectWith(TypeCombinator::intersect(new ArrayType(new IntegerType(), $returnValueType))); - return $encoding === null && !$this->phpVersion->strSplitReturnsEmptyArray() + return $isInputNonEmptyString || ($encoding === null && !$this->phpVersion->strSplitReturnsEmptyArray()) ? TypeCombinator::intersect($returnType, new NonEmptyArrayType()) : $returnType; } diff --git a/tests/PHPStan/Analyser/NodeScopeResolverTest.php b/tests/PHPStan/Analyser/NodeScopeResolverTest.php index 1e3afb4e70..84e12bba9e 100644 --- a/tests/PHPStan/Analyser/NodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/NodeScopeResolverTest.php @@ -29,6 +29,12 @@ private static function findTestFiles(): iterable yield $testFile; } + if (PHP_VERSION_ID >= 80200) { + yield __DIR__ . '/data/str-split-php82.php'; + } else { + yield __DIR__ . '/data/str-split.php'; + } + if (PHP_VERSION_ID < 80200 && PHP_VERSION_ID >= 80100) { yield __DIR__ . '/data/enum-reflection-php81.php'; } diff --git a/tests/PHPStan/Analyser/data/str-split-php82.php b/tests/PHPStan/Analyser/data/str-split-php82.php new file mode 100644 index 0000000000..0ef85a3e81 --- /dev/null +++ b/tests/PHPStan/Analyser/data/str-split-php82.php @@ -0,0 +1,46 @@ +', str_split($string)); + assertType('non-empty-list', str_split($nonEmptyString)); + assertType('non-empty-list', str_split($nonFalsyString)); + assertType('list', str_split($lowercaseString)); + assertType('list', str_split($uppercaseString)); + + assertType('list', str_split($string, $integer)); + assertType('non-empty-list', str_split($nonEmptyString, $integer)); + assertType('non-empty-list', str_split($nonFalsyString, $integer)); + assertType('list', str_split($lowercaseString, $integer)); + assertType('list', str_split($uppercaseString, $integer)); + + assertType('list', mb_str_split($string)); + assertType('non-empty-list', mb_str_split($nonEmptyString)); + assertType('non-empty-list', mb_str_split($nonFalsyString)); + assertType('list', mb_str_split($lowercaseString)); + assertType('list', mb_str_split($uppercaseString)); + + assertType('list', mb_str_split($string, $integer)); + assertType('non-empty-list', mb_str_split($nonEmptyString, $integer)); + assertType('non-empty-list', mb_str_split($nonFalsyString, $integer)); + assertType('list', mb_str_split($lowercaseString, $integer)); + assertType('list', mb_str_split($uppercaseString, $integer)); + } +} diff --git a/tests/PHPStan/Analyser/data/str-split.php b/tests/PHPStan/Analyser/data/str-split.php new file mode 100644 index 0000000000..78b7f36721 --- /dev/null +++ b/tests/PHPStan/Analyser/data/str-split.php @@ -0,0 +1,46 @@ +', str_split($string)); + assertType('non-empty-list', str_split($nonEmptyString)); + assertType('non-empty-list', str_split($nonFalsyString)); + assertType('non-empty-list', str_split($lowercaseString)); + assertType('non-empty-list', str_split($uppercaseString)); + + assertType('non-empty-list', str_split($string, $integer)); + assertType('non-empty-list', str_split($nonEmptyString, $integer)); + assertType('non-empty-list', str_split($nonFalsyString, $integer)); + assertType('non-empty-list', str_split($lowercaseString, $integer)); + assertType('non-empty-list', str_split($uppercaseString, $integer)); + + assertType('list', mb_str_split($string)); + assertType('non-empty-list', mb_str_split($nonEmptyString)); + assertType('non-empty-list', mb_str_split($nonFalsyString)); + assertType('list', mb_str_split($lowercaseString)); + assertType('list', mb_str_split($uppercaseString)); + + assertType('list', mb_str_split($string, $integer)); + assertType('non-empty-list', mb_str_split($nonEmptyString, $integer)); + assertType('non-empty-list', mb_str_split($nonFalsyString, $integer)); + assertType('list', mb_str_split($lowercaseString, $integer)); + assertType('list', mb_str_split($uppercaseString, $integer)); + } +}