Skip to content

Commit 6dd9603

Browse files
committed
Report maybe division by zero in InvalidBinaryOperationRule
1 parent 5d4f259 commit 6dd9603

File tree

5 files changed

+342
-4
lines changed

5 files changed

+342
-4
lines changed

conf/config.level2.neon

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,7 @@ services:
184184
class: PHPStan\Rules\Operators\InvalidBinaryOperationRule
185185
arguments:
186186
bleedingEdge: %featureToggles.bleedingEdge%
187+
reportMaybes: %reportMaybes%
187188
tags:
188189
- phpstan.rules.rule
189190
-

src/Parser/TryCatchTypeVisitor.php

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,14 @@ public function beforeTraverse(array $nodes): ?array
2424

2525
public function enterNode(Node $node): ?Node
2626
{
27-
if ($node instanceof Node\Stmt || $node instanceof Node\Expr\Match_) {
27+
if (
28+
$node instanceof Node\Stmt
29+
|| $node instanceof Node\Expr\Match_
30+
|| $node instanceof Node\Expr\AssignOp\Div
31+
|| $node instanceof Node\Expr\AssignOp\Mod
32+
|| $node instanceof Node\Expr\BinaryOp\Div
33+
|| $node instanceof Node\Expr\BinaryOp\Mod
34+
) {
2835
if (count($this->typeStack) > 0) {
2936
$node->setAttribute(self::ATTRIBUTE_NAME, $this->typeStack[count($this->typeStack) - 1]);
3037
}

src/Rules/Operators/InvalidBinaryOperationRule.php

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,24 @@
22

33
namespace PHPStan\Rules\Operators;
44

5+
use DivisionByZeroError;
56
use PhpParser\Node;
67
use PHPStan\Analyser\MutatingScope;
78
use PHPStan\Analyser\Scope;
89
use PHPStan\Node\Printer\ExprPrinter;
10+
use PHPStan\Parser\TryCatchTypeVisitor;
911
use PHPStan\Rules\Rule;
1012
use PHPStan\Rules\RuleErrorBuilder;
1113
use PHPStan\Rules\RuleLevelHelper;
1214
use PHPStan\ShouldNotHappenException;
15+
use PHPStan\Type\BenevolentUnionType;
16+
use PHPStan\Type\Constant\ConstantIntegerType;
1317
use PHPStan\Type\ErrorType;
18+
use PHPStan\Type\ObjectType;
1419
use PHPStan\Type\Type;
20+
use PHPStan\Type\TypeCombinator;
1521
use PHPStan\Type\VerbosityLevel;
22+
use function array_map;
1623
use function sprintf;
1724
use function strlen;
1825
use function substr;
@@ -27,6 +34,7 @@ public function __construct(
2734
private ExprPrinter $exprPrinter,
2835
private RuleLevelHelper $ruleLevelHelper,
2936
private bool $bleedingEdge,
37+
private bool $reportMaybes,
3038
)
3139
{
3240
}
@@ -108,6 +116,39 @@ public function processNode(Node $node, Scope $scope): array
108116
->assignVariable($rightName, $rightType, $rightType);
109117

110118
if (!$scope->getType($newNode) instanceof ErrorType) {
119+
if (
120+
$this->reportMaybes
121+
&& (
122+
$node instanceof Node\Expr\AssignOp\Div
123+
|| $node instanceof Node\Expr\AssignOp\Mod
124+
|| $node instanceof Node\Expr\BinaryOp\Div
125+
|| $node instanceof Node\Expr\BinaryOp\Mod
126+
)
127+
&& !$this->isDivisionByZeroCaught($node)
128+
&& !$this->hasDivisionByZeroThrowsTag($scope)
129+
) {
130+
$rightType = $scope->getType($right);
131+
// as long as we don't support float-ranges, we prevent false positives for maybe floats
132+
if ($rightType instanceof BenevolentUnionType && $rightType->isFloat()->maybe()) {
133+
return [];
134+
}
135+
136+
$zeroType = new ConstantIntegerType(0);
137+
if ($zeroType->isSuperTypeOf($rightType)->maybe()) {
138+
return [
139+
RuleErrorBuilder::message(sprintf(
140+
'Binary operation "%s" between %s and %s might result in an error.',
141+
substr(substr($this->exprPrinter->printExpr($newNode), strlen($leftName) + 2), 0, -(strlen($rightName) + 2)),
142+
$scope->getType($left)->describe(VerbosityLevel::value()),
143+
$scope->getType($right)->describe(VerbosityLevel::value()),
144+
))
145+
->line($left->getStartLine())
146+
->identifier(sprintf('%s.invalid', $identifier))
147+
->build(),
148+
];
149+
}
150+
}
151+
111152
return [];
112153
}
113154

@@ -124,4 +165,31 @@ public function processNode(Node $node, Scope $scope): array
124165
];
125166
}
126167

168+
private function isDivisionByZeroCaught(Node $node): bool
169+
{
170+
$tryCatchTypes = $node->getAttribute(TryCatchTypeVisitor::ATTRIBUTE_NAME);
171+
if ($tryCatchTypes === null) {
172+
return false;
173+
}
174+
175+
$tryCatchType = TypeCombinator::union(...array_map(static fn (string $class) => new ObjectType($class), $tryCatchTypes));
176+
177+
return $tryCatchType->isSuperTypeOf(new ObjectType(DivisionByZeroError::class))->yes();
178+
}
179+
180+
private function hasDivisionByZeroThrowsTag(Scope $scope): bool
181+
{
182+
$function = $scope->getFunction();
183+
if ($function === null) {
184+
return false;
185+
}
186+
187+
$throwsType = $function->getThrowType();
188+
if ($throwsType === null) {
189+
return false;
190+
}
191+
192+
return $throwsType->isSuperTypeOf(new ObjectType(DivisionByZeroError::class))->yes();
193+
}
194+
127195
}

