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,21 +73,22 @@ 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-
77- $ encoding = $ values [0 ];
78- if (!$ this ->isSupportedEncoding ($ encoding )) {
79- return new ConstantBooleanType (false );
76+ if (count ($ values ) === 1 ) {
77+ $ encoding = $ values [0 ];
78+ if (!$ this ->isSupportedEncoding ($ encoding )) {
79+ return $ this ->phpVersion ->throwsValueErrorForInternalFunctions () ? new NeverType () : new ConstantBooleanType (false );
80+ }
8081 }
8182 } else {
8283 $ encoding = mb_internal_encoding ();
8384 }
8485 }
8586
8687 $ stringType = $ scope ->getType ($ functionCall ->getArgs ()[0 ]->value );
87- if (isset ($ splitLength )) {
88+ if (
89+ isset ($ splitLength )
90+ && ($ functionReflection ->getName () === 'str_split ' || $ encoding !== null )
91+ ) {
8892 $ constantStrings = $ stringType ->getConstantStrings ();
8993 if (count ($ constantStrings ) > 0 ) {
9094 $ results = [];
@@ -118,10 +122,23 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection,
118122 $ returnValueType = TypeCombinator::intersect (new StringType (), ...$ valueTypes );
119123
120124 $ returnType = AccessoryArrayListType::intersectWith (TypeCombinator::intersect (new ArrayType (new IntegerType (), $ returnValueType )));
125+ if (
126+ // Non-empty-string will return an array with at least an element
127+ $ isInputNonEmptyString
128+ // str_split('', 1) returns [''] on old PHP version and [] on new ones
129+ || ($ functionReflection ->getName () === 'str_split ' && !$ this ->phpVersion ->strSplitReturnsEmptyArray ())
130+ ) {
131+ $ returnType = TypeCombinator::intersect ($ returnType , new NonEmptyArrayType ());
132+ }
133+ if (
134+ // Length parameter accepts int<1, max> or throws a ValueError/return false based on PHP Version.
135+ !$ this ->phpVersion ->throwsValueErrorForInternalFunctions ()
136+ && !IntegerRangeType::fromInterval (1 , null )->isSuperTypeOf ($ splitLengthType )->yes ()
137+ ) {
138+ $ returnType = TypeCombinator::union ($ returnType , new ConstantBooleanType (false ));
139+ }
121140
122- return $ isInputNonEmptyString || ($ encoding === null && !$ this ->phpVersion ->strSplitReturnsEmptyArray ())
123- ? TypeCombinator::intersect ($ returnType , new NonEmptyArrayType ())
124- : $ returnType ;
141+ return $ returnType ;
125142 }
126143
127144 /**
0 commit comments