Skip to content

Commit

Permalink
Dynamic return type extension for functions that return non-empty-str…
Browse files Browse the repository at this point in the history
…ing when given one
  • Loading branch information
ondrejmirtes committed Jul 14, 2021
1 parent b864a95 commit 4a9e069
Show file tree
Hide file tree
Showing 6 changed files with 171 additions and 8 deletions.
10 changes: 10 additions & 0 deletions conf/config.neon
Original file line number Diff line number Diff line change
Expand Up @@ -1255,6 +1255,16 @@ services:
tags:
- phpstan.broker.dynamicFunctionReturnTypeExtension

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

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

-
class: PHPStan\Type\Php\ParseUrlFunctionDynamicReturnTypeExtension
tags:
Expand Down
54 changes: 54 additions & 0 deletions src/Type/Php/NonEmptyStringFunctionsReturnTypeExtension.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?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\DynamicFunctionReturnTypeExtension;
use PHPStan\Type\IntersectionType;
use PHPStan\Type\StringType;

class NonEmptyStringFunctionsReturnTypeExtension implements DynamicFunctionReturnTypeExtension
{

public function isFunctionSupported(FunctionReflection $functionReflection): bool
{
return in_array($functionReflection->getName(), [
'strtoupper',
'strtolower',
'mb_strtoupper',
'mb_strtolower',
'lcfirst',
'ucfirst',
'ucwords',
'htmlspecialchars',
'vsprintf',
], true);
}

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

$argType = $scope->getType($args[0]->value);
if ($argType->isNonEmptyString()->yes()) {
return new IntersectionType([
new StringType(),
new AccessoryNonEmptyStringType(),
]);
}

return new StringType();
}

}
26 changes: 19 additions & 7 deletions src/Type/Php/SprintfFunctionDynamicReturnTypeExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\FunctionReflection;
use PHPStan\Reflection\ParametersAcceptorSelector;
use PHPStan\Type\Accessory\AccessoryNonEmptyStringType;
use PHPStan\Type\ConstantScalarType;
use PHPStan\Type\DynamicFunctionReturnTypeExtension;
use PHPStan\Type\IntersectionType;
use PHPStan\Type\StringType;
use PHPStan\Type\Type;

Expand All @@ -25,9 +27,23 @@ public function getTypeFromFunctionCall(
Scope $scope
): Type
{
$args = $functionCall->args;
if (count($args) === 0) {
return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType();
}

$formatType = $scope->getType($args[0]->value);
if ($formatType->isNonEmptyString()->yes()) {
$returnType = new IntersectionType([
new StringType(),
new AccessoryNonEmptyStringType(),
]);
} else {
$returnType = new StringType();
}

$values = [];
$returnType = new StringType();
foreach ($functionCall->args as $arg) {
foreach ($args as $arg) {
$argType = $scope->getType($arg->value);
if (!$argType instanceof ConstantScalarType) {
return $returnType;
Expand All @@ -36,13 +52,9 @@ public function getTypeFromFunctionCall(
$values[] = $argType->getValue();
}

if (count($values) === 0) {
return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType();
}

$format = array_shift($values);
if (!is_string($format)) {
return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType();
return $returnType;
}

try {
Expand Down
45 changes: 45 additions & 0 deletions src/Type/Php/StrlenFunctionReturnTypeExtension.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?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\Constant\ConstantIntegerType;
use PHPStan\Type\DynamicFunctionReturnTypeExtension;
use PHPStan\Type\IntegerRangeType;

class StrlenFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension
{

public function isFunctionSupported(FunctionReflection $functionReflection): bool
{
return $functionReflection->getName() === 'strlen';
}

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

$argType = $scope->getType($args[0]->value);
$isNonEmpty = $argType->isNonEmptyString();
if ($isNonEmpty->yes()) {
return IntegerRangeType::fromInterval(1, null);
}

if ($isNonEmpty->no()) {
return new ConstantIntegerType(0);
}

return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType();
}

}
2 changes: 1 addition & 1 deletion tests/PHPStan/Analyser/data/bug-5219.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ protected function foo(string $message): void
{
$header = sprintf('%s-%s', '', implode('-', ['x']));

assertType('string', $header);
assertType('non-empty-string', $header);
assertType('array<string, string>&nonEmpty', [$header => $message]);
}

Expand Down
42 changes: 42 additions & 0 deletions tests/PHPStan/Analyser/data/non-empty-string.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@

namespace NonEmptyString;

use function htmlspecialchars;
use function lcfirst;
use function PHPStan\Testing\assertType;
use function strtolower;
use function strtoupper;
use function ucfirst;

class Foo
{
Expand Down Expand Up @@ -265,3 +270,40 @@ public function doFoo2(array $a, string $s): void
}

}

class MoreNonEmptyStringFunctions
{

/**
* @param non-empty-string $nonEmpty
*/
public function doFoo(string $s, string $nonEmpty)
{
assertType('string', strtoupper($s));
assertType('non-empty-string', strtoupper($nonEmpty));
assertType('string', strtolower($s));
assertType('non-empty-string', strtolower($nonEmpty));
assertType('string', mb_strtoupper($s));
assertType('non-empty-string', mb_strtoupper($nonEmpty));
assertType('string', mb_strtolower($s));
assertType('non-empty-string', mb_strtolower($nonEmpty));
assertType('string', lcfirst($s));
assertType('non-empty-string', lcfirst($nonEmpty));
assertType('string', ucfirst($s));
assertType('non-empty-string', ucfirst($nonEmpty));
assertType('string', ucwords($s));
assertType('non-empty-string', ucwords($nonEmpty));
assertType('string', htmlspecialchars($s));
assertType('non-empty-string', htmlspecialchars($nonEmpty));

assertType('string', sprintf($s));
assertType('non-empty-string', sprintf($nonEmpty));
assertType('string', vsprintf($s, []));
assertType('non-empty-string', vsprintf($nonEmpty, []));

assertType('0', strlen(''));
assertType('int<0, max>', strlen($s));
assertType('int<1, max>', strlen($nonEmpty));
}

}

0 comments on commit 4a9e069

Please sign in to comment.