Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/Php/PhpVersion.php
Original file line number Diff line number Diff line change
Expand Up @@ -287,4 +287,9 @@ public function supportsNeverReturnTypeInArrowFunction(): bool
return $this->versionId >= 80200;
}

public function deprecatesImplicitConversions(): bool
{
return $this->versionId >= 80100;
}

}
8 changes: 8 additions & 0 deletions src/Reflection/InitializerExprTypeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -874,6 +874,14 @@ public function getModType(Expr $left, Expr $right, callable $getTypeCallback):
}
}

$leftNumberType = $leftType->toNumber();
$rightNumberType = $rightType->toNumber();
if ($leftNumberType instanceof ErrorType || $rightNumberType instanceof ErrorType) {
return new ErrorType();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How the type system responds shouldn't change because it's not actually true. The % operator still returns a number but only on some PHP versions it throws a deprecation notice.

} elseif ($leftNumberType->isInteger()->or($leftNumberType->isFloat())->no() && $rightNumberType->isInteger()->or($rightNumberType->isFloat())->no()) {
return new ErrorType();
}

$positiveInt = IntegerRangeType::fromInterval(0, null);
if ($rightType->isInteger()->yes()) {
$rangeMin = null;
Expand Down
55 changes: 55 additions & 0 deletions src/Rules/Operators/InvalidBinaryOperationRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use PHPStan\Analyser\MutatingScope;
use PHPStan\Analyser\Scope;
use PHPStan\Node\Printer\ExprPrinter;
use PHPStan\Php\PhpVersion;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleErrorBuilder;
use PHPStan\Rules\RuleLevelHelper;
Expand All @@ -26,6 +27,7 @@ class InvalidBinaryOperationRule implements Rule
public function __construct(
private ExprPrinter $exprPrinter,
private RuleLevelHelper $ruleLevelHelper,
private PhpVersion $phpVersion,
)
{
}
Expand Down Expand Up @@ -116,6 +118,59 @@ public function processNode(Node $node, Scope $scope): array
->identifier(sprintf('%s.invalid', $identifier))
->build(),
];
} else {
if ($node instanceof Node\Expr\BinaryOp\Mod) {
$callback = static fn (Type $type): bool => !$type->isFloat()->no();

$leftType = $this->ruleLevelHelper->findTypeToCheck(
$scope,
$node->left,
'',
$callback,
)->getType();
if ($leftType instanceof ErrorType) {
return [];
}

$rightType = $this->ruleLevelHelper->findTypeToCheck(
$scope,
$node->right,
'',
$callback,
)->getType();
if ($rightType instanceof ErrorType) {
return [];
}

$leftNumberType = $leftType->toNumber();
$rightNumberType = $rightType->toNumber();
if ($leftNumberType->isFloat()->no() && $rightNumberType->isFloat()->no()) {
return [];
}

if (!$leftNumberType->isFloat()->no() && $this->phpVersion->deprecatesImplicitConversions()) {
return [
RuleErrorBuilder::message(sprintf(
'Deprecated in PHP 8.1: Implicit conversion from %s to int loses precision.',
$leftType->describe(VerbosityLevel::value()),
))
->line($node->left->getStartLine())
->identifier('binaryOp.mod.implicitConversion')
->build(),
];
}
if (!$rightNumberType->isFloat()->no() && $this->phpVersion->deprecatesImplicitConversions()) {
return [
RuleErrorBuilder::message(sprintf(
'Deprecated in PHP 8.1: Implicit conversion from %s to int loses precision.',
$rightType->describe(VerbosityLevel::value()),
))
->line($node->left->getStartLine())
->identifier('binaryOp.mod.implicitConversion')
->build(),
];
}
}
}

return [];
Expand Down
171 changes: 171 additions & 0 deletions tests/PHPStan/Rules/Operators/InvalidBinaryOperationRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@

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 function array_merge;
use const PHP_VERSION_ID;

/**
Expand All @@ -20,6 +22,7 @@ protected function getRule(): Rule
return new InvalidBinaryOperationRule(
new ExprPrinter(new Printer()),
new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false, false, true, false),
new PhpVersion(PHP_VERSION_ID),
);
}

Expand Down Expand Up @@ -264,6 +267,174 @@ public function testBug8827(): void
$this->analyse([__DIR__ . '/../../Analyser/data/bug-8827.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 testRuleWithNullsafeVariant(): void
{
if (PHP_VERSION_ID < 80000) {
Expand Down
70 changes: 70 additions & 0 deletions tests/PHPStan/Rules/Operators/data/bug8288.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<?php

namespace Bug8288;

use Stringable;

class Bug8288 {

/**
* @param numeric-string $numericString
*/
public function opMod(int $int, float $float, string $string, string $numericString, Stringable $stringable, array $array)
{
# Safe
$int % $int;
# Deprecated in PHP 8.1
$int % $float;
$int % $numericString;
$float % $int;
$float % $float;
$float % $numericString;
$numericString % $int;
$numericString % $float;
$numericString % $numericString;
# Not safe
$int % $string;
$int % $stringable;
$int % $array;
$float % $string;
$float % $stringable;
$float % $array;
$string % $int;
$string % $float;
$string % $string;
$string % $numericString;
$string % $stringable;
$string % $array;
$numericString % $string;
$numericString % $stringable;
$numericString % $array;
$stringable % $int;
$stringable % $float;
$stringable % $string;
$stringable % $numericString;
$stringable % $stringable;
$stringable % $array;
$array % $int;
$array % $float;
$array % $string;
$array % $numericString;
$array % $stringable;
$array % $array;

// 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.
$singleByteCode = ord('a');
$float1 = ( $singleByteCode / 16 );
// This is not safe
$float1 % 15;
( 97 / 16 ) % 15;
6.0625 % 15;

// The following three examples should not result in any error
$singleByteCode = ord('a');
$int1 = intdiv( $singleByteCode, 16 );
// This is safe
$int1 % 15;
intdiv( 97, 16 ) % 15;
6 % 15;
}
}