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