1: <?php declare(strict_types = 1);
2:
3: namespace PHPStan\Type;
4:
5: use PHPStan\TrinaryLogic;
6: use PHPStan\Type\Constant\ConstantBooleanType;
7: use PHPStan\Type\Constant\ConstantIntegerType;
8: use function array_filter;
9: use function assert;
10: use function ceil;
11: use function count;
12: use function floor;
13: use function get_class;
14: use function is_int;
15: use function max;
16: use function min;
17: use function sprintf;
18: use const PHP_INT_MAX;
19: use const PHP_INT_MIN;
20:
21: /** @api */
22: class IntegerRangeType extends IntegerType implements CompoundType
23: {
24:
25: private function __construct(private ?int $min, private ?int $max)
26: {
27: parent::__construct();
28: assert($min === null || $max === null || $min <= $max);
29: assert($min !== null || $max !== null);
30: }
31:
32: public static function fromInterval(?int $min, ?int $max, int $shift = 0): Type
33: {
34: if ($min !== null && $max !== null) {
35: if ($min > $max) {
36: return new NeverType();
37: }
38: if ($min === $max) {
39: return new ConstantIntegerType($min + $shift);
40: }
41: }
42:
43: if ($min === null && $max === null) {
44: return new IntegerType();
45: }
46:
47: return (new self($min, $max))->shift($shift);
48: }
49:
50: protected static function isDisjoint(?int $minA, ?int $maxA, ?int $minB, ?int $maxB, bool $touchingIsDisjoint = true): bool
51: {
52: $offset = $touchingIsDisjoint ? 0 : 1;
53: return $minA !== null && $maxB !== null && $minA > $maxB + $offset
54: || $maxA !== null && $minB !== null && $maxA + $offset < $minB;
55: }
56:
57: /**
58: * Return the range of integers smaller than the given value
59: *
60: * @param int|float $value
61: */
62: public static function createAllSmallerThan($value): Type
63: {
64: if (is_int($value)) {
65: return self::fromInterval(null, $value, -1);
66: }
67:
68: if ($value > PHP_INT_MAX) {
69: return new IntegerType();
70: }
71:
72: if ($value <= PHP_INT_MIN) {
73: return new NeverType();
74: }
75:
76: return self::fromInterval(null, (int) ceil($value), -1);
77: }
78:
79: /**
80: * Return the range of integers smaller than or equal to the given value
81: *
82: * @param int|float $value
83: */
84: public static function createAllSmallerThanOrEqualTo($value): Type
85: {
86: if (is_int($value)) {
87: return self::fromInterval(null, $value);
88: }
89:
90: if ($value >= PHP_INT_MAX) {
91: return new IntegerType();
92: }
93:
94: if ($value < PHP_INT_MIN) {
95: return new NeverType();
96: }
97:
98: return self::fromInterval(null, (int) floor($value));
99: }
100:
101: /**
102: * Return the range of integers greater than the given value
103: *
104: * @param int|float $value
105: */
106: public static function createAllGreaterThan($value): Type
107: {
108: if (is_int($value)) {
109: return self::fromInterval($value, null, 1);
110: }
111:
112: if ($value < PHP_INT_MIN) {
113: return new IntegerType();
114: }
115:
116: if ($value >= PHP_INT_MAX) {
117: return new NeverType();
118: }
119:
120: return self::fromInterval((int) floor($value), null, 1);
121: }
122:
123: /**
124: * Return the range of integers greater than or equal to the given value
125: *
126: * @param int|float $value
127: */
128: public static function createAllGreaterThanOrEqualTo($value): Type
129: {
130: if (is_int($value)) {
131: return self::fromInterval($value, null);
132: }
133:
134: if ($value <= PHP_INT_MIN) {
135: return new IntegerType();
136: }
137:
138: if ($value > PHP_INT_MAX) {
139: return new NeverType();
140: }
141:
142: return self::fromInterval((int) ceil($value), null);
143: }
144:
145: public function getMin(): ?int
146: {
147: return $this->min;
148: }
149:
150: public function getMax(): ?int
151: {
152: return $this->max;
153: }
154:
155: public function describe(VerbosityLevel $level): string
156: {
157: return sprintf('int<%s, %s>', $this->min ?? 'min', $this->max ?? 'max');
158: }
159:
160: public function shift(int $amount): Type
161: {
162: if ($amount === 0) {
163: return $this;
164: }
165:
166: $min = $this->min;
167: $max = $this->max;
168:
169: if ($amount < 0) {
170: if ($max !== null) {
171: if ($max < PHP_INT_MIN - $amount) {
172: return new NeverType();
173: }
174: $max += $amount;
175: }
176: if ($min !== null) {
177: $min = $min < PHP_INT_MIN - $amount ? null : $min + $amount;
178: }
179: } else {
180: if ($min !== null) {
181: if ($min > PHP_INT_MAX - $amount) {
182: return new NeverType();
183: }
184: $min += $amount;
185: }
186: if ($max !== null) {
187: $max = $max > PHP_INT_MAX - $amount ? null : $max + $amount;
188: }
189: }
190:
191: return self::fromInterval($min, $max);
192: }
193:
194: public function accepts(Type $type, bool $strictTypes): TrinaryLogic
195: {
196: if ($type instanceof parent) {
197: return $this->isSuperTypeOf($type);
198: }
199:
200: if ($type instanceof CompoundType) {
201: return $type->isAcceptedBy($this, $strictTypes);
202: }
203:
204: return TrinaryLogic::createNo();
205: }
206:
207: public function isSuperTypeOf(Type $type): TrinaryLogic
208: {
209: if ($type instanceof self || $type instanceof ConstantIntegerType) {
210: if ($type instanceof self) {
211: $typeMin = $type->min;
212: $typeMax = $type->max;
213: } else {
214: $typeMin = $type->getValue();
215: $typeMax = $type->getValue();
216: }
217:
218: if (self::isDisjoint($this->min, $this->max, $typeMin, $typeMax)) {
219: return TrinaryLogic::createNo();
220: }
221:
222: if (
223: ($this->min === null || $typeMin !== null && $this->min <= $typeMin)
224: && ($this->max === null || $typeMax !== null && $this->max >= $typeMax)
225: ) {
226: return TrinaryLogic::createYes();
227: }
228:
229: return TrinaryLogic::createMaybe();
230: }
231:
232: if ($type instanceof parent) {
233: return TrinaryLogic::createMaybe();
234: }
235:
236: if ($type instanceof CompoundType) {
237: return $type->isSubTypeOf($this);
238: }
239:
240: return TrinaryLogic::createNo();
241: }
242:
243: public function isSubTypeOf(Type $otherType): TrinaryLogic
244: {
245: if ($otherType instanceof parent) {
246: return $otherType->isSuperTypeOf($this);
247: }
248:
249: if ($otherType instanceof UnionType) {
250: return $this->isSubTypeOfUnion($otherType);
251: }
252:
253: if ($otherType instanceof IntersectionType) {
254: return $otherType->isSuperTypeOf($this);
255: }
256:
257: return TrinaryLogic::createNo();
258: }
259:
260: private function isSubTypeOfUnion(UnionType $otherType): TrinaryLogic
261: {
262: if ($this->min !== null && $this->max !== null) {
263: $matchingConstantIntegers = array_filter(
264: $otherType->getTypes(),
265: fn (Type $type): bool => $type instanceof ConstantIntegerType && $type->getValue() >= $this->min && $type->getValue() <= $this->max,
266: );
267:
268: if (count($matchingConstantIntegers) === ($this->max - $this->min + 1)) {
269: return TrinaryLogic::createYes();
270: }
271: }
272:
273: return TrinaryLogic::createNo()->lazyOr($otherType->getTypes(), fn (Type $innerType) => $this->isSubTypeOf($innerType));
274: }
275:
276: public function isAcceptedBy(Type $acceptingType, bool $strictTypes): TrinaryLogic
277: {
278: return $this->isSubTypeOf($acceptingType);
279: }
280:
281: public function equals(Type $type): bool
282: {
283: return $type instanceof self && $this->min === $type->min && $this->max === $type->max;
284: }
285:
286: public function generalize(GeneralizePrecision $precision): Type
287: {
288: return new IntegerType();
289: }
290:
291: public function isSmallerThan(Type $otherType): TrinaryLogic
292: {
293: if ($this->min === null) {
294: $minIsSmaller = TrinaryLogic::createYes();
295: } else {
296: $minIsSmaller = (new ConstantIntegerType($this->min))->isSmallerThan($otherType);
297: }
298:
299: if ($this->max === null) {
300: $maxIsSmaller = TrinaryLogic::createNo();
301: } else {
302: $maxIsSmaller = (new ConstantIntegerType($this->max))->isSmallerThan($otherType);
303: }
304:
305: return TrinaryLogic::extremeIdentity($minIsSmaller, $maxIsSmaller);
306: }
307:
308: public function isSmallerThanOrEqual(Type $otherType): TrinaryLogic
309: {
310: if ($this->min === null) {
311: $minIsSmaller = TrinaryLogic::createYes();
312: } else {
313: $minIsSmaller = (new ConstantIntegerType($this->min))->isSmallerThanOrEqual($otherType);
314: }
315:
316: if ($this->max === null) {
317: $maxIsSmaller = TrinaryLogic::createNo();
318: } else {
319: $maxIsSmaller = (new ConstantIntegerType($this->max))->isSmallerThanOrEqual($otherType);
320: }
321:
322: return TrinaryLogic::extremeIdentity($minIsSmaller, $maxIsSmaller);
323: }
324:
325: public function isGreaterThan(Type $otherType): TrinaryLogic
326: {
327: if ($this->min === null) {
328: $minIsSmaller = TrinaryLogic::createNo();
329: } else {
330: $minIsSmaller = $otherType->isSmallerThan((new ConstantIntegerType($this->min)));
331: }
332:
333: if ($this->max === null) {
334: $maxIsSmaller = TrinaryLogic::createYes();
335: } else {
336: $maxIsSmaller = $otherType->isSmallerThan((new ConstantIntegerType($this->max)));
337: }
338:
339: return TrinaryLogic::extremeIdentity($minIsSmaller, $maxIsSmaller);
340: }
341:
342: public function isGreaterThanOrEqual(Type $otherType): TrinaryLogic
343: {
344: if ($this->min === null) {
345: $minIsSmaller = TrinaryLogic::createNo();
346: } else {
347: $minIsSmaller = $otherType->isSmallerThanOrEqual((new ConstantIntegerType($this->min)));
348: }
349:
350: if ($this->max === null) {
351: $maxIsSmaller = TrinaryLogic::createYes();
352: } else {
353: $maxIsSmaller = $otherType->isSmallerThanOrEqual((new ConstantIntegerType($this->max)));
354: }
355:
356: return TrinaryLogic::extremeIdentity($minIsSmaller, $maxIsSmaller);
357: }
358:
359: public function getSmallerType(): Type
360: {
361: $subtractedTypes = [
362: new ConstantBooleanType(true),
363: ];
364:
365: if ($this->max !== null) {
366: $subtractedTypes[] = self::createAllGreaterThanOrEqualTo($this->max);
367: }
368:
369: return TypeCombinator::remove(new MixedType(), TypeCombinator::union(...$subtractedTypes));
370: }
371:
372: public function getSmallerOrEqualType(): Type
373: {
374: $subtractedTypes = [];
375:
376: if ($this->max !== null) {
377: $subtractedTypes[] = self::createAllGreaterThan($this->max);
378: }
379:
380: return TypeCombinator::remove(new MixedType(), TypeCombinator::union(...$subtractedTypes));
381: }
382:
383: public function getGreaterType(): Type
384: {
385: $subtractedTypes = [
386: new NullType(),
387: new ConstantBooleanType(false),
388: ];
389:
390: if ($this->min !== null) {
391: $subtractedTypes[] = self::createAllSmallerThanOrEqualTo($this->min);
392: }
393:
394: if ($this->min !== null && $this->min > 0 || $this->max !== null && $this->max < 0) {
395: $subtractedTypes[] = new ConstantBooleanType(true);
396: }
397:
398: return TypeCombinator::remove(new MixedType(), TypeCombinator::union(...$subtractedTypes));
399: }
400:
401: public function getGreaterOrEqualType(): Type
402: {
403: $subtractedTypes = [];
404:
405: if ($this->min !== null) {
406: $subtractedTypes[] = self::createAllSmallerThan($this->min);
407: }
408:
409: if ($this->min !== null && $this->min > 0 || $this->max !== null && $this->max < 0) {
410: $subtractedTypes[] = new NullType();
411: $subtractedTypes[] = new ConstantBooleanType(false);
412: }
413:
414: return TypeCombinator::remove(new MixedType(), TypeCombinator::union(...$subtractedTypes));
415: }
416:
417: public function toBoolean(): BooleanType
418: {
419: $isZero = (new ConstantIntegerType(0))->isSuperTypeOf($this);
420: if ($isZero->no()) {
421: return new ConstantBooleanType(true);
422: }
423:
424: if ($isZero->maybe()) {
425: return new BooleanType();
426: }
427:
428: return new ConstantBooleanType(false);
429: }
430:
431: /**
432: * Return the union with another type, but only if it can be expressed in a simpler way than using UnionType
433: *
434: */
435: public function tryUnion(Type $otherType): ?Type
436: {
437: if ($otherType instanceof self || $otherType instanceof ConstantIntegerType) {
438: if ($otherType instanceof self) {
439: $otherMin = $otherType->min;
440: $otherMax = $otherType->max;
441: } else {
442: $otherMin = $otherType->getValue();
443: $otherMax = $otherType->getValue();
444: }
445:
446: if (self::isDisjoint($this->min, $this->max, $otherMin, $otherMax, false)) {
447: return null;
448: }
449:
450: return self::fromInterval(
451: $this->min !== null && $otherMin !== null ? min($this->min, $otherMin) : null,
452: $this->max !== null && $otherMax !== null ? max($this->max, $otherMax) : null,
453: );
454: }
455:
456: if (get_class($otherType) === parent::class) {
457: return $otherType;
458: }
459:
460: return null;
461: }
462:
463: /**
464: * Return the intersection with another type, but only if it can be expressed in a simpler way than using
465: * IntersectionType
466: *
467: */
468: public function tryIntersect(Type $otherType): ?Type
469: {
470: if ($otherType instanceof self || $otherType instanceof ConstantIntegerType) {
471: if ($otherType instanceof self) {
472: $otherMin = $otherType->min;
473: $otherMax = $otherType->max;
474: } else {
475: $otherMin = $otherType->getValue();
476: $otherMax = $otherType->getValue();
477: }
478:
479: if (self::isDisjoint($this->min, $this->max, $otherMin, $otherMax, false)) {
480: return new NeverType();
481: }
482:
483: if ($this->min === null) {
484: $newMin = $otherMin;
485: } elseif ($otherMin === null) {
486: $newMin = $this->min;
487: } else {
488: $newMin = max($this->min, $otherMin);
489: }
490:
491: if ($this->max === null) {
492: $newMax = $otherMax;
493: } elseif ($otherMax === null) {
494: $newMax = $this->max;
495: } else {
496: $newMax = min($this->max, $otherMax);
497: }
498:
499: return self::fromInterval($newMin, $newMax);
500: }
501:
502: if (get_class($otherType) === parent::class) {
503: return $this;
504: }
505:
506: return null;
507: }
508:
509: /**
510: * Return the different with another type, or null if it cannot be represented.
511: *
512: */
513: public function tryRemove(Type $typeToRemove): ?Type
514: {
515: if (get_class($typeToRemove) === parent::class) {
516: return new NeverType();
517: }
518:
519: if ($typeToRemove instanceof self || $typeToRemove instanceof ConstantIntegerType) {
520: if ($typeToRemove instanceof self) {
521: $removeMin = $typeToRemove->min;
522: $removeMax = $typeToRemove->max;
523: } else {
524: $removeMin = $typeToRemove->getValue();
525: $removeMax = $typeToRemove->getValue();
526: }
527:
528: if (
529: $this->min !== null && $removeMax !== null && $removeMax < $this->min
530: || $this->max !== null && $removeMin !== null && $this->max < $removeMin
531: ) {
532: return $this;
533: }
534:
535: if ($removeMin !== null && $removeMin !== PHP_INT_MIN) {
536: $lowerPart = self::fromInterval($this->min, $removeMin - 1);
537: } else {
538: $lowerPart = null;
539: }
540: if ($removeMax !== null && $removeMax !== PHP_INT_MAX) {
541: $upperPart = self::fromInterval($removeMax + 1, $this->max);
542: } else {
543: $upperPart = null;
544: }
545:
546: if ($lowerPart !== null && $upperPart !== null) {
547: return TypeCombinator::union($lowerPart, $upperPart);
548: }
549:
550: return $lowerPart ?? $upperPart;
551: }
552:
553: return null;
554: }
555:
556: /**
557: * @param mixed[] $properties
558: */
559: public static function __set_state(array $properties): Type
560: {
561: return new self($properties['min'], $properties['max']);
562: }
563:
564: }
565: