Skip to content

Commit ff622ae

Browse files
Rework extension
1 parent a74b5f9 commit ff622ae

File tree

5 files changed

+80
-35
lines changed

5 files changed

+80
-35
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: 54 additions & 31 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,52 +56,60 @@ 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+
// When none of the length are positive integers the result is an error/false based on PHP version.
64+
if ($splitLengthType->accepts(IntegerRangeType::fromInterval(1, null), $scope->isDeclareStrictTypes())->no()) {
65+
return $this->phpVersion->throwsValueErrorForInternalFunctions() ? new NeverType() : new ConstantBooleanType(false);
6566
}
6667

67-
$encoding = null;
6868
if ($functionReflection->getName() === 'mb_str_split') {
6969
if (count($functionCall->getArgs()) >= 3) {
7070
$strings = $scope->getType($functionCall->getArgs()[2]->value)->getConstantStrings();
71-
$values = array_unique(array_map(static fn (ConstantStringType $encoding): string => $encoding->getValue(), $strings));
71+
$encodings = array_unique(array_map(static fn (ConstantStringType $encoding): string => $encoding->getValue(), $strings));
7272

73-
if (count($values) !== 1) {
74-
return null;
73+
$encodingIsSupported = false;
74+
foreach ($encodings as $encoding) {
75+
if ($this->isSupportedEncoding($encoding)) {
76+
$encodingIsSupported = true;
77+
break;
78+
}
7579
}
7680

77-
$encoding = $values[0];
78-
if (!$this->isSupportedEncoding($encoding)) {
79-
return new ConstantBooleanType(false);
81+
if (!$encodingIsSupported) {
82+
return $this->phpVersion->throwsValueErrorForInternalFunctions() ? new NeverType() : new ConstantBooleanType(false);
8083
}
8184
} else {
82-
$encoding = mb_internal_encoding();
85+
$encodings = [mb_internal_encoding()];
8386
}
8487
}
8588

8689
$stringType = $scope->getType($functionCall->getArgs()[0]->value);
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();
90+
// To simplify we only supports one encoding (or none for str_split)
91+
if (!isset($encodings) || count($encodings) === 1) {
92+
$constantSplitLengths = $splitLengthType->getConstantScalarTypes();
93+
// To simplify we only supports one split length
94+
if (count($constantSplitLengths) === 1) {
95+
$splitLength = (int) $constantSplitLengths[0]->getValue();
96+
97+
$constantStrings = $stringType->getConstantStrings();
98+
if (count($constantStrings) > 0) {
99+
$results = [];
100+
foreach ($constantStrings as $constantString) {
101+
$items = !isset($encodings)
102+
? str_split($constantString->getValue(), $splitLength)
103+
: @mb_str_split($constantString->getValue(), $splitLength, $encodings[0]);
104+
if ($items === false) {
105+
throw new ShouldNotHappenException();
106+
}
107+
108+
$results[] = self::createConstantArrayFrom($items, $scope);
97109
}
98110

99-
$results[] = self::createConstantArrayFrom($items, $scope);
111+
return TypeCombinator::union(...$results);
100112
}
101-
102-
return TypeCombinator::union(...$results);
103113
}
104114
}
105115

@@ -118,10 +128,23 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection,
118128
$returnValueType = TypeCombinator::intersect(new StringType(), ...$valueTypes);
119129

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

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

127150
/**

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)