Skip to content

Commit d83ff0c

Browse files
committed
Narrow explode return to constant arrays for small positive limits
1 parent 60b29fa commit d83ff0c

File tree

5 files changed

+92
-9
lines changed

5 files changed

+92
-9
lines changed

src/Type/Php/ExplodeFunctionDynamicReturnTypeExtension.php

+20-5
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@
1111
use PHPStan\Type\Accessory\AccessoryUppercaseStringType;
1212
use PHPStan\Type\Accessory\NonEmptyArrayType;
1313
use PHPStan\Type\ArrayType;
14+
use PHPStan\Type\Constant\ConstantArrayTypeBuilder;
1415
use PHPStan\Type\Constant\ConstantBooleanType;
16+
use PHPStan\Type\Constant\ConstantIntegerType;
1517
use PHPStan\Type\Constant\ConstantStringType;
1618
use PHPStan\Type\DynamicFunctionReturnTypeExtension;
1719
use PHPStan\Type\IntegerRangeType;
@@ -24,10 +26,13 @@
2426
use PHPStan\Type\TypeCombinator;
2527
use PHPStan\Type\TypeUtils;
2628
use function count;
29+
use const PHP_INT_MAX;
2730

2831
final class ExplodeFunctionDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension
2932
{
3033

34+
private const CONST_ARRAY_LIMIT = 8;
35+
3136
public function __construct(private PhpVersion $phpVersion)
3237
{
3338
}
@@ -73,11 +78,21 @@ public function getTypeFromFunctionCall(
7378
}
7479

7580
$returnType = TypeCombinator::intersect(new ArrayType(new IntegerType(), $returnValueType), new AccessoryArrayListType());
76-
if (
77-
!isset($args[2])
78-
|| IntegerRangeType::fromInterval(0, null)->isSuperTypeOf($scope->getType($args[2]->value))->yes()
79-
) {
80-
$returnType = TypeCombinator::intersect($returnType, new NonEmptyArrayType());
81+
$limitType = isset($args[2]) ? $scope->getType($args[2]->value) : new ConstantIntegerType(PHP_INT_MAX);
82+
if (IntegerRangeType::fromInterval(0, null)->isSuperTypeOf($limitType)->yes()) {
83+
$constantScalarTypes = $limitType->getConstantScalarTypes();
84+
if (count($constantScalarTypes) === 1 && IntegerRangeType::fromInterval(0, self::CONST_ARRAY_LIMIT)->isSuperTypeOf($limitType)->yes()) {
85+
$limit = (int) $constantScalarTypes[0]->getValue() ?: 1; // 0 is treated as 1
86+
87+
$builder = ConstantArrayTypeBuilder::createEmpty();
88+
for ($i = 0; $i < $limit; $i++) {
89+
$builder->setOffsetValueType(null, $returnValueType, $i !== 0);
90+
}
91+
92+
$returnType = $builder->getArray();
93+
} else {
94+
$returnType = TypeCombinator::intersect($returnType, new NonEmptyArrayType());
95+
}
8196
}
8297

8398
if (!$this->phpVersion->throwsValueErrorForInternalFunctions() && $isEmptyString->maybe()) {

tests/PHPStan/Analyser/nsrt/bug-3961-php8.php

+2-2
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ public function doFoo(string $v, string $d, $m): void
1212
assertType('non-empty-list<string>', explode('.', $v));
1313
assertType('*NEVER*', explode('', $v));
1414
assertType('list<string>', explode('.', $v, -2));
15-
assertType('non-empty-list<string>', explode('.', $v, 0));
16-
assertType('non-empty-list<string>', explode('.', $v, 1));
15+
assertType('array{string}', explode('.', $v, 0));
16+
assertType('array{string}', explode('.', $v, 1));
1717
assertType('non-empty-list<string>', explode($d, $v));
1818
assertType('non-empty-list<string>', explode($m, $v));
1919
}

tests/PHPStan/Analyser/nsrt/bug-3961.php

+2-2
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ public function doFoo(string $v, string $d, $m): void
1212
assertType('non-empty-list<string>', explode('.', $v));
1313
assertType('false', explode('', $v));
1414
assertType('list<string>', explode('.', $v, -2));
15-
assertType('non-empty-list<string>', explode('.', $v, 0));
16-
assertType('non-empty-list<string>', explode('.', $v, 1));
15+
assertType('array{string}', explode('.', $v, 0));
16+
assertType('array{string}', explode('.', $v, 1));
1717
assertType('non-empty-list<string>|false', explode($d, $v));
1818
assertType('(non-empty-list<string>|false)', explode($m, $v));
1919
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php // lint < 8.0
2+
3+
namespace ExplodePhp7;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
class Foo
8+
{
9+
10+
/**
11+
* @param non-empty-string $nonEmptyString
12+
*/
13+
public function constantArrays(string $string, string $nonEmptyString): void
14+
{
15+
$strings = explode(',', $string, 0);
16+
assertType('array{string}', $strings);
17+
18+
$strings = explode(',', $string, 2);
19+
assertType('array{0: string, 1?: string}', $strings);
20+
21+
$strings = explode(rand(0, 1) ? '' : ',', $string, 2);
22+
assertType('array{0: string, 1?: string}|false', $strings);
23+
24+
$strings = explode(',', $string, 16);
25+
assertType('non-empty-list<string>', $strings);
26+
27+
$strings = explode(',', $nonEmptyString, 2);
28+
assertType('array{0: string, 1?: string}', $strings);
29+
30+
$strings = explode(',', $nonEmptyString, 16);
31+
assertType('non-empty-list<string>', $strings);
32+
}
33+
34+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php // lint >= 8.0
2+
3+
namespace ExplodePhp8;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
class Foo
8+
{
9+
10+
/**
11+
* @param non-empty-string $nonEmptyString
12+
*/
13+
public function constantArrays(string $string, string $nonEmptyString): void
14+
{
15+
$strings = explode(',', $string, 0);
16+
assertType('array{string}', $strings);
17+
18+
$strings = explode(',', $string, 2);
19+
assertType('array{0: string, 1?: string}', $strings);
20+
21+
$strings = explode(rand(0, 1) ? '' : ',', $string, 2);
22+
assertType('array{0: string, 1?: string}', $strings);
23+
24+
$strings = explode(',', $string, 16);
25+
assertType('non-empty-list<string>', $strings);
26+
27+
$strings = explode(',', $nonEmptyString, 2);
28+
assertType('array{0: string, 1?: string}', $strings);
29+
30+
$strings = explode(',', $nonEmptyString, 16);
31+
assertType('non-empty-list<string>', $strings);
32+
}
33+
34+
}

0 commit comments

Comments
 (0)