1: <?php declare(strict_types = 1);
2:
3: namespace PHPStan\Type;
4:
5: use PHPStan\TrinaryLogic;
6: use PHPStan\Type\Constant\ConstantBooleanType;
7: use PHPStan\Type\Constant\ConstantIntegerType;
8: use function array_filter;
9: use function assert;
10: use function ceil;
11: use function count;
12: use function floor;
13: use function get_class;
14: use function is_float;
15: use function is_int;
16: use function max;
17: use function min;
18: use function sprintf;
19: use const PHP_INT_MAX;
20: use const PHP_INT_MIN;
21:
22: /** @api */
23: class IntegerRangeType extends IntegerType implements CompoundType
24: {
25:
26: private function __construct(private ?int $min, private ?int $max)
27: {
28: parent::__construct();
29: assert($min === null || $max === null || $min <= $max);
30: assert($min !== null || $max !== null);
31: }
32:
33: public static function fromInterval(?int $min, ?int $max, int $shift = 0): Type
34: {
35: if ($min !== null && $max !== null) {
36: if ($min > $max) {
37: return new NeverType();
38: }
39: if ($min === $max) {
40: return new ConstantIntegerType($min + $shift);
41: }
42: }
43:
44: if ($min === null && $max === null) {
45: return new IntegerType();
46: }
47:
48: return (new self($min, $max))->shift($shift);
49: }
50:
51: protected static function isDisjoint(?int $minA, ?int $maxA, ?int $minB, ?int $maxB, bool $touchingIsDisjoint = true): bool
52: {
53: $offset = $touchingIsDisjoint ? 0 : 1;
54: return $minA !== null && $maxB !== null && $minA > $maxB + $offset
55: || $maxA !== null && $minB !== null && $maxA + $offset < $minB;
56: }
57:
58: /**
59: * Return the range of integers smaller than the given value
60: *
61: * @param int|float $value
62: */
63: public static function createAllSmallerThan($value): Type
64: {
65: if (is_int($value)) {
66: return self::fromInterval(null, $value, -1);
67: }
68:
69: if ($value > PHP_INT_MAX) {
70: return new IntegerType();
71: }
72:
73: if ($value <= PHP_INT_MIN) {
74: return new NeverType();
75: }
76:
77: return self::fromInterval(null, (int) ceil($value), -1);
78: }
79:
80: /**
81: * Return the range of integers smaller than or equal to the given value
82: *
83: * @param int|float $value
84: */
85: public static function createAllSmallerThanOrEqualTo($value): Type
86: {
87: if (is_int($value)) {
88: return self::fromInterval(null, $value);
89: }
90:
91: if ($value >= PHP_INT_MAX) {
92: return new IntegerType();
93: }
94:
95: if ($value < PHP_INT_MIN) {
96: return new NeverType();
97: }
98:
99: return self::fromInterval(null, (int) floor($value));
100: }
101:
102: /**
103: * Return the range of integers greater than the given value
104: *
105: * @param int|float $value
106: */
107: public static function createAllGreaterThan($value): Type
108: {
109: if (is_int($value)) {
110: return self::fromInterval($value, null, 1);
111: }
112:
113: if ($value < PHP_INT_MIN) {
114: return new IntegerType();
115: }
116:
117: if ($value >= PHP_INT_MAX) {
118: return new NeverType();
119: }
120:
121: return self::fromInterval((int) floor($value), null, 1);
122: }
123:
124: /**
125: * Return the range of integers greater than or equal to the given value
126: *
127: * @param int|float $value
128: */
129: public static function createAllGreaterThanOrEqualTo($value): Type
130: {
131: if (is_int($value)) {
132: return self::fromInterval($value, null);
133: }
134:
135: if ($value <= PHP_INT_MIN) {
136: return new IntegerType();
137: }
138:
139: if ($value > PHP_INT_MAX) {
140: return new NeverType();
141: }
142:
143: return self::fromInterval((int) ceil($value), null);
144: }
145:
146: public function getMin(): ?int
147: {
148: return $this->min;
149: }
150:
151: public function getMax(): ?int
152: {
153: return $this->max;
154: }
155:
156: public function describe(VerbosityLevel $level): string
157: {
158: return sprintf('int<%s, %s>', $this->min ?? 'min', $this->max ?? 'max');
159: }
160:
161: public function shift(int $amount): Type
162: {
163: if ($amount === 0) {
164: return $this;
165: }
166:
167: $min = $this->min;
168: $max = $this->max;
169:
170: if ($amount < 0) {
171: if ($max !== null) {
172: if ($max < PHP_INT_MIN - $amount) {
173: return new NeverType();
174: }
175: $max += $amount;
176: }
177: if ($min !== null) {
178: $min = $min < PHP_INT_MIN - $amount ? null : $min + $amount;
179: }
180: } else {
181: if ($min !== null) {
182: if ($min > PHP_INT_MAX - $amount) {
183: return new NeverType();
184: }
185: $min += $amount;
186: }
187: if ($max !== null) {
188: $max = $max > PHP_INT_MAX - $amount ? null : $max + $amount;
189: }
190: }
191:
192: return self::fromInterval($min, $max);
193: }
194:
195: public function accepts(Type $type, bool $strictTypes): TrinaryLogic
196: {
197: return $this->acceptsWithReason($type, $strictTypes)->result;
198: }
199:
200: public function acceptsWithReason(Type $type, bool $strictTypes): AcceptsResult
201: {
202: if ($type instanceof parent) {
203: return new AcceptsResult($this->isSuperTypeOf($type), []);
204: }
205:
206: if ($type instanceof CompoundType) {
207: return $type->isAcceptedWithReasonBy($this, $strictTypes);
208: }
209:
210: return AcceptsResult::createNo();
211: }
212:
213: public function isSuperTypeOf(Type $type): TrinaryLogic
214: {
215: if ($type instanceof self || $type instanceof ConstantIntegerType) {
216: if ($type instanceof self) {
217: $typeMin = $type->min;
218: $typeMax = $type->max;
219: } else {
220: $typeMin = $type->getValue();
221: $typeMax = $type->getValue();
222: }
223:
224: if (self::isDisjoint($this->min, $this->max, $typeMin, $typeMax)) {
225: return TrinaryLogic::createNo();
226: }
227:
228: if (
229: ($this->min === null || $typeMin !== null && $this->min <= $typeMin)
230: && ($this->max === null || $typeMax !== null && $this->max >= $typeMax)
231: ) {
232: return TrinaryLogic::createYes();
233: }
234:
235: return TrinaryLogic::createMaybe();
236: }
237:
238: if ($type instanceof parent) {
239: return TrinaryLogic::createMaybe();
240: }
241:
242: if ($type instanceof CompoundType) {
243: return $type->isSubTypeOf($this);
244: }
245:
246: return TrinaryLogic::createNo();
247: }
248:
249: public function isSubTypeOf(Type $otherType): TrinaryLogic
250: {
251: if ($otherType instanceof parent) {
252: return $otherType->isSuperTypeOf($this);
253: }
254:
255: if ($otherType instanceof UnionType) {
256: return $this->isSubTypeOfUnion($otherType);
257: }
258:
259: if ($otherType instanceof IntersectionType) {
260: return $otherType->isSuperTypeOf($this);
261: }
262:
263: return TrinaryLogic::createNo();
264: }
265:
266: private function isSubTypeOfUnion(UnionType $otherType): TrinaryLogic
267: {
268: if ($this->min !== null && $this->max !== null) {
269: $matchingConstantIntegers = array_filter(
270: $otherType->getTypes(),
271: fn (Type $type): bool => $type instanceof ConstantIntegerType && $type->getValue() >= $this->min && $type->getValue() <= $this->max,
272: );
273:
274: if (count($matchingConstantIntegers) === ($this->max - $this->min + 1)) {
275: return TrinaryLogic::createYes();
276: }
277: }
278:
279: return TrinaryLogic::createNo()->lazyOr($otherType->getTypes(), fn (Type $innerType) => $this->isSubTypeOf($innerType));
280: }
281:
282: public function isAcceptedBy(Type $acceptingType, bool $strictTypes): TrinaryLogic
283: {
284: return $this->isAcceptedWithReasonBy($acceptingType, $strictTypes)->result;
285: }
286:
287: public function isAcceptedWithReasonBy(Type $acceptingType, bool $strictTypes): AcceptsResult
288: {
289: return new AcceptsResult($this->isSubTypeOf($acceptingType), []);
290: }
291:
292: public function equals(Type $type): bool
293: {
294: return $type instanceof self && $this->min === $type->min && $this->max === $type->max;
295: }
296:
297: public function generalize(GeneralizePrecision $precision): Type
298: {
299: return new IntegerType();
300: }
301:
302: public function isSmallerThan(Type $otherType): TrinaryLogic
303: {
304: if ($this->min === null) {
305: $minIsSmaller = TrinaryLogic::createYes();
306: } else {
307: $minIsSmaller = (new ConstantIntegerType($this->min))->isSmallerThan($otherType);
308: }
309:
310: if ($this->max === null) {
311: $maxIsSmaller = TrinaryLogic::createNo();
312: } else {
313: $maxIsSmaller = (new ConstantIntegerType($this->max))->isSmallerThan($otherType);
314: }
315:
316: return TrinaryLogic::extremeIdentity($minIsSmaller, $maxIsSmaller);
317: }
318:
319: public function isSmallerThanOrEqual(Type $otherType): TrinaryLogic
320: {
321: if ($this->min === null) {
322: $minIsSmaller = TrinaryLogic::createYes();
323: } else {
324: $minIsSmaller = (new ConstantIntegerType($this->min))->isSmallerThanOrEqual($otherType);
325: }
326:
327: if ($this->max === null) {
328: $maxIsSmaller = TrinaryLogic::createNo();
329: } else {
330: $maxIsSmaller = (new ConstantIntegerType($this->max))->isSmallerThanOrEqual($otherType);
331: }
332:
333: return TrinaryLogic::extremeIdentity($minIsSmaller, $maxIsSmaller);
334: }
335:
336: public function isGreaterThan(Type $otherType): TrinaryLogic
337: {
338: if ($this->min === null) {
339: $minIsSmaller = TrinaryLogic::createNo();
340: } else {
341: $minIsSmaller = $otherType->isSmallerThan((new ConstantIntegerType($this->min)));
342: }
343:
344: if ($this->max === null) {
345: $maxIsSmaller = TrinaryLogic::createYes();
346: } else {
347: $maxIsSmaller = $otherType->isSmallerThan((new ConstantIntegerType($this->max)));
348: }
349:
350: return TrinaryLogic::extremeIdentity($minIsSmaller, $maxIsSmaller);
351: }
352:
353: public function isGreaterThanOrEqual(Type $otherType): TrinaryLogic
354: {
355: if ($this->min === null) {
356: $minIsSmaller = TrinaryLogic::createNo();
357: } else {
358: $minIsSmaller = $otherType->isSmallerThanOrEqual((new ConstantIntegerType($this->min)));
359: }
360:
361: if ($this->max === null) {
362: $maxIsSmaller = TrinaryLogic::createYes();
363: } else {
364: $maxIsSmaller = $otherType->isSmallerThanOrEqual((new ConstantIntegerType($this->max)));
365: }
366:
367: return TrinaryLogic::extremeIdentity($minIsSmaller, $maxIsSmaller);
368: }
369:
370: public function getSmallerType(): Type
371: {
372: $subtractedTypes = [
373: new ConstantBooleanType(true),
374: ];
375:
376: if ($this->max !== null) {
377: $subtractedTypes[] = self::createAllGreaterThanOrEqualTo($this->max);
378: }
379:
380: return TypeCombinator::remove(new MixedType(), TypeCombinator::union(...$subtractedTypes));
381: }
382:
383: public function getSmallerOrEqualType(): Type
384: {
385: $subtractedTypes = [];
386:
387: if ($this->max !== null) {
388: $subtractedTypes[] = self::createAllGreaterThan($this->max);
389: }
390:
391: return TypeCombinator::remove(new MixedType(), TypeCombinator::union(...$subtractedTypes));
392: }
393:
394: public function getGreaterType(): Type
395: {
396: $subtractedTypes = [
397: new NullType(),
398: new ConstantBooleanType(false),
399: ];
400:
401: if ($this->min !== null) {
402: $subtractedTypes[] = self::createAllSmallerThanOrEqualTo($this->min);
403: }
404:
405: if ($this->min !== null && $this->min > 0 || $this->max !== null && $this->max < 0) {
406: $subtractedTypes[] = new ConstantBooleanType(true);
407: }
408:
409: return TypeCombinator::remove(new MixedType(), TypeCombinator::union(...$subtractedTypes));
410: }
411:
412: public function getGreaterOrEqualType(): Type
413: {
414: $subtractedTypes = [];
415:
416: if ($this->min !== null) {
417: $subtractedTypes[] = self::createAllSmallerThan($this->min);
418: }
419:
420: if ($this->min !== null && $this->min > 0 || $this->max !== null && $this->max < 0) {
421: $subtractedTypes[] = new NullType();
422: $subtractedTypes[] = new ConstantBooleanType(false);
423: }
424:
425: return TypeCombinator::remove(new MixedType(), TypeCombinator::union(...$subtractedTypes));
426: }
427:
428: public function toBoolean(): BooleanType
429: {
430: $isZero = (new ConstantIntegerType(0))->isSuperTypeOf($this);
431: if ($isZero->no()) {
432: return new ConstantBooleanType(true);
433: }
434:
435: if ($isZero->maybe()) {
436: return new BooleanType();
437: }
438:
439: return new ConstantBooleanType(false);
440: }
441:
442: /**
443: * Return the union with another type, but only if it can be expressed in a simpler way than using UnionType
444: *
445: */
446: public function tryUnion(Type $otherType): ?Type
447: {
448: if ($otherType instanceof self || $otherType instanceof ConstantIntegerType) {
449: if ($otherType instanceof self) {
450: $otherMin = $otherType->min;
451: $otherMax = $otherType->max;
452: } else {
453: $otherMin = $otherType->getValue();
454: $otherMax = $otherType->getValue();
455: }
456:
457: if (self::isDisjoint($this->min, $this->max, $otherMin, $otherMax, false)) {
458: return null;
459: }
460:
461: return self::fromInterval(
462: $this->min !== null && $otherMin !== null ? min($this->min, $otherMin) : null,
463: $this->max !== null && $otherMax !== null ? max($this->max, $otherMax) : null,
464: );
465: }
466:
467: if (get_class($otherType) === parent::class) {
468: return $otherType;
469: }
470:
471: return null;
472: }
473:
474: /**
475: * Return the intersection with another type, but only if it can be expressed in a simpler way than using
476: * IntersectionType
477: *
478: */
479: public function tryIntersect(Type $otherType): ?Type
480: {
481: if ($otherType instanceof self || $otherType instanceof ConstantIntegerType) {
482: if ($otherType instanceof self) {
483: $otherMin = $otherType->min;
484: $otherMax = $otherType->max;
485: } else {
486: $otherMin = $otherType->getValue();
487: $otherMax = $otherType->getValue();
488: }
489:
490: if (self::isDisjoint($this->min, $this->max, $otherMin, $otherMax, false)) {
491: return new NeverType();
492: }
493:
494: if ($this->min === null) {
495: $newMin = $otherMin;
496: } elseif ($otherMin === null) {
497: $newMin = $this->min;
498: } else {
499: $newMin = max($this->min, $otherMin);
500: }
501:
502: if ($this->max === null) {
503: $newMax = $otherMax;
504: } elseif ($otherMax === null) {
505: $newMax = $this->max;
506: } else {
507: $newMax = min($this->max, $otherMax);
508: }
509:
510: return self::fromInterval($newMin, $newMax);
511: }
512:
513: if (get_class($otherType) === parent::class) {
514: return $this;
515: }
516:
517: return null;
518: }
519:
520: /**
521: * Return the different with another type, or null if it cannot be represented.
522: *
523: */
524: public function tryRemove(Type $typeToRemove): ?Type
525: {
526: if (get_class($typeToRemove) === parent::class) {
527: return new NeverType();
528: }
529:
530: if ($typeToRemove instanceof self || $typeToRemove instanceof ConstantIntegerType) {
531: if ($typeToRemove instanceof self) {
532: $removeMin = $typeToRemove->min;
533: $removeMax = $typeToRemove->max;
534: } else {
535: $removeMin = $typeToRemove->getValue();
536: $removeMax = $typeToRemove->getValue();
537: }
538:
539: if (
540: $this->min !== null && $removeMax !== null && $removeMax < $this->min
541: || $this->max !== null && $removeMin !== null && $this->max < $removeMin
542: ) {
543: return $this;
544: }
545:
546: if ($removeMin !== null && $removeMin !== PHP_INT_MIN) {
547: $lowerPart = self::fromInterval($this->min, $removeMin - 1);
548: } else {
549: $lowerPart = null;
550: }
551: if ($removeMax !== null && $removeMax !== PHP_INT_MAX) {
552: $upperPart = self::fromInterval($removeMax + 1, $this->max);
553: } else {
554: $upperPart = null;
555: }
556:
557: if ($lowerPart !== null && $upperPart !== null) {
558: return TypeCombinator::union($lowerPart, $upperPart);
559: }
560:
561: return $lowerPart ?? $upperPart;
562: }
563:
564: return null;
565: }
566:
567: public function exponentiate(Type $exponent): Type
568: {
569: if ($exponent instanceof UnionType) {
570: $results = [];
571: foreach ($exponent->getTypes() as $unionType) {
572: $results[] = $this->exponentiate($unionType);
573: }
574: return TypeCombinator::union(...$results);
575: }
576:
577: if ($exponent instanceof IntegerRangeType) {
578: $min = null;
579: $max = null;
580: if ($this->getMin() !== null && $exponent->getMin() !== null) {
581: $min = $this->getMin() ** $exponent->getMin();
582: }
583: if ($this->getMax() !== null && $exponent->getMax() !== null) {
584: $max = $this->getMax() ** $exponent->getMax();
585: }
586:
587: if (($min !== null || $max !== null) && !is_float($min) && !is_float($max)) {
588: return self::fromInterval($min, $max);
589: }
590: }
591:
592: if ($exponent instanceof ConstantScalarType) {
593: $exponentValue = $exponent->getValue();
594: if (is_int($exponentValue)) {
595: $min = null;
596: $max = null;
597: if ($this->getMin() !== null) {
598: $min = $this->getMin() ** $exponentValue;
599: }
600: if ($this->getMax() !== null) {
601: $max = $this->getMax() ** $exponentValue;
602: }
603:
604: if (!is_float($min) && !is_float($max)) {
605: return self::fromInterval($min, $max);
606: }
607: }
608: }
609:
610: return parent::exponentiate($exponent);
611: }
612:
613: /**
614: * @param mixed[] $properties
615: */
616: public static function __set_state(array $properties): Type
617: {
618: return new self($properties['min'], $properties['max']);
619: }
620:
621: }
622: