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