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 toAbsoluteNumber(): Type
451: {
452: if ($this->min !== null && $this->min >= 0) {
453: return $this;
454: }
455:
456: if ($this->max === null || $this->max >= 0) {
457: $inversedMin = $this->min !== null ? $this->min * -1 : null;
458:
459: return self::fromInterval(0, $inversedMin !== null && $this->max !== null ? max($inversedMin, $this->max) : null);
460: }
461:
462: return self::fromInterval($this->max * -1, $this->min !== null ? $this->min * -1 : null);
463: }
464:
465: public function toString(): Type
466: {
467: $isZero = (new ConstantIntegerType(0))->isSuperTypeOf($this);
468: if ($isZero->no()) {
469: return new IntersectionType([
470: new StringType(),
471: new AccessoryNumericStringType(),
472: new AccessoryNonFalsyStringType(),
473: ]);
474: }
475:
476: return new IntersectionType([
477: new StringType(),
478: new AccessoryNumericStringType(),
479: ]);
480: }
481:
482: /**
483: * Return the union with another type, but only if it can be expressed in a simpler way than using UnionType
484: *
485: */
486: public function tryUnion(Type $otherType): ?Type
487: {
488: if ($otherType instanceof self || $otherType instanceof ConstantIntegerType) {
489: if ($otherType instanceof self) {
490: $otherMin = $otherType->min;
491: $otherMax = $otherType->max;
492: } else {
493: $otherMin = $otherType->getValue();
494: $otherMax = $otherType->getValue();
495: }
496:
497: if (self::isDisjoint($this->min, $this->max, $otherMin, $otherMax, false)) {
498: return null;
499: }
500:
501: return self::fromInterval(
502: $this->min !== null && $otherMin !== null ? min($this->min, $otherMin) : null,
503: $this->max !== null && $otherMax !== null ? max($this->max, $otherMax) : null,
504: );
505: }
506:
507: if (get_class($otherType) === parent::class) {
508: return $otherType;
509: }
510:
511: return null;
512: }
513:
514: /**
515: * Return the intersection with another type, but only if it can be expressed in a simpler way than using
516: * IntersectionType
517: *
518: */
519: public function tryIntersect(Type $otherType): ?Type
520: {
521: if ($otherType instanceof self || $otherType instanceof ConstantIntegerType) {
522: if ($otherType instanceof self) {
523: $otherMin = $otherType->min;
524: $otherMax = $otherType->max;
525: } else {
526: $otherMin = $otherType->getValue();
527: $otherMax = $otherType->getValue();
528: }
529:
530: if (self::isDisjoint($this->min, $this->max, $otherMin, $otherMax, false)) {
531: return new NeverType();
532: }
533:
534: if ($this->min === null) {
535: $newMin = $otherMin;
536: } elseif ($otherMin === null) {
537: $newMin = $this->min;
538: } else {
539: $newMin = max($this->min, $otherMin);
540: }
541:
542: if ($this->max === null) {
543: $newMax = $otherMax;
544: } elseif ($otherMax === null) {
545: $newMax = $this->max;
546: } else {
547: $newMax = min($this->max, $otherMax);
548: }
549:
550: return self::fromInterval($newMin, $newMax);
551: }
552:
553: if (get_class($otherType) === parent::class) {
554: return $this;
555: }
556:
557: return null;
558: }
559:
560: /**
561: * Return the different with another type, or null if it cannot be represented.
562: *
563: */
564: public function tryRemove(Type $typeToRemove): ?Type
565: {
566: if (get_class($typeToRemove) === parent::class) {
567: return new NeverType();
568: }
569:
570: if ($typeToRemove instanceof self || $typeToRemove instanceof ConstantIntegerType) {
571: if ($typeToRemove instanceof self) {
572: $removeMin = $typeToRemove->min;
573: $removeMax = $typeToRemove->max;
574: } else {
575: $removeMin = $typeToRemove->getValue();
576: $removeMax = $typeToRemove->getValue();
577: }
578:
579: if (
580: $this->min !== null && $removeMax !== null && $removeMax < $this->min
581: || $this->max !== null && $removeMin !== null && $this->max < $removeMin
582: ) {
583: return $this;
584: }
585:
586: if ($removeMin !== null && $removeMin !== PHP_INT_MIN) {
587: $lowerPart = self::fromInterval($this->min, $removeMin - 1);
588: } else {
589: $lowerPart = null;
590: }
591: if ($removeMax !== null && $removeMax !== PHP_INT_MAX) {
592: $upperPart = self::fromInterval($removeMax + 1, $this->max);
593: } else {
594: $upperPart = null;
595: }
596:
597: if ($lowerPart !== null && $upperPart !== null) {
598: return TypeCombinator::union($lowerPart, $upperPart);
599: }
600:
601: return $lowerPart ?? $upperPart;
602: }
603:
604: return null;
605: }
606:
607: public function exponentiate(Type $exponent): Type
608: {
609: if ($exponent instanceof UnionType) {
610: $results = [];
611: foreach ($exponent->getTypes() as $unionType) {
612: $results[] = $this->exponentiate($unionType);
613: }
614: return TypeCombinator::union(...$results);
615: }
616:
617: if ($exponent instanceof IntegerRangeType) {
618: $min = null;
619: $max = null;
620: if ($this->getMin() !== null && $exponent->getMin() !== null) {
621: $min = $this->getMin() ** $exponent->getMin();
622: }
623: if ($this->getMax() !== null && $exponent->getMax() !== null) {
624: $max = $this->getMax() ** $exponent->getMax();
625: }
626:
627: if (($min !== null || $max !== null) && !is_float($min) && !is_float($max)) {
628: return self::fromInterval($min, $max);
629: }
630: }
631:
632: if ($exponent instanceof ConstantScalarType) {
633: $exponentValue = $exponent->getValue();
634: if (is_int($exponentValue)) {
635: $min = null;
636: $max = null;
637: if ($this->getMin() !== null) {
638: $min = $this->getMin() ** $exponentValue;
639: }
640: if ($this->getMax() !== null) {
641: $max = $this->getMax() ** $exponentValue;
642: }
643:
644: if (!is_float($min) && !is_float($max)) {
645: return self::fromInterval($min, $max);
646: }
647: }
648: }
649:
650: return parent::exponentiate($exponent);
651: }
652:
653: /**
654: * @return list<ConstantIntegerType>
655: */
656: public function getFiniteTypes(): array
657: {
658: if ($this->min === null || $this->max === null) {
659: return [];
660: }
661:
662: $size = $this->max - $this->min;
663: if ($size > InitializerExprTypeResolver::CALCULATE_SCALARS_LIMIT) {
664: return [];
665: }
666:
667: $types = [];
668: for ($i = $this->min; $i <= $this->max; $i++) {
669: $types[] = new ConstantIntegerType($i);
670: }
671:
672: return $types;
673: }
674:
675: public function toPhpDocNode(): TypeNode
676: {
677: if ($this->min === null) {
678: $min = new IdentifierTypeNode('min');
679: } else {
680: $min = new ConstTypeNode(new ConstExprIntegerNode((string) $this->min));
681: }
682:
683: if ($this->max === null) {
684: $max = new IdentifierTypeNode('max');
685: } else {
686: $max = new ConstTypeNode(new ConstExprIntegerNode((string) $this->max));
687: }
688:
689: return new GenericTypeNode(new IdentifierTypeNode('int'), [$min, $max]);
690: }
691:
692: /**
693: * @param mixed[] $properties
694: */
695: public static function __set_state(array $properties): Type
696: {
697: return new self($properties['min'], $properties['max']);
698: }
699:
700: }
701: