Skip to content

Commit 9bbc3d2

Browse files
ryiumondrejmirtes
authored andcommitted
Report the deprecated implicit float to int conversion of the % operator
On PHP 8.1+, a float operand of % (or a numeric string that may be a float) loses precision when it is converted to int, which PHP reports as deprecated. Amp-Thread-ID: https://ampcode.com/threads/T-01a19219-3b71-70b4-9e7a-3291211fe45f
1 parent 091c5a7 commit 9bbc3d2

3 files changed

Lines changed: 302 additions & 2 deletions

File tree

‎src/Rules/Operators/InvalidBinaryOperationRule.php‎

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
use PHPStan\DependencyInjection\RegisteredRule;
88
use PHPStan\Node\Expr\TypeExpr;
99
use PHPStan\Node\Printer\ExprPrinter;
10+
use PHPStan\Php\PhpVersion;
11+
use PHPStan\Rules\IdentifierRuleError;
1012
use PHPStan\Rules\MultipleNodeTypesRule;
1113
use PHPStan\Rules\RuleErrorBuilder;
1214
use PHPStan\Rules\RuleLevelHelper;
@@ -29,6 +31,7 @@ final class InvalidBinaryOperationRule implements MultipleNodeTypesRule
2931
public function __construct(
3032
private ExprPrinter $exprPrinter,
3133
private RuleLevelHelper $ruleLevelHelper,
34+
private PhpVersion $phpVersion,
3235
)
3336
{
3437
}
@@ -44,6 +47,19 @@ public function getNodeType(): string
4447
}
4548

4649
public function processNode(Node $node, Scope $scope): array
50+
{
51+
$errors = $this->checkInvalidOperation($node, $scope);
52+
if ($errors !== [] || !$node instanceof Node\Expr\BinaryOp\Mod || $scope->getType($node) instanceof ErrorType) {
53+
return $errors;
54+
}
55+
56+
return $this->checkModImplicitConversion($node, $scope);
57+
}
58+
59+
/**
60+
* @return list<IdentifierRuleError>
61+
*/
62+
private function checkInvalidOperation(Node $node, Scope $scope): array
4763
{
4864
if (
4965
!$node instanceof Node\Expr\BinaryOp
@@ -144,4 +160,37 @@ public function processNode(Node $node, Scope $scope): array
144160
];
145161
}
146162

163+
/**
164+
* `%` converts both operands to int: a float operand loses precision,
165+
* which PHP 8.1+ reports as deprecated.
166+
*
167+
* @return list<IdentifierRuleError>
168+
*/
169+
private function checkModImplicitConversion(Node\Expr\BinaryOp\Mod $node, Scope $scope): array
170+
{
171+
if (!$this->phpVersion->deprecatesImplicitlyFloatConversionToInt()) {
172+
return [];
173+
}
174+
175+
$callback = static fn (Type $type): bool => !$type->isFloat()->no();
176+
foreach ([$node->left, $node->right] as $operand) {
177+
$type = $this->ruleLevelHelper->findTypeToCheck($scope, $operand, '', $callback)->getType();
178+
if ($type instanceof ErrorType || $type->toNumber()->isFloat()->no()) {
179+
continue;
180+
}
181+
182+
return [
183+
RuleErrorBuilder::message(sprintf(
184+
'Deprecated in PHP 8.1: Implicit conversion from %s to int loses precision.',
185+
$type->describe(VerbosityLevel::value()),
186+
))
187+
->line($node->left->getStartLine())
188+
->identifier('binaryOp.mod.implicitConversion')
189+
->build(),
190+
];
191+
}
192+
193+
return [];
194+
}
195+
147196
}

‎tests/PHPStan/Rules/Operators/InvalidBinaryOperationRuleTest.php‎

Lines changed: 183 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,14 @@
44

55
use PHPStan\Node\Printer\ExprPrinter;
66
use PHPStan\Node\Printer\Printer;
7+
use PHPStan\Php\PhpVersion;
78
use PHPStan\Rules\Rule;
89
use PHPStan\Rules\RuleLevelHelper;
910
use PHPStan\Testing\RuleTestCase;
1011
use PHPUnit\Framework\Attributes\RequiresPhp;
12+
use function array_merge;
13+
use function usort;
14+
use const PHP_VERSION_ID;
1115

