From ffc1a150b79910ed9ad2af178bec5a4f15a2840e Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Sun, 18 May 2025 21:57:29 +0200 Subject: [PATCH 1/5] Improve StrSplit returnType --- .../StrSplitFunctionReturnTypeExtension.php | 50 ++++++++++++------- .../Analyser/NodeScopeResolverTest.php | 6 +++ tests/PHPStan/Analyser/data/str-split.php | 46 +++++++++++++++++ 3 files changed, 84 insertions(+), 18 deletions(-) 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 5facb9795c..ec48b6b3fc 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.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)); + } +} From a74b5f90d7aea1f4712c0f784ffa15c23aaa14ea Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Thu, 12 Jun 2025 23:39:39 +0200 Subject: [PATCH 2/5] Add tests --- .../Analyser/NodeScopeResolverTest.php | 10 +- .../Analyser/data/mb-str-split-php80.php | 35 ++++- .../Analyser/data/mb-str-split-php82.php | 121 ++++++++++++++++++ .../PHPStan/Analyser/data/str-split-php74.php | 4 +- .../PHPStan/Analyser/data/str-split-php80.php | 31 ++++- .../PHPStan/Analyser/data/str-split-php82.php | 33 ++++- tests/PHPStan/Analyser/data/str-split.php | 46 ------- 7 files changed, 216 insertions(+), 64 deletions(-) create mode 100644 tests/PHPStan/Analyser/data/mb-str-split-php82.php delete mode 100644 tests/PHPStan/Analyser/data/str-split.php diff --git a/tests/PHPStan/Analyser/NodeScopeResolverTest.php b/tests/PHPStan/Analyser/NodeScopeResolverTest.php index ec48b6b3fc..eaf05ff43f 100644 --- a/tests/PHPStan/Analyser/NodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/NodeScopeResolverTest.php @@ -29,12 +29,6 @@ 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'; } @@ -62,7 +56,9 @@ private static function findTestFiles(): iterable } else { yield __DIR__ . '/data/str-split-php74.php'; } - if (PHP_VERSION_ID >= 80000) { + if (PHP_VERSION_ID >= 80200) { + yield __DIR__ . '/data/mb-str-split-php82.php'; + } elseif (PHP_VERSION_ID >= 80000) { yield __DIR__ . '/data/mb-str-split-php80.php'; } elseif (PHP_VERSION_ID >= 74000) { yield __DIR__ . '/data/mb-str-split-php74.php'; diff --git a/tests/PHPStan/Analyser/data/mb-str-split-php80.php b/tests/PHPStan/Analyser/data/mb-str-split-php80.php index 418c3bfb5c..686bb1d847 100644 --- a/tests/PHPStan/Analyser/data/mb-str-split-php80.php +++ b/tests/PHPStan/Analyser/data/mb-str-split-php80.php @@ -29,13 +29,13 @@ public function legacyTest(): void assertType('false', $mbStrSplitConstantStringWithFailureSplitLength); $mbStrSplitConstantStringWithInvalidSplitLengthType = mb_str_split('abcdef', []); - assertType('list', $mbStrSplitConstantStringWithInvalidSplitLengthType); + assertType('non-empty-list', $mbStrSplitConstantStringWithInvalidSplitLengthType); $mbStrSplitConstantStringWithVariableStringAndConstantSplitLength = mb_str_split(doFoo() ? 'abcdef' : 'ghijkl', 1); assertType("array{'a', 'b', 'c', 'd', 'e', 'f'}|array{'g', 'h', 'i', 'j', 'k', 'l'}", $mbStrSplitConstantStringWithVariableStringAndConstantSplitLength); $mbStrSplitConstantStringWithVariableStringAndVariableSplitLength = mb_str_split(doFoo() ? 'abcdef' : 'ghijkl', doFoo() ? 1 : 2); - assertType('list', $mbStrSplitConstantStringWithVariableStringAndVariableSplitLength); + assertType('non-empty-list', $mbStrSplitConstantStringWithVariableStringAndVariableSplitLength); $mbStrSplitConstantStringWithOneSplitLengthAndValidEncoding = mb_str_split('abcdef', 1, 'UTF-8'); assertType("array{'a', 'b', 'c', 'd', 'e', 'f'}", $mbStrSplitConstantStringWithOneSplitLengthAndValidEncoding); @@ -65,7 +65,7 @@ public function legacyTest(): void assertType('false', $mbStrSplitConstantStringWithFailureSplitLengthAndVariableEncoding); $mbStrSplitConstantStringWithInvalidSplitLengthTypeAndValidEncoding = mb_str_split('abcdef', [], 'UTF-8'); - assertType('list', $mbStrSplitConstantStringWithInvalidSplitLengthTypeAndValidEncoding); + assertType('non-empty-list', $mbStrSplitConstantStringWithInvalidSplitLengthTypeAndValidEncoding); $mbStrSplitConstantStringWithInvalidSplitLengthTypeAndInvalidEncoding = mb_str_split('abcdef', [], 'FAKE'); assertType('false', $mbStrSplitConstantStringWithInvalidSplitLengthTypeAndInvalidEncoding); @@ -83,7 +83,7 @@ public function legacyTest(): void assertType('list', $mbStrSplitConstantStringWithVariableStringAndConstantSplitLengthAndVariableEncoding); $mbStrSplitConstantStringWithVariableStringAndVariableSplitLengthAndValidEncoding = mb_str_split(doFoo() ? 'abcdef' : 'ghijkl', doFoo() ? 1 : 2, 'UTF-8'); - assertType('list', $mbStrSplitConstantStringWithVariableStringAndVariableSplitLengthAndValidEncoding); + assertType('non-empty-list', $mbStrSplitConstantStringWithVariableStringAndVariableSplitLengthAndValidEncoding); $mbStrSplitConstantStringWithVariableStringAndVariableSplitLengthAndInvalidEncoding = mb_str_split(doFoo() ? 'abcdef' : 'ghijkl', doFoo() ? 1 : 2, 'FAKE'); assertType('false', $mbStrSplitConstantStringWithVariableStringAndVariableSplitLengthAndInvalidEncoding); @@ -91,4 +91,31 @@ public function legacyTest(): void $mbStrSplitConstantStringWithVariableStringAndVariableSplitLengthAndVariableEncoding = mb_str_split(doFoo() ? 'abcdef' : 'ghijkl', doFoo() ? 1 : 2, doFoo()); assertType('list', $mbStrSplitConstantStringWithVariableStringAndVariableSplitLengthAndVariableEncoding); } + + /** + * @param non-empty-string $nonEmptyString + * @param non-falsy-string $nonFalsyString + * @param lowercase-string $lowercaseString + * @param uppercase-string $uppercaseString + */ + function doFoo( + string $string, + string $nonEmptyString, + string $nonFalsyString, + string $lowercaseString, + string $uppercaseString, + int $integer, + ):void { + 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/mb-str-split-php82.php b/tests/PHPStan/Analyser/data/mb-str-split-php82.php new file mode 100644 index 0000000000..8620feee13 --- /dev/null +++ b/tests/PHPStan/Analyser/data/mb-str-split-php82.php @@ -0,0 +1,121 @@ +', $mbStrSplitConstantStringWithoutDefinedParameters); + + $mbStrSplitConstantStringWithoutDefinedSplitLength = mb_str_split('abcdef'); + assertType('array{\'a\', \'b\', \'c\', \'d\', \'e\', \'f\'}', $mbStrSplitConstantStringWithoutDefinedSplitLength); + + $mbStrSplitStringWithoutDefinedSplitLength = mb_str_split($string); + assertType('list', $mbStrSplitStringWithoutDefinedSplitLength); + + $mbStrSplitConstantStringWithOneSplitLength = mb_str_split('abcdef', 1); + assertType('array{\'a\', \'b\', \'c\', \'d\', \'e\', \'f\'}', $mbStrSplitConstantStringWithOneSplitLength); + + $mbStrSplitConstantStringWithGreaterSplitLengthThanStringLength = mb_str_split('abcdef', 999); + assertType('array{\'abcdef\'}', $mbStrSplitConstantStringWithGreaterSplitLengthThanStringLength); + + $mbStrSplitConstantStringWithFailureSplitLength = mb_str_split('abcdef', 0); + assertType('false', $mbStrSplitConstantStringWithFailureSplitLength); + + $mbStrSplitConstantStringWithInvalidSplitLengthType = mb_str_split('abcdef', []); + assertType('non-empty-list', $mbStrSplitConstantStringWithInvalidSplitLengthType); + + $mbStrSplitConstantStringWithVariableStringAndConstantSplitLength = mb_str_split(doFoo() ? 'abcdef' : 'ghijkl', 1); + assertType("array{'a', 'b', 'c', 'd', 'e', 'f'}|array{'g', 'h', 'i', 'j', 'k', 'l'}", $mbStrSplitConstantStringWithVariableStringAndConstantSplitLength); + + $mbStrSplitConstantStringWithVariableStringAndVariableSplitLength = mb_str_split(doFoo() ? 'abcdef' : 'ghijkl', doFoo() ? 1 : 2); + assertType('non-empty-list', $mbStrSplitConstantStringWithVariableStringAndVariableSplitLength); + + $mbStrSplitConstantStringWithOneSplitLengthAndValidEncoding = mb_str_split('abcdef', 1, 'UTF-8'); + assertType("array{'a', 'b', 'c', 'd', 'e', 'f'}", $mbStrSplitConstantStringWithOneSplitLengthAndValidEncoding); + + $mbStrSplitConstantStringWithOneSplitLengthAndInvalidEncoding = mb_str_split('abcdef', 1, 'FAKE'); + assertType('false', $mbStrSplitConstantStringWithOneSplitLengthAndInvalidEncoding); + + $mbStrSplitConstantStringWithOneSplitLengthAndVariableEncoding = mb_str_split('abcdef', 1, doFoo()); + assertType('list', $mbStrSplitConstantStringWithOneSplitLengthAndVariableEncoding); + + $mbStrSplitConstantStringWithGreaterSplitLengthThanStringLengthAndValidEncoding = mb_str_split('abcdef', 999, 'UTF-8'); + assertType("array{'abcdef'}", $mbStrSplitConstantStringWithGreaterSplitLengthThanStringLengthAndValidEncoding); + + $mbStrSplitConstantStringWithGreaterSplitLengthThanStringLengthAndInvalidEncoding = mb_str_split('abcdef', 999, 'FAKE'); + assertType('false', $mbStrSplitConstantStringWithGreaterSplitLengthThanStringLengthAndInvalidEncoding); + + $mbStrSplitConstantStringWithGreaterSplitLengthThanStringLengthAndVariableEncoding = mb_str_split('abcdef', 999, doFoo()); + assertType('list', $mbStrSplitConstantStringWithGreaterSplitLengthThanStringLengthAndVariableEncoding); + + $mbStrSplitConstantStringWithFailureSplitLengthAndValidEncoding = mb_str_split('abcdef', 0, 'UTF-8'); + assertType('false', $mbStrSplitConstantStringWithFailureSplitLengthAndValidEncoding); + + $mbStrSplitConstantStringWithFailureSplitLengthAndInvalidEncoding = mb_str_split('abcdef', 0, 'FAKE'); + assertType('false', $mbStrSplitConstantStringWithFailureSplitLengthAndInvalidEncoding); + + $mbStrSplitConstantStringWithFailureSplitLengthAndVariableEncoding = mb_str_split('abcdef', 0, doFoo()); + assertType('false', $mbStrSplitConstantStringWithFailureSplitLengthAndVariableEncoding); + + $mbStrSplitConstantStringWithInvalidSplitLengthTypeAndValidEncoding = mb_str_split('abcdef', [], 'UTF-8'); + assertType('non-empty-list', $mbStrSplitConstantStringWithInvalidSplitLengthTypeAndValidEncoding); + + $mbStrSplitConstantStringWithInvalidSplitLengthTypeAndInvalidEncoding = mb_str_split('abcdef', [], 'FAKE'); + assertType('false', $mbStrSplitConstantStringWithInvalidSplitLengthTypeAndInvalidEncoding); + + $mbStrSplitConstantStringWithInvalidSplitLengthTypeAndVariableEncoding = mb_str_split('abcdef', [], doFoo()); + assertType('list', $mbStrSplitConstantStringWithInvalidSplitLengthTypeAndVariableEncoding); + + $mbStrSplitConstantStringWithVariableStringAndConstantSplitLengthAndValidEncoding = mb_str_split(doFoo() ? 'abcdef' : 'ghijkl', 1, 'UTF-8'); + assertType("array{'a', 'b', 'c', 'd', 'e', 'f'}|array{'g', 'h', 'i', 'j', 'k', 'l'}", $mbStrSplitConstantStringWithVariableStringAndConstantSplitLengthAndValidEncoding); + + $mbStrSplitConstantStringWithVariableStringAndConstantSplitLengthAndInvalidEncoding = mb_str_split(doFoo() ? 'abcdef' : 'ghijkl', 1, 'FAKE'); + assertType('false', $mbStrSplitConstantStringWithVariableStringAndConstantSplitLengthAndInvalidEncoding); + + $mbStrSplitConstantStringWithVariableStringAndConstantSplitLengthAndVariableEncoding = mb_str_split(doFoo() ? 'abcdef' : 'ghijkl', 1, doFoo()); + assertType('list', $mbStrSplitConstantStringWithVariableStringAndConstantSplitLengthAndVariableEncoding); + + $mbStrSplitConstantStringWithVariableStringAndVariableSplitLengthAndValidEncoding = mb_str_split(doFoo() ? 'abcdef' : 'ghijkl', doFoo() ? 1 : 2, 'UTF-8'); + assertType('non-empty-list', $mbStrSplitConstantStringWithVariableStringAndVariableSplitLengthAndValidEncoding); + + $mbStrSplitConstantStringWithVariableStringAndVariableSplitLengthAndInvalidEncoding = mb_str_split(doFoo() ? 'abcdef' : 'ghijkl', doFoo() ? 1 : 2, 'FAKE'); + assertType('false', $mbStrSplitConstantStringWithVariableStringAndVariableSplitLengthAndInvalidEncoding); + + $mbStrSplitConstantStringWithVariableStringAndVariableSplitLengthAndVariableEncoding = mb_str_split(doFoo() ? 'abcdef' : 'ghijkl', doFoo() ? 1 : 2, doFoo()); + assertType('list', $mbStrSplitConstantStringWithVariableStringAndVariableSplitLengthAndVariableEncoding); + } + + /** + * @param non-empty-string $nonEmptyString + * @param non-falsy-string $nonFalsyString + * @param lowercase-string $lowercaseString + * @param uppercase-string $uppercaseString + */ + function doFoo( + string $string, + string $nonEmptyString, + string $nonFalsyString, + string $lowercaseString, + string $uppercaseString, + int $integer, + ):void { + 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-php74.php b/tests/PHPStan/Analyser/data/str-split-php74.php index 972f247a7a..aa2c94dd54 100644 --- a/tests/PHPStan/Analyser/data/str-split-php74.php +++ b/tests/PHPStan/Analyser/data/str-split-php74.php @@ -28,13 +28,13 @@ public function legacyTest() { assertType('false', $strSplitConstantStringWithFailureSplitLength); $strSplitConstantStringWithInvalidSplitLengthType = str_split('abcdef', []); - assertType('non-empty-list|false', $strSplitConstantStringWithInvalidSplitLengthType); + assertType('non-empty-list|false', $strSplitConstantStringWithInvalidSplitLengthType); $strSplitConstantStringWithVariableStringAndConstantSplitLength = str_split(doFoo() ? 'abcdef' : 'ghijkl', 1); assertType("array{'a', 'b', 'c', 'd', 'e', 'f'}|array{'g', 'h', 'i', 'j', 'k', 'l'}", $strSplitConstantStringWithVariableStringAndConstantSplitLength); $strSplitConstantStringWithVariableStringAndVariableSplitLength = str_split(doFoo() ? 'abcdef' : 'ghijkl', doFoo() ? 1 : 2); - assertType('non-empty-list|false', $strSplitConstantStringWithVariableStringAndVariableSplitLength); + assertType('non-empty-list|false', $strSplitConstantStringWithVariableStringAndVariableSplitLength); } } diff --git a/tests/PHPStan/Analyser/data/str-split-php80.php b/tests/PHPStan/Analyser/data/str-split-php80.php index 1cf04a0cfd..067fd36440 100644 --- a/tests/PHPStan/Analyser/data/str-split-php80.php +++ b/tests/PHPStan/Analyser/data/str-split-php80.php @@ -28,13 +28,40 @@ public function legacyTest() { assertType('false', $strSplitConstantStringWithFailureSplitLength); $strSplitConstantStringWithInvalidSplitLengthType = str_split('abcdef', []); - assertType('non-empty-list', $strSplitConstantStringWithInvalidSplitLengthType); + assertType('non-empty-list', $strSplitConstantStringWithInvalidSplitLengthType); $strSplitConstantStringWithVariableStringAndConstantSplitLength = str_split(doFoo() ? 'abcdef' : 'ghijkl', 1); assertType("array{'a', 'b', 'c', 'd', 'e', 'f'}|array{'g', 'h', 'i', 'j', 'k', 'l'}", $strSplitConstantStringWithVariableStringAndConstantSplitLength); $strSplitConstantStringWithVariableStringAndVariableSplitLength = str_split(doFoo() ? 'abcdef' : 'ghijkl', doFoo() ? 1 : 2); - assertType('non-empty-list', $strSplitConstantStringWithVariableStringAndVariableSplitLength); + assertType('non-empty-list', $strSplitConstantStringWithVariableStringAndVariableSplitLength); } + + /** + * @param non-empty-string $nonEmptyString + * @param non-falsy-string $nonFalsyString + * @param lowercase-string $lowercaseString + * @param uppercase-string $uppercaseString + */ + function doFoo( + string $string, + string $nonEmptyString, + string $nonFalsyString, + string $lowercaseString, + string $uppercaseString, + int $integer, + ):void { + assertType('non-empty-list', 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)); + } } diff --git a/tests/PHPStan/Analyser/data/str-split-php82.php b/tests/PHPStan/Analyser/data/str-split-php82.php index 8d1e00d59f..34dc686f91 100644 --- a/tests/PHPStan/Analyser/data/str-split-php82.php +++ b/tests/PHPStan/Analyser/data/str-split-php82.php @@ -16,7 +16,7 @@ public function legacyTest() { assertType('array{\'a\', \'b\', \'c\', \'d\', \'e\', \'f\'}', $strSplitConstantStringWithoutDefinedSplitLength); $strSplitStringWithoutDefinedSplitLength = str_split($string); - assertType('list', $strSplitStringWithoutDefinedSplitLength); + assertType('list', $strSplitStringWithoutDefinedSplitLength); $strSplitConstantStringWithOneSplitLength = str_split('abcdef', 1); assertType('array{\'a\', \'b\', \'c\', \'d\', \'e\', \'f\'}', $strSplitConstantStringWithOneSplitLength); @@ -28,13 +28,40 @@ public function legacyTest() { assertType('false', $strSplitConstantStringWithFailureSplitLength); $strSplitConstantStringWithInvalidSplitLengthType = str_split('abcdef', []); - assertType('list', $strSplitConstantStringWithInvalidSplitLengthType); + assertType('non-empty-list', $strSplitConstantStringWithInvalidSplitLengthType); $strSplitConstantStringWithVariableStringAndConstantSplitLength = str_split(doFoo() ? 'abcdef' : 'ghijkl', 1); assertType("array{'a', 'b', 'c', 'd', 'e', 'f'}|array{'g', 'h', 'i', 'j', 'k', 'l'}", $strSplitConstantStringWithVariableStringAndConstantSplitLength); $strSplitConstantStringWithVariableStringAndVariableSplitLength = str_split(doFoo() ? 'abcdef' : 'ghijkl', doFoo() ? 1 : 2); - assertType('list', $strSplitConstantStringWithVariableStringAndVariableSplitLength); + assertType('non-empty-list', $strSplitConstantStringWithVariableStringAndVariableSplitLength); } + + /** + * @param non-empty-string $nonEmptyString + * @param non-falsy-string $nonFalsyString + * @param lowercase-string $lowercaseString + * @param uppercase-string $uppercaseString + */ + function doFoo( + string $string, + string $nonEmptyString, + string $nonFalsyString, + string $lowercaseString, + string $uppercaseString, + int $integer, + ):void { + assertType('list', 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)); + } } diff --git a/tests/PHPStan/Analyser/data/str-split.php b/tests/PHPStan/Analyser/data/str-split.php deleted file mode 100644 index 78b7f36721..0000000000 --- a/tests/PHPStan/Analyser/data/str-split.php +++ /dev/null @@ -1,46 +0,0 @@ -', 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)); - } -} From f30c52a5951de27592a0d04e8e49cc81ca673366 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Fri, 13 Jun 2025 00:16:01 +0200 Subject: [PATCH 3/5] Rework extension --- .../StrSplitFunctionReturnTypeExtension.php | 67 +++++++++++-------- .../Analyser/data/mb-str-split-php80.php | 40 +++++------ .../Analyser/data/mb-str-split-php82.php | 44 +++++------- .../PHPStan/Analyser/data/str-split-php74.php | 4 +- .../PHPStan/Analyser/data/str-split-php80.php | 12 +--- .../PHPStan/Analyser/data/str-split-php82.php | 12 +--- .../PHPStan/Analyser/nsrt/bug-7580-php82.php | 20 ++++++ tests/PHPStan/Analyser/nsrt/bug-7580.php | 4 +- 8 files changed, 102 insertions(+), 101 deletions(-) create mode 100644 tests/PHPStan/Analyser/nsrt/bug-7580-php82.php diff --git a/src/Type/Php/StrSplitFunctionReturnTypeExtension.php b/src/Type/Php/StrSplitFunctionReturnTypeExtension.php index 224c2f5a2b..fc33e2d560 100644 --- a/src/Type/Php/StrSplitFunctionReturnTypeExtension.php +++ b/src/Type/Php/StrSplitFunctionReturnTypeExtension.php @@ -9,9 +9,7 @@ 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; @@ -19,7 +17,9 @@ use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; +use PHPStan\Type\IntegerRangeType; use PHPStan\Type\IntegerType; +use PHPStan\Type\NeverType; use PHPStan\Type\StringType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; @@ -54,14 +54,15 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, if (count($functionCall->getArgs()) >= 2) { $splitLengthType = $scope->getType($functionCall->getArgs()[1]->value); - if ($splitLengthType instanceof ConstantIntegerType) { - $splitLength = $splitLengthType->getValue(); - if ($splitLength < 1) { - return new ConstantBooleanType(false); - } - } } else { - $splitLength = 1; + $splitLengthType = new ConstantIntegerType(1); + } + + if ($splitLengthType instanceof ConstantIntegerType) { + $splitLength = $splitLengthType->getValue(); + if ($splitLength < 1) { + return $this->phpVersion->throwsValueErrorForInternalFunctions() ? new NeverType() : new ConstantBooleanType(false); + } } $encoding = null; @@ -70,13 +71,11 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $strings = $scope->getType($functionCall->getArgs()[2]->value)->getConstantStrings(); $values = array_unique(array_map(static fn (ConstantStringType $encoding): string => $encoding->getValue(), $strings)); - if (count($values) !== 1) { - return null; - } - - $encoding = $values[0]; - if (!$this->isSupportedEncoding($encoding)) { - return new ConstantBooleanType(false); + if (count($values) === 1) { + $encoding = $values[0]; + if (!$this->isSupportedEncoding($encoding)) { + return $this->phpVersion->throwsValueErrorForInternalFunctions() ? new NeverType() : new ConstantBooleanType(false); + } } } else { $encoding = mb_internal_encoding(); @@ -84,7 +83,10 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, } $stringType = $scope->getType($functionCall->getArgs()[0]->value); - if (isset($splitLength)) { + if ( + isset($splitLength) + && ($functionReflection->getName() === 'str_split' || $encoding !== null) + ) { $constantStrings = $stringType->getConstantStrings(); if (count($constantStrings) > 0) { $results = []; @@ -105,23 +107,30 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $isInputNonEmptyString = $stringType->isNonEmptyString()->yes(); - $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(), new AccessoryNonEmptyStringType()); + } else { + $returnValueType = new StringType(); } - $returnValueType = TypeCombinator::intersect(new StringType(), ...$valueTypes); $returnType = AccessoryArrayListType::intersectWith(TypeCombinator::intersect(new ArrayType(new IntegerType(), $returnValueType))); + if ( + // Non-empty-string will return an array with at least an element + $isInputNonEmptyString + // str_split('', 1) returns [''] on old PHP version and [] on new ones + || ($functionReflection->getName() === 'str_split' && !$this->phpVersion->strSplitReturnsEmptyArray()) + ) { + $returnType = TypeCombinator::intersect($returnType, new NonEmptyArrayType()); + } + if ( + // Length parameter accepts int<1, max> or throws a ValueError/return false based on PHP Version. + !$this->phpVersion->throwsValueErrorForInternalFunctions() + && !IntegerRangeType::fromInterval(1, null)->isSuperTypeOf($splitLengthType)->yes() + ) { + $returnType = TypeCombinator::union($returnType, new ConstantBooleanType(false)); + } - return $isInputNonEmptyString || ($encoding === null && !$this->phpVersion->strSplitReturnsEmptyArray()) - ? TypeCombinator::intersect($returnType, new NonEmptyArrayType()) - : $returnType; + return $returnType; } /** diff --git a/tests/PHPStan/Analyser/data/mb-str-split-php80.php b/tests/PHPStan/Analyser/data/mb-str-split-php80.php index 686bb1d847..2e1b4b3845 100644 --- a/tests/PHPStan/Analyser/data/mb-str-split-php80.php +++ b/tests/PHPStan/Analyser/data/mb-str-split-php80.php @@ -29,74 +29,72 @@ public function legacyTest(): void assertType('false', $mbStrSplitConstantStringWithFailureSplitLength); $mbStrSplitConstantStringWithInvalidSplitLengthType = mb_str_split('abcdef', []); - assertType('non-empty-list', $mbStrSplitConstantStringWithInvalidSplitLengthType); + assertType('non-empty-list', $mbStrSplitConstantStringWithInvalidSplitLengthType); $mbStrSplitConstantStringWithVariableStringAndConstantSplitLength = mb_str_split(doFoo() ? 'abcdef' : 'ghijkl', 1); assertType("array{'a', 'b', 'c', 'd', 'e', 'f'}|array{'g', 'h', 'i', 'j', 'k', 'l'}", $mbStrSplitConstantStringWithVariableStringAndConstantSplitLength); $mbStrSplitConstantStringWithVariableStringAndVariableSplitLength = mb_str_split(doFoo() ? 'abcdef' : 'ghijkl', doFoo() ? 1 : 2); - assertType('non-empty-list', $mbStrSplitConstantStringWithVariableStringAndVariableSplitLength); + assertType('non-empty-list', $mbStrSplitConstantStringWithVariableStringAndVariableSplitLength); $mbStrSplitConstantStringWithOneSplitLengthAndValidEncoding = mb_str_split('abcdef', 1, 'UTF-8'); assertType("array{'a', 'b', 'c', 'd', 'e', 'f'}", $mbStrSplitConstantStringWithOneSplitLengthAndValidEncoding); $mbStrSplitConstantStringWithOneSplitLengthAndInvalidEncoding = mb_str_split('abcdef', 1, 'FAKE'); - assertType('false', $mbStrSplitConstantStringWithOneSplitLengthAndInvalidEncoding); + assertType('*NEVER*', $mbStrSplitConstantStringWithOneSplitLengthAndInvalidEncoding); $mbStrSplitConstantStringWithOneSplitLengthAndVariableEncoding = mb_str_split('abcdef', 1, doFoo()); - assertType('list', $mbStrSplitConstantStringWithOneSplitLengthAndVariableEncoding); + assertType('non-empty-list', $mbStrSplitConstantStringWithOneSplitLengthAndVariableEncoding); $mbStrSplitConstantStringWithGreaterSplitLengthThanStringLengthAndValidEncoding = mb_str_split('abcdef', 999, 'UTF-8'); assertType("array{'abcdef'}", $mbStrSplitConstantStringWithGreaterSplitLengthThanStringLengthAndValidEncoding); $mbStrSplitConstantStringWithGreaterSplitLengthThanStringLengthAndInvalidEncoding = mb_str_split('abcdef', 999, 'FAKE'); - assertType('false', $mbStrSplitConstantStringWithGreaterSplitLengthThanStringLengthAndInvalidEncoding); + assertType('*NEVER*', $mbStrSplitConstantStringWithGreaterSplitLengthThanStringLengthAndInvalidEncoding); $mbStrSplitConstantStringWithGreaterSplitLengthThanStringLengthAndVariableEncoding = mb_str_split('abcdef', 999, doFoo()); - assertType('list', $mbStrSplitConstantStringWithGreaterSplitLengthThanStringLengthAndVariableEncoding); + assertType('non-empty-list', $mbStrSplitConstantStringWithGreaterSplitLengthThanStringLengthAndVariableEncoding); $mbStrSplitConstantStringWithFailureSplitLengthAndValidEncoding = mb_str_split('abcdef', 0, 'UTF-8'); - assertType('false', $mbStrSplitConstantStringWithFailureSplitLengthAndValidEncoding); + assertType('*NEVER*', $mbStrSplitConstantStringWithFailureSplitLengthAndValidEncoding); $mbStrSplitConstantStringWithFailureSplitLengthAndInvalidEncoding = mb_str_split('abcdef', 0, 'FAKE'); - assertType('false', $mbStrSplitConstantStringWithFailureSplitLengthAndInvalidEncoding); + assertType('*NEVER*', $mbStrSplitConstantStringWithFailureSplitLengthAndInvalidEncoding); $mbStrSplitConstantStringWithFailureSplitLengthAndVariableEncoding = mb_str_split('abcdef', 0, doFoo()); - assertType('false', $mbStrSplitConstantStringWithFailureSplitLengthAndVariableEncoding); + assertType('*NEVER*', $mbStrSplitConstantStringWithFailureSplitLengthAndVariableEncoding); $mbStrSplitConstantStringWithInvalidSplitLengthTypeAndValidEncoding = mb_str_split('abcdef', [], 'UTF-8'); - assertType('non-empty-list', $mbStrSplitConstantStringWithInvalidSplitLengthTypeAndValidEncoding); + assertType('non-empty-list', $mbStrSplitConstantStringWithInvalidSplitLengthTypeAndValidEncoding); $mbStrSplitConstantStringWithInvalidSplitLengthTypeAndInvalidEncoding = mb_str_split('abcdef', [], 'FAKE'); - assertType('false', $mbStrSplitConstantStringWithInvalidSplitLengthTypeAndInvalidEncoding); + assertType('*NEVER*', $mbStrSplitConstantStringWithInvalidSplitLengthTypeAndInvalidEncoding); $mbStrSplitConstantStringWithInvalidSplitLengthTypeAndVariableEncoding = mb_str_split('abcdef', [], doFoo()); - assertType('list', $mbStrSplitConstantStringWithInvalidSplitLengthTypeAndVariableEncoding); + assertType('non-empty-list', $mbStrSplitConstantStringWithInvalidSplitLengthTypeAndVariableEncoding); $mbStrSplitConstantStringWithVariableStringAndConstantSplitLengthAndValidEncoding = mb_str_split(doFoo() ? 'abcdef' : 'ghijkl', 1, 'UTF-8'); assertType("array{'a', 'b', 'c', 'd', 'e', 'f'}|array{'g', 'h', 'i', 'j', 'k', 'l'}", $mbStrSplitConstantStringWithVariableStringAndConstantSplitLengthAndValidEncoding); $mbStrSplitConstantStringWithVariableStringAndConstantSplitLengthAndInvalidEncoding = mb_str_split(doFoo() ? 'abcdef' : 'ghijkl', 1, 'FAKE'); - assertType('false', $mbStrSplitConstantStringWithVariableStringAndConstantSplitLengthAndInvalidEncoding); + assertType('*NEVER*', $mbStrSplitConstantStringWithVariableStringAndConstantSplitLengthAndInvalidEncoding); $mbStrSplitConstantStringWithVariableStringAndConstantSplitLengthAndVariableEncoding = mb_str_split(doFoo() ? 'abcdef' : 'ghijkl', 1, doFoo()); - assertType('list', $mbStrSplitConstantStringWithVariableStringAndConstantSplitLengthAndVariableEncoding); + assertType('non-empty-list', $mbStrSplitConstantStringWithVariableStringAndConstantSplitLengthAndVariableEncoding); $mbStrSplitConstantStringWithVariableStringAndVariableSplitLengthAndValidEncoding = mb_str_split(doFoo() ? 'abcdef' : 'ghijkl', doFoo() ? 1 : 2, 'UTF-8'); - assertType('non-empty-list', $mbStrSplitConstantStringWithVariableStringAndVariableSplitLengthAndValidEncoding); + assertType('non-empty-list', $mbStrSplitConstantStringWithVariableStringAndVariableSplitLengthAndValidEncoding); $mbStrSplitConstantStringWithVariableStringAndVariableSplitLengthAndInvalidEncoding = mb_str_split(doFoo() ? 'abcdef' : 'ghijkl', doFoo() ? 1 : 2, 'FAKE'); - assertType('false', $mbStrSplitConstantStringWithVariableStringAndVariableSplitLengthAndInvalidEncoding); + assertType('*NEVER*', $mbStrSplitConstantStringWithVariableStringAndVariableSplitLengthAndInvalidEncoding); $mbStrSplitConstantStringWithVariableStringAndVariableSplitLengthAndVariableEncoding = mb_str_split(doFoo() ? 'abcdef' : 'ghijkl', doFoo() ? 1 : 2, doFoo()); - assertType('list', $mbStrSplitConstantStringWithVariableStringAndVariableSplitLengthAndVariableEncoding); + assertType('non-empty-list', $mbStrSplitConstantStringWithVariableStringAndVariableSplitLengthAndVariableEncoding); } /** * @param non-empty-string $nonEmptyString * @param non-falsy-string $nonFalsyString - * @param lowercase-string $lowercaseString - * @param uppercase-string $uppercaseString */ function doFoo( string $string, @@ -109,13 +107,9 @@ function doFoo( 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/mb-str-split-php82.php b/tests/PHPStan/Analyser/data/mb-str-split-php82.php index 8620feee13..0f905fe37a 100644 --- a/tests/PHPStan/Analyser/data/mb-str-split-php82.php +++ b/tests/PHPStan/Analyser/data/mb-str-split-php82.php @@ -26,96 +26,88 @@ public function legacyTest(): void assertType('array{\'abcdef\'}', $mbStrSplitConstantStringWithGreaterSplitLengthThanStringLength); $mbStrSplitConstantStringWithFailureSplitLength = mb_str_split('abcdef', 0); - assertType('false', $mbStrSplitConstantStringWithFailureSplitLength); + assertType('*NEVER*', $mbStrSplitConstantStringWithFailureSplitLength); $mbStrSplitConstantStringWithInvalidSplitLengthType = mb_str_split('abcdef', []); - assertType('non-empty-list', $mbStrSplitConstantStringWithInvalidSplitLengthType); + assertType('non-empty-list', $mbStrSplitConstantStringWithInvalidSplitLengthType); $mbStrSplitConstantStringWithVariableStringAndConstantSplitLength = mb_str_split(doFoo() ? 'abcdef' : 'ghijkl', 1); assertType("array{'a', 'b', 'c', 'd', 'e', 'f'}|array{'g', 'h', 'i', 'j', 'k', 'l'}", $mbStrSplitConstantStringWithVariableStringAndConstantSplitLength); $mbStrSplitConstantStringWithVariableStringAndVariableSplitLength = mb_str_split(doFoo() ? 'abcdef' : 'ghijkl', doFoo() ? 1 : 2); - assertType('non-empty-list', $mbStrSplitConstantStringWithVariableStringAndVariableSplitLength); + assertType('non-empty-list', $mbStrSplitConstantStringWithVariableStringAndVariableSplitLength); $mbStrSplitConstantStringWithOneSplitLengthAndValidEncoding = mb_str_split('abcdef', 1, 'UTF-8'); assertType("array{'a', 'b', 'c', 'd', 'e', 'f'}", $mbStrSplitConstantStringWithOneSplitLengthAndValidEncoding); $mbStrSplitConstantStringWithOneSplitLengthAndInvalidEncoding = mb_str_split('abcdef', 1, 'FAKE'); - assertType('false', $mbStrSplitConstantStringWithOneSplitLengthAndInvalidEncoding); + assertType('*NEVER*', $mbStrSplitConstantStringWithOneSplitLengthAndInvalidEncoding); $mbStrSplitConstantStringWithOneSplitLengthAndVariableEncoding = mb_str_split('abcdef', 1, doFoo()); - assertType('list', $mbStrSplitConstantStringWithOneSplitLengthAndVariableEncoding); + assertType('non-empty-list', $mbStrSplitConstantStringWithOneSplitLengthAndVariableEncoding); $mbStrSplitConstantStringWithGreaterSplitLengthThanStringLengthAndValidEncoding = mb_str_split('abcdef', 999, 'UTF-8'); assertType("array{'abcdef'}", $mbStrSplitConstantStringWithGreaterSplitLengthThanStringLengthAndValidEncoding); $mbStrSplitConstantStringWithGreaterSplitLengthThanStringLengthAndInvalidEncoding = mb_str_split('abcdef', 999, 'FAKE'); - assertType('false', $mbStrSplitConstantStringWithGreaterSplitLengthThanStringLengthAndInvalidEncoding); + assertType('*NEVER*', $mbStrSplitConstantStringWithGreaterSplitLengthThanStringLengthAndInvalidEncoding); $mbStrSplitConstantStringWithGreaterSplitLengthThanStringLengthAndVariableEncoding = mb_str_split('abcdef', 999, doFoo()); - assertType('list', $mbStrSplitConstantStringWithGreaterSplitLengthThanStringLengthAndVariableEncoding); + assertType('non-empty-list', $mbStrSplitConstantStringWithGreaterSplitLengthThanStringLengthAndVariableEncoding); $mbStrSplitConstantStringWithFailureSplitLengthAndValidEncoding = mb_str_split('abcdef', 0, 'UTF-8'); - assertType('false', $mbStrSplitConstantStringWithFailureSplitLengthAndValidEncoding); + assertType('*NEVER*', $mbStrSplitConstantStringWithFailureSplitLengthAndValidEncoding); $mbStrSplitConstantStringWithFailureSplitLengthAndInvalidEncoding = mb_str_split('abcdef', 0, 'FAKE'); - assertType('false', $mbStrSplitConstantStringWithFailureSplitLengthAndInvalidEncoding); + assertType('*NEVER*', $mbStrSplitConstantStringWithFailureSplitLengthAndInvalidEncoding); $mbStrSplitConstantStringWithFailureSplitLengthAndVariableEncoding = mb_str_split('abcdef', 0, doFoo()); - assertType('false', $mbStrSplitConstantStringWithFailureSplitLengthAndVariableEncoding); + assertType('*NEVER*', $mbStrSplitConstantStringWithFailureSplitLengthAndVariableEncoding); $mbStrSplitConstantStringWithInvalidSplitLengthTypeAndValidEncoding = mb_str_split('abcdef', [], 'UTF-8'); - assertType('non-empty-list', $mbStrSplitConstantStringWithInvalidSplitLengthTypeAndValidEncoding); + assertType('non-empty-list', $mbStrSplitConstantStringWithInvalidSplitLengthTypeAndValidEncoding); $mbStrSplitConstantStringWithInvalidSplitLengthTypeAndInvalidEncoding = mb_str_split('abcdef', [], 'FAKE'); - assertType('false', $mbStrSplitConstantStringWithInvalidSplitLengthTypeAndInvalidEncoding); + assertType('*NEVER*', $mbStrSplitConstantStringWithInvalidSplitLengthTypeAndInvalidEncoding); $mbStrSplitConstantStringWithInvalidSplitLengthTypeAndVariableEncoding = mb_str_split('abcdef', [], doFoo()); - assertType('list', $mbStrSplitConstantStringWithInvalidSplitLengthTypeAndVariableEncoding); + assertType('non-empty-list', $mbStrSplitConstantStringWithInvalidSplitLengthTypeAndVariableEncoding); $mbStrSplitConstantStringWithVariableStringAndConstantSplitLengthAndValidEncoding = mb_str_split(doFoo() ? 'abcdef' : 'ghijkl', 1, 'UTF-8'); assertType("array{'a', 'b', 'c', 'd', 'e', 'f'}|array{'g', 'h', 'i', 'j', 'k', 'l'}", $mbStrSplitConstantStringWithVariableStringAndConstantSplitLengthAndValidEncoding); $mbStrSplitConstantStringWithVariableStringAndConstantSplitLengthAndInvalidEncoding = mb_str_split(doFoo() ? 'abcdef' : 'ghijkl', 1, 'FAKE'); - assertType('false', $mbStrSplitConstantStringWithVariableStringAndConstantSplitLengthAndInvalidEncoding); + assertType('*NEVER*', $mbStrSplitConstantStringWithVariableStringAndConstantSplitLengthAndInvalidEncoding); $mbStrSplitConstantStringWithVariableStringAndConstantSplitLengthAndVariableEncoding = mb_str_split(doFoo() ? 'abcdef' : 'ghijkl', 1, doFoo()); - assertType('list', $mbStrSplitConstantStringWithVariableStringAndConstantSplitLengthAndVariableEncoding); + assertType('non-empty-list', $mbStrSplitConstantStringWithVariableStringAndConstantSplitLengthAndVariableEncoding); $mbStrSplitConstantStringWithVariableStringAndVariableSplitLengthAndValidEncoding = mb_str_split(doFoo() ? 'abcdef' : 'ghijkl', doFoo() ? 1 : 2, 'UTF-8'); - assertType('non-empty-list', $mbStrSplitConstantStringWithVariableStringAndVariableSplitLengthAndValidEncoding); + assertType('non-empty-list', $mbStrSplitConstantStringWithVariableStringAndVariableSplitLengthAndValidEncoding); $mbStrSplitConstantStringWithVariableStringAndVariableSplitLengthAndInvalidEncoding = mb_str_split(doFoo() ? 'abcdef' : 'ghijkl', doFoo() ? 1 : 2, 'FAKE'); - assertType('false', $mbStrSplitConstantStringWithVariableStringAndVariableSplitLengthAndInvalidEncoding); + assertType('*NEVER*', $mbStrSplitConstantStringWithVariableStringAndVariableSplitLengthAndInvalidEncoding); $mbStrSplitConstantStringWithVariableStringAndVariableSplitLengthAndVariableEncoding = mb_str_split(doFoo() ? 'abcdef' : 'ghijkl', doFoo() ? 1 : 2, doFoo()); - assertType('list', $mbStrSplitConstantStringWithVariableStringAndVariableSplitLengthAndVariableEncoding); + assertType('non-empty-list', $mbStrSplitConstantStringWithVariableStringAndVariableSplitLengthAndVariableEncoding); } /** * @param non-empty-string $nonEmptyString * @param non-falsy-string $nonFalsyString - * @param lowercase-string $lowercaseString - * @param uppercase-string $uppercaseString */ function doFoo( string $string, string $nonEmptyString, string $nonFalsyString, - string $lowercaseString, - string $uppercaseString, int $integer, ):void { 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-php74.php b/tests/PHPStan/Analyser/data/str-split-php74.php index aa2c94dd54..da1c955fb0 100644 --- a/tests/PHPStan/Analyser/data/str-split-php74.php +++ b/tests/PHPStan/Analyser/data/str-split-php74.php @@ -28,13 +28,13 @@ public function legacyTest() { assertType('false', $strSplitConstantStringWithFailureSplitLength); $strSplitConstantStringWithInvalidSplitLengthType = str_split('abcdef', []); - assertType('non-empty-list|false', $strSplitConstantStringWithInvalidSplitLengthType); + assertType('non-empty-list|false', $strSplitConstantStringWithInvalidSplitLengthType); $strSplitConstantStringWithVariableStringAndConstantSplitLength = str_split(doFoo() ? 'abcdef' : 'ghijkl', 1); assertType("array{'a', 'b', 'c', 'd', 'e', 'f'}|array{'g', 'h', 'i', 'j', 'k', 'l'}", $strSplitConstantStringWithVariableStringAndConstantSplitLength); $strSplitConstantStringWithVariableStringAndVariableSplitLength = str_split(doFoo() ? 'abcdef' : 'ghijkl', doFoo() ? 1 : 2); - assertType('non-empty-list|false', $strSplitConstantStringWithVariableStringAndVariableSplitLength); + assertType('non-empty-list', $strSplitConstantStringWithVariableStringAndVariableSplitLength); } } diff --git a/tests/PHPStan/Analyser/data/str-split-php80.php b/tests/PHPStan/Analyser/data/str-split-php80.php index 067fd36440..342b28ad2f 100644 --- a/tests/PHPStan/Analyser/data/str-split-php80.php +++ b/tests/PHPStan/Analyser/data/str-split-php80.php @@ -28,40 +28,32 @@ public function legacyTest() { assertType('false', $strSplitConstantStringWithFailureSplitLength); $strSplitConstantStringWithInvalidSplitLengthType = str_split('abcdef', []); - assertType('non-empty-list', $strSplitConstantStringWithInvalidSplitLengthType); + assertType('non-empty-list', $strSplitConstantStringWithInvalidSplitLengthType); $strSplitConstantStringWithVariableStringAndConstantSplitLength = str_split(doFoo() ? 'abcdef' : 'ghijkl', 1); assertType("array{'a', 'b', 'c', 'd', 'e', 'f'}|array{'g', 'h', 'i', 'j', 'k', 'l'}", $strSplitConstantStringWithVariableStringAndConstantSplitLength); $strSplitConstantStringWithVariableStringAndVariableSplitLength = str_split(doFoo() ? 'abcdef' : 'ghijkl', doFoo() ? 1 : 2); - assertType('non-empty-list', $strSplitConstantStringWithVariableStringAndVariableSplitLength); + assertType('non-empty-list', $strSplitConstantStringWithVariableStringAndVariableSplitLength); } /** * @param non-empty-string $nonEmptyString * @param non-falsy-string $nonFalsyString - * @param lowercase-string $lowercaseString - * @param uppercase-string $uppercaseString */ function doFoo( string $string, string $nonEmptyString, string $nonFalsyString, - string $lowercaseString, - string $uppercaseString, int $integer, ):void { assertType('non-empty-list', 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)); } } diff --git a/tests/PHPStan/Analyser/data/str-split-php82.php b/tests/PHPStan/Analyser/data/str-split-php82.php index 34dc686f91..0b0436bf7b 100644 --- a/tests/PHPStan/Analyser/data/str-split-php82.php +++ b/tests/PHPStan/Analyser/data/str-split-php82.php @@ -28,40 +28,32 @@ public function legacyTest() { assertType('false', $strSplitConstantStringWithFailureSplitLength); $strSplitConstantStringWithInvalidSplitLengthType = str_split('abcdef', []); - assertType('non-empty-list', $strSplitConstantStringWithInvalidSplitLengthType); + assertType('non-empty-list', $strSplitConstantStringWithInvalidSplitLengthType); $strSplitConstantStringWithVariableStringAndConstantSplitLength = str_split(doFoo() ? 'abcdef' : 'ghijkl', 1); assertType("array{'a', 'b', 'c', 'd', 'e', 'f'}|array{'g', 'h', 'i', 'j', 'k', 'l'}", $strSplitConstantStringWithVariableStringAndConstantSplitLength); $strSplitConstantStringWithVariableStringAndVariableSplitLength = str_split(doFoo() ? 'abcdef' : 'ghijkl', doFoo() ? 1 : 2); - assertType('non-empty-list', $strSplitConstantStringWithVariableStringAndVariableSplitLength); + assertType('non-empty-list', $strSplitConstantStringWithVariableStringAndVariableSplitLength); } /** * @param non-empty-string $nonEmptyString * @param non-falsy-string $nonFalsyString - * @param lowercase-string $lowercaseString - * @param uppercase-string $uppercaseString */ function doFoo( string $string, string $nonEmptyString, string $nonFalsyString, - string $lowercaseString, - string $uppercaseString, int $integer, ):void { assertType('list', 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)); } } diff --git a/tests/PHPStan/Analyser/nsrt/bug-7580-php82.php b/tests/PHPStan/Analyser/nsrt/bug-7580-php82.php new file mode 100644 index 0000000000..40031601b4 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-7580-php82.php @@ -0,0 +1,20 @@ += 8.2 + +declare(strict_types = 1); + +namespace Bug7580TypesPHP82; + +use function PHPStan\Testing\assertType; + +assertType('array{}', mb_str_split('', 1)); + +assertType('array{\'x\'}', mb_str_split('x', 1)); + +$v = (string) (mt_rand() === 0 ? '' : 'x'); +assertType('\'\'|\'x\'', $v); +assertType('array{}|array{\'x\'}', mb_str_split($v, 1)); + +function x(): string { throw new \Exception(); }; +$v = x(); +assertType('string', $v); +assertType('list', mb_str_split($v, 1)); diff --git a/tests/PHPStan/Analyser/nsrt/bug-7580.php b/tests/PHPStan/Analyser/nsrt/bug-7580.php index 26fdbc18cb..1bfc0b7544 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-7580.php +++ b/tests/PHPStan/Analyser/nsrt/bug-7580.php @@ -1,4 +1,6 @@ - Date: Fri, 13 Jun 2025 02:24:08 +0200 Subject: [PATCH 4/5] Fix --- build/baseline-8.0.neon | 5 ----- src/Type/Php/StrSplitFunctionReturnTypeExtension.php | 3 --- tests/PHPStan/Analyser/data/mb-str-split-php80.php | 2 +- tests/PHPStan/Analyser/data/str-split-php80.php | 2 +- tests/PHPStan/Analyser/data/str-split-php82.php | 2 +- 5 files changed, 3 insertions(+), 11 deletions(-) diff --git a/build/baseline-8.0.neon b/build/baseline-8.0.neon index 95dfa6cf8a..f4a83e799c 100644 --- a/build/baseline-8.0.neon +++ b/build/baseline-8.0.neon @@ -25,11 +25,6 @@ parameters: count: 1 path: ../src/Type/Php/StrSplitFunctionReturnTypeExtension.php - - - message: "#^Strict comparison using \\=\\=\\= between list and false will always evaluate to false\\.$#" - count: 1 - path: ../src/Type/Php/StrSplitFunctionReturnTypeExtension.php - - message: "#^Call to function is_bool\\(\\) with string will always evaluate to false\\.$#" count: 1 diff --git a/src/Type/Php/StrSplitFunctionReturnTypeExtension.php b/src/Type/Php/StrSplitFunctionReturnTypeExtension.php index fc33e2d560..9e9bf1016c 100644 --- a/src/Type/Php/StrSplitFunctionReturnTypeExtension.php +++ b/src/Type/Php/StrSplitFunctionReturnTypeExtension.php @@ -94,9 +94,6 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $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); } diff --git a/tests/PHPStan/Analyser/data/mb-str-split-php80.php b/tests/PHPStan/Analyser/data/mb-str-split-php80.php index 2e1b4b3845..9f5d0def9c 100644 --- a/tests/PHPStan/Analyser/data/mb-str-split-php80.php +++ b/tests/PHPStan/Analyser/data/mb-str-split-php80.php @@ -26,7 +26,7 @@ public function legacyTest(): void assertType('array{\'abcdef\'}', $mbStrSplitConstantStringWithGreaterSplitLengthThanStringLength); $mbStrSplitConstantStringWithFailureSplitLength = mb_str_split('abcdef', 0); - assertType('false', $mbStrSplitConstantStringWithFailureSplitLength); + assertType('*NEVER*', $mbStrSplitConstantStringWithFailureSplitLength); $mbStrSplitConstantStringWithInvalidSplitLengthType = mb_str_split('abcdef', []); assertType('non-empty-list', $mbStrSplitConstantStringWithInvalidSplitLengthType); diff --git a/tests/PHPStan/Analyser/data/str-split-php80.php b/tests/PHPStan/Analyser/data/str-split-php80.php index 342b28ad2f..7fe3a36ef9 100644 --- a/tests/PHPStan/Analyser/data/str-split-php80.php +++ b/tests/PHPStan/Analyser/data/str-split-php80.php @@ -25,7 +25,7 @@ public function legacyTest() { assertType('array{\'abcdef\'}', $strSplitConstantStringWithGreaterSplitLengthThanStringLength); $strSplitConstantStringWithFailureSplitLength = str_split('abcdef', 0); - assertType('false', $strSplitConstantStringWithFailureSplitLength); + assertType('*NEVER*', $strSplitConstantStringWithFailureSplitLength); $strSplitConstantStringWithInvalidSplitLengthType = str_split('abcdef', []); assertType('non-empty-list', $strSplitConstantStringWithInvalidSplitLengthType); diff --git a/tests/PHPStan/Analyser/data/str-split-php82.php b/tests/PHPStan/Analyser/data/str-split-php82.php index 0b0436bf7b..22720747e6 100644 --- a/tests/PHPStan/Analyser/data/str-split-php82.php +++ b/tests/PHPStan/Analyser/data/str-split-php82.php @@ -25,7 +25,7 @@ public function legacyTest() { assertType('array{\'abcdef\'}', $strSplitConstantStringWithGreaterSplitLengthThanStringLength); $strSplitConstantStringWithFailureSplitLength = str_split('abcdef', 0); - assertType('false', $strSplitConstantStringWithFailureSplitLength); + assertType('*NEVER*', $strSplitConstantStringWithFailureSplitLength); $strSplitConstantStringWithInvalidSplitLengthType = str_split('abcdef', []); assertType('non-empty-list', $strSplitConstantStringWithInvalidSplitLengthType); From e4718956f0068c8925f362ceef6487f625154c0b Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Fri, 13 Jun 2025 02:52:17 +0200 Subject: [PATCH 5/5] Improve --- src/Php/PhpVersion.php | 2 +- .../Php/StrSplitFunctionReturnTypeExtension.php | 17 ++++++++++++----- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src/Php/PhpVersion.php b/src/Php/PhpVersion.php index b275c464eb..ceb8345df5 100644 --- a/src/Php/PhpVersion.php +++ b/src/Php/PhpVersion.php @@ -236,7 +236,7 @@ public function deprecatesDynamicProperties(): bool return $this->versionId >= 80200; } - public function strSplitReturnsEmptyArray(): bool + public function strSplitReturnsEmptyArrayOnEmptyString(): bool { return $this->versionId >= 80200; } diff --git a/src/Type/Php/StrSplitFunctionReturnTypeExtension.php b/src/Type/Php/StrSplitFunctionReturnTypeExtension.php index 9e9bf1016c..a3d43371dc 100644 --- a/src/Type/Php/StrSplitFunctionReturnTypeExtension.php +++ b/src/Type/Php/StrSplitFunctionReturnTypeExtension.php @@ -91,9 +91,16 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, if (count($constantStrings) > 0) { $results = []; foreach ($constantStrings as $constantString) { - $items = $encoding === null - ? str_split($constantString->getValue(), $splitLength) - : @mb_str_split($constantString->getValue(), $splitLength, $encoding); + $value = $constantString->getValue(); + + if ($encoding === null && $value === '') { + // Simulate the str_split call with the analysed PHP Version instead of the runtime one. + $items = $this->phpVersion->strSplitReturnsEmptyArrayOnEmptyString() ? [] : ['']; + } else { + $items = $encoding === null + ? str_split($value, $splitLength) + : @mb_str_split($value, $splitLength, $encoding); + } $results[] = self::createConstantArrayFrom($items, $scope); } @@ -104,7 +111,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $isInputNonEmptyString = $stringType->isNonEmptyString()->yes(); - if ($isInputNonEmptyString || $this->phpVersion->strSplitReturnsEmptyArray()) { + if ($isInputNonEmptyString || $this->phpVersion->strSplitReturnsEmptyArrayOnEmptyString()) { $returnValueType = TypeCombinator::intersect(new StringType(), new AccessoryNonEmptyStringType()); } else { $returnValueType = new StringType(); @@ -115,7 +122,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, // Non-empty-string will return an array with at least an element $isInputNonEmptyString // str_split('', 1) returns [''] on old PHP version and [] on new ones - || ($functionReflection->getName() === 'str_split' && !$this->phpVersion->strSplitReturnsEmptyArray()) + || ($functionReflection->getName() === 'str_split' && !$this->phpVersion->strSplitReturnsEmptyArrayOnEmptyString()) ) { $returnType = TypeCombinator::intersect($returnType, new NonEmptyArrayType()); }