@@ -99,7 +99,19 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection,
9999 // range() would allocate every item before the length could be checked
100100 $ rangeLength = self ::getRangeLength ($ startConstant ->getValue (), $ endConstant ->getValue (), $ stepConstant ->getValue ());
101101 if ($ rangeLength !== null && $ rangeLength > ConstantArrayTypeBuilder::ARRAY_COUNT_LIMIT ) {
102- return self ::getLongRangeType ($ startConstant , $ endConstant , $ stepConstant , $ stepType );
102+ // without calling range() nothing rejects a negative step on an increasing range, which PHP 8.3 does
103+ if (self ::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+ return self ::getLongRangeType ($ hasStricterRange , $ startConstant , $ endConstant , $ stepConstant , $ stepType , null );
103115 }
104116
105117 try {
@@ -123,31 +135,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection,
123135 }
124136
125137 if (count ($ rangeValues ) > self ::RANGE_LENGTH_THRESHOLD ) {
126- if (
127- $ startConstant instanceof ConstantIntegerType
128- && $ endConstant instanceof ConstantIntegerType
129- && $ stepConstant instanceof ConstantIntegerType
130- ) {
131- if ($ startConstant ->getValue () > $ endConstant ->getValue ()) {
132- $ tmp = $ startConstant ;
133- $ startConstant = $ endConstant ;
134- $ endConstant = $ tmp ;
135- }
136- return self ::getNonEmptyListOfType (
137- IntegerRangeType ::fromInterval (
138- $ startConstant ->getValue (),
139- $ endConstant ->getValue (),
140- ),
141- );
142- }
143-
144- return self ::getLargeRangeType (
145- $ hasStricterRange ,
146- $ rangeValues ,
147- $ startConstant ->getValue (),
148- $ endConstant ->getValue (),
149- $ stepConstant ->getValue (),
150- );
138+ return self ::getLongRangeType ($ hasStricterRange , $ startConstant , $ endConstant , $ stepConstant , $ stepType , $ rangeValues );
151139 }
152140 $ arrayBuilder = ConstantArrayTypeBuilder ::createEmpty ();
153141 foreach ($ rangeValues as $ value ) {
@@ -256,48 +244,6 @@ private static function failsBeforePhp83(int|float|string $start, int|float|stri
256244 return $ step === 0.0 || abs ($ end - $ start ) < $ step ;
257245 }
258246
259- /**
260- * @param non-empty-list<int|float|string> $rangeValues
261- */
262- private static function getLargeRangeType (TrinaryLogic $ hasStricterRange , array $ rangeValues , int |float |string $ start , int |float |string $ end , int |float $ step ): Type
263- {
264- // range() only ever returns values of a single type
265- $ firstValue = $ rangeValues [0 ];
266- $ lastValue = $ rangeValues [count ($ rangeValues ) - 1 ];
267-
268- if (is_string ($ firstValue ) || is_string ($ lastValue )) {
269- // a character range consists of single bytes taken from constant boundaries
270- return self ::getNonEmptyListOfType (TypeCombinator ::intersect (
271- new StringType (),
272- new AccessoryNonEmptyStringType (),
273- new AccessoryLiteralStringType (),
274- ));
275- }
276-
277- $ floatListType = self ::getNonEmptyListOfType (new FloatType ());
278- if (is_float ($ firstValue ) || is_float ($ lastValue )) {
279- return $ floatListType ;
280- }
281-
282- // the sequence is monotonic, so the first and the last value are its bounds
283- if ($ firstValue > $ lastValue ) {
284- [$ firstValue , $ lastValue ] = [$ lastValue , $ firstValue ];
285- }
286-
287- $ intListType = self ::getNonEmptyListOfType (IntegerRangeType ::fromInterval ($ firstValue , $ lastValue ));
288- if ($ hasStricterRange ->yes () || (!is_float ($ start ) && !is_float ($ end ) && !is_float ($ step ))) {
289- return $ intListType ;
290- }
291-
292- // before PHP 8.3 a float argument produced floats even when none of
293- // the arguments had a fractional part
294- if ($ hasStricterRange ->no ()) {
295- return $ floatListType ;
296- }
297-
298- return TypeCombinator ::union ($ intListType , $ floatListType );
299- }
300-
301247 /**
302248 * The number of items range() creates for numeric arguments, or null when
303249 * only calling it tells - for a character range, which has at most 256
@@ -325,37 +271,94 @@ private static function getRangeLength(int|float|string $start, int|float|string
325271 return floor ($ length ) + 1 ;
326272 }
327273
274+ private static function isNegativeStepOnIncreasingRange (int |float |string $ start , int |float |string $ end , int |float $ step ): bool
275+ {
276+ // a non-numeric string next to a number is 0
277+ $ startNumber = is_numeric ($ start ) ? (float ) $ start : 0.0 ;
278+ $ endNumber = is_numeric ($ end ) ? (float ) $ end : 0.0 ;
279+
280+ return $ step < 0 && $ startNumber < $ endNumber ;
281+ }
282+
283+ /**
284+ * @param non-empty-list<int|float|string>|null $rangeValues null when range() was not called
285+ */
328286 private static function getLongRangeType (
287+ TrinaryLogic $ hasStricterRange ,
329288 ConstantIntegerType |ConstantFloatType |ConstantStringType $ startConstant ,
330289 ConstantIntegerType |ConstantFloatType |ConstantStringType $ endConstant ,
331290 ConstantIntegerType |ConstantFloatType $ stepConstant ,
332291 Type $ stepType ,
292+ ?array $ rangeValues ,
333293 ): Type
334294 {
335- if (
336- $ startConstant instanceof ConstantIntegerType
337- && $ endConstant instanceof ConstantIntegerType
338- && $ stepConstant instanceof ConstantIntegerType
339- ) {
295+ $ floatListType = self ::getNonEmptyListOfType (new FloatType ());
296+
297+ if ($ rangeValues !== null ) {
298+ // range() only ever returns values of a single type
299+ $ firstValue = $ rangeValues [0 ];
300+ $ lastValue = $ rangeValues [count ($ rangeValues ) - 1 ];
301+
302+ if (is_string ($ firstValue ) || is_string ($ lastValue )) {
303+ // a character range consists of single bytes taken from constant boundaries
304+ return self ::getNonEmptyListOfType (TypeCombinator ::intersect (
305+ new StringType (),
306+ new AccessoryNonEmptyStringType (),
307+ new AccessoryLiteralStringType (),
308+ ));
309+ }
310+
311+ if (is_float ($ firstValue ) || is_float ($ lastValue )) {
312+ return $ floatListType ;
313+ }
314+ } elseif ($ startConstant instanceof ConstantFloatType || $ endConstant instanceof ConstantFloatType) {
315+ return $ floatListType ;
316+ } elseif (!$ startConstant instanceof ConstantIntegerType || !$ endConstant instanceof ConstantIntegerType) {
340317 return self ::getNonEmptyListOfType (
341- IntegerRangeType ::fromInterval (
342- min ($ startConstant ->getValue (), $ endConstant ->getValue ()),
343- max ($ startConstant ->getValue (), $ endConstant ->getValue ()),
318+ TypeCombinator ::union (
319+ $ startConstant ->generalize (GeneralizePrecision ::moreSpecific ()),
320+ $ endConstant ->generalize (GeneralizePrecision ::moreSpecific ()),
321+ $ stepType ->generalize (GeneralizePrecision ::moreSpecific ()),
344322 ),
345323 );
324+ } elseif (floor ($ stepConstant ->getValue ()) !== (float ) $ stepConstant ->getValue ()) {
325+ // a step with a fractional part produces floats
326+ return $ floatListType ;
327+ } else {
328+ $ firstValue = $ startConstant ->getValue ();
329+ $ lastValue = $ endConstant ->getValue ();
346330 }
347331
348- if ($ stepType ->isFloat ()->yes ()) {
349- return self ::getNonEmptyListOfType (new FloatType ());
332+ if ($ startConstant instanceof ConstantIntegerType && $ endConstant instanceof ConstantIntegerType) {
333+ $ firstValue = $ startConstant ->getValue ();
334+ $ lastValue = $ endConstant ->getValue ();
350335 }
351336
352- return self ::getNonEmptyListOfType (
353- TypeCombinator ::union (
354- $ startConstant ->generalize (GeneralizePrecision ::moreSpecific ()),
355- $ endConstant ->generalize (GeneralizePrecision ::moreSpecific ()),
356- $ stepType ->generalize (GeneralizePrecision ::moreSpecific ()),
357- ),
358- );
337+ // the sequence is monotonic, so the first and the last value are its bounds
338+ $ intListType = self ::getNonEmptyListOfType (IntegerRangeType ::fromInterval (
339+ min ($ firstValue , $ lastValue ),
340+ max ($ firstValue , $ lastValue ),
341+ ));
342+
343+ if (
344+ !$ startConstant instanceof ConstantFloatType
345+ && !$ endConstant instanceof ConstantFloatType
346+ && !$ stepConstant instanceof ConstantFloatType
347+ ) {
348+ return $ intListType ;
349+ }
350+
351+ // before PHP 8.3 a float argument produced floats even when none of
352+ // the arguments had a fractional part
353+ if ($ hasStricterRange ->yes ()) {
354+ return $ intListType ;
355+ }
356+
357+ if ($ hasStricterRange ->no ()) {
358+ return $ floatListType ;
359+ }
360+
361+ return TypeCombinator ::union ($ intListType , $ floatListType );
359362 }
360363
361364 private static function getNonEmptyListOfType (Type $ type ): IntersectionType
0 commit comments