Skip to content

Commit dfa4efd

Browse files
Improve StrSplit returnType
1 parent 74b909a commit dfa4efd

File tree

4 files changed

+142
-18
lines changed

4 files changed

+142
-18
lines changed

src/Type/Php/StrSplitFunctionReturnTypeExtension.php

Lines changed: 38 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@
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\AccessoryNonFalsyStringType;
15+
use PHPStan\Type\Accessory\AccessoryUppercaseStringType;
1216
use PHPStan\Type\Accessory\NonEmptyArrayType;
1317
use PHPStan\Type\ArrayType;
1418
use PHPStan\Type\Constant\ConstantArrayType;
@@ -80,32 +84,48 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection,
8084
}
8185
}
8286

83-
if (!isset($splitLength)) {
84-
return null;
85-
}
86-
8787
$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();
88+
if (isset($splitLength)) {
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();
98+
}
99+
100+
$results[] = self::createConstantArrayFrom($items, $scope);
98101
}
99102

100-
$results[] = self::createConstantArrayFrom($items, $scope);
103+
return TypeCombinator::union(...$results);
101104
}
105+
}
106+
107+
$isInputNonEmptyString = $stringType->isNonEmptyString()->yes();
102108

103-
return TypeCombinator::union(...$results);
109+
$valueTypes = [new StringType()];
110+
if ($isInputNonEmptyString || $this->phpVersion->strSplitReturnsEmptyArray()) {
111+
$valueTypes[] = new AccessoryNonEmptyStringType();
112+
}
113+
if ($stringType->isLowercaseString()->yes()) {
114+
$valueTypes[] = new AccessoryLowercaseStringType();
115+
}
116+
if ($stringType->isUppercaseString()->yes()) {
117+
$valueTypes[] = new AccessoryUppercaseStringType();
118+
}
119+
120+
if (count($valueTypes) > 0) {
121+
$returnValueType = TypeCombinator::intersect(new StringType(), ...$valueTypes);
122+
} else {
123+
$returnValueType = new StringType();
104124
}
105125

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

108-
return $encoding === null && !$this->phpVersion->strSplitReturnsEmptyArray()
128+
return $isInputNonEmptyString || ($encoding === null && !$this->phpVersion->strSplitReturnsEmptyArray())
109129
? TypeCombinator::intersect($returnType, new NonEmptyArrayType())
110130
: $returnType;
111131
}

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)