Skip to content

Commit

Permalink
Implement strlen in the same way as mb_strlen
Browse files Browse the repository at this point in the history
  • Loading branch information
is-hoku committed Mar 6, 2024
1 parent 5c5f6cb commit 6b52280
Show file tree
Hide file tree
Showing 2 changed files with 29 additions and 36 deletions.
61 changes: 26 additions & 35 deletions src/Type/Php/StrlenFunctionReturnTypeExtension.php
Expand Up @@ -15,7 +15,13 @@
use PHPStan\Type\IntegerType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use function array_map;
use function array_unique;
use function count;
use function max;
use function min;
use function range;
use function sort;
use function strlen;

class StrlenFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension
Expand Down Expand Up @@ -49,56 +55,41 @@ public function getTypeFromFunctionCall(
$constantScalars = $argType->getConstantScalarTypes();
}

$min = null;
$max = null;
$lengths = [];
foreach ($constantScalars as $constantScalar) {
$stringScalar = $constantScalar->toString();
if (!($stringScalar instanceof ConstantStringType)) {
$min = $max = null;
$lengths = [];
break;
}
$len = strlen($stringScalar->getValue());

if ($min === null) {
$min = $len;
$max = $len;
}

if ($len < $min) {
$min = $len;
}
if ($len <= $max) {
continue;
}

$max = $len;
}

// $max is always != null, when $min is != null
if ($min !== null) {
return IntegerRangeType::fromInterval($min, $max);
}

$bool = new BooleanType();
if ($bool->isSuperTypeOf($argType)->yes()) {
return IntegerRangeType::fromInterval(0, 1);
$length = strlen($stringScalar->getValue());
$lengths[] = $length;
}

$isNonEmpty = $argType->isNonEmptyString();
$numeric = TypeCombinator::union(new IntegerType(), new FloatType());
if (
$range = null;
if (count($lengths) > 0) {
$lengths = array_unique($lengths);
sort($lengths);
if ($lengths === range(min($lengths), max($lengths))) {
$range = IntegerRangeType::fromInterval(min($lengths), max($lengths));
} else {
$range = TypeCombinator::union(...array_map(static fn ($l) => new ConstantIntegerType($l), $lengths));
}
} elseif ($argType->isBoolean()->yes()) {
$range = IntegerRangeType::fromInterval(0, 1);
} elseif (
$isNonEmpty->yes()
|| $numeric->isSuperTypeOf($argType)->yes()
|| TypeCombinator::remove($argType, $numeric)->isNonEmptyString()->yes()
) {
return IntegerRangeType::fromInterval(1, null);
}

if ($argType->isString()->yes() && $isNonEmpty->no()) {
return new ConstantIntegerType(0);
$range = IntegerRangeType::fromInterval(1, null);
} elseif ($argType->isString()->yes() && $isNonEmpty->no()) {
$range = new ConstantIntegerType(0);
}

return null;
return $range;
}

}
4 changes: 3 additions & 1 deletion tests/PHPStan/Analyser/data/weird-strlen-cases.php
Expand Up @@ -8,14 +8,16 @@
class Foo
{
/**
* @param 'foo'|'foooooo' $constUnionString
* @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
public function strlenTests(string $constUnionString, $constUnionMixed, float $float, $intFloat, $nonEmptyStringIntFloat, $emptyStringFalseNull, $emptyStringBoolNull): void
{
assertType('3|7', strlen($constUnionString));
assertType('int<0, 4>', strlen($constUnionMixed));
assertType('3', strlen(123));
assertType('1', strlen(true));
Expand Down

0 comments on commit 6b52280

Please sign in to comment.