Skip to content

Commit 80b46e3

Browse files
authored
Support multi-byte string function variants
1 parent 173587f commit 80b46e3

File tree

6 files changed

+115
-11
lines changed

6 files changed

+115
-11
lines changed

src/Analyser/TypeSpecifier.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1016,7 +1016,11 @@ private function specifyTypesForConstantStringBinaryExpression(
10161016
$context->truthy()
10171017
&& $exprNode instanceof FuncCall
10181018
&& $exprNode->name instanceof Name
1019-
&& in_array(strtolower($exprNode->name->toString()), ['substr', 'strstr', 'stristr', 'strchr', 'strrchr', 'strtolower', 'strtoupper', 'mb_strtolower', 'mb_strtoupper', 'ucfirst', 'lcfirst', 'ucwords', 'mb_convert_case', 'mb_convert_kana'], true)
1019+
&& in_array(strtolower($exprNode->name->toString()), [
1020+
'substr', 'strstr', 'stristr', 'strchr', 'strrchr', 'strtolower', 'strtoupper',
1021+
'mb_substr', 'mb_strstr', 'mb_stristr', 'mb_strchr', 'mb_strrchr', 'mb_strtolower', 'mb_strtoupper',
1022+
'ucfirst', 'lcfirst', 'ucwords', 'mb_convert_case', 'mb_convert_kana',
1023+
], true)
10201024
&& isset($exprNode->getArgs()[0])
10211025
&& $constantType->getValue() !== ''
10221026
) {

src/Type/Php/StrContainingTypeSpecifyingExtension.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,11 @@ final class StrContainingTypeSpecifyingExtension implements FunctionTypeSpecifyi
3838
'stripos' => [0, 1],
3939
'strripos' => [0, 1],
4040
'strstr' => [0, 1],
41+
'mb_strpos' => [0, 1],
42+
'mb_strrpos' => [0, 1],
43+
'mb_stripos' => [0, 1],
44+
'mb_strripos' => [0, 1],
45+
'mb_strstr' => [0, 1],
4146
];
4247

4348
private TypeSpecifier $typeSpecifier;

src/Type/Php/SubstrDynamicReturnTypeExtension.php

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,17 @@
1717
use PHPStan\Type\Type;
1818
use PHPStan\Type\TypeCombinator;
1919
use function count;
20+
use function in_array;
2021
use function is_bool;
22+
use function mb_substr;
2123
use function substr;
2224

