Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions conf/config.neon
Original file line number Diff line number Diff line change
Expand Up @@ -1275,6 +1275,11 @@ services:
tags:
- phpstan.broker.dynamicFunctionReturnTypeExtension

-
class: PHPStan\Type\Php\SubstrDynamicReturnTypeExtension
tags:
- phpstan.broker.dynamicFunctionReturnTypeExtension

-
class: PHPStan\Type\Php\ParseUrlFunctionDynamicReturnTypeExtension
tags:
Expand Down
59 changes: 59 additions & 0 deletions src/Type/Php/SubstrDynamicReturnTypeExtension.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?php declare(strict_types = 1);

namespace PHPStan\Type\Php;

use PhpParser\Node\Expr\FuncCall;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\FunctionReflection;
use PHPStan\Reflection\ParametersAcceptorSelector;
use PHPStan\Type\Accessory\AccessoryNonEmptyStringType;
use PHPStan\Type\Constant\ConstantIntegerType;
use PHPStan\Type\DynamicFunctionReturnTypeExtension;
use PHPStan\Type\IntegerRangeType;
use PHPStan\Type\IntersectionType;
use PHPStan\Type\StringType;

class SubstrDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension
{

public function isFunctionSupported(FunctionReflection $functionReflection): bool
{
return $functionReflection->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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should ask about whether a number is positive or negative this way:

IntegerRangeType::fromInterval(null, -1)->isSuperTypeOf($offset)->yes() // is negative
IntegerRangeType::fromInterval(1, null)->isSuperTypeOf($offset)->yes() // is positive

It also accounts for unions of ConstantIntegerType, like 1|3.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thx, fixed.

Copy link
Contributor Author

@staabm staabm Jul 19, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does this mean I should check for a zero offset with
IntegerRangeType::fromInterval(0, 0)->isSuperTypeOf($offset)->yes() ?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can check for that with:

(new ConstantIntegerType(0))->isSuperTypeOf($offset)->yes()

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ConstantIntegerType, like 1|3.

also added testcases to cover that


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();
}

}
38 changes: 32 additions & 6 deletions tests/PHPStan/Analyser/data/non-empty-string.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -186,15 +209,16 @@ 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));
}

/**
* @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";
Expand All @@ -206,15 +230,16 @@ 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));
}

/**
* @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";
Expand All @@ -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`");
}

Expand Down Expand Up @@ -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));
Expand Down