diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ArithmeticOpAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ArithmeticOpAnalyzer.php index e6155f74710..793c3245ec9 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ArithmeticOpAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ArithmeticOpAnalyzer.php @@ -22,6 +22,8 @@ use Psalm\Issue\PossiblyNullOperand; use Psalm\Issue\StringIncrement; use Psalm\IssueBuffer; +use Psalm\Node\Expr\BinaryOp\VirtualMinus; +use Psalm\Node\Expr\BinaryOp\VirtualPlus; use Psalm\StatementsSource; use Psalm\Type; use Psalm\Type\Atomic; @@ -826,10 +828,15 @@ private static function analyzeOperands( $result_type = Type::getInt(); } } - } else { + } else if ($parent instanceof VirtualPlus) { + $start = $left_type_part instanceof TLiteralInt ? $left_type_part->value : 1; + $result_type = Type::combineUnionTypes(Type::getIntRange($start, null), $result_type); + } else if ($parent instanceof VirtualMinus) { $start = $left_type_part instanceof TLiteralInt ? $left_type_part->value : 1; + $result_type = Type::combineUnionTypes(Type::getIntRange(null, $start), $result_type); + } else { $result_type = Type::combineUnionTypes( - $always_positive ? Type::getIntRange($start, null) : Type::getInt(true), + $always_positive ? Type::getIntRange(1, null) : Type::getInt(true), $result_type, ); } diff --git a/tests/BinaryOperationTest.php b/tests/BinaryOperationTest.php index 7a69480005b..637a480a928 100644 --- a/tests/BinaryOperationTest.php +++ b/tests/BinaryOperationTest.php @@ -970,6 +970,23 @@ function scope(array $a): int|float { '$j' => 'int<100, 110>', ], ], + 'decrementInLoop' => [ + 'code' => ' 0; $i--) { + if (rand(0,1)) { + break; + } + } + for ($j = 110; $j > 100; $j--) { + if (rand(0,1)) { + break; + } + }', + 'assertions' => [ + '$i' => 'int<0, 10>', + '$j' => 'int<100, 110>', + ], + ], 'coalesceFilterOutNullEvenWithTernary' => [ 'code' => '