Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for float and null for strlen, improve support for booleans #1199

Merged
merged 1 commit into from Apr 12, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
36 changes: 25 additions & 11 deletions src/Type/Php/StrlenFunctionReturnTypeExtension.php
Expand Up @@ -7,12 +7,16 @@
use PHPStan\Reflection\FunctionReflection;
use PHPStan\Reflection\ParametersAcceptorSelector;
use PHPStan\Type\BooleanType;
use PHPStan\Type\Constant\ConstantBooleanType;
use PHPStan\Type\Constant\ConstantIntegerType;
use PHPStan\Type\Constant\ConstantStringType;
use PHPStan\Type\DynamicFunctionReturnTypeExtension;
use PHPStan\Type\FloatType;
use PHPStan\Type\IntegerRangeType;
use PHPStan\Type\IntegerType;
use PHPStan\Type\StringType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use PHPStan\Type\TypeUtils;
use function count;
use function strlen;
Expand All @@ -38,19 +42,25 @@ public function getTypeFromFunctionCall(

$argType = $scope->getType($args[0]->value);

$constantScalars = TypeUtils::getConstantScalars($argType);
if ($argType->isSuperTypeOf(new BooleanType())->yes()) {
Copy link
Member

Choose a reason for hiding this comment

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

Is this condition right? This is true for bool but also for bool|object, and also mixed.

Copy link
Member

Choose a reason for hiding this comment

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

And it's false for true and false.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, this condition is right. There a test that covers it:
assertType('int<0, 1>', strlen($emptyStringBoolNull)); // ""|bool|null

The thing is TypeUtils::getConstantScalars() returns constant scalars only if the passed argument contains only constant types. So if the arg is a super type of bool, this code tries to remove bool out of it, and if TypeUtils::getConstantScalars() returns some constant scalars after it, then it's a case when it's a union of bool and some scalars. So in this case this code replaces bool to true|false.
If it's a bool|object or mixed, this code will do nothing, because TypeUtils::getConstantScalars() will return an empty array.

$constantScalars = TypeUtils::getConstantScalars(TypeCombinator::remove($argType, new BooleanType()));
if (count($constantScalars) > 0) {
$constantScalars[] = new ConstantBooleanType(true);
$constantScalars[] = new ConstantBooleanType(false);
}
} else {
$constantScalars = TypeUtils::getConstantScalars($argType);
}

$min = null;
$max = null;
foreach ($constantScalars as $constantScalar) {
if ((new IntegerType())->isSuperTypeOf($constantScalar)->yes()) {
$len = strlen((string) $constantScalar->getValue());
} elseif ((new StringType())->isSuperTypeOf($constantScalar)->yes()) {
$len = strlen((string) $constantScalar->getValue());
} elseif ((new BooleanType())->isSuperTypeOf($constantScalar)->yes()) {
$len = strlen((string) $constantScalar->getValue());
} else {
$stringScalar = $constantScalar->toString();
if (!($stringScalar instanceof ConstantStringType)) {
$min = $max = null;
break;
}
$len = strlen($stringScalar->getValue());

if ($min === null) {
$min = $len;
Expand Down Expand Up @@ -78,12 +88,16 @@ public function getTypeFromFunctionCall(
}

$isNonEmpty = $argType->isNonEmptyString();
$integer = new IntegerType();
if ($isNonEmpty->yes() || $integer->isSuperTypeOf($argType)->yes()) {
$numeric = TypeCombinator::union(new IntegerType(), new FloatType());
if (
$isNonEmpty->yes()
|| $numeric->isSuperTypeOf($argType)->yes()
|| TypeCombinator::remove($argType, $numeric)->isNonEmptyString()->yes()
) {
return IntegerRangeType::fromInterval(1, null);
}

if ($isNonEmpty->no()) {
if ((new StringType())->isSuperTypeOf($argType)->yes() && $isNonEmpty->no()) {
return new ConstantIntegerType(0);
}

Expand Down
2 changes: 2 additions & 0 deletions tests/PHPStan/Analyser/NodeScopeResolverTest.php
Expand Up @@ -830,6 +830,8 @@ public function dataFileAsserts(): iterable

yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6576.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6584.php');

yield from $this->gatherAssertTypes(__DIR__ . '/data/weird-strlen-cases.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6439.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6748.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/array-search-type-specifying.php');
Expand Down
4 changes: 0 additions & 4 deletions tests/PHPStan/Analyser/data/non-empty-string.php
Expand Up @@ -337,10 +337,6 @@ public function doFoo(string $s, string $nonEmpty, int $i, bool $bool, $constUni
assertType('int<0, max>', strlen($s));
assertType('int<1, max>', strlen($nonEmpty));
assertType('int<1, 2>', strlen($constUnion));
assertType('int<0, 4>', strlen($constUnionMixed));
assertType('3', strlen(123));
assertType('1', strlen(true));
assertType('0', strlen(false));

assertType('non-empty-string', str_pad($nonEmpty, 0));
assertType('non-empty-string', str_pad($nonEmpty, 1));
Expand Down
32 changes: 32 additions & 0 deletions tests/PHPStan/Analyser/data/weird-strlen-cases.php
@@ -0,0 +1,32 @@
<?php

namespace WeirdStrlenCases;

use function PHPStan\Testing\assertType;
use function strlen;

class Foo
{
/**
* @param 1|2|5|10|123|'1234'|false $constUnionMixed
* @param int|float $intFloat
* @param non-empty-string|int|float $nonEmptyStringIntFloat
* @param ""|false|null $emptyStringFalseNull
* @param ""|bool|null $emptyStringBoolNull
*/
public function strlenTests($constUnionMixed, float $float, $intFloat, $nonEmptyStringIntFloat, $emptyStringFalseNull, $emptyStringBoolNull): void
{
assertType('int<0, 4>', strlen($constUnionMixed));
assertType('3', strlen(123));
assertType('1', strlen(true));
assertType('0', strlen(false));
assertType('0', strlen(null));
assertType('1', strlen(1.0));
assertType('4', strlen(1.23));
assertType('int<1, max>', strlen($float));
assertType('int<1, max>', strlen($intFloat));
assertType('int<1, max>', strlen($nonEmptyStringIntFloat));
assertType('0', strlen($emptyStringFalseNull));
assertType('int<0, 1>', strlen($emptyStringBoolNull));
}
}