assertEquals(5, $range->min_bound); $this->assertEquals(10, $range->max_bound); } public function providerValidCodeParse(): iterable { return [ 'intRangeContained' => [ 'code' => ' $a * @return int<-1, max> */ function scope(int $a){ return $a; }', ], 'positiveIntRange' => [ 'code' => ' $a * @return positive-int */ function scope(int $a){ return $a; }', ], 'nonNegativeIntRange' => [ 'code' => ' $a * @return non-negative-int */ function scope(int $a){ return $a; }', ], 'negativeIntRange' => [ 'code' => ' $a * @return negative-int */ function scope(int $a){ return $a; }', ], 'nonPositiveIntRange' => [ 'code' => ' $a * @return non-positive-int */ function scope(int $a){ return $a; }', ], 'intRangeToInt' => [ 'code' => ' $a * @return int */ function scope(int $a){ return $a; }', ], 'intReduced' => [ 'code' => '= 500); assert($a < 5000); assert($b >= -5000); assert($b < -501); assert(-60 > $c); assert(-500 < $c);', 'assertions' => [ '$a===' => 'int<500, 4999>', '$b===' => 'int<-5000, -502>', '$c===' => 'int<-499, -61>', ], ], 'complexAssertions' => [ 'code' => '= 495 + 5); $b = 5000; assert($a < $b); ', 'assertions' => [ '$a===' => 'int<500, 4999>', ], ], 'negatedAssertions' => [ 'code' => ' if($a > 10){ die(); } if($b > -10){ die(); } //< if($c < 500){ die(); } if($d < -500){ die(); } //>= if($e >= 10){ die(); } if($f >= -10){ die(); } //<= if($g <= 500){ die(); } if($h <= -500){ die(); } //> if(10 > $i){ die(); } if(-10 > $j){ die(); } //< if(500 < $k){ die(); } if(-500 < $l){ die(); } //>= if(10 >= $m){ die(); } if(-10 >= $n){ die(); } //<= if(500 <= $o){ die(); } if(-500 <= $p){ die(); } //inverse ', 'assertions' => [ '$a===' => 'int', '$b===' => 'int', '$c===' => 'int<500, max>', '$d===' => 'int<-500, max>', '$e===' => 'int', '$f===' => 'int', '$g===' => 'int<501, max>', '$h===' => 'int<-499, max>', '$i===' => 'int<10, max>', '$j===' => 'int<-10, max>', '$k===' => 'int', '$l===' => 'int', '$m===' => 'int<11, max>', '$n===' => 'int<-9, max>', '$o===' => 'int', '$p===' => 'int', ], ], 'intOperations' => [ 'code' => '= 500); assert($a < 5000); $b = $a % 10; $c = $a ** 2; $d = $a - 5; $e = $a * 1;', 'assertions' => [ '$b===' => 'int<0, 9>', '$c===' => 'int<1, max>', '$d===' => 'int<495, 4994>', '$e===' => 'int<500, 4999>', ], ], 'mod' => [ 'code' => '= 20);//positive range assert($b <= -20);//negative range /** @var int<0, 0> $c */; // 0 range assert($d >= -100);// mixed range assert($d <= 100);// mixed range /** @var int<5, 5> $e */; // 5 range $f = $a % $e; $g = $b % $e; $h = $d % $e; $i = -3 % $a; $j = -3 % $b; /** @psalm-suppress NoValue */ $k = -3 % $c; $l = -3 % $d; $m = 3 % $a; $n = 3 % $b; /** @psalm-suppress NoValue */ $o = 3 % $c; $p = 3 % $d; /** @psalm-suppress NoValue */ $q = $a % 0; $r = $a % 3; $s = $a % -3; /** @psalm-suppress NoValue */ $t = $b % 0; $u = $b % 3; $v = $b % -3; /** @psalm-suppress NoValue */ $w = $c % 0; $x = $c % 3; $y = $c % -3; /** @psalm-suppress NoValue */ $z = $d % 0; $aa = $d % 3; $ab = $d % -3; ', 'assertions' => [ '$f===' => 'int<0, 4>', '$g===' => 'int<-4, 0>', '$h===' => 'int<-4, 4>', '$i===' => 'int', '$j===' => 'int', '$k===' => 'never', '$l===' => 'int', '$m===' => 'int<0, max>', '$n===' => 'int', '$o===' => 'never', '$p===' => 'int', '$q===' => 'never', '$r===' => 'int<0, 2>', '$s===' => 'int<-2, 0>', '$t===' => 'never', '$u===' => 'int<-2, 0>', '$v===' => 'int<2, 0>', '$w===' => 'never', '$x===' => 'int<0, 2>', '$y===' => 'int<-2, 0>', '$z===' => 'never', '$aa===' => 'int<-2, 2>', '$ab===' => 'int<-2, 2>', ], ], 'pow' => [ 'code' => '= 2);//positive range assert($b <= -2);//negative range /** @var int<0, 0> $c */; // 0 range assert($d >= -100);// mixed range assert($d <= 100);// mixed range $e = 0 ** $a; $f = 0 ** $b; $g = 0 ** $c; $h = 0 ** $d; $i = (-2) ** $a; $j = (-2) ** $b; $k = (-2) ** $c; $l = (-2) ** $d; $m = 2 ** $a; $n = 2 ** $b; $o = 2 ** $c; $p = 2 ** $d; $q = $a ** 0; $r = $a ** 2; $s = $a ** -2; $t = $b ** 0; $u = $b ** 2; $v = $b ** -2; $w = $c ** 0; $x = $c ** 2; $y = $c ** -2; $z = $d ** 0; $aa = $d ** 2; $ab = $d ** -2; ', 'assertions' => [ '$e===' => '0', '$f===' => 'float', '$g===' => '1', '$h===' => '0|1|float', '$i===' => 'int', '$j===' => 'float', '$k===' => '-1', '$l===' => 'float|int', '$m===' => 'int<1, max>', '$n===' => 'float', '$o===' => '1', '$p===' => 'float|int', '$q===' => '1', '$r===' => 'int<1, max>', '$s===' => 'float', '$t===' => '-1', '$u===' => 'int<1, max>', '$v===' => 'float', '$w===' => '1', '$x===' => '0', '$y===' => 'float', '$z===' => '1', '$aa===' => 'int<1, max>', '$ab===' => 'float', ], ], 'multiplications' => [ 'code' => '= -2); assert($e >= 2); assert($f >= -2); assert($f <= 2); $g = $a * $b; $h = $a * $c; $i = $a * $d; $j = $a * $e; $k = $a * $f; $l = $b * $b; $m = $b * $c; $n = $b * $d; $o = $b * $e; $p = $b * $f; $q = $c * $c; $r = $c * $d; $s = $c * $e; $t = $c * $f; $u = $d * $d; $v = $d * $e; $w = $d * $f; $x = $e * $e; $y = $d * $f; $z = $f * $f; ', 'assertions' => [ '$g===' => 'int', '$h===' => 'int', '$i===' => 'int', '$j===' => 'int', '$k===' => 'int', '$l===' => 'int<4, max>', '$m===' => 'int', '$n===' => 'int', '$o===' => 'int', '$p===' => 'int', '$q===' => 'int', '$r===' => 'int', '$s===' => 'int', '$t===' => 'int', '$u===' => 'int', '$v===' => 'int', '$w===' => 'int', '$x===' => 'int<4, max>', '$y===' => 'int', '$z===' => 'int<-4, 4>', ], ], 'SKIPPED-intLoopPositive' => [ 'code' => ' [ '$i===' => 'int<0, 9>', ], ], 'SKIPPED-intLoopNegative' => [ 'code' => ' 1; $i--){ }', 'assertions' => [ '$i===' => 'int<2, 10>', ], ], 'integrateExistingArrayPositive' => [ 'code' => ' */ function getInt() { return 7; } $_arr = ["a", "b", "c"]; $a = getInt(); $_arr[$a] = 12;', 'assertions' => [ '$_arr===' => "non-empty-array, 'a'|'b'|'c'|12>", ], ], 'integrateExistingArrayNegative' => [ 'code' => ' */ function getInt() { return -2; } $_arr = ["a", "b", "c"]; $a = getInt(); $_arr[$a] = 12;', 'assertions' => [ '$_arr===' => "non-empty-array, 'a'|'b'|'c'|12>", ], ], 'SKIPPED-statementsInLoopAffectsEverything' => [ 'code' => ' [ '$remainder===' => 'int', ], ], 'IntRangeRestrictWhenUntouched' => [ 'code' => ' 1) { takesInt($i); } } /** @psalm-param int<2, 3> $i */ function takesInt(int $i): void{ return; }', ], 'intRangeExpandedByLoop' => [ 'code' => ' $i */ function takesInt(int $i): void{ return; }', ], 'statementsInLoopPreserveNonNegativeIntRange' => [ 'code' => ' 0) { $sum += $i; } } takesNonNegativeInt($sum); /** @psalm-param int<0, max> $i */ function takesNonNegativeInt(int $i): void{ return; }', ], 'wrongLoopAssertion' => [ 'code' => ' 0 && rand(0,1)) { continue; } $type_tokens[$i] = ""; if ($i > 1) { $type_tokens[$i - 2]; } } return []; } /** @return array */ function getArray(): array { return []; }', ], 'IntRangeContainedInMultipleInt' => [ 'code' => ' $j */ $j = 0; echo $_arr[$j];', ], 'modulo' => [ 'code' => ' 0); $c = $a % 10; $d = $a % $a;', 'assertions' => [ '$b===' => 'int<-9, 9>', '$c===' => 'int<0, 9>', '$d===' => 'int<0, max>', ], ], 'minus' => [ 'code' => ' 5); assert($a <= 10); assert($b > -10); assert($b <= 100); $c = $a - $b; $f = $a - $d; assert($e > 0); $g = $a - $e; ', 'assertions' => [ '$c===' => 'int<-94, 19>', '$f===' => 'int', '$g===' => 'int', ], ], 'bits' => [ 'code' => ' 5); assert($b <= 6); $d = $a ^ $b; $e = $a & $b; $f = $a | $b; $g = $a << $b; $h = $a >> $b; ', 'assertions' => [ '$d===' => 'int', '$e===' => 'int', '$f===' => 'int', '$g===' => 'int', '$h===' => 'int', ], ], 'UnaryMinus' => [ 'code' => ' 5); $b = -$a; assert($c > 0); $d = -$c; assert($e > 5); assert($e < 10); $f = -$e; ', 'assertions' => [ '$b===' => 'int', '$d===' => 'int', '$f===' => 'int<-9, -6>', ], ], 'countOnKeyedArray' => [ 'code' => ' [ '$i===' => '2', ], ], 'intersections' => [ 'code' => ' */ function getInt(): int{ return rand(0, 10); } $a = getInt(); $b = -$a; $c = null; if($b === $a){ //$b and $a should intersect at 0, so $c should be 0 $c = $b; } ', 'assertions' => [ '$c===' => 'int<0, 0>|null', ], ], 'minMax' => [ 'code' => ' 10); assert($c < -15); assert($d === 20); assert($e > 0); $f = min($a, $b, $c, $d); $g = min($b, $c, $d); $h = min($d, $e); $i = max($b, $c, $d); $j = max($d, $e); $k = max($e, 40); $l = min($a, ...[$b, $c], $d); $m = max(...[$a, ...[$b, $c]], $d); ', 'assertions' => [ '$f===' => 'int', '$g===' => 'int', '$h===' => 'int<1, 20>', '$i===' => 'int<20, max>', '$j===' => 'int<20, max>', '$k===' => 'int<40, max>', '$l===' => 'int', '$m===' => 'int<20, max>', ], ], 'dontCrashOnFalsy' => [ 'code' => ' $shuffle_count */ $shuffle_count = 1; /** @var list $file_paths */ $file_paths = []; /** @var int<0, max> $count */ $count = 1; /** @var int<0, max> $middle */ $middle = 1; /** @var int<0, max> $remainder */ $remainder = 1; for ($i = 0; $i < $shuffle_count; $i++) { for ($j = 0; $j < $middle; $j++) { if ($j * $shuffle_count + $i < $count) { echo $file_paths[$j * $shuffle_count + $i]; } } if ($remainder) { echo $file_paths[$middle * $shuffle_count + $remainder - 1]; $remainder--; } } }', ], 'positiveIntToRangeWithInferior' => [ 'code' => ' [ '$length===' => 'int<8, max>', ], ], 'nonNegativeIntToRangeWithInferior' => [ 'code' => ' [ '$length===' => 'int<8, max>', ], ], 'negativeIntToRangeWithSuperior' => [ 'code' => ' -8) { throw new \RuntimeException(); }', 'assertions' => [ '$length===' => 'int', ], ], 'nonPositiveIntToRangeWithSuperior' => [ 'code' => ' -8) { throw new \RuntimeException(); }', 'assertions' => [ '$length===' => 'int', ], ], 'positiveIntToRangeWithSuperiorOrEqual' => [ 'code' => '= 8) { throw new \RuntimeException(); }', 'assertions' => [ '$length===' => 'int<1, 7>', ], ], 'nonNegativeIntToRangeWithSuperiorOrEqual' => [ 'code' => '= 8) { throw new \RuntimeException(); }', 'assertions' => [ '$length===' => 'int<0, 7>', ], ], 'negativeIntToRangeWithInferiorOrEqual' => [ 'code' => ' [ '$length===' => 'int<-7, -1>', ], ], 'nonPositiveIntToRangeWithInferiorOrEqual' => [ 'code' => ' [ '$length===' => 'int<-7, 0>', ], ], 'literalEquality' => [ 'code' => ' 16) { throw new Exception(""); } assert($length === 1); ', 'assertions' => [ '$length===' => '1', ], ], 'PositiveIntCombinedWithIntRange' => [ 'code' => ', int<0, max>> */ $_arr = []; $_arr[1] = $int; $_arr[$int] = 2;', 'assertions' => [ '$_arr===' => 'non-empty-array, int<0, max>>', ], ], 'nonNegativeIntCombinedWithIntRange' => [ 'code' => ', int<0, max>> */ $_arr = []; $_arr[0] = 0; $_arr[1] = $int; $_arr[$int] = 2;', 'assertions' => [ '$_arr===' => 'non-empty-array, int<0, max>>', ], ], 'negativeIntCombinedWithIntRange' => [ 'code' => ', int> */ $_arr = []; $_arr[-1] = $int; $_arr[$int] = -2;', 'assertions' => [ '$_arr===' => 'non-empty-array, int>', ], ], 'nonPositiveIntCombinedWithIntRange' => [ 'code' => ', int> */ $_arr = []; $_arr[0] = 0; $_arr[-1] = $int; $_arr[$int] = -2;', 'assertions' => [ '$_arr===' => 'non-empty-array, int>', ], ], 'noErrorPushingBigShapeIntoConstant' => [ 'code' => ' [ ], ], 'assertionsAndNegationsOnRanges' => [ 'code' => ' throw new Exception(); } $res2 = $a; //should be int<1, max> if ($b > 1) { $res3 = $b; //should be int<2, max> throw new Exception(); } $res4 = $b; //should be int if ($c <= 1) { $res5 = $c; //should be int throw new Exception(); } $res6 = $c; //should be int<2, max> if ($d >= 1) { $res7 = $d; //should be int<1, max> throw new Exception(); } $res8 = $d; //should be int if (1 < $e) { $res9 = $e; //should be int<2, max> throw new Exception(); } $res10 = $e; //should be int if (1 > $f) { $res11 = $f; //should be int throw new Exception(); } $res12 = $f; //should be int<1, max> if (1 <= $g) { $res13 = $g; //should be int<1, max> throw new Exception(); } $res14 = $g; //should be int if (1 >= $h) { $res15 = $h; //should be int throw new Exception(); } $res16 = $h; //should be int<2, max>', 'assertions' => [ //'$res1' => 'int', '$res2' => 'int<1, max>', //'$res3' => 'int<2, max>', '$res4' => 'int', //'$res5' => 'int', '$res6' => 'int<2, max>', //'$res7' => 'int<1, max>', '$res8' => 'int', //'$res9' => 'int<2, max>', '$res10' => 'int', //'$res11' => 'int', '$res12' => 'int<1, max>', //'$res13' => 'int<1, max>', '$res14' => 'int', //'$res15' => 'int', '$res16' => 'int<2, max>', ], ], 'arraykeyCanBeRange' => [ 'code' => ' [ 'code' => ' */ $a = 2; /** @var int<6, 10> */ $b = 9; /** * @param int<0, 5> $_a * @param int<6, 10> $_b */ function foo(int $_a, int $_b): void {} foo(...[$a, $b]); ', ], 'minMaxInNamespace' => [ 'code' => ' $_a * @param int $_b */ function bar(int $_a, int $_b): void {} } ', ], 'rangeOverflow' => [ 'code' => ' [ '$z' => 'int|null', ], ], ]; } public function providerInvalidCodeParse(): iterable { return [ 'intRangeNotContained' => [ 'code' => ' $a * @return int<-1, 11> * @psalm-suppress InvalidReturnStatement */ function scope(int $a){ return $a; }', 'error_message' => 'InvalidReturnType', ], 'assertOutOfRange' => [ 'code' => ' $a */ function scope(int $a): void{ assert($a === 0); }', 'error_message' => 'DocblockTypeContradiction', ], 'assertRedundantInferior' => [ 'code' => ' $a */ function scope(int $a): void{ assert($a < 10); }', 'error_message' => 'RedundantConditionGivenDocblockType', ], 'assertImpossibleInferior' => [ 'code' => ' $a */ function scope(int $a): void{ assert($a < 4); }', 'error_message' => 'DocblockTypeContradiction', ], 'maxSpecifiedAsFirst' => [ 'code' => ' $a */ function scope(int $a){ return $a; }', 'error_message' => 'InvalidDocblock', ], 'minSpecifiedAsSecond' => [ 'code' => ' $a */ function scope(int $a){ return $a; }', 'error_message' => 'InvalidDocblock', ], 'unknownConstant' => [ 'code' => ' $a */ function scope(int $a){ return $a; }', 'error_message' => 'InvalidDocblock', ], 'floatAsABoundary' => [ 'code' => ' $a */ function scope(int $a){ return $a; }', 'error_message' => 'InvalidDocblock', ], 'stringAsABoundary' => [ 'code' => ' $a */ function scope(int $a){ return $a; }', 'error_message' => 'InvalidDocblock', ], 'minGreaterThanMax' => [ 'code' => ' $a */ function scope(int $a){ return $a; }', 'error_message' => 'InvalidDocblock', ], ]; } }