Skip to content

Commit fff96fc

Browse files
Improve StrSplit returnType
1 parent 74b909a commit fff96fc

File tree

4 files changed

+136
-18
lines changed

4 files changed

+136
-18
lines changed

src/Type/Php/StrSplitFunctionReturnTypeExtension.php

Lines changed: 32 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@
99
use PHPStan\ShouldNotHappenException;
1010
use PHPStan\TrinaryLogic;
1111
use PHPStan\Type\Accessory\AccessoryArrayListType;
12+
use PHPStan\Type\Accessory\AccessoryLowercaseStringType;
13+
use PHPStan\Type\Accessory\AccessoryNonEmptyStringType;
14+
use PHPStan\Type\Accessory\AccessoryUppercaseStringType;
1215
use PHPStan\Type\Accessory\NonEmptyArrayType;
1316
use PHPStan\Type\ArrayType;
1417
use PHPStan\Type\Constant\ConstantArrayType;
@@ -80,32 +83,43 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection,
8083
}
8184
}
8285

83-
if (!isset($splitLength)) {
84-
return null;
85-
}
86-
8786
$stringType = $scope->getType($functionCall->getArgs()[0]->value);
88-
89-
$constantStrings = $stringType->getConstantStrings();
90-
if (count($constantStrings) > 0) {
91-
$results = [];
92-
foreach ($constantStrings as $constantString) {
93-
$items = $encoding === null
94-
? str_split($constantString->getValue(), $splitLength)
95-
: @mb_str_split($constantString->getValue(), $splitLength, $encoding);
96-
if ($items === false) {
97-
throw new ShouldNotHappenException();
87+
if (isset($splitLength)) {
88+
$constantStrings = $stringType->getConstantStrings();
89+
if (count($constantStrings) > 0) {
90+
$results = [];
91+
foreach ($constantStrings as $constantString) {
92+
$items = $encoding === null
93+
? str_split($constantString->getValue(), $splitLength)
94+
: @mb_str_split($constantString->getValue(), $splitLength, $encoding);
95+
if ($items === false) {
96+
throw new ShouldNotHappenException();
97+
}
98+
99+
$results[] = self::createConstantArrayFrom($items, $scope);
98100
}
99101

100-
$results[] = self::createConstantArrayFrom($items, $scope);
102+
return TypeCombinator::union(...$results);
101103
}
104+
}
105+
106+
$isInputNonEmptyString = $stringType->isNonEmptyString()->yes();
102107

103-
return TypeCombinator::union(...$results);
108+
$valueTypes = [new StringType()];
109+
if ($isInputNonEmptyString || $this->phpVersion->strSplitReturnsEmptyArray()) {
110+
$valueTypes[] = new AccessoryNonEmptyStringType();
111+
}
112+
if ($stringType->isLowercaseString()->yes()) {
113+
$valueTypes[] = new AccessoryLowercaseStringType();
114+
}
115+
if ($stringType->isUppercaseString()->yes()) {
116+
$valueTypes[] = new AccessoryUppercaseStringType();
104117
}
118+
$returnValueType = TypeCombinator::intersect(new StringType(), ...$valueTypes);
105119

106-
$returnType = AccessoryArrayListType::intersectWith(new ArrayType(new IntegerType(), new StringType()));
120+
$returnType = AccessoryArrayListType::intersectWith(TypeCombinator::intersect(new ArrayType(new IntegerType(), $returnValueType)));
107121

108-
return $encoding === null && !$this->phpVersion->strSplitReturnsEmptyArray()
122+
return $isInputNonEmptyString || ($encoding === null && !$this->phpVersion->strSplitReturnsEmptyArray())
109123
? TypeCombinator::intersect($returnType, new NonEmptyArrayType())
110124
: $returnType;
111125
}

tests/PHPStan/Analyser/NodeScopeResolverTest.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,18 @@ private static function findTestFiles(): iterable
2929
yield $testFile;
3030
}
3131

32+
if (PHP_VERSION_ID >= 80200) {
33+
yield __DIR__ . '/data/str-split-php82.php';
34+
} else {
35+
yield __DIR__ . '/data/str-split.php';
36+
}
37+
38+
if (PHP_VERSION_ID >= 80200) {
39+
yield __DIR__ . '/data/str-split-php82.php';
40+
} else {
41+
yield __DIR__ . '/data/str-split.php';
42+
}
43+
3244
if (PHP_VERSION_ID < 80200 && PHP_VERSION_ID >= 80100) {
3345
yield __DIR__ . '/data/enum-reflection-php81.php';
3446
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<?php
2+
3+
namespace StrSplit82;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
class Foo {
8+
/**
9+
* @param non-empty-string $nonEmptyString
10+
* @param non-falsy-string $nonFalsyString
11+
* @param lowercase-string $lowercaseString
12+
* @param uppercase-string $uppercaseString
13+
*/
14+
function doFoo(
15+
string $string,
16+
string $nonEmptyString,
17+
string $nonFalsyString,
18+
string $lowercaseString,
19+
string $uppercaseString,
20+
int $integer,
21+
):void {
22+
assertType('list<non-empty-string>', str_split($string));
23+
assertType('non-empty-list<non-empty-string>', str_split($nonEmptyString));
24+
assertType('non-empty-list<non-empty-string>', str_split($nonFalsyString));
25+
assertType('list<lowercase-string&non-empty-string>', str_split($lowercaseString));
26+
assertType('list<non-empty-string&uppercase-string>', str_split($uppercaseString));
27+
28+
assertType('list<non-empty-string>', str_split($string, $integer));
29+
assertType('non-empty-list<non-empty-string>', str_split($nonEmptyString, $integer));
30+
assertType('non-empty-list<non-empty-string>', str_split($nonFalsyString, $integer));
31+
assertType('list<lowercase-string&non-empty-string>', str_split($lowercaseString, $integer));
32+
assertType('list<non-empty-string&uppercase-string>', str_split($uppercaseString, $integer));
33+
34+
assertType('list<non-empty-string>', mb_str_split($string));
35+
assertType('non-empty-list<non-empty-string>', mb_str_split($nonEmptyString));
36+
assertType('non-empty-list<non-empty-string>', mb_str_split($nonFalsyString));
37+
assertType('list<lowercase-string&non-empty-string>', mb_str_split($lowercaseString));
38+
assertType('list<non-empty-string&uppercase-string>', mb_str_split($uppercaseString));
39+
40+
assertType('list<non-empty-string>', mb_str_split($string, $integer));
41+
assertType('non-empty-list<non-empty-string>', mb_str_split($nonEmptyString, $integer));
42+
assertType('non-empty-list<non-empty-string>', mb_str_split($nonFalsyString, $integer));
43+
assertType('list<lowercase-string&non-empty-string>', mb_str_split($lowercaseString, $integer));
44+
assertType('list<non-empty-string&uppercase-string>', mb_str_split($uppercaseString, $integer));
45+
}
46+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<?php
2+
3+
namespace StrSplit;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
class Foo {
8+
/**
9+
* @param non-empty-string $nonEmptyString
10+
* @param non-falsy-string $nonFalsyString
11+
* @param lowercase-string $lowercaseString
12+
* @param uppercase-string $uppercaseString
13+
*/
14+
function doFoo(
15+
string $string,
16+
string $nonEmptyString,
17+
string $nonFalsyString,
18+
string $lowercaseString,
19+
string $uppercaseString,
20+
int $integer,
21+
):void {
22+
assertType('non-empty-list<string>', str_split($string));
23+
assertType('non-empty-list<non-empty-string>', str_split($nonEmptyString));
24+
assertType('non-empty-list<non-empty-string>', str_split($nonFalsyString));
25+
assertType('non-empty-list<lowercase-string>', str_split($lowercaseString));
26+
assertType('non-empty-list<uppercase-string>', str_split($uppercaseString));
27+
28+
assertType('non-empty-list<string>', str_split($string, $integer));
29+
assertType('non-empty-list<non-empty-string>', str_split($nonEmptyString, $integer));
30+
assertType('non-empty-list<non-empty-string>', str_split($nonFalsyString, $integer));
31+
assertType('non-empty-list<lowercase-string>', str_split($lowercaseString, $integer));
32+
assertType('non-empty-list<uppercase-string>', str_split($uppercaseString, $integer));
33+
34+
assertType('list<string>', mb_str_split($string));
35+
assertType('non-empty-list<non-empty-string>', mb_str_split($nonEmptyString));
36+
assertType('non-empty-list<non-empty-string>', mb_str_split($nonFalsyString));
37+
assertType('list<lowercase-string>', mb_str_split($lowercaseString));
38+
assertType('list<uppercase-string>', mb_str_split($uppercaseString));
39+
40+
assertType('list<string>', mb_str_split($string, $integer));
41+
assertType('non-empty-list<non-empty-string>', mb_str_split($nonEmptyString, $integer));
42+
assertType('non-empty-list<non-empty-string>', mb_str_split($nonFalsyString, $integer));
43+
assertType('list<lowercase-string>', mb_str_split($lowercaseString, $integer));
44+
assertType('list<uppercase-string>', mb_str_split($uppercaseString, $integer));
45+
}
46+
}

0 commit comments

Comments
 (0)