2325
class SubstrDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension
2426
{
2527

2628
public function isFunctionSupported(FunctionReflection $functionReflection): bool
2729
{
28-
return $functionReflection->getName() === 'substr';
30+
return in_array($functionReflection->getName(), ['substr', 'mb_substr'], true);
2931
}
3032

3133
public function getTypeFromFunctionCall(
@@ -62,16 +64,17 @@ public function getTypeFromFunctionCall(
6264
$results = [];
6365
foreach ($constantStrings as $constantString) {
6466
if ($length !== null) {
65-
$substr = substr(
66-
$constantString->getValue(),
67-
$offset->getValue(),
68-
$length->getValue(),
69-
);
67+
if ($functionReflection->getName() === 'mb_substr') {
68+
$substr = mb_substr($constantString->getValue(), $offset->getValue(), $length->getValue());
69+
} else {
70+
$substr = substr($constantString->getValue(), $offset->getValue(), $length->getValue());
71+
}
7072
} else {
71-
$substr = substr(
72-
$constantString->getValue(),
73-
$offset->getValue(),
74-
);
73+
if ($functionReflection->getName() === 'mb_substr') {
74+
$substr = mb_substr($constantString->getValue(), $offset->getValue());
75+
} else {
76+
$substr = substr($constantString->getValue(), $offset->getValue());
77+
}
7578
}
7679

7780
if (is_bool($substr)) {

tests/PHPStan/Analyser/data/non-empty-string-str-containing-fns.php

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,58 @@ public function variants(string $s) {
131131
assertType('non-falsy-string', $s);
132132
}
133133
assertType('string', $s);
134+
135+
if (mb_strpos($s, ':') !== false) {
136+
assertType('non-falsy-string', $s);
137+
}
138+
assertType('string', $s);
139+
if (mb_strpos($s, ':') === false) {
140+
assertType('string', $s);
141+
}
142+
assertType('string', $s);
143+
144+
if (mb_strpos($s, ':') === 5) {
145+
assertType('string', $s); // could be non-empty-string
146+
}
147+
assertType('string', $s);
148+
if (mb_strpos($s, ':') !== 5) {
149+
assertType('string', $s);
150+
}
151+
assertType('string', $s);
152+
153+
if (mb_strrpos($s, ':') !== false) {
154+
assertType('non-falsy-string', $s);
155+
}
156+
assertType('string', $s);
157+
158+
if (mb_stripos($s, ':') !== false) {
159+
assertType('non-falsy-string', $s);
160+
}
161+
assertType('string', $s);
162+
163+
if (mb_strripos($s, ':') !== false) {
164+
assertType('non-falsy-string', $s);
165+
}
166+
assertType('string', $s);
167+
168+
if (mb_strstr($s, ':') === 'hallo') {
169+
assertType('non-falsy-string', $s);
170+
}
171+
assertType('string', $s);
172+
if (mb_strstr($s, ':', true) === 'hallo') {
173+
assertType('non-falsy-string', $s);
174+
}
175+
assertType('string', $s);
176+
if (mb_strstr($s, ':', true) !== false) {
177+
assertType('non-falsy-string', $s);
178+
}
179+
assertType('string', $s);
180+
if (mb_strstr($s, ':', true) === false) {
181+
assertType('string', $s);
182+
} else {
183+
assertType('non-falsy-string', $s);
184+
}
185+
assertType('string', $s);
134186
}
135187

136188
}

tests/PHPStan/Analyser/data/non-empty-string-strstr-specifying.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@ public function nonEmptyStrstr(string $s, string $needle, bool $before_needle):
3939
assertType('non-falsy-string', $s);
4040
}
4141
assertType('string', $s);
42+
if (mb_strstr($s, $needle, $before_needle) === 'hallo') {
43+
assertType('non-falsy-string', $s);
44+
}
45+
assertType('string', $s);
4246

4347
if (strstr($s, $needle, $before_needle) !== 'hallo') {
4448
assertType('string', $s);
@@ -81,6 +85,10 @@ public function nonEmptyStristr(string $s, string $needle, bool $before_needle):
8185
if ('hallo' === stristr($s, 'abc')) {
8286
assertType('non-falsy-string', $s);
8387
}
88+
assertType('string', $s);
89+
if ('hallo' === mb_stristr($s, 'abc')) {
90+
assertType('non-falsy-string', $s);
91+
}
8492

8593
if (stristr($s, $needle, $before_needle) == '') {
8694
assertType('string', $s);
@@ -107,6 +115,10 @@ public function nonEmptyStrchr(string $s, string $needle, bool $before_needle):
107115
if ('hallo' === strchr($s, 'abc')) {
108116
assertType('non-falsy-string', $s);
109117
}
118+
assertType('string', $s);
119+
if ('hallo' === mb_strchr($s, 'abc')) {
120+
assertType('non-falsy-string', $s);
121+
}
110122

111123
if (strchr($s, $needle, $before_needle) == '') {
112124
assertType('string', $s);

tests/PHPStan/Analyser/data/non-empty-string-substr.php

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,32 @@ public function doSubstr(string $s, $nonEmpty, $positiveInt, $postiveRange, $neg
3131
assertType('non-empty-string', substr($nonEmpty, 0, $positiveInt));
3232
}
3333

34+
/**
35+
* @param non-empty-string $nonEmpty
36+
* @param positive-int $positiveInt
37+
* @param 1|2|3 $postiveRange
38+
* @param -1|-2|-3 $negativeRange
39+
*/
40+
public function doMbSubstr(string $s, $nonEmpty, $positiveInt, $postiveRange, $negativeRange): void
41+
{
42+
assertType('string', mb_substr($s, 5));
43+
44+
assertType('string', mb_substr($s, -5));
45+
assertType('non-empty-string', mb_substr($nonEmpty, -5));
46+
assertType('non-empty-string', mb_substr($nonEmpty, $negativeRange));
47+
48+
assertType('string', mb_substr($s, 0, 5));
49+
assertType('non-empty-string', mb_substr($nonEmpty, 0, 5));
50+
assertType('non-empty-string', mb_substr($nonEmpty, 0, $postiveRange));
51+
52+
assertType('string', mb_substr($nonEmpty, 0, -5));
53+
54+
assertType('string', mb_substr($s, 0, $positiveInt));
55+
assertType('non-empty-string', mb_substr($nonEmpty, 0, $positiveInt));
56+
57+
assertType('non-falsy-string', mb_substr("déjà_vu", 0, $positiveInt));
58+
assertType("'déjà_vu'", mb_substr("déjà_vu", 0));
59+
assertType("'déj'", mb_substr("déjà_vu", 0, 3));
60+
}
61+
3462
}

0 commit comments

Comments
 (0)