1919use PHPStan \Type \Constant \ConstantIntegerType ;
2020use PHPStan \Type \Constant \ConstantStringType ;
2121use PHPStan \Type \DynamicFunctionReturnTypeExtension ;
22+ use PHPStan \Type \IntegerRangeType ;
2223use PHPStan \Type \IntegerType ;
24+ use PHPStan \Type \NeverType ;
2325use PHPStan \Type \StringType ;
2426use PHPStan \Type \Type ;
2527use 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 /**
0 commit comments