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: return TrinaryLogic::extremeIdentity($minIsSmaller, $maxIsSmaller);
319: }
320:
321: public function isSmallerThanOrEqual(Type $otherType, PhpVersion $phpVersion): TrinaryLogic
322: {
323: if ($this->min === null) {
324: $minIsSmaller = TrinaryLogic::createYes();
325: } else {
326: $minIsSmaller = (new ConstantIntegerType($this->min))->isSmallerThanOrEqual($otherType, $phpVersion);
327: }
328:
329: if ($this->max === null) {
330: $maxIsSmaller = TrinaryLogic::createNo();
331: } else {
332: $maxIsSmaller = (new ConstantIntegerType($this->max))->isSmallerThanOrEqual($otherType, $phpVersion);
333: }
334:
335: return TrinaryLogic::extremeIdentity($minIsSmaller, $maxIsSmaller);
336: }
337:
338: public function isGreaterThan(Type $otherType, PhpVersion $phpVersion): TrinaryLogic
339: {
340: if ($this->min === null) {
341: $minIsSmaller = TrinaryLogic::createNo();
342: } else {
343: $minIsSmaller = $otherType->isSmallerThan((new ConstantIntegerType($this->min)), $phpVersion);
344: }
345:
346: if ($this->max === null) {
347: $maxIsSmaller = TrinaryLogic::createYes();
348: } else {
349: $maxIsSmaller = $otherType->isSmallerThan((new ConstantIntegerType($this->max)), $phpVersion);
350: }
351:
352: return TrinaryLogic::extremeIdentity($minIsSmaller, $maxIsSmaller);
353: }
354:
355: public function isGreaterThanOrEqual(Type $otherType, PhpVersion $phpVersion): TrinaryLogic
356: {
357: if ($this->min === null) {
358: $minIsSmaller = TrinaryLogic::createNo();
359: } else {
360: $minIsSmaller = $otherType->isSmallerThanOrEqual((new ConstantIntegerType($this->min)), $phpVersion);
361: }
362:
363: if ($this->max === null) {
364: $maxIsSmaller = TrinaryLogic::createYes();
365: } else {
366: $maxIsSmaller = $otherType->isSmallerThanOrEqual((new ConstantIntegerType($this->max)), $phpVersion);
367: }
368:
369: return TrinaryLogic::extremeIdentity($minIsSmaller, $maxIsSmaller);
370: }
371:
372: public function getSmallerType(PhpVersion $phpVersion): Type
373: {
374: $subtractedTypes = [
375: new ConstantBooleanType(true),
376: ];
377:
378: if ($this->max !== null) {
379: $subtractedTypes[] = self::createAllGreaterThanOrEqualTo($this->max);
380: }
381:
382: return TypeCombinator::remove(new MixedType(), TypeCombinator::union(...$subtractedTypes));
383: }
384:
385: public function getSmallerOrEqualType(PhpVersion $phpVersion): Type
386: {
387: $subtractedTypes = [];
388:
389: if ($this->max !== null) {
390: $subtractedTypes[] = self::createAllGreaterThan($this->max);
391: }
392:
393: return TypeCombinator::remove(new MixedType(), TypeCombinator::union(...$subtractedTypes));
394: }
395:
396: public function getGreaterType(PhpVersion $phpVersion): Type
397: {
398: $subtractedTypes = [
399: new NullType(),
400: new ConstantBooleanType(false),
401: ];
402:
403: if ($this->min !== null) {
404: $subtractedTypes[] = self::createAllSmallerThanOrEqualTo($this->min);
405: }
406:
407: if ($this->min !== null && $this->min > 0 || $this->max !== null && $this->max < 0) {
408: $subtractedTypes[] = new ConstantBooleanType(true);
409: }
410:
411: return TypeCombinator::remove(new MixedType(), TypeCombinator::union(...$subtractedTypes));
412: }
413:
414: public function getGreaterOrEqualType(PhpVersion $phpVersion): Type
415: {
416: $subtractedTypes = [];
417:
418: if ($this->min !== null) {
419: $subtractedTypes[] = self::createAllSmallerThan($this->min);
420: }
421:
422: if ($this->min !== null && $this->min > 0 || $this->max !== null && $this->max < 0) {
423: $subtractedTypes[] = new NullType();
424: $subtractedTypes[] = new ConstantBooleanType(false);
425: }
426:
427: return TypeCombinator::remove(new MixedType(), TypeCombinator::union(...$subtractedTypes));
428: }
429:
430: public function toBoolean(): BooleanType
431: {
432: $isZero = (new ConstantIntegerType(0))->isSuperTypeOf($this);
433: if ($isZero->no()) {
434: return new ConstantBooleanType(true);
435: }
436:
437: if ($isZero->maybe()) {
438: return new BooleanType();
439: }
440:
441: return new ConstantBooleanType(false);
442: }
443:
444: public function toAbsoluteNumber(): Type
445: {
446: if ($this->min !== null && $this->min >= 0) {
447: return $this;
448: }
449:
450: if ($this->max === null || $this->max >= 0) {
451: $inversedMin = $this->min !== null ? $this->min * -1 : null;
452:
453: return self::fromInterval(0, $inversedMin !== null && $this->max !== null ? max($inversedMin, $this->max) : null);
454: }
455:
456: return self::fromInterval($this->max * -1, $this->min !== null ? $this->min * -1 : null);
457: }
458:
459: public function toString(): Type
460: {
461: $finiteTypes = $this->getFiniteTypes();
462: if ($finiteTypes !== []) {
463: return TypeCombinator::union(...$finiteTypes)->toString();
464: }
465:
466: $isZero = (new ConstantIntegerType(0))->isSuperTypeOf($this);
467: if ($isZero->no()) {
468: return new IntersectionType([
469: new StringType(),
470: new AccessoryLowercaseStringType(),
471: new AccessoryUppercaseStringType(),
472: new AccessoryNumericStringType(),
473: new AccessoryNonFalsyStringType(),
474: ]);
475: }
476:
477: return new IntersectionType([
478: new StringType(),
479: new AccessoryLowercaseStringType(),
480: new AccessoryUppercaseStringType(),
481: new AccessoryNumericStringType(),
482: ]);
483: }
484:
485: /**
486: * Return the union with another type, but only if it can be expressed in a simpler way than using UnionType
487: *
488: */
489: public function tryUnion(Type $otherType): ?Type
490: {
491: if ($otherType instanceof self || $otherType instanceof ConstantIntegerType) {
492: if ($otherType instanceof self) {
493: $otherMin = $otherType->min;
494: $otherMax = $otherType->max;
495: } else {
496: $otherMin = $otherType->getValue();
497: $otherMax = $otherType->getValue();
498: }
499:
500: if (self::isDisjoint($this->min, $this->max, $otherMin, $otherMax, false)) {
501: return null;
502: }
503:
504: return self::fromInterval(
505: $this->min !== null && $otherMin !== null ? min($this->min, $otherMin) : null,
506: $this->max !== null && $otherMax !== null ? max($this->max, $otherMax) : null,
507: );
508: }
509:
510: if (get_class($otherType) === parent::class) {
511: return $otherType;
512: }
513:
514: return null;
515: }
516:
517: /**
518: * Return the intersection with another type, but only if it can be expressed in a simpler way than using
519: * IntersectionType
520: *
521: */
522: public function tryIntersect(Type $otherType): ?Type
523: {
524: if ($otherType instanceof self || $otherType instanceof ConstantIntegerType) {
525: if ($otherType instanceof self) {
526: $otherMin = $otherType->min;
527: $otherMax = $otherType->max;
528: } else {
529: $otherMin = $otherType->getValue();
530: $otherMax = $otherType->getValue();
531: }
532:
533: if (self::isDisjoint($this->min, $this->max, $otherMin, $otherMax, false)) {
534: return new NeverType();
535: }
536:
537: if ($this->min === null) {
538: $newMin = $otherMin;
539: } elseif ($otherMin === null) {
540: $newMin = $this->min;
541: } else {
542: $newMin = max($this->min, $otherMin);
543: }
544:
545: if ($this->max === null) {
546: $newMax = $otherMax;
547: } elseif ($otherMax === null) {
548: $newMax = $this->max;
549: } else {
550: $newMax = min($this->max, $otherMax);
551: }
552:
553: return self::fromInterval($newMin, $newMax);
554: }
555:
556: if (get_class($otherType) === parent::class) {
557: return $this;
558: }
559:
560: return null;
561: }
562:
563: /**
564: * Return the different with another type, or null if it cannot be represented.
565: *
566: */
567: public function tryRemove(Type $typeToRemove): ?Type
568: {
569: if (get_class($typeToRemove) === parent::class) {
570: return new NeverType();
571: }
572:
573: if ($typeToRemove instanceof self || $typeToRemove instanceof ConstantIntegerType) {
574: if ($typeToRemove instanceof self) {
575: $removeMin = $typeToRemove->min;
576: $removeMax = $typeToRemove->max;
577: } else {
578: $removeMin = $typeToRemove->getValue();
579: $removeMax = $typeToRemove->getValue();
580: }
581:
582: if (
583: $this->min !== null && $removeMax !== null && $removeMax < $this->min
584: || $this->max !== null && $removeMin !== null && $this->max < $removeMin
585: ) {
586: return $this;
587: }
588:
589: if ($removeMin !== null && $removeMin !== PHP_INT_MIN) {
590: $lowerPart = self::fromInterval($this->min, $removeMin - 1);
591: } else {
592: $lowerPart = null;
593: }
594: if ($removeMax !== null && $removeMax !== PHP_INT_MAX) {
595: $upperPart = self::fromInterval($removeMax + 1, $this->max);
596: } else {
597: $upperPart = null;
598: }
599:
600: if ($lowerPart !== null && $upperPart !== null) {
601: return TypeCombinator::union($lowerPart, $upperPart);
602: }
603:
604: return $lowerPart ?? $upperPart;
605: }
606:
607: return null;
608: }
609:
610: public function exponentiate(Type $exponent): Type
611: {
612: if ($exponent instanceof UnionType) {
613: $results = [];
614: foreach ($exponent->getTypes() as $unionType) {
615: $results[] = $this->exponentiate($unionType);
616: }
617: return TypeCombinator::union(...$results);
618: }
619:
620: if ($exponent instanceof IntegerRangeType) {
621: $min = null;
622: $max = null;
623: if ($this->getMin() !== null && $exponent->getMin() !== null) {
624: $min = $this->getMin() ** $exponent->getMin();
625: }
626: if ($this->getMax() !== null && $exponent->getMax() !== null) {
627: $max = $this->getMax() ** $exponent->getMax();
628: }
629:
630: if (($min !== null || $max !== null) && !is_float($min) && !is_float($max)) {
631: return self::fromInterval($min, $max);
632: }
633: }
634:
635: if ($exponent instanceof ConstantScalarType) {
636: $exponentValue = $exponent->getValue();
637: if (is_int($exponentValue)) {
638: $min = null;
639: $max = null;
640: if ($this->getMin() !== null) {
641: $min = $this->getMin() ** $exponentValue;
642: }
643: if ($this->getMax() !== null) {
644: $max = $this->getMax() ** $exponentValue;
645: }
646:
647: if (!is_float($min) && !is_float($max)) {
648: return self::fromInterval($min, $max);
649: }
650: }
651: }
652:
653: return parent::exponentiate($exponent);
654: }
655:
656: /**
657: * @return list<ConstantIntegerType>
658: */
659: public function getFiniteTypes(): array
660: {
661: if ($this->min === null || $this->max === null) {
662: return [];
663: }
664:
665: $size = $this->max - $this->min;
666: if ($size > InitializerExprTypeResolver::CALCULATE_SCALARS_LIMIT) {
667: return [];
668: }
669:
670: $types = [];
671: for ($i = $this->min; $i <= $this->max; $i++) {
672: $types[] = new ConstantIntegerType($i);
673: }
674:
675: return $types;
676: }
677:
678: public function toPhpDocNode(): TypeNode
679: {
680: if ($this->min === null) {
681: $min = new IdentifierTypeNode('min');
682: } else {
683: $min = new ConstTypeNode(new ConstExprIntegerNode((string) $this->min));
684: }
685:
686: if ($this->max === null) {
687: $max = new IdentifierTypeNode('max');
688: } else {
689: $max = new ConstTypeNode(new ConstExprIntegerNode((string) $this->max));
690: }
691:
692: return new GenericTypeNode(new IdentifierTypeNode('int'), [$min, $max]);
693: }
694:
695: public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType
696: {
697: if ($this->isSmallerThan($type, $phpVersion)->yes() || $this->isGreaterThan($type, $phpVersion)->yes()) {
698: return new ConstantBooleanType(false);
699: }
700:
701: return parent::looseCompare($type, $phpVersion);
702: }
703:
704: }
705: