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