66use PHPStan \Analyser \Scope ;
77use PHPStan \DependencyInjection \AutowiredService ;
88use PHPStan \Reflection \FunctionReflection ;
9+ use PHPStan \TrinaryLogic ;
910use PHPStan \Type \Accessory \AccessoryArrayListType ;
11+ use PHPStan \Type \Accessory \AccessoryLiteralStringType ;
12+ use PHPStan \Type \Accessory \AccessoryNonEmptyStringType ;
1013use PHPStan \Type \Accessory \NonEmptyArrayType ;
1114use PHPStan \Type \ArrayType ;
1215use PHPStan \Type \BenevolentUnionType ;
1316use PHPStan \Type \Constant \ConstantArrayTypeBuilder ;
17+ use PHPStan \Type \Constant \ConstantBooleanType ;
1418use PHPStan \Type \Constant \ConstantFloatType ;
1519use PHPStan \Type \Constant \ConstantIntegerType ;
1620use PHPStan \Type \Constant \ConstantStringType ;
2024use PHPStan \Type \IntegerRangeType ;
2125use PHPStan \Type \IntegerType ;
2226use PHPStan \Type \IntersectionType ;
27+ use PHPStan \Type \NeverType ;
2328use PHPStan \Type \StringType ;
2429use PHPStan \Type \Type ;
2530use PHPStan \Type \TypeCombinator ;
2631use PHPStan \Type \UnionType ;
27- use ValueError ;
2832use function abs ;
2933use function count ;
3034use function floor ;
3135use function intdiv ;
32- use function is_array ;
3336use function is_finite ;
37+ use function is_float ;
3438use function is_numeric ;
3539use function is_string ;
3640use function max ;
3741use function min ;
38- use function range ;
3942use const PHP_INT_MAX ;
4043
4144#[AutowiredService]
@@ -60,46 +63,77 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection,
6063 $ endType = $ scope ->getType ($ args [1 ]->value );
6164 $ stepType = count ($ args ) >= 3 ? $ scope ->getType ($ args [2 ]->value ) : new ConstantIntegerType (1 );
6265
66+ $ phpVersions = $ scope ->getPhpVersion ();
67+ $ hasStricterRange = $ phpVersions ->hasStricterRangeFunction ();
68+ $ throwsValueError = $ phpVersions ->throwsValueErrorForInternalFunctions ();
69+
6370 $ constantReturnTypes = [];
71+ $ constantCombinations = 0 ;
72+ $ throwingCombinations = 0 ;
73+ $ hasSkippedCombination = false ;
74+ $ hasUnknownCombination = false ;
6475
6576 $ startConstants = $ startType ->getConstantScalarTypes ();
6677 foreach ($ startConstants as $ startConstant ) {
6778 if (!$ startConstant instanceof ConstantIntegerType && !$ startConstant instanceof ConstantFloatType && !$ startConstant instanceof ConstantStringType) {
79+ $ hasSkippedCombination = true ;
6880 continue ;
6981 }
7082
7183 $ endConstants = $ endType ->getConstantScalarTypes ();
7284 foreach ($ endConstants as $ endConstant ) {
7385 if (!$ endConstant instanceof ConstantIntegerType && !$ endConstant instanceof ConstantFloatType && !$ endConstant instanceof ConstantStringType) {
86+ $ hasSkippedCombination = true ;
7487 continue ;
7588 }
7689
7790 $ stepConstants = $ stepType ->getConstantScalarTypes ();
7891 foreach ($ stepConstants as $ stepConstant ) {
7992 if (!$ stepConstant instanceof ConstantIntegerType && !$ stepConstant instanceof ConstantFloatType) {
93+ $ hasSkippedCombination = true ;
8094 continue ;
8195 }
8296
97+ $ constantCombinations ++;
98+
8399 // range() would allocate every item before the length could be checked
84100 $ rangeLength = self ::getRangeLength ($ startConstant ->getValue (), $ endConstant ->getValue (), $ stepConstant ->getValue ());
85101 if ($ rangeLength !== null && $ rangeLength > ConstantArrayTypeBuilder::ARRAY_COUNT_LIMIT ) {
86- return self ::getLongRangeType ($ startConstant , $ endConstant , $ stepConstant , $ stepType );
87- }
88-
89- try {
90- $ rangeValues = @range ($ startConstant ->getValue (), $ endConstant ->getValue (), $ stepConstant ->getValue ());
91- } catch (ValueError ) {
102+ // without calling range() nothing rejects a negative step on an increasing range, which PHP 8.3 does
103+ if (RangeFunctionArgumentsHelper ::isNegativeStepOnIncreasingRange ($ startConstant ->getValue (), $ endConstant ->getValue (), $ stepConstant ->getValue ())) {
104+ if ($ hasStricterRange ->yes ()) {
105+ $ throwingCombinations ++;
106+ continue ;
107+ }
108+ if ($ hasStricterRange ->maybe ()) {
109+ $ hasUnknownCombination = true ;
110+ continue ;
111+ }
112+ }
113+
114+ $ constantReturnTypes [] = self ::getLongRangeType ($ hasStricterRange , $ startConstant , $ endConstant , $ stepConstant , $ stepType , null );
92115 continue ;
93116 }
94117
95- // @phpstan-ignore function.alreadyNarrowedType
96- if (!is_array ($ rangeValues )) {
118+ $ rangeValues = RangeFunctionArgumentsHelper ::callRange ($ startConstant ->getValue (), $ endConstant ->getValue (), $ stepConstant ->getValue ());
119+ if ($ rangeValues === false ) {
120+ $ fails = RangeFunctionArgumentsHelper ::rejects ($ hasStricterRange , $ startConstant ->getValue (), $ endConstant ->getValue (), $ stepConstant ->getValue (), true );
121+ if (!$ fails ->yes ()) {
122+ // the analysed PHP version might accept the step, but only the runtime's verdict is known
123+ $ hasUnknownCombination = true ;
124+ } elseif ($ throwsValueError ->yes ()) {
125+ $ throwingCombinations ++;
126+ } else {
127+ $ constantReturnTypes [] = new ConstantBooleanType (false );
128+ }
97129 continue ;
98130 }
99131
100132 if (count ($ rangeValues ) > self ::RANGE_LENGTH_THRESHOLD ) {
101- return self ::getLongRangeType ($ startConstant , $ endConstant , $ stepConstant , $ stepType );
133+ $ constantReturnTypes [] = self ::getLongRangeType ($ hasStricterRange , $ startConstant , $ endConstant , $ stepConstant , $ stepType , $ rangeValues );
134+ continue ;
102135 }
136+
103137 $ arrayBuilder = ConstantArrayTypeBuilder ::createEmpty ();
104138 foreach ($ rangeValues as $ value ) {
105139 $ arrayBuilder ->setOffsetValueType (null , $ scope ->getTypeFromValue ($ value ));
@@ -110,10 +144,29 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection,
110144 }
111145 }
112146
113- if (count ($ constantReturnTypes ) > 0 ) {
114- return TypeCombinator ::union (...$ constantReturnTypes );
147+ if (!$ hasUnknownCombination && !$ hasSkippedCombination ) {
148+ if (count ($ constantReturnTypes ) > 0 ) {
149+ return TypeCombinator ::union (...$ constantReturnTypes );
150+ }
151+
152+ // nothing is returned when every combination of the constant arguments throws
153+ if (
154+ $ constantCombinations > 0
155+ && $ throwingCombinations === $ constantCombinations
156+ && $ startType ->isConstantScalarValue ()->yes ()
157+ && $ endType ->isConstantScalarValue ()->yes ()
158+ && $ stepType ->isConstantScalarValue ()->yes ()
159+ ) {
160+ return new NeverType ();
161+ }
115162 }
116163
164+ // the general type covers the combinations that could not be decided, the rest keep their own types
165+ return TypeCombinator ::union (self ::getGeneralType ($ startType , $ endType , $ stepType ), ...$ constantReturnTypes );
166+ }
167+
168+ private static function getGeneralType (Type $ startType , Type $ endType , Type $ stepType ): Type
169+ {
117170 $ argType = TypeCombinator ::union ($ startType , $ endType );
118171 $ isInteger = $ argType ->isInteger ()->yes ();
119172 $ isStepInteger = $ stepType ->isInteger ()->yes ();
@@ -176,39 +229,109 @@ private static function getRangeLength(int|float|string $start, int|float|string
176229 return floor ($ length ) + 1 ;
177230 }
178231
232+ /**
233+ * @param non-empty-list<int|float|string>|null $rangeValues null when range() was not called
234+ */
179235 private static function getLongRangeType (
236+ TrinaryLogic $ hasStricterRange ,
180237 ConstantIntegerType |ConstantFloatType |ConstantStringType $ startConstant ,
181238 ConstantIntegerType |ConstantFloatType |ConstantStringType $ endConstant ,
182239 ConstantIntegerType |ConstantFloatType $ stepConstant ,
183240 Type $ stepType ,
241+ ?array $ rangeValues ,
184242 ): Type
185243 {
244+ $ type = self ::getLongRangeItemsType ($ startConstant , $ endConstant , $ stepConstant , $ stepType , $ rangeValues );
186245 if (
187- $ startConstant instanceof ConstantIntegerType
188- && $ endConstant instanceof ConstantIntegerType
189- && $ stepConstant instanceof ConstantIntegerType
246+ $ hasStricterRange ->yes ()
247+ || (
248+ !$ startConstant instanceof ConstantFloatType
249+ && !$ endConstant instanceof ConstantFloatType
250+ && !$ stepConstant instanceof ConstantFloatType
251+ )
190252 ) {
191- $ low = min ($ startConstant ->getValue (), $ endConstant ->getValue ());
192- $ high = max ($ startConstant ->getValue (), $ endConstant ->getValue ());
253+ return $ type ;
254+ }
255+
256+ // before PHP 8.3 a float argument produced floats even when none of the arguments had a fractional
257+ // part, and even for two strings, of which only a numeric one kept its value
258+ $ floatListType = self ::getNonEmptyListOfType (new FloatType ());
259+ if ($ hasStricterRange ->no ()) {
260+ return $ floatListType ;
261+ }
262+
263+ return TypeCombinator ::union ($ type , $ floatListType );
264+ }
193265
266+ /**
267+ * The type of the items the runtime returned. Without them it follows PHP 8.3 for numeric
268+ * boundaries and generalizes the arguments for a string one.
269+ *
270+ * @param non-empty-list<int|float|string>|null $rangeValues
271+ */
272+ private static function getLongRangeItemsType (
273+ ConstantIntegerType |ConstantFloatType |ConstantStringType $ startConstant ,
274+ ConstantIntegerType |ConstantFloatType |ConstantStringType $ endConstant ,
275+ ConstantIntegerType |ConstantFloatType $ stepConstant ,
276+ Type $ stepType ,
277+ ?array $ rangeValues ,
278+ ): Type
279+ {
280+ $ floatListType = self ::getNonEmptyListOfType (new FloatType ());
281+
282+ if ($ rangeValues !== null ) {
283+ // range() only ever returns values of a single type
284+ $ firstValue = $ rangeValues [0 ];
285+ $ lastValue = $ rangeValues [count ($ rangeValues ) - 1 ];
286+
287+ if (is_string ($ firstValue ) || is_string ($ lastValue )) {
288+ // a character range consists of single bytes taken from constant boundaries
289+ return self ::getNonEmptyListOfType (TypeCombinator ::intersect (
290+ new StringType (),
291+ new AccessoryNonEmptyStringType (),
292+ new AccessoryLiteralStringType (),
293+ ));
294+ }
295+
296+ if (is_float ($ firstValue ) || is_float ($ lastValue )) {
297+ return $ floatListType ;
298+ }
299+
300+ $ bounds = $ startConstant instanceof ConstantIntegerType && $ endConstant instanceof ConstantIntegerType
301+ ? [$ startConstant ->getValue (), $ endConstant ->getValue ()]
302+ : [$ firstValue , $ lastValue ];
303+ } elseif ($ startConstant instanceof ConstantFloatType || $ endConstant instanceof ConstantFloatType) {
304+ return $ floatListType ;
305+ } elseif (!$ startConstant instanceof ConstantIntegerType || !$ endConstant instanceof ConstantIntegerType) {
194306 return self ::getNonEmptyListOfType (
195- IntegerRangeType ::fromInterval ($ low , $ high ),
196- // the keys stay unbounded when $high - $low overflows
197- $ low >= 0 || $ high <= PHP_INT_MAX + $ low ? intdiv ($ high - $ low , abs ($ stepConstant ->getValue ())) : null ,
307+ TypeCombinator ::union (
308+ $ startConstant ->generalize (GeneralizePrecision ::moreSpecific ()),
309+ $ endConstant ->generalize (GeneralizePrecision ::moreSpecific ()),
310+ $ stepType ->generalize (GeneralizePrecision ::moreSpecific ()),
311+ ),
198312 );
313+ } elseif (floor ($ stepConstant ->getValue ()) !== (float ) $ stepConstant ->getValue ()) {
314+ // a step with a fractional part produces floats
315+ return $ floatListType ;
316+ } else {
317+ $ bounds = [$ startConstant ->getValue (), $ endConstant ->getValue ()];
199318 }
200319
201- if ($ stepType ->isFloat ()->yes ()) {
202- return self ::getNonEmptyListOfType (new FloatType ());
320+ // the sequence is monotonic, so the first and the last value are its bounds
321+ $ low = min ($ bounds );
322+ $ high = max ($ bounds );
323+ $ maxKey = null ;
324+ if (
325+ $ startConstant instanceof ConstantIntegerType
326+ && $ endConstant instanceof ConstantIntegerType
327+ && $ stepConstant instanceof ConstantIntegerType
328+ // the keys stay unbounded when $high - $low overflows
329+ && ($ low >= 0 || $ high <= PHP_INT_MAX + $ low )
330+ ) {
331+ $ maxKey = intdiv ($ high - $ low , abs ($ stepConstant ->getValue ()));
203332 }
204333
205- return self ::getNonEmptyListOfType (
206- TypeCombinator ::union (
207- $ startConstant ->generalize (GeneralizePrecision ::moreSpecific ()),
208- $ endConstant ->generalize (GeneralizePrecision ::moreSpecific ()),
209- $ stepType ->generalize (GeneralizePrecision ::moreSpecific ()),
210- ),
211- );
334+ return self ::getNonEmptyListOfType (IntegerRangeType ::fromInterval ($ low , $ high ), $ maxKey );
212335 }
213336
214337 private static function getNonEmptyListOfType (Type $ type , ?int $ maxKey = null ): IntersectionType
0 commit comments