19
19
use PHPStan \Type \Constant \ConstantIntegerType ;
20
20
use PHPStan \Type \Constant \ConstantStringType ;
21
21
use PHPStan \Type \DynamicFunctionReturnTypeExtension ;
22
+ use PHPStan \Type \IntegerRangeType ;
22
23
use PHPStan \Type \IntegerType ;
24
+ use PHPStan \Type \NeverType ;
23
25
use PHPStan \Type \StringType ;
24
26
use PHPStan \Type \Type ;
25
27
use PHPStan \Type \TypeCombinator ;
@@ -54,14 +56,15 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection,
54
56
55
57
if (count ($ functionCall ->getArgs ()) >= 2 ) {
56
58
$ 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
- }
63
59
} 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
+ }
65
68
}
66
69
67
70
$ encoding = null ;
@@ -70,21 +73,22 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection,
70
73
$ strings = $ scope ->getType ($ functionCall ->getArgs ()[2 ]->value )->getConstantStrings ();
71
74
$ values = array_unique (array_map (static fn (ConstantStringType $ encoding ): string => $ encoding ->getValue (), $ strings ));
72
75
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
+ }
80
81
}
81
82
} else {
82
83
$ encoding = mb_internal_encoding ();
83
84
}
84
85
}
85
86
86
87
$ stringType = $ scope ->getType ($ functionCall ->getArgs ()[0 ]->value );
87
- if (isset ($ splitLength )) {
88
+ if (
89
+ isset ($ splitLength )
90
+ && ($ functionReflection ->getName () === 'str_split ' || $ encoding !== null )
91
+ ) {
88
92
$ constantStrings = $ stringType ->getConstantStrings ();
89
93
if (count ($ constantStrings ) > 0 ) {
90
94
$ results = [];
@@ -118,10 +122,23 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection,
118
122
$ returnValueType = TypeCombinator::intersect (new StringType (), ...$ valueTypes );
119
123
120
124
$ 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
+ }
121
140
122
- return $ isInputNonEmptyString || ($ encoding === null && !$ this ->phpVersion ->strSplitReturnsEmptyArray ())
123
- ? TypeCombinator::intersect ($ returnType , new NonEmptyArrayType ())
124
- : $ returnType ;
141
+ return $ returnType ;
125
142
}
126
143
127
144
/**
0 commit comments