55use PhpParser \Node \Expr \FuncCall ;
66use PHPStan \Analyser \Scope ;
77use PHPStan \DependencyInjection \AutowiredService ;
8- use PHPStan \Php \PhpVersion ;
98use PHPStan \Reflection \FunctionReflection ;
9+ use PHPStan \TrinaryLogic ;
1010use PHPStan \Type \Accessory \AccessoryArrayListType ;
1111use PHPStan \Type \Accessory \AccessoryLiteralStringType ;
1212use PHPStan \Type \Accessory \AccessoryNonEmptyStringType ;
@@ -42,10 +42,6 @@ final class RangeFunctionReturnTypeExtension implements DynamicFunctionReturnTyp
4242
4343 private const RANGE_LENGTH_THRESHOLD = 50 ;
4444
45- public function __construct (private PhpVersion $ phpVersion )
46- {
47- }
48-
4945 public function isFunctionSupported (FunctionReflection $ functionReflection ): bool
5046 {
5147 return $ functionReflection ->getName () === 'range ' ;
@@ -62,10 +58,15 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection,
6258 $ endType = $ scope ->getType ($ args [1 ]->value );
6359 $ stepType = count ($ args ) >= 3 ? $ scope ->getType ($ args [2 ]->value ) : new ConstantIntegerType (1 );
6460
61+ $ phpVersions = $ scope ->getPhpVersion ();
62+ $ hasStricterRange = $ phpVersions ->hasStricterRangeFunction ();
63+ $ throwsValueError = $ phpVersions ->throwsValueErrorForInternalFunctions ();
64+
6565 $ constantReturnTypes = [];
6666 $ constantCombinations = 0 ;
6767 $ throwingCombinations = 0 ;
6868 $ hasSkippedCombination = false ;
69+ $ hasUnknownCombination = false ;
6970
7071 $ startConstants = $ startType ->getConstantScalarTypes ();
7172 foreach ($ startConstants as $ startConstant ) {
@@ -98,12 +99,14 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection,
9899
99100 // PHP 7 reports an invalid step by returning false instead of throwing a ValueError
100101 if ($ rangeValues === false ) {
101- if ($ this ->failsOnAnalysedVersion ($ startConstant ->getValue (), $ endConstant ->getValue (), $ stepConstant ->getValue ())) {
102- if ($ this ->phpVersion ->throwsValueErrorForInternalFunctions ()) {
103- $ throwingCombinations ++;
104- } else {
105- $ constantReturnTypes [] = new ConstantBooleanType (false );
106- }
102+ $ fails = self ::failsOnAnalysedVersion ($ hasStricterRange , $ startConstant ->getValue (), $ endConstant ->getValue (), $ stepConstant ->getValue ());
103+ if (!$ fails ->yes ()) {
104+ // the analysed PHP version might accept the step, but only the runtime's verdict is known
105+ $ hasUnknownCombination = true ;
106+ } elseif ($ throwsValueError ->yes ()) {
107+ $ throwingCombinations ++;
108+ } else {
109+ $ constantReturnTypes [] = new ConstantBooleanType (false );
107110 }
108111 continue ;
109112 }
@@ -127,7 +130,8 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection,
127130 );
128131 }
129132
130- return $ this ->getLargeRangeType (
133+ return self ::getLargeRangeType (
134+ $ hasStricterRange ,
131135 $ rangeValues ,
132136 $ startConstant ->getValue (),
133137 $ endConstant ->getValue (),
@@ -144,6 +148,10 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection,
144148 }
145149 }
146150
151+ if ($ hasUnknownCombination ) {
152+ $ constantReturnTypes = [];
153+ }
154+
147155 if (count ($ constantReturnTypes ) > 0 ) {
148156 return TypeCombinator ::union (...$ constantReturnTypes );
149157 }
@@ -198,14 +206,22 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection,
198206 /**
199207 * The constant arguments are folded by calling range() on the PHP version PHPStan
200208 * itself runs on, so a failure only says that the runtime rejects the step.
201- * This checks whether the analysed PHP version rejects it as well.
209+ * This checks whether the analysed PHP versions reject it as well.
202210 */
203- private function failsOnAnalysedVersion (int |float |string $ start , int |float |string $ end , int |float $ step ): bool
211+ private static function failsOnAnalysedVersion (TrinaryLogic $ hasStricterRange , int |float |string $ start , int |float |string $ end , int |float $ step ): TrinaryLogic
204212 {
205- if ($ this -> phpVersion -> hasStricterRangeFunction ( )) {
206- return true ;
213+ if (self :: failsBeforePhp83 ( $ start , $ end , $ step )) {
214+ return TrinaryLogic :: createYes () ;
207215 }
208216
217+ return $ hasStricterRange ;
218+ }
219+
220+ /**
221+ * PHP 7.x reported these steps with a warning and false, PHP 8.0-8.2 with a ValueError.
222+ */
223+ private static function failsBeforePhp83 (int |float |string $ start , int |float |string $ end , int |float $ step ): bool
224+ {
209225 if (is_string ($ start ) || is_string ($ end )) {
210226 // how a string boundary was coerced before PHP 8.3 is not modelled here
211227 return false ;
@@ -232,7 +248,7 @@ private function failsOnAnalysedVersion(int|float|string $start, int|float|strin
232248 /**
233249 * @param non-empty-list<int|float|string> $rangeValues
234250 */
235- private function getLargeRangeType (array $ rangeValues , int |float |string $ start , int |float |string $ end , int |float $ step ): IntersectionType
251+ private static function getLargeRangeType (TrinaryLogic $ hasStricterRange , array $ rangeValues , int |float |string $ start , int |float |string $ end , int |float $ step ): Type
236252 {
237253 // range() only ever returns values of a single type
238254 $ firstValue = $ rangeValues [0 ];
@@ -247,25 +263,28 @@ private function getLargeRangeType(array $rangeValues, int|float|string $start,
247263 ));
248264 }
249265
250- if (
251- is_float ($ firstValue )
252- || is_float ($ lastValue )
253- // before PHP 8.3 a float argument produced floats even when none of
254- // the arguments had a fractional part
255- || (
256- !$ this ->phpVersion ->hasStricterRangeFunction ()
257- && (is_float ($ start ) || is_float ($ end ) || is_float ($ step ))
258- )
259- ) {
260- return self ::getNonEmptyListOfType (new FloatType ());
266+ $ floatListType = self ::getNonEmptyListOfType (new FloatType ());
267+ if (is_float ($ firstValue ) || is_float ($ lastValue )) {
268+ return $ floatListType ;
261269 }
262270
263271 // the sequence is monotonic, so the first and the last value are its bounds
264272 if ($ firstValue > $ lastValue ) {
265273 [$ firstValue , $ lastValue ] = [$ lastValue , $ firstValue ];
266274 }
267275
268- return self ::getNonEmptyListOfType (IntegerRangeType ::fromInterval ($ firstValue , $ lastValue ));
276+ $ intListType = self ::getNonEmptyListOfType (IntegerRangeType ::fromInterval ($ firstValue , $ lastValue ));
277+ if ($ hasStricterRange ->yes () || (!is_float ($ start ) && !is_float ($ end ) && !is_float ($ step ))) {
278+ return $ intListType ;
279+ }
280+
281+ // before PHP 8.3 a float argument produced floats even when none of
282+ // the arguments had a fractional part
283+ if ($ hasStricterRange ->no ()) {
284+ return $ floatListType ;
285+ }
286+
287+ return TypeCombinator ::union ($ intListType , $ floatListType );
269288 }
270289
271290 private static function getNonEmptyListOfType (Type $ type ): IntersectionType
0 commit comments