diff --git a/conf/config.neon b/conf/config.neon index 40a8c57fab..c87e525cce 100644 --- a/conf/config.neon +++ b/conf/config.neon @@ -1275,6 +1275,11 @@ services: tags: - phpstan.broker.dynamicFunctionReturnTypeExtension + - + class: PHPStan\Type\Php\SubstrDynamicReturnTypeExtension + tags: + - phpstan.broker.dynamicFunctionReturnTypeExtension + - class: PHPStan\Type\Php\ParseUrlFunctionDynamicReturnTypeExtension tags: diff --git a/src/Type/Php/SubstrDynamicReturnTypeExtension.php b/src/Type/Php/SubstrDynamicReturnTypeExtension.php new file mode 100644 index 0000000000..f86d919eab --- /dev/null +++ b/src/Type/Php/SubstrDynamicReturnTypeExtension.php @@ -0,0 +1,59 @@ +getName() === 'substr'; + } + + public function getTypeFromFunctionCall( + FunctionReflection $functionReflection, + FuncCall $functionCall, + Scope $scope + ): \PHPStan\Type\Type + { + $args = $functionCall->args; + if (count($args) === 0) { + return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + } + + if (count($args) >= 2) { + $string = $scope->getType($args[0]->value); + $offset = $scope->getType($args[1]->value); + + $negativeOffset = IntegerRangeType::fromInterval(null, -1)->isSuperTypeOf($offset)->yes(); + $zeroOffset = (new ConstantIntegerType(0))->isSuperTypeOf($offset)->yes(); + $positiveLength = false; + + if (count($args) === 3) { + $length = $scope->getType($args[2]->value); + $positiveLength = IntegerRangeType::fromInterval(1, null)->isSuperTypeOf($length)->yes(); + } + + if ($string->isNonEmptyString()->yes() && ($negativeOffset || $zeroOffset && $positiveLength)) { + return new IntersectionType([ + new StringType(), + new AccessoryNonEmptyStringType(), + ]); + } + } + + return new StringType(); + } + +} diff --git a/tests/PHPStan/Analyser/data/non-empty-string.php b/tests/PHPStan/Analyser/data/non-empty-string.php index a235ea6ef8..a233259c5a 100644 --- a/tests/PHPStan/Analyser/data/non-empty-string.php +++ b/tests/PHPStan/Analyser/data/non-empty-string.php @@ -134,6 +134,29 @@ public function doEmpty2(string $s): void } } + /** + * @param non-empty-string $nonEmpty + * @param positive-int $positiveInt + * @param 1|2|3 $postiveRange + * @param -1|-2|-3 $negativeRange + */ + public function doSubstr(string $s, $nonEmpty, $positiveInt, $postiveRange, $negativeRange): void + { + assertType('string', substr($s, 5)); + + assertType('string', substr($s, -5)); + assertType('non-empty-string', substr($nonEmpty, -5)); + assertType('non-empty-string', substr($nonEmpty, $negativeRange)); + + assertType('string', substr($s, 0, 5)); + assertType('non-empty-string', substr($nonEmpty, 0, 5)); + assertType('non-empty-string', substr($nonEmpty, 0, $postiveRange)); + + assertType('string', substr($nonEmpty, 0, -5)); + + assertType('string', substr($s, 0, $positiveInt)); + assertType('non-empty-string', substr($nonEmpty, 0, $positiveInt)); + } } class ImplodingStrings @@ -186,7 +209,7 @@ public function doFoo4(string $s, array $nonEmptyArrayWithNonEmptyStrings): void public function sayHello(): void { // coming from issue #5291 - $s = array(1,2); + $s = array(1, 2); assertType('non-empty-string', implode("a", $s)); } @@ -194,7 +217,8 @@ public function sayHello(): void /** * @param non-empty-string $glue */ - public function nonE($glue, array $a) { + public function nonE($glue, array $a) + { // coming from issue #5291 if (empty($a)) { return "xyz"; @@ -206,7 +230,7 @@ public function nonE($glue, array $a) { public function sayHello2(): void { // coming from issue #5291 - $s = array(1,2); + $s = array(1, 2); assertType('non-empty-string', join("a", $s)); } @@ -214,7 +238,8 @@ public function sayHello2(): void /** * @param non-empty-string $glue */ - public function nonE2($glue, array $a) { + public function nonE2($glue, array $a) + { // coming from issue #5291 if (empty($a)) { return "xyz"; @@ -228,7 +253,8 @@ public function nonE2($glue, array $a) { class LiteralString { - function x(string $tableName, string $original): void { + function x(string $tableName, string $original): void + { assertType('non-empty-string', "from `$tableName`"); } @@ -297,7 +323,7 @@ public function doFoo(string $s, string $nonEmpty, int $i) assertType('non-empty-string', htmlspecialchars($nonEmpty)); assertType('string', htmlentities($s)); assertType('non-empty-string', htmlentities($nonEmpty)); - + assertType('string', urlencode($s)); assertType('non-empty-string', urlencode($nonEmpty)); assertType('string', urldecode($s));