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\Constant\ConstantBooleanType;
17: use PHPStan\Type\Constant\ConstantIntegerType;
18: use function array_filter;
19: use function array_map;
20: use function assert;
21: use function ceil;
22: use function count;
23: use function floor;
24: use function get_class;
25: use function is_float;
26: use function is_int;
27: use function max;
28: use function min;
29: use function sprintf;
30: use const PHP_INT_MAX;
31: use const PHP_INT_MIN;
32:
33: /** @api */
34: class IntegerRangeType extends IntegerType implements CompoundType
35: {
36:
37: private function __construct(private ?int $min, private ?int $max)
38: {
39: parent::__construct();
40: assert($min === null || $max === null || $min <= $max);
41: assert($min !== null || $max !== null);
42: }
43:
44: public static function fromInterval(?int $min, ?int $max, int $shift = 0): Type
45: {
46: if ($min !== null && $max !== null) {
47: if ($min > $max) {
48: return new NeverType();
49: }
50: if ($min === $max) {
51: return new ConstantIntegerType($min + $shift);
52: }
53: }
54:
55: if ($min === null && $max === null) {
56: return new IntegerType();
57: }
58:
59: return (new self($min, $max))->shift($shift);
60: }
61:
62: protected static function isDisjoint(?int $minA, ?int $maxA, ?int $minB, ?int $maxB, bool $touchingIsDisjoint = true): bool
63: {
64: $offset = $touchingIsDisjoint ? 0 : 1;
65: return $minA !== null && $maxB !== null && $minA > $maxB + $offset
66: || $maxA !== null && $minB !== null && $maxA + $offset < $minB;
67: }
68:
69: /**
70: * Return the range of integers smaller than the given value
71: *
72: * @param int|float $value
73: */
74: public static function createAllSmallerThan($value): Type
75: {
76: if (is_int($value)) {
77: return self::fromInterval(null, $value, -1);
78: }
79:
80: if ($value > PHP_INT_MAX) {
81: return new IntegerType();
82: }
83:
84: if ($value <= PHP_INT_MIN) {
85: return new NeverType();
86: }
87:
88: return self::fromInterval(null, (int) ceil($value), -1);
89: }
90:
91: /**
92: * Return the range of integers smaller than or equal to the given value
93: *
94: * @param int|float $value
95: */
96: public static function createAllSmallerThanOrEqualTo($value): Type
97: {
98: if (is_int($value)) {
99: return self::fromInterval(null, $value);
100: }
101:
102: if ($value >= PHP_INT_MAX) {
103: return new IntegerType();
104: }
105:
106: if ($value < PHP_INT_MIN) {
107: return new NeverType();
108: }
109:
110: return self::fromInterval(null, (int) floor($value));
111: }
112:
113: /**
114: * Return the range of integers greater than the given value
115: *
116: * @param int|float $value
117: */
118: public static function createAllGreaterThan($value): Type
119: {
120: if (is_int($value)) {
121: return self::fromInterval($value, null, 1);
122: }
123:
124: if ($value < PHP_INT_MIN) {
125: return new IntegerType();
126: }
127:
128: if ($value >= PHP_INT_MAX) {
129: return new NeverType();
130: }
131:
132: return self::fromInterval((int) floor($value), null, 1);
133: }
134:
135: /**
136: * Return the range of integers greater than or equal to the given value
137: *
138: * @param int|float $value
139: */
140: public static function createAllGreaterThanOrEqualTo($value): Type
141: {
142: if (is_int($value)) {
143: return self::fromInterval($value, null);
144: }
145:
146: if ($value <= PHP_INT_MIN) {
147: return new IntegerType();
148: }
149:
150: if ($value > PHP_INT_MAX) {
151: return new NeverType();
152: }
153:
154: return self::fromInterval((int) ceil($value), null);
155: }
156:
157: public function getMin(): ?int
158: {
159: return $this->min;
160: }
161:
162: public function getMax(): ?int
163: {
164: return $this->max;
165: }
166:
167: public function describe(VerbosityLevel $level): string
168: {
169: return sprintf('int<%s, %s>', $this->min ?? 'min', $this->max ?? 'max');
170: }
171:
172: public function shift(int $amount): Type
173: {
174: if ($amount === 0) {
175: return $this;
176: }
177:
178: $min = $this->min;
179: $max = $this->max;
180:
181: if ($amount < 0) {
182: if ($max !== null) {
183: if ($max < PHP_INT_MIN - $amount) {
184: return new NeverType();
185: }
186: $max += $amount;
187: }
188: if ($min !== null) {
189: $min = $min < PHP_INT_MIN - $amount ? null : $min + $amount;
190: }
191: } else {
192: if ($min !== null) {
193: if ($min > PHP_INT_MAX - $amount) {
194: return new NeverType();
195: }
196: $min += $amount;
197: }
198: if ($max !== null) {
199: $max = $max > PHP_INT_MAX - $amount ? null : $max + $amount;
200: }
201: }
202:
203: return self::fromInterval($min, $max);
204: }
205:
206: public function accepts(Type $type, bool $strictTypes): AcceptsResult
207: {
208: if ($type instanceof parent) {
209: return $this->isSuperTypeOf($type)->toAcceptsResult();
210: }
211:
212: if ($type instanceof CompoundType) {
213: return $type->isAcceptedBy($this, $strictTypes);
214: }
215:
216: return AcceptsResult::createNo();
217: }
218:
219: public function isSuperTypeOf(Type $type): IsSuperTypeOfResult
220: {
221: if ($type instanceof self || $type instanceof ConstantIntegerType) {
222: if ($type instanceof self) {
223: $typeMin = $type->min;
224: $typeMax = $type->max;
225: } else {
226: $typeMin = $type->getValue();
227: $typeMax = $type->getValue();
228: }
229:
230: if (self::isDisjoint($this->min, $this->max, $typeMin, $typeMax)) {
231: return IsSuperTypeOfResult::createNo();
232: }
233:
234: if (
235: ($this->min === null || $typeMin !== null && $this->min <= $typeMin)
236: && ($this->max === null || $typeMax !== null && $this->max >= $typeMax)
237: ) {
238: return IsSuperTypeOfResult::createYes();
239: }
240:
241: return IsSuperTypeOfResult::createMaybe();
242: }
243:
244: if ($type instanceof parent) {
245: return IsSuperTypeOfResult::createMaybe();
246: }
247:
248: if ($type instanceof CompoundType) {
249: return $type->isSubTypeOf($this);
250: }
251:
252: return IsSuperTypeOfResult::createNo();
253: }
254:
255: public function isSubTypeOf(Type $otherType): IsSuperTypeOfResult
256: {
257: if ($otherType instanceof parent) {
258: return $otherType->isSuperTypeOf($this);
259: }
260:
261: if ($otherType instanceof UnionType) {
262: return $this->isSubTypeOfUnionWithReason($otherType);
263: }
264:
265: if ($otherType instanceof IntersectionType) {
266: return $otherType->isSuperTypeOf($this);
267: }
268:
269: return IsSuperTypeOfResult::createNo();
270: }
271:
272: private function isSubTypeOfUnionWithReason(UnionType $otherType): IsSuperTypeOfResult
273: {
274: if ($this->min !== null && $this->max !== null) {
275: $matchingConstantIntegers = array_filter(
276: $otherType->getTypes(),
277: fn (Type $type): bool => $type instanceof ConstantIntegerType && $type->getValue() >= $this->min && $type->getValue() <= $this->max,
278: );
279:
280: if (count($matchingConstantIntegers) === ($this->max - $this->min + 1)) {
281: return IsSuperTypeOfResult::createYes();
282: }
283: }
284:
285: return IsSuperTypeOfResult::createNo()->or(...array_map(fn (Type $innerType) => $this->isSubTypeOf($innerType), $otherType->getTypes()));
286: }
287:
288: public function isAcceptedBy(Type $acceptingType, bool $strictTypes): AcceptsResult
289: {
290: return $this->isSubTypeOf($acceptingType)->toAcceptsResult();
291: }
292:
293: public function equals(Type $type): bool
294: {
295: return $type instanceof self && $this->min === $type->min && $this->max === $type->max;
296: }
297:
298: public function generalize(GeneralizePrecision $precision): Type
299: {
300: return new IntegerType();
301: }
302:
303: public function isSmallerThan(Type $otherType, PhpVersion $phpVersion): TrinaryLogic
304: {
305: if ($this->min === null) {
306: $minIsSmaller = TrinaryLogic::createYes();
307: } else {
308: $minIsSmaller = (new ConstantIntegerType($this->min))->isSmallerThan($otherType, $phpVersion);
309: }
310:
311: if ($this->max === null) {
312: $maxIsSmaller = TrinaryLogic::createNo();
313: } else {
314: $maxIsSmaller = (new ConstantIntegerType($this->max))->isSmallerThan($otherType, $phpVersion);
315: }
316:
317: return TrinaryLogic::extremeIdentity($minIsSmaller, $maxIsSmaller);
318: }
319:
320: public function isSmallerThanOrEqual(Type $otherType, PhpVersion $phpVersion): TrinaryLogic
321: {
322: if ($this->min === null) {
323: $minIsSmaller = TrinaryLogic::createYes();
324: } else {
325: $minIsSmaller = (new ConstantIntegerType($this->min))->isSmallerThanOrEqual($otherType, $phpVersion);
326: }
327:
328: if ($this->max === null) {
329: $maxIsSmaller = TrinaryLogic::createNo();
330: } else {
331: $maxIsSmaller = (new ConstantIntegerType($this->max))->isSmallerThanOrEqual($otherType, $phpVersion);
332: }
333:
334: return TrinaryLogic::extremeIdentity($minIsSmaller, $maxIsSmaller);
335: }
336:
337: public function isGreaterThan(Type $otherType, PhpVersion $phpVersion): TrinaryLogic
338: {
339: if ($this->min === null) {
340: $minIsSmaller = TrinaryLogic::createNo();
341: } else {
342: $minIsSmaller = $otherType->isSmallerThan((new ConstantIntegerType($this->min)), $phpVersion);
343: }
344:
345: if ($this->max === null) {
346: $maxIsSmaller = TrinaryLogic::createYes();
347: } else {
348: $maxIsSmaller = $otherType->isSmallerThan((new ConstantIntegerType($this->max)), $phpVersion);
349: }
350:
351: return TrinaryLogic::extremeIdentity($minIsSmaller, $maxIsSmaller);
352: }
353:
354: public function isGreaterThanOrEqual(Type $otherType, PhpVersion $phpVersion): TrinaryLogic
355: {
356: if ($this->min === null) {
357: $minIsSmaller = TrinaryLogic::createNo();
358: } else {
359: $minIsSmaller = $otherType->isSmallerThanOrEqual((new ConstantIntegerType($this->min)), $phpVersion);
360: }
361:
362: if ($this->max === null) {
363: $maxIsSmaller = TrinaryLogic::createYes();
364: } else {
365: $maxIsSmaller = $otherType->isSmallerThanOrEqual((new ConstantIntegerType($this->max)), $phpVersion);
366: }
367:
368: return TrinaryLogic::extremeIdentity($minIsSmaller, $maxIsSmaller);
369: }
370:
371: public function getSmallerType(PhpVersion $phpVersion): Type
372: {
373: $subtractedTypes = [
374: new ConstantBooleanType(true),
375: ];
376:
377: if ($this->max !== null) {
378: $subtractedTypes[] = self::createAllGreaterThanOrEqualTo($this->max);
379: }
380:
381: return TypeCombinator::remove(new MixedType(), TypeCombinator::union(...$subtractedTypes));
382: }
383:
384: public function getSmallerOrEqualType(PhpVersion $phpVersion): Type
385: {
386: $subtractedTypes = [];
387:
388: if ($this->max !== null) {
389: $subtractedTypes[] = self::createAllGreaterThan($this->max);
390: }
391:
392: return TypeCombinator::remove(new MixedType(), TypeCombinator::union(...$subtractedTypes));
393: }
394:
395: public function getGreaterType(PhpVersion $phpVersion): Type
396: {
397: $subtractedTypes = [
398: new NullType(),
399: new ConstantBooleanType(false),
400: ];
401:
402: if ($this->min !== null) {
403: $subtractedTypes[] = self::createAllSmallerThanOrEqualTo($this->min);
404: }
405:
406: if ($this->min !== null && $this->min > 0 || $this->max !== null && $this->max < 0) {
407: $subtractedTypes[] = new ConstantBooleanType(true);
408: }
409:
410: return TypeCombinator::remove(new MixedType(), TypeCombinator::union(...$subtractedTypes));
411: }
412:
413: public function getGreaterOrEqualType(PhpVersion $phpVersion): Type
414: {
415: $subtractedTypes = [];
416:
417: if ($this->min !== null) {
418: $subtractedTypes[] = self::createAllSmallerThan($this->min);
419: }
420:
421: if ($this->min !== null && $this->min > 0 || $this->max !== null && $this->max < 0) {
422: $subtractedTypes[] = new NullType();
423: $subtractedTypes[] = new ConstantBooleanType(false);
424: }
425:
426: return TypeCombinator::remove(new MixedType(), TypeCombinator::union(...$subtractedTypes));
427: }
428:
429: public function toBoolean(): BooleanType
430: {
431: $isZero = (new ConstantIntegerType(0))->isSuperTypeOf($this);
432: if ($isZero->no()) {
433: return new ConstantBooleanType(true);
434: }
435:
436: if ($isZero->maybe()) {
437: return new BooleanType();
438: }
439:
440: return new ConstantBooleanType(false);
441: }
442:
443: public function toAbsoluteNumber(): Type
444: {
445: if ($this->min !== null && $this->min >= 0) {
446: return $this;
447: }
448:
449: if ($this->max === null || $this->max >= 0) {
450: $inversedMin = $this->min !== null ? $this->min * -1 : null;
451:
452: return self::fromInterval(0, $inversedMin !== null && $this->max !== null ? max($inversedMin, $this->max) : null);
453: }
454:
455: return self::fromInterval($this->max * -1, $this->min !== null ? $this->min * -1 : null);
456: }
457:
458: public function toString(): Type
459: {
460: $finiteTypes = $this->getFiniteTypes();
461: if ($finiteTypes !== []) {
462: return TypeCombinator::union(...$finiteTypes)->toString();
463: }
464:
465: $isZero = (new ConstantIntegerType(0))->isSuperTypeOf($this);
466: if ($isZero->no()) {
467: return new IntersectionType([
468: new StringType(),
469: new AccessoryLowercaseStringType(),
470: new AccessoryNumericStringType(),
471: new AccessoryNonFalsyStringType(),
472: ]);
473: }
474:
475: return new IntersectionType([
476: new StringType(),
477: new AccessoryLowercaseStringType(),
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: public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType
693: {
694: if ($this->isSmallerThan($type, $phpVersion)->yes() || $this->isGreaterThan($type, $phpVersion)->yes()) {
695: return new ConstantBooleanType(false);
696: }
697:
698: return parent::looseCompare($type, $phpVersion);
699: }
700:
701: }
702: