Skip to content

Commit b4969b9

Browse files
Rework extension
1 parent a74b5f9 commit b4969b9

File tree

5 files changed

+61
-22
lines changed

5 files changed

+61
-22
lines changed

build/baseline-8.0.neon

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ parameters:
2626
path: ../src/Type/Php/StrSplitFunctionReturnTypeExtension.php
2727

2828
-
29-
message: "#^Strict comparison using \\=\\=\\= between list<string> and false will always evaluate to false\\.$#"
29+
message: "#^Strict comparison using \\=\\=\\= between non\\-empty\\-list<string> and false will always evaluate to false\\.$#"
3030
count: 1
3131
path: ../src/Type/Php/StrSplitFunctionReturnTypeExtension.php
3232

src/Type/Php/StrSplitFunctionReturnTypeExtension.php

Lines changed: 35 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@
1919
use PHPStan\Type\Constant\ConstantIntegerType;
2020
use PHPStan\Type\Constant\ConstantStringType;
2121
use PHPStan\Type\DynamicFunctionReturnTypeExtension;
22+
use PHPStan\Type\IntegerRangeType;
2223
use PHPStan\Type\IntegerType;
24+
use PHPStan\Type\NeverType;
2325
use PHPStan\Type\StringType;
2426
use PHPStan\Type\Type;
2527
use PHPStan\Type\TypeCombinator;
@@ -54,14 +56,15 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection,
5456

5557
if (count($functionCall->getArgs()) >= 2) {
5658
$splitLengthType = $scope->getType($functionCall->getArgs()[1]->value);
57-
if ($splitLengthType instanceof ConstantIntegerType) {
58-
$splitLength = $splitLengthType->getValue();
59-
if ($splitLength < 1) {
60-
return new ConstantBooleanType(false);
61-
}
62-
}
6359
} else {
64-
$splitLength = 1;
60+
$splitLengthType = new ConstantIntegerType(1);
61+
}
62+
63+
if ($splitLengthType instanceof ConstantIntegerType) {
64+
$splitLength = $splitLengthType->getValue();
65+
if ($splitLength < 1) {
66+
return new ConstantBooleanType(false);
67+
}
6568
}
6669

6770
$encoding = null;
@@ -70,21 +73,22 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection,
7073
$strings = $scope->getType($functionCall->getArgs()[2]->value)->getConstantStrings();
7174
$values = array_unique(array_map(static fn (ConstantStringType $encoding): string => $encoding->getValue(), $strings));
7275

73-
if (count($values) !== 1) {
74-
return null;
75-
}
76-
77-
$encoding = $values[0];
78-
if (!$this->isSupportedEncoding($encoding)) {
79-
return new ConstantBooleanType(false);
76+
if (count($values) === 1) {
77+
$encoding = $values[0];
78+
if (!$this->isSupportedEncoding($encoding)) {
79+
return $this->phpVersion->throwsValueErrorForInternalFunctions() ? new NeverType() : new ConstantBooleanType(false);
80+
}
8081
}
8182
} else {
8283
$encoding = mb_internal_encoding();
8384
}
8485
}
8586

8687
$stringType = $scope->getType($functionCall->getArgs()[0]->value);
87-
if (isset($splitLength)) {
88+
if (
89+
isset($splitLength)
90+
&& ($functionReflection->getName() === 'str_split' || $encoding !== null)
91+
) {
8892
$constantStrings = $stringType->getConstantStrings();
8993
if (count($constantStrings) > 0) {
9094
$results = [];
@@ -118,10 +122,23 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection,
118122
$returnValueType = TypeCombinator::intersect(new StringType(), ...$valueTypes);
119123

120124
$returnType = AccessoryArrayListType::intersectWith(TypeCombinator::intersect(new ArrayType(new IntegerType(), $returnValueType)));
125+
if (
126+
// Non-empty-string will return an array with at least an element
127+
$isInputNonEmptyString
128+
// str_split('', 1) returns [''] on old PHP version and [] on new ones
129+
|| ($functionReflection->getName() === 'str_split' && !$this->phpVersion->strSplitReturnsEmptyArray())
130+
) {
131+
$returnType = TypeCombinator::intersect($returnType, new NonEmptyArrayType());
132+
}
133+
if (
134+
// Length parameter accepts int<1, max> or throws a ValueError/return false based on PHP Version.
135+
!$this->phpVersion->throwsValueErrorForInternalFunctions()
136+
&& !IntegerRangeType::fromInterval(1, null)->isSuperTypeOf($splitLengthType)->yes()
137+
) {
138+
$returnType = TypeCombinator::union($returnType, new ConstantBooleanType(false));
139+
}
121140

122-
return $isInputNonEmptyString || ($encoding === null && !$this->phpVersion->strSplitReturnsEmptyArray())
123-
? TypeCombinator::intersect($returnType, new NonEmptyArrayType())
124-
: $returnType;
141+
return $returnType;
125142
}
126143

127144
/**

tests/PHPStan/Analyser/data/str-split-php74.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,13 @@ public function legacyTest() {
2828
assertType('false', $strSplitConstantStringWithFailureSplitLength);
2929

3030
$strSplitConstantStringWithInvalidSplitLengthType = str_split('abcdef', []);
31-
assertType('non-empty-list<lowercase-string&non-empty-string>|false', $strSplitConstantStringWithInvalidSplitLengthType);
31+
assertType('(non-empty-list<lowercase-string&non-empty-string>)|false', $strSplitConstantStringWithInvalidSplitLengthType);
3232

3333
$strSplitConstantStringWithVariableStringAndConstantSplitLength = str_split(doFoo() ? 'abcdef' : 'ghijkl', 1);
3434
assertType("array{'a', 'b', 'c', 'd', 'e', 'f'}|array{'g', 'h', 'i', 'j', 'k', 'l'}", $strSplitConstantStringWithVariableStringAndConstantSplitLength);
3535

3636
$strSplitConstantStringWithVariableStringAndVariableSplitLength = str_split(doFoo() ? 'abcdef' : 'ghijkl', doFoo() ? 1 : 2);
37-
assertType('non-empty-list<lowercase-string&non-empty-string>|false', $strSplitConstantStringWithVariableStringAndVariableSplitLength);
37+
assertType('(non-empty-list<lowercase-string&non-empty-string>)|false', $strSplitConstantStringWithVariableStringAndVariableSplitLength);
3838

3939
}
4040
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php // lint >= 8.2
2+
3+
declare(strict_types = 1);
4+
5+
namespace Bug7580TypesPHP82;
6+
7+
use function PHPStan\Testing\assertType;
8+
9+
assertType('array{}', mb_str_split('', 1));
10+
11+
assertType('array{\'x\'}', mb_str_split('x', 1));
12+
13+
$v = (string) (mt_rand() === 0 ? '' : 'x');
14+
assertType('\'\'|\'x\'', $v);
15+
assertType('array{}|array{\'x\'}', mb_str_split($v, 1));
16+
17+
function x(): string { throw new \Exception(); };
18+
$v = x();
19+
assertType('string', $v);
20+
assertType('list<non-empty-string>', mb_str_split($v, 1));

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
<?php declare(strict_types = 1);
1+
<?php // lint < 8.2
2+
3+
declare(strict_types = 1);
24

35
namespace Bug7580Types;
46

0 commit comments

Comments
 (0)