1515use PHPStan \Type \IntegerType ;
1616use PHPStan \Type \StringType ;
1717use PHPStan \Type \Type ;
18+ use PHPStan \Type \TypeUtils ;
1819
1920final class StrSplitFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension
2021{
2122
23+ /** @var string[] */
24+ private array $ supportedEncodings ;
25+
26+ public function __construct ()
27+ {
28+ $ supportedEncodings = [];
29+ if (function_exists ('mb_list_encodings ' )) {
30+ foreach (mb_list_encodings () as $ encoding ) {
31+ $ aliases = mb_encoding_aliases ($ encoding );
32+ if ($ aliases === false ) {
33+ throw new \PHPStan \ShouldNotHappenException ();
34+ }
35+ $ supportedEncodings = array_merge ($ supportedEncodings , $ aliases , [$ encoding ]);
36+ }
37+ }
38+ $ this ->supportedEncodings = array_map ('strtoupper ' , $ supportedEncodings );
39+ }
40+
2241 public function isFunctionSupported (FunctionReflection $ functionReflection ): bool
2342 {
2443 return in_array ($ functionReflection ->getName (), ['str_split ' , 'mb_str_split ' ], true );
@@ -32,32 +51,63 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection,
3251 return $ defaultReturnType ;
3352 }
3453
35- $ splitLength = 1 ;
3654 if (count ($ functionCall ->args ) >= 2 ) {
3755 $ splitLengthType = $ scope ->getType ($ functionCall ->args [1 ]->value );
38- if (!$ splitLengthType instanceof ConstantIntegerType) {
39- return $ defaultReturnType ;
56+ if ($ splitLengthType instanceof ConstantIntegerType) {
57+ $ splitLength = $ splitLengthType ->getValue ();
58+ if ($ splitLength < 1 ) {
59+ return new ConstantBooleanType (false );
60+ }
4061 }
41- $ splitLength = $ splitLengthType ->getValue ();
42- if ($ splitLength < 1 ) {
43- return new ConstantBooleanType (false );
62+ } else {
63+ $ splitLength = 1 ;
64+ }
65+
66+ if ($ functionReflection ->getName () === 'mb_str_split ' ) {
67+ if (count ($ functionCall ->args ) >= 3 ) {
68+ $ strings = TypeUtils::getConstantStrings ($ scope ->getType ($ functionCall ->args [2 ]->value ));
69+ $ values = array_unique (array_map (static function (ConstantStringType $ encoding ): string {
70+ return $ encoding ->getValue ();
71+ }, $ strings ));
72+
73+ if (count ($ values ) !== 1 ) {
74+ return $ defaultReturnType ;
75+ }
76+
77+ $ encoding = $ values [0 ];
78+ if (!$ this ->isSupportedEncoding ($ encoding )) {
79+ return new ConstantBooleanType (false );
80+ }
81+ } else {
82+ $ encoding = mb_internal_encoding ();
4483 }
4584 }
4685
86+ if (!isset ($ splitLength )) {
87+ return $ defaultReturnType ;
88+ }
89+
4790 $ stringType = $ scope ->getType ($ functionCall ->args [0 ]->value );
4891 if (!$ stringType instanceof ConstantStringType) {
4992 return new ArrayType (new IntegerType (), new StringType ());
5093 }
5194 $ stringValue = $ stringType ->getValue ();
5295
53- $ items = str_split ($ stringValue , $ splitLength );
96+ $ items = isset ($ encoding )
97+ ? mb_str_split ($ stringValue , $ splitLength , $ encoding )
98+ : str_split ($ stringValue , $ splitLength );
5499 if (!is_array ($ items )) {
55100 throw new \PHPStan \ShouldNotHappenException ();
56101 }
57102
58103 return self ::createConstantArrayFrom ($ items , $ scope );
59104 }
60105
106+ private function isSupportedEncoding (string $ encoding ): bool
107+ {
108+ return in_array (strtoupper ($ encoding ), $ this ->supportedEncodings , true );
109+ }
110+
61111 /**
62112 * @param string[] $constantArray
63113 * @param \PHPStan\Analyser\Scope $scope
0 commit comments