Skip to content

Commit

Permalink
Narrow explode return to constant arrays for small positive limits
Browse files Browse the repository at this point in the history
  • Loading branch information
herndlm committed Dec 4, 2024
1 parent c586014 commit afe6896
Show file tree
Hide file tree
Showing 4 changed files with 90 additions and 7 deletions.
25 changes: 20 additions & 5 deletions src/Type/Php/ExplodeFunctionDynamicReturnTypeExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@
use PHPStan\Type\Accessory\AccessoryUppercaseStringType;
use PHPStan\Type\Accessory\NonEmptyArrayType;
use PHPStan\Type\ArrayType;
use PHPStan\Type\Constant\ConstantArrayTypeBuilder;
use PHPStan\Type\Constant\ConstantBooleanType;
use PHPStan\Type\Constant\ConstantIntegerType;
use PHPStan\Type\Constant\ConstantStringType;
use PHPStan\Type\DynamicFunctionReturnTypeExtension;
use PHPStan\Type\IntegerRangeType;
Expand All @@ -24,10 +26,13 @@
use PHPStan\Type\TypeCombinator;
use PHPStan\Type\TypeUtils;
use function count;
use const PHP_INT_MAX;

final class ExplodeFunctionDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension
{

private const CONST_ARRAY_LIMIT = 8;

public function __construct(private PhpVersion $phpVersion)
{
}
Expand Down Expand Up @@ -74,11 +79,21 @@ public function getTypeFromFunctionCall(

$returnType = AccessoryArrayListType::intersectWith(new ArrayType(new IntegerType(), $returnValueType));

if (
!isset($args[2])
|| IntegerRangeType::fromInterval(0, null)->isSuperTypeOf($scope->getType($args[2]->value))->yes()
) {
$returnType = TypeCombinator::intersect($returnType, new NonEmptyArrayType());
$limitType = isset($args[2]) ? $scope->getType($args[2]->value) : new ConstantIntegerType(PHP_INT_MAX);
if (IntegerRangeType::fromInterval(0, null)->isSuperTypeOf($limitType)->yes()) {
$constantScalarTypes = $limitType->getConstantScalarTypes();
if (count($constantScalarTypes) === 1 && IntegerRangeType::fromInterval(0, self::CONST_ARRAY_LIMIT)->isSuperTypeOf($limitType)->yes()) {
$limit = (int) $constantScalarTypes[0]->getValue() ?: 1; // 0 is treated as 1

$builder = ConstantArrayTypeBuilder::createEmpty();
for ($i = 0; $i < $limit; $i++) {
$builder->setOffsetValueType(null, $returnValueType, $i !== 0);
}

$returnType = $builder->getArray();
} else {
$returnType = TypeCombinator::intersect($returnType, new NonEmptyArrayType());
}
}

if (!$this->phpVersion->throwsValueErrorForInternalFunctions() && $isEmptyString->maybe()) {
Expand Down
4 changes: 2 additions & 2 deletions tests/PHPStan/Analyser/nsrt/bug-3961-php8.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ public function doFoo(string $v, string $d, $m): void
assertType('non-empty-list<string>', explode('.', $v));
assertType('*NEVER*', explode('', $v));
assertType('list<string>', explode('.', $v, -2));
assertType('non-empty-list<string>', explode('.', $v, 0));
assertType('non-empty-list<string>', explode('.', $v, 1));
assertType('array{string}', explode('.', $v, 0));
assertType('array{string}', explode('.', $v, 1));
assertType('non-empty-list<string>', explode($d, $v));
assertType('non-empty-list<string>', explode($m, $v));
}
Expand Down
34 changes: 34 additions & 0 deletions tests/PHPStan/Analyser/nsrt/explode-php7.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php // lint < 8.0

namespace ExplodePhp7;

use function PHPStan\Testing\assertType;

class Foo
{

/**
* @param non-empty-string $nonEmptyString
*/
public function constantArrays(string $string, string $nonEmptyString): void
{
$strings = explode(',', $string, 0);
assertType('array{string}', $strings);

$strings = explode(',', $string, 2);
assertType('array{0: string, 1?: string}', $strings);

$strings = explode(rand(0, 1) ? '' : ',', $string, 2);
assertType('array{0: string, 1?: string}|false', $strings);

$strings = explode(',', $string, 16);
assertType('non-empty-list<string>', $strings);

$strings = explode(',', $nonEmptyString, 2);
assertType('array{0: string, 1?: string}', $strings);

$strings = explode(',', $nonEmptyString, 16);
assertType('non-empty-list<string>', $strings);
}

}
34 changes: 34 additions & 0 deletions tests/PHPStan/Analyser/nsrt/explode-php8.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php // lint >= 8.0

namespace ExplodePhp8;

use function PHPStan\Testing\assertType;

class Foo
{

/**
* @param non-empty-string $nonEmptyString
*/
public function constantArrays(string $string, string $nonEmptyString): void
{
$strings = explode(',', $string, 0);
assertType('array{string}', $strings);

$strings = explode(',', $string, 2);
assertType('array{0: string, 1?: string}', $strings);

$strings = explode(rand(0, 1) ? '' : ',', $string, 2);
assertType('array{0: string, 1?: string}', $strings);

$strings = explode(',', $string, 16);
assertType('non-empty-list<string>', $strings);

$strings = explode(',', $nonEmptyString, 2);
assertType('array{0: string, 1?: string}', $strings);

$strings = explode(',', $nonEmptyString, 16);
assertType('non-empty-list<string>', $strings);
}

}

0 comments on commit afe6896

Please sign in to comment.