tests/PHPStan/Rules/Operators/InvalidBinaryOperationRuleTest.php

Lines changed: 141 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,14 @@ class InvalidBinaryOperationRuleTest extends RuleTestCase
1919

2020
private bool $checkImplicitMixed = false;
2121

22+
private bool $checkBenevolentUnionTypes = false;
23+
2224
protected function getRule(): Rule
2325
{
2426
return new InvalidBinaryOperationRule(
2527
new ExprPrinter(new Printer()),
26-
new RuleLevelHelper($this->createReflectionProvider(), true, false, true, $this->checkExplicitMixed, $this->checkImplicitMixed, true, false),
28+
new RuleLevelHelper($this->createReflectionProvider(), true, false, true, $this->checkExplicitMixed, $this->checkImplicitMixed, true, $this->checkBenevolentUnionTypes),
29+
true,
2730
true,
2831
);
2932
}
@@ -282,7 +285,12 @@ public function testBug3515(): void
282285

283286
public function testBug8827(): void
284287
{
285-
$this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-8827.php'], []);
288+
$this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-8827.php'], [
289+
[
290+
'Binary operation "/" between int<0, max> and int<0, max> might result in an error.',
291+
25,
292+
],
293+
]);
286294
}
287295

288296
public function testRuleWithNullsafeVariant(): void
@@ -659,6 +667,14 @@ public function testBenevolentUnion(): void
659667
'Binary operation "/" between (array<string, string>|bool|int|object|resource) and BinaryOpBenevolentUnion\\Foo results in an error.',
660668
105,
661669
],
670+
[
671+
'Binary operation "/" between (array<string, string>|bool|int|object|resource) and (array<string, string>|bool|int|object|resource) might result in an error.',
672+
108,
673+
],
674+
[
675+
'Binary operation "/=" between 1 and (array<string, string>|bool|int|object|resource) might result in an error.',
676+
111,
677+
],
662678
[
663679
'Binary operation "/=" between array{} and (array<string, string>|bool|int|object|resource) results in an error.',
664680
114,
@@ -667,14 +683,34 @@ public function testBenevolentUnion(): void
667683
'Binary operation "/=" between BinaryOpBenevolentUnion\\Foo and (array<string, string>|bool|int|object|resource) results in an error.',
668684
117,
669685
],
686+
[
687+
"Binary operation \"/=\" between '123' and (array<string, string>|bool|int|object|resource) might result in an error.",
688+
120,
689+
],
690+
[
691+
'Binary operation "/=" between 1.23 and (array<string, string>|bool|int|object|resource) might result in an error.',
692+
123,
693+
],
694+
[
695+
'Binary operation "/=" between (array<string, string>|bool|int|object|resource) and (array<string, string>|bool|int|object|resource) might result in an error.',
696+
126,
697+
],
670698
[
671699
'Binary operation "%" between (array<string, string>|bool|int|object|resource) and array{} results in an error.',
672700
135,
673701
],
674702
[
675-
'Binary operation "%" between (array<string, string>|bool|int|object|resource) and BinaryOpBenevolentUnion\\Foo results in an error.',
703+
'Binary operation "%" between (array<string, string>|bool|int|object|resource) and BinaryOpBenevolentUnion\Foo results in an error.',
676704
136,
677705
],
706+
[
707+
'Binary operation "%" between (array<string, string>|bool|int|object|resource) and (array<string, string>|bool|int|object|resource) might result in an error.',
708+
139,
709+
],
710+
[
711+
'Binary operation "%=" between 1 and (array<string, string>|bool|int|object|resource) might result in an error.',
712+
142,
713+
],
678714
[
679715
'Binary operation "%=" between array{} and (array<string, string>|bool|int|object|resource) results in an error.',
680716
145,
@@ -683,6 +719,18 @@ public function testBenevolentUnion(): void
683719
'Binary operation "%=" between BinaryOpBenevolentUnion\\Foo and (array<string, string>|bool|int|object|resource) results in an error.',
684720
148,
685721
],
722+
[
723+
"Binary operation \"%=\" between '123' and (array<string, string>|bool|int|object|resource) might result in an error.",
724+
151,
725+
],
726+
[
727+
'Binary operation "%=" between 1.23 and (array<string, string>|bool|int|object|resource) might result in an error.',
728+
154,
729+
],
730+
[
731+
'Binary operation "%=" between (array<string, string>|bool|int|object|resource) and (array<string, string>|bool|int|object|resource) might result in an error.',
732+
157,
733+
],
686734
[
687735
'Binary operation "-" between (array<string, string>|bool|int|object|resource) and array{} results in an error.',
688736
166,
@@ -810,4 +858,94 @@ public function testBug7863(): void
810858
]);
811859
}
812860

