diff --git a/src/Rules/Operators/InvalidBinaryOperationRule.php b/src/Rules/Operators/InvalidBinaryOperationRule.php index 8786a69ced6..0c3147957e0 100644 --- a/src/Rules/Operators/InvalidBinaryOperationRule.php +++ b/src/Rules/Operators/InvalidBinaryOperationRule.php @@ -7,6 +7,8 @@ use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Node\Expr\TypeExpr; use PHPStan\Node\Printer\ExprPrinter; +use PHPStan\Php\PhpVersion; +use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\MultipleNodeTypesRule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Rules\RuleLevelHelper; @@ -29,6 +31,7 @@ final class InvalidBinaryOperationRule implements MultipleNodeTypesRule public function __construct( private ExprPrinter $exprPrinter, private RuleLevelHelper $ruleLevelHelper, + private PhpVersion $phpVersion, ) { } @@ -44,6 +47,19 @@ public function getNodeType(): string } public function processNode(Node $node, Scope $scope): array + { + $errors = $this->checkInvalidOperation($node, $scope); + if ($errors !== [] || !$node instanceof Node\Expr\BinaryOp\Mod || $scope->getType($node) instanceof ErrorType) { + return $errors; + } + + return $this->checkModImplicitConversion($node, $scope); + } + + /** + * @return list + */ + private function checkInvalidOperation(Node $node, Scope $scope): array { if ( !$node instanceof Node\Expr\BinaryOp @@ -144,4 +160,37 @@ public function processNode(Node $node, Scope $scope): array ]; } + /** + * `%` converts both operands to int: a float operand loses precision, + * which PHP 8.1+ reports as deprecated. + * + * @return list + */ + private function checkModImplicitConversion(Node\Expr\BinaryOp\Mod $node, Scope $scope): array + { + if (!$this->phpVersion->deprecatesImplicitlyFloatConversionToInt()) { + return []; + } + + $callback = static fn (Type $type): bool => !$type->isFloat()->no(); + foreach ([$node->left, $node->right] as $operand) { + $type = $this->ruleLevelHelper->findTypeToCheck($scope, $operand, '', $callback)->getType(); + if ($type instanceof ErrorType || $type->toNumber()->isFloat()->no()) { + continue; + } + + return [ + RuleErrorBuilder::message(sprintf( + 'Deprecated in PHP 8.1: Implicit conversion from %s to int loses precision.', + $type->describe(VerbosityLevel::value()), + )) + ->line($node->left->getStartLine()) + ->identifier('binaryOp.mod.implicitConversion') + ->build(), + ]; + } + + return []; + } + } diff --git a/tests/PHPStan/Rules/Operators/InvalidBinaryOperationRuleTest.php b/tests/PHPStan/Rules/Operators/InvalidBinaryOperationRuleTest.php index 771080edd3b..a4c418cc74c 100644 --- a/tests/PHPStan/Rules/Operators/InvalidBinaryOperationRuleTest.php +++ b/tests/PHPStan/Rules/Operators/InvalidBinaryOperationRuleTest.php @@ -4,10 +4,14 @@ use PHPStan\Node\Printer\ExprPrinter; use PHPStan\Node\Printer\Printer; +use PHPStan\Php\PhpVersion; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; use PHPUnit\Framework\Attributes\RequiresPhp; +use function array_merge; +use function usort; +use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -33,6 +37,7 @@ protected function getRule(): Rule checkBenevolentUnionTypes: false, discoveringSymbolsTip: true, ), + new PhpVersion(PHP_VERSION_ID), ); } @@ -616,7 +621,7 @@ public function testBug10440(): void public function testBenevolentUnion(): void { - $this->analyse([__DIR__ . '/data/binary-op-benevolent-union.php'], [ + $errors = [ [ 'Binary operation "+" between (array|bool|int|object|resource) and BinaryOpBenevolentUnion\Foo results in an error.', 12, @@ -801,7 +806,15 @@ public function testBenevolentUnion(): void 'Binary operation "|=" between BinaryOpBenevolentUnion\\Foo and (array|bool|int|object|resource) results in an error.', 365, ], - ]); + ]; + if (PHP_VERSION_ID >= 80100) { + $errors[] = [ + 'Deprecated in PHP 8.1: Implicit conversion from 1.23 to int loses precision.', + 138, + ]; + usort($errors, static fn (array $a, array $b): int => $a[1] <=> $b[1]); + } + $this->analyse([__DIR__ . '/data/binary-op-benevolent-union.php'], $errors); } public function testBug12123(): void @@ -858,4 +871,184 @@ public function testBug1452(): void $this->analyse([__DIR__ . '/data/bug-1452.php'], []); } + public function testBug8288(): void + { + $expectedErrors = []; + if (PHP_VERSION_ID >= 80100) { + $expectedErrors = [ + [ + 'Deprecated in PHP 8.1: Implicit conversion from float to int loses precision.', + 17, + ], + [ + 'Deprecated in PHP 8.1: Implicit conversion from numeric-string to int loses precision.', + 18, + ], + [ + 'Deprecated in PHP 8.1: Implicit conversion from float to int loses precision.', + 19, + ], + [ + 'Deprecated in PHP 8.1: Implicit conversion from float to int loses precision.', + 20, + ], + [ + 'Deprecated in PHP 8.1: Implicit conversion from float to int loses precision.', + 21, + ], + [ + 'Deprecated in PHP 8.1: Implicit conversion from numeric-string to int loses precision.', + 22, + ], + [ + 'Deprecated in PHP 8.1: Implicit conversion from numeric-string to int loses precision.', + 23, + ], + [ + 'Deprecated in PHP 8.1: Implicit conversion from numeric-string to int loses precision.', + 24, + ], + ]; + } + $expectedErrors = array_merge($expectedErrors, [ + [ + 'Binary operation "%" between int and string results in an error.', + 26, + ], + [ + 'Binary operation "%" between int and Stringable results in an error.', + 27, + ], + [ + 'Binary operation "%" between int and array results in an error.', + 28, + ], + [ + 'Binary operation "%" between float and string results in an error.', + 29, + ], + [ + 'Binary operation "%" between float and Stringable results in an error.', + 30, + ], + [ + 'Binary operation "%" between float and array results in an error.', + 31, + ], + [ + 'Binary operation "%" between string and int results in an error.', + 32, + ], + [ + 'Binary operation "%" between string and float results in an error.', + 33, + ], + [ + 'Binary operation "%" between string and string results in an error.', + 34, + ], + [ + 'Binary operation "%" between string and numeric-string results in an error.', + 35, + ], + [ + 'Binary operation "%" between string and Stringable results in an error.', + 36, + ], + [ + 'Binary operation "%" between string and array results in an error.', + 37, + ], + [ + 'Binary operation "%" between numeric-string and string results in an error.', + 38, + ], + [ + 'Binary operation "%" between numeric-string and Stringable results in an error.', + 39, + ], + [ + 'Binary operation "%" between numeric-string and array results in an error.', + 40, + ], + [ + 'Binary operation "%" between Stringable and int results in an error.', + 41, + ], + [ + 'Binary operation "%" between Stringable and float results in an error.', + 42, + ], + [ + 'Binary operation "%" between Stringable and string results in an error.', + 43, + ], + [ + 'Binary operation "%" between Stringable and numeric-string results in an error.', + 44, + ], + [ + 'Binary operation "%" between Stringable and Stringable results in an error.', + 45, + ], + [ + 'Binary operation "%" between Stringable and array results in an error.', + 46, + ], + [ + 'Binary operation "%" between array and int results in an error.', + 47, + ], + [ + 'Binary operation "%" between array and float results in an error.', + 48, + ], + [ + 'Binary operation "%" between array and string results in an error.', + 49, + ], + [ + 'Binary operation "%" between array and numeric-string results in an error.', + 50, + ], + [ + 'Binary operation "%" between array and Stringable results in an error.', + 51, + ], + [ + 'Binary operation "%" between array and array results in an error.', + 52, + ], + ]); + if (PHP_VERSION_ID >= 80100) { + $expectedErrors = array_merge($expectedErrors, [ + [ + 'Deprecated in PHP 8.1: Implicit conversion from float|int<0, 15> to int loses precision.', + 58, + ], + [ + 'Deprecated in PHP 8.1: Implicit conversion from 6.0625 to int loses precision.', + 59, + ], + [ + 'Deprecated in PHP 8.1: Implicit conversion from 6.0625 to int loses precision.', + 60, + ], + ]); + } + $this->analyse([__DIR__ . '/data/bug8288.php'], $expectedErrors); + } + + public function testBug11886(): void + { + $errors = []; + if (PHP_VERSION_ID >= 80100) { + $errors[] = [ + 'Deprecated in PHP 8.1: Implicit conversion from 4.1 to int loses precision.', + 3, + ]; + } + $this->analyse([__DIR__ . '/data/bug-11886.php'], $errors); + } + } diff --git a/tests/PHPStan/Rules/Operators/data/bug-11886.php b/tests/PHPStan/Rules/Operators/data/bug-11886.php new file mode 100644 index 00000000000..31bbdb3707d --- /dev/null +++ b/tests/PHPStan/Rules/Operators/data/bug-11886.php @@ -0,0 +1,3 @@ +