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