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,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,26 +73,25 @@ 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-
7776 $ encoding = $ values [0 ];
7877 if (!$ this ->isSupportedEncoding ($ encoding )) {
79- return new ConstantBooleanType (false );
78+ return $ this -> phpVersion -> throwsValueErrorForInternalFunctions () ? new NeverType () : new ConstantBooleanType (false );
8079 }
8180 } else {
8281 $ encoding = mb_internal_encoding ();
8382 }
8483 }
8584
8685 $ stringType = $ scope ->getType ($ functionCall ->getArgs ()[0 ]->value );
87- if (isset ($ splitLength )) {
86+ if (
87+ isset ($ splitLength )
88+ && ($ functionReflection ->getName () === 'str_split ' || null !== $ encoding )
89+ ) {
8890 $ constantStrings = $ stringType ->getConstantStrings ();
8991 if (count ($ constantStrings ) > 0 ) {
9092 $ results = [];
9193 foreach ($ constantStrings as $ constantString ) {
92- $ items = $ encoding === null
94+ $ items = null === $ encoding
9395 ? str_split ($ constantString ->getValue (), $ splitLength )
9496 : @mb_str_split ($ constantString ->getValue (), $ splitLength , $ encoding );
9597 if ($ items === false ) {
@@ -118,10 +120,23 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection,
118120 $ returnValueType = TypeCombinator::intersect (new StringType (), ...$ valueTypes );
119121
120122 $ returnType = AccessoryArrayListType::intersectWith (TypeCombinator::intersect (new ArrayType (new IntegerType (), $ returnValueType )));
123+ if (
124+ // Non-empty-string will return an array with at least an element
125+ $ isInputNonEmptyString
126+ // str_split('', 1) returns [''] on old PHP version and [] on new ones
127+ || ($ functionReflection ->getName () === 'str_split ' && !$ this ->phpVersion ->strSplitReturnsEmptyArray ())
128+ ) {
129+ $ returnType = TypeCombinator::intersect ($ returnType , new NonEmptyArrayType ());
130+ }
131+ if (
132+ // Length parameter accepts int<1, max> or throws a ValueError/return false based on PHP Version.
133+ !$ this ->phpVersion ->throwsValueErrorForInternalFunctions ()
134+ && !IntegerRangeType::fromInterval (1 , null )->isSuperTypeOf ($ splitLengthType )->yes ()
135+ ) {
136+ $ returnType = TypeCombinator::union ($ returnType , new ConstantBooleanType (false ));
137+ }
121138
122- return $ isInputNonEmptyString || ($ encoding === null && !$ this ->phpVersion ->strSplitReturnsEmptyArray ())
123- ? TypeCombinator::intersect ($ returnType , new NonEmptyArrayType ())
124- : $ returnType ;
139+ return $ returnType ;
125140 }
126141
127142 /**
0 commit comments