861+
/**
862+
* @dataProvider dataDivisionByMaybeZero
863+
* @param list<array{0: string, 1: int, 2?: string|null}> $expectedErrors
864+
*/
865+
public function testDivisionByMaybeZero(bool $checkBenevolentUnionTypes, array $expectedErrors): void
866+
{
867+
$this->checkBenevolentUnionTypes = $checkBenevolentUnionTypes;
868+
869+
$this->analyse([__DIR__ . '/data/invalid-division.php'], $expectedErrors);
870+
}
871+
872+
public function dataDivisionByMaybeZero(): iterable
873+
{
874+
yield [
875+
false,
876+
[
877+
[
878+
'Binary operation "/" between int and int<0, max> might result in an error.',
879+
12,
880+
],
881+
[
882+
'Binary operation "/=" between int and int<0, max> might result in an error.',
883+
13,
884+
],
885+
[
886+
'Binary operation "%" between int and int<0, max> might result in an error.',
887+
21,
888+
],
889+
[
890+
'Binary operation "%=" between int and int<0, max> might result in an error.',
891+
22,
892+
],
893+
[
894+
'Binary operation "/" between int and int<0, max> might result in an error.',
895+
61,
896+
],
897+
[
898+
'Binary operation "/" between int and float|int might result in an error.',
899+
90,
900+
],
901+
[
902+
'Binary operation "/" between int and (int|true) might result in an error.',
903+
106,
904+
],
905+
[
906+
'Binary operation "/" between int and -5|int<0, max> might result in an error.',
907+
123,
908+
],
909+
],
910+
];
911+
912+
yield [
913+
true,
914+
[
915+
[
916+
'Binary operation "/" between int and int<0, max> might result in an error.',
917+
12,
918+
],
919+
[
920+
'Binary operation "/=" between int and int<0, max> might result in an error.',
921+
13,
922+
],
923+
[
924+
'Binary operation "%" between int and int<0, max> might result in an error.',
925+
21,
926+
],
927+
[
928+
'Binary operation "%=" between int and int<0, max> might result in an error.',
929+
22,
930+
],
931+
[
932+
'Binary operation "/" between int and int<0, max> might result in an error.',
933+
61,
934+
],
935+
[
936+
'Binary operation "/" between int and float|int might result in an error.',
937+
90,
938+
],
939+
[
940+
'Binary operation "/" between int and (int|true) might result in an error.',
941+
106,
942+
],
943+
[
944+
'Binary operation "/" between int and -5|int<0, max> might result in an error.',
945+
123,
946+
],
947+
],
948+
];
949+
}
950+
813951
}

0 commit comments

Comments
 (0)