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 ;
31- use function is_array ;
3235use function is_finite ;
36+ use function is_float ;
3337use function is_numeric ;
3438use function is_string ;
3539use function max ;
3640use function min ;
37- use function range ;
3841
3942#[AutowiredService]
4043final class RangeFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension
@@ -58,46 +61,77 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection,
5861 $ endType = $ scope ->getType ($ args [1 ]->value );
5962 $ stepType = count ($ args ) >= 3 ? $ scope ->getType ($ args [2 ]->value ) : new ConstantIntegerType (1 );
6063
64+ $ phpVersions = $ scope ->getPhpVersion ();
65+ $ hasStricterRange = $ phpVersions ->hasStricterRangeFunction ();
66+ $ throwsValueError = $ phpVersions ->throwsValueErrorForInternalFunctions ();
67+
6168 $ constantReturnTypes = [];
69+ $ constantCombinations = 0 ;
70+ $ throwingCombinations = 0 ;
71+ $ hasSkippedCombination = false ;
72+ $ hasUnknownCombination = false ;
6273
6374 $ startConstants = $ startType ->getConstantScalarTypes ();
6475 foreach ($ startConstants as $ startConstant ) {
6576 if (!$ startConstant instanceof ConstantIntegerType && !$ startConstant instanceof ConstantFloatType && !$ startConstant instanceof ConstantStringType) {
77+ $ hasSkippedCombination = true ;
6678 continue ;
6779 }
6880
6981 $ endConstants = $ endType ->getConstantScalarTypes ();
7082 foreach ($ endConstants as $ endConstant ) {
7183 if (!$ endConstant instanceof ConstantIntegerType && !$ endConstant instanceof ConstantFloatType && !$ endConstant instanceof ConstantStringType) {
84+ $ hasSkippedCombination = true ;
7285 continue ;
7386 }
7487
7588 $ stepConstants = $ stepType ->getConstantScalarTypes ();
7689 foreach ($ stepConstants as $ stepConstant ) {
7790 if (!$ stepConstant instanceof ConstantIntegerType && !$ stepConstant instanceof ConstantFloatType) {
91+ $ hasSkippedCombination = true ;
7892 continue ;
7993 }
8094
95+ $ constantCombinations ++;
96+
8197 // range() would allocate every item before the length could be checked
8298 $ rangeLength = self ::getRangeLength ($ startConstant ->getValue (), $ endConstant ->getValue (), $ stepConstant ->getValue ());
8399 if ($ rangeLength !== null && $ rangeLength > ConstantArrayTypeBuilder::ARRAY_COUNT_LIMIT ) {
84- return self ::getLongRangeType ($ startConstant , $ endConstant , $ stepConstant , $ stepType );
85- }
86-
87- try {
88- $ rangeValues = @range ($ startConstant ->getValue (), $ endConstant ->getValue (), $ stepConstant ->getValue ());
89- } catch (ValueError ) {
100+ // without calling range() nothing rejects a negative step on an increasing range, which PHP 8.3 does
101+ if (RangeFunctionArgumentsHelper ::isNegativeStepOnIncreasingRange ($ startConstant ->getValue (), $ endConstant ->getValue (), $ stepConstant ->getValue ())) {
102+ if ($ hasStricterRange ->yes ()) {
103+ $ throwingCombinations ++;
104+ continue ;
105+ }
106+ if ($ hasStricterRange ->maybe ()) {
107+ $ hasUnknownCombination = true ;
108+ continue ;
109+ }
110+ }
111+
112+ $ constantReturnTypes [] = self ::getLongRangeType ($ hasStricterRange , $ startConstant , $ endConstant , $ stepConstant , $ stepType , null );
90113 continue ;
91114 }
92115
93- // @phpstan-ignore function.alreadyNarrowedType
94- if (!is_array ($ rangeValues )) {
116+ $ rangeValues = RangeFunctionArgumentsHelper ::callRange ($ startConstant ->getValue (), $ endConstant ->getValue (), $ stepConstant ->getValue ());
117+ if ($ rangeValues === false ) {
118+ $ fails = RangeFunctionArgumentsHelper ::rejects ($ hasStricterRange , $ startConstant ->getValue (), $ endConstant ->getValue (), $ stepConstant ->getValue (), true );
119+ if (!$ fails ->yes ()) {
120+ // the analysed PHP version might accept the step, but only the runtime's verdict is known
121+ $ hasUnknownCombination = true ;
122+ } elseif ($ throwsValueError ->yes ()) {
123+ $ throwingCombinations ++;
124+ } else {
125+ $ constantReturnTypes [] = new ConstantBooleanType (false );
126+ }
95127 continue ;
96128 }
97129
98130 if (count ($ rangeValues ) > self ::RANGE_LENGTH_THRESHOLD ) {
99- return self ::getLongRangeType ($ startConstant , $ endConstant , $ stepConstant , $ stepType );
131+ $ constantReturnTypes [] = self ::getLongRangeType ($ hasStricterRange , $ startConstant , $ endConstant , $ stepConstant , $ stepType , $ rangeValues );
132+ continue ;
100133 }
134+
101135 $ arrayBuilder = ConstantArrayTypeBuilder ::createEmpty ();
102136 foreach ($ rangeValues as $ value ) {
103137 $ arrayBuilder ->setOffsetValueType (null , $ scope ->getTypeFromValue ($ value ));
@@ -108,10 +142,29 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection,
108142 }
109143 }
110144
111- if (count ($ constantReturnTypes ) > 0 ) {
112- return TypeCombinator ::union (...$ constantReturnTypes );
145+ if (!$ hasUnknownCombination && !$ hasSkippedCombination ) {
146+ if (count ($ constantReturnTypes ) > 0 ) {
147+ return TypeCombinator ::union (...$ constantReturnTypes );
148+ }
149+
150+ // nothing is returned when every combination of the constant arguments throws
151+ if (
152+ $ constantCombinations > 0
153+ && $ throwingCombinations === $ constantCombinations
154+ && $ startType ->isConstantScalarValue ()->yes ()
155+ && $ endType ->isConstantScalarValue ()->yes ()
156+ && $ stepType ->isConstantScalarValue ()->yes ()
157+ ) {
158+ return new NeverType ();
159+ }
113160 }
114161
162+ // the general type covers the combinations that could not be decided, the rest keep their own types
163+ return TypeCombinator ::union (self ::getGeneralType ($ startType , $ endType , $ stepType ), ...$ constantReturnTypes );
164+ }
165+
166+ private static function getGeneralType (Type $ startType , Type $ endType , Type $ stepType ): Type
167+ {
115168 $ argType = TypeCombinator ::union ($ startType , $ endType );
116169 $ isInteger = $ argType ->isInteger ()->yes ();
117170 $ isStepInteger = $ stepType ->isInteger ()->yes ();
@@ -174,37 +227,96 @@ private static function getRangeLength(int|float|string $start, int|float|string
174227 return floor ($ length ) + 1 ;
175228 }
176229
230+ /**
231+ * @param non-empty-list<int|float|string>|null $rangeValues null when range() was not called
232+ */
177233 private static function getLongRangeType (
234+ TrinaryLogic $ hasStricterRange ,
178235 ConstantIntegerType |ConstantFloatType |ConstantStringType $ startConstant ,
179236 ConstantIntegerType |ConstantFloatType |ConstantStringType $ endConstant ,
180237 ConstantIntegerType |ConstantFloatType $ stepConstant ,
181238 Type $ stepType ,
239+ ?array $ rangeValues ,
182240 ): Type
183241 {
242+ $ type = self ::getLongRangeItemsType ($ startConstant , $ endConstant , $ stepConstant , $ stepType , $ rangeValues );
184243 if (
185- $ startConstant instanceof ConstantIntegerType
186- && $ endConstant instanceof ConstantIntegerType
187- && $ stepConstant instanceof ConstantIntegerType
244+ $ hasStricterRange ->yes ()
245+ || (
246+ !$ startConstant instanceof ConstantFloatType
247+ && !$ endConstant instanceof ConstantFloatType
248+ && !$ stepConstant instanceof ConstantFloatType
249+ )
188250 ) {
251+ return $ type ;
252+ }
253+
254+ // before PHP 8.3 a float argument produced floats even when none of the arguments had a fractional
255+ // part, and even for two strings, of which only a numeric one kept its value
256+ $ floatListType = self ::getNonEmptyListOfType (new FloatType ());
257+ if ($ hasStricterRange ->no ()) {
258+ return $ floatListType ;
259+ }
260+
261+ return TypeCombinator ::union ($ type , $ floatListType );
262+ }
263+
264+ /**
265+ * The type of the items the runtime returned. Without them it follows PHP 8.3 for numeric
266+ * boundaries and generalizes the arguments for a string one.
267+ *
268+ * @param non-empty-list<int|float|string>|null $rangeValues
269+ */
270+ private static function getLongRangeItemsType (
271+ ConstantIntegerType |ConstantFloatType |ConstantStringType $ startConstant ,
272+ ConstantIntegerType |ConstantFloatType |ConstantStringType $ endConstant ,
273+ ConstantIntegerType |ConstantFloatType $ stepConstant ,
274+ Type $ stepType ,
275+ ?array $ rangeValues ,
276+ ): Type
277+ {
278+ $ floatListType = self ::getNonEmptyListOfType (new FloatType ());
279+
280+ if ($ rangeValues !== null ) {
281+ // range() only ever returns values of a single type
282+ $ firstValue = $ rangeValues [0 ];
283+ $ lastValue = $ rangeValues [count ($ rangeValues ) - 1 ];
284+
285+ if (is_string ($ firstValue ) || is_string ($ lastValue )) {
286+ // a character range consists of single bytes taken from constant boundaries
287+ return self ::getNonEmptyListOfType (TypeCombinator ::intersect (
288+ new StringType (),
289+ new AccessoryNonEmptyStringType (),
290+ new AccessoryLiteralStringType (),
291+ ));
292+ }
293+
294+ if (is_float ($ firstValue ) || is_float ($ lastValue )) {
295+ return $ floatListType ;
296+ }
297+
298+ $ bounds = $ startConstant instanceof ConstantIntegerType && $ endConstant instanceof ConstantIntegerType
299+ ? [$ startConstant ->getValue (), $ endConstant ->getValue ()]
300+ : [$ firstValue , $ lastValue ];
301+ } elseif ($ startConstant instanceof ConstantFloatType || $ endConstant instanceof ConstantFloatType) {
302+ return $ floatListType ;
303+ } elseif (!$ startConstant instanceof ConstantIntegerType || !$ endConstant instanceof ConstantIntegerType) {
189304 return self ::getNonEmptyListOfType (
190- IntegerRangeType ::fromInterval (
191- min ($ startConstant ->getValue (), $ endConstant ->getValue ()),
192- max ($ startConstant ->getValue (), $ endConstant ->getValue ()),
305+ TypeCombinator ::union (
306+ $ startConstant ->generalize (GeneralizePrecision ::moreSpecific ()),
307+ $ endConstant ->generalize (GeneralizePrecision ::moreSpecific ()),
308+ $ stepType ->generalize (GeneralizePrecision ::moreSpecific ()),
193309 ),
194310 );
311+ } elseif (floor ($ stepConstant ->getValue ()) !== (float ) $ stepConstant ->getValue ()) {
312+ // a step with a fractional part produces floats
313+ return $ floatListType ;
314+ } else {
315+ $ bounds = [$ startConstant ->getValue (), $ endConstant ->getValue ()];
195316 }
196317
197- if ($ stepType ->isFloat ()->yes ()) {
198- return self ::getNonEmptyListOfType (new FloatType ());
199- }
200-
201- return self ::getNonEmptyListOfType (
202- TypeCombinator ::union (
203- $ startConstant ->generalize (GeneralizePrecision ::moreSpecific ()),
204- $ endConstant ->generalize (GeneralizePrecision ::moreSpecific ()),
205- $ stepType ->generalize (GeneralizePrecision ::moreSpecific ()),
206- ),
207- );
318+ // the sequence is monotonic, so the first and the last value are its bounds
319+ return self ::getNonEmptyListOfType (IntegerRangeType ::fromInterval (min ($ bounds ), max ($ bounds )));
208320 }
209321
210322 private static function getNonEmptyListOfType (Type $ type ): IntersectionType
0 commit comments