1216
/**
1317
* @extends RuleTestCase<InvalidBinaryOperationRule>
@@ -33,6 +37,7 @@ protected function getRule(): Rule
3337
checkBenevolentUnionTypes: false,
3438
discoveringSymbolsTip: true,
3539
),
40+
new PhpVersion(PHP_VERSION_ID),
3641
);
3742
}
3843

@@ -616,7 +621,7 @@ public function testBug10440(): void
616621

617622
public function testBenevolentUnion(): void
618623
{
619-
$this->analyse([__DIR__ . '/data/binary-op-benevolent-union.php'], [
624+
$errors = [
620625
[
621626
'Binary operation "+" between (array<string, string>|bool|int|object|resource) and BinaryOpBenevolentUnion\Foo results in an error.',
622627
12,
@@ -801,7 +806,15 @@ public function testBenevolentUnion(): void
801806
'Binary operation "|=" between BinaryOpBenevolentUnion\\Foo and (array<string, string>|bool|int|object|resource) results in an error.',
802807
365,
803808
],
804-
]);
809+
];
810+
if (PHP_VERSION_ID >= 80100) {
811+
$errors[] = [
812+
'Deprecated in PHP 8.1: Implicit conversion from 1.23 to int loses precision.',
813+
138,
814+
];
815+
usort($errors, static fn (array $a, array $b): int => $a[1] <=> $b[1]);
816+
}
817+
$this->analyse([__DIR__ . '/data/binary-op-benevolent-union.php'], $errors);
805818
}
806819

807820
public function testBug12123(): void
@@ -858,4 +871,172 @@ public function testBug1452(): void
858871
$this->analyse([__DIR__ . '/data/bug-1452.php'], []);
859872
}
860873

874+
public function testBug8288(): void
875+
{
876+
$expectedErrors = [];
877+
if (PHP_VERSION_ID >= 80100) {
878+
$expectedErrors = [
879+
[
880+
'Deprecated in PHP 8.1: Implicit conversion from float to int loses precision.',
881+
17,
882+
],
883+
[
884+
'Deprecated in PHP 8.1: Implicit conversion from numeric-string to int loses precision.',
885+
18,
886+
],
887+
[
888+
'Deprecated in PHP 8.1: Implicit conversion from float to int loses precision.',
889+
19,
890+
],
891+
[
892+
'Deprecated in PHP 8.1: Implicit conversion from float to int loses precision.',
893+
20,
894+
],
895+
[
896+
'Deprecated in PHP 8.1: Implicit conversion from float to int loses precision.',
897+
21,
898+
],
899+
[
900+
'Deprecated in PHP 8.1: Implicit conversion from numeric-string to int loses precision.',
901+
22,
902+
],
903+
[
904+
'Deprecated in PHP 8.1: Implicit conversion from numeric-string to int loses precision.',
905+
23,
906+
],
907+
[
908+
'Deprecated in PHP 8.1: Implicit conversion from numeric-string to int loses precision.',
909+
24,
910+
],
911+
];
912+
}
913+
$expectedErrors = array_merge($expectedErrors, [
914+
[
915+
'Binary operation "%" between int and string results in an error.',
916+
26,
917+
],
918+
[
919+
'Binary operation "%" between int and Stringable results in an error.',
920+
27,
921+
],
922+
[
923+
'Binary operation "%" between int and array results in an error.',
924+
28,
925+
],
926+
[
927+
'Binary operation "%" between float and string results in an error.',
928+
29,
929+
],
930+
[
931+
'Binary operation "%" between float and Stringable results in an error.',
932+
30,
933+
],
934+
[
935+
'Binary operation "%" between float and array results in an error.',
936+
31,
937+
],
938+
[
939+
'Binary operation "%" between string and int results in an error.',
940+
32,
941+
],
942+
[
943+
'Binary operation "%" between string and float results in an error.',
944+
33,
945+
],
946+
[
947+
'Binary operation "%" between string and string results in an error.',
948+
34,
949+
],
950+
[
951+
'Binary operation "%" between string and numeric-string results in an error.',
952+
35,
953+
],
954+
[
955+
'Binary operation "%" between string and Stringable results in an error.',
956+
36,
957+
],
958+
[
959+
'Binary operation "%" between string and array results in an error.',
960+
37,
961+
],
962+
[
963+
'Binary operation "%" between numeric-string and string results in an error.',
964+
38,
965+
],
966+
[
967+
'Binary operation "%" between numeric-string and Stringable results in an error.',
968+
39,
969+
],
970+
[
971+
'Binary operation "%" between numeric-string and array results in an error.',
972+
40,
973+
],
974+
[
975+
'Binary operation "%" between Stringable and int results in an error.',
976+
41,
977+
],
978+
[
979+
'Binary operation "%" between Stringable and float results in an error.',
980+
42,
981+
],
982+
[
983+
'Binary operation "%" between Stringable and string results in an error.',
984+
43,
985+
],
986+
[
987+
'Binary operation "%" between Stringable and numeric-string results in an error.',
988+
44,
989+
],
990+
[
991+
'Binary operation "%" between Stringable and Stringable results in an error.',
992+
45,
993+
],
994+
[
995+
'Binary operation "%" between Stringable and array results in an error.',
996+
46,
997+
],
998+
[
999+
'Binary operation "%" between array and int results in an error.',
1000+
47,
1001+
],
1002+
[
1003+
'Binary operation "%" between array and float results in an error.',
1004+
48,
1005+
],
1006+
[
1007+
'Binary operation "%" between array and string results in an error.',
1008+
49,
1009+
],
1010+
[
1011+
'Binary operation "%" between array and numeric-string results in an error.',
1012+
50,
1013+
],
1014+
[
1015+
'Binary operation "%" between array and Stringable results in an error.',
1016+
51,
1017+
],
1018+
[
1019+
'Binary operation "%" between array and array results in an error.',
1020+
52,
1021+
],
1022+
]);
1023+
if (PHP_VERSION_ID >= 80100) {
1024+
$expectedErrors = array_merge($expectedErrors, [
1025+
[
1026+
'Deprecated in PHP 8.1: Implicit conversion from float|int<0, 15> to int loses precision.',
1027+
58,
1028+
],
1029+
[
1030+
'Deprecated in PHP 8.1: Implicit conversion from 6.0625 to int loses precision.',
1031+
59,
1032+
],
1033+
[
1034+
'Deprecated in PHP 8.1: Implicit conversion from 6.0625 to int loses precision.',
1035+
60,
1036+
],
1037+
]);
1038+
}
1039+
$this->analyse([__DIR__ . '/data/bug8288.php'], $expectedErrors);
1040+
}
1041+
8611042
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
<?php
2+
3+
namespace Bug8288;
4+
5+
use Stringable;
6+
7+
class Bug8288 {
8+
9+
/**
10+
* @param numeric-string $numericString
11+
*/
12+
public function opMod(int $int, float $float, string $string, string $numericString, Stringable $stringable, array $array)
13+
{
14+
# Safe
15+
$int % $int;
16+
# Deprecated in PHP 8.1
17+
$int % $float;
18+
$int % $numericString;
19+
$float % $int;
20+
$float % $float;
21+
$float % $numericString;
22+
$numericString % $int;
23+
$numericString % $float;
24+
$numericString % $numericString;
25+
# Not safe
26+
$int % $string;
27+
$int % $stringable;
28+
$int % $array;
29+
$float % $string;
30+
$float % $stringable;
31+
$float % $array;
32+
$string % $int;
33+
$string % $float;
34+
$string % $string;
35+
$string % $numericString;
36+
$string % $stringable;
37+
$string % $array;
38+
$numericString % $string;
39+
$numericString % $stringable;
40+
$numericString % $array;
41+
$stringable % $int;
42+
$stringable % $float;
43+
$stringable % $string;
44+
$stringable % $numericString;
45+
$stringable % $stringable;
46+
$stringable % $array;
47+
$array % $int;
48+
$array % $float;
49+
$array % $string;
50+
$array % $numericString;
51+
$array % $stringable;
52+
$array % $array;
53+
54+
// The following three examples should all be flagged as resulting in implicit float to int conversion with potential loss of precision. This behaviour is deprecated in PHP 8.1, and results in a deprecation warning (if deprecation warnings are enabled, which they are by default PHP 8+), if encountered at runtime. It would be helpful if phpstan could catch and flag such cases during static analysis.
55+
$singleByteCode = ord('a');
56+
$float1 = ( $singleByteCode / 16 );
57+
// This is not safe
58+
$float1 % 15;
59+
( 97 / 16 ) % 15;
60+
6.0625 % 15;
61+
62+
// The following three examples should not result in any error
63+
$singleByteCode = ord('a');
64+
$int1 = intdiv( $singleByteCode, 16 );
65+
// This is safe
66+
$int1 % 15;
67+
intdiv( 97, 16 ) % 15;
68+
6 % 15;
69+
}
70+
}

0 commit comments

Comments
 (0)