Skip to content

Commit 7b569cc

Browse files
committedJun 27, 2023
Check incompatible operand values for greater/smaller operands (boolean value for greater / smaller comparison is not allowed).
1 parent b7edb14 commit 7b569cc

File tree

4 files changed

+243
-0
lines changed

4 files changed

+243
-0
lines changed
 

‎rules.neon

+9
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ parameters:
1919
allRules: true
2020
disallowedLooseComparison: [%strictRules.allRules%, %featureToggles.bleedingEdge%]
2121
booleansInConditions: %strictRules.allRules%
22+
boolOperandsInGreaterSmallerOperators: %strictRules.allRules%
2223
uselessCast: %strictRules.allRules%
2324
requireParentConstructorCall: %strictRules.allRules%
2425
disallowedConstructs: %strictRules.allRules%
@@ -35,6 +36,7 @@ parametersSchema:
3536
allRules: anyOf(bool(), arrayOf(bool())),
3637
disallowedLooseComparison: anyOf(bool(), arrayOf(bool())),
3738
booleansInConditions: anyOf(bool(), arrayOf(bool()))
39+
boolOperandsInGreaterSmallerOperators: anyOf(bool(), arrayOf(bool()))
3840
uselessCast: anyOf(bool(), arrayOf(bool()))
3941
requireParentConstructorCall: anyOf(bool(), arrayOf(bool()))
4042
disallowedConstructs: anyOf(bool(), arrayOf(bool()))
@@ -102,6 +104,8 @@ conditionalTags:
102104
phpstan.rules.rule: %strictRules.numericOperandsInArithmeticOperators%
103105
PHPStan\Rules\Operators\OperandsInArithmeticSubtractionRule:
104106
phpstan.rules.rule: %strictRules.numericOperandsInArithmeticOperators%
107+
PHPStan\Rules\Operators\OperandsIncompatibleGreaterSmallerRule:
108+
phpstan.rules.rule: %strictRules.boolOperandsInGreaterSmallerOperators%
105109
PHPStan\Rules\StrictCalls\DynamicCallOnStaticMethodsRule:
106110
phpstan.rules.rule: %strictRules.strictCalls%
107111
PHPStan\Rules\StrictCalls\DynamicCallOnStaticMethodsCallableRule:
@@ -236,6 +240,11 @@ services:
236240
arguments:
237241
bleedingEdge: %featureToggles.bleedingEdge%
238242

243+
-
244+
class: PHPStan\Rules\Operators\OperandsIncompatibleGreaterSmallerRule
245+
arguments:
246+
bleedingEdge: %featureToggles.bleedingEdge%
247+
239248
-
240249
class: PHPStan\Rules\StrictCalls\DynamicCallOnStaticMethodsRule
241250

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Operators;
4+
5+
use PhpParser\Node;
6+
use PhpParser\Node\Expr;
7+
use PhpParser\Node\Expr\BinaryOp\Greater as BinaryOpGreater;
8+
use PhpParser\Node\Expr\BinaryOp\GreaterOrEqual as BinaryOpGreaterOrEqual;
9+
use PhpParser\Node\Expr\BinaryOp\Smaller as BinaryOpSmaller;
10+
use PhpParser\Node\Expr\BinaryOp\SmallerOrEqual as BinaryOpSmallerOrEqual;
11+
use PhpParser\Node\Expr\BinaryOp\Spaceship as BinaryOpSpaceship;
12+
use PHPStan\Analyser\Scope;
13+
use PHPStan\Rules\Rule;
14+
use PHPStan\Rules\RuleErrorBuilder;
15+
use PHPStan\Type\Type;
16+
use PHPStan\Type\UnionType;
17+
use PHPStan\Type\VerbosityLevel;
18+
use function sprintf;
19+
20+
/**
21+
* @implements Rule<Expr>
22+
*/
23+
class OperandsIncompatibleGreaterSmallerRule implements Rule
24+
{
25+
26+
/** @var bool */
27+
private $bleedingEdge;
28+
29+
public function __construct(bool $bleedingEdge)
30+
{
31+
$this->bleedingEdge = $bleedingEdge;
32+
}
33+
34+
public function getNodeType(): string
35+
{
36+
return Expr::class;
37+
}
38+
39+
public function processNode(Node $node, Scope $scope): array
40+
{
41+
if (!$this->bleedingEdge) {
42+
return [];
43+
}
44+
45+
if (!$this->nodeIsSpaceship($node)
46+
&& !$this->nodeIsGreater($node)
47+
&& !$this->nodeIsSmaller($node)
48+
) {
49+
return [];
50+
}
51+
52+
$leftType = $scope->getType($node->left);
53+
$rightType = $scope->getType($node->right);
54+
55+
if ($this->nodeIsSpaceship($node) && $leftType->isBoolean()->yes() && $rightType->isBoolean()->yes()) {
56+
return [];
57+
}
58+
59+
if ($leftType->isInteger()->yes() && $this->nodeIsSmaller($node) && $this->containsBoolean($rightType)) {
60+
return [];
61+
}
62+
63+
if ($rightType->isInteger()->yes() && $this->nodeIsGreater($node) && $this->containsBoolean($leftType)) {
64+
return [];
65+
}
66+
67+
if ($this->containsBoolean($leftType) || $this->containsBoolean($rightType)) {
68+
return [RuleErrorBuilder::message(sprintf(
69+
'Comparison operator "%s" between %s and %s is not allowed.',
70+
$node->getOperatorSigil(),
71+
$leftType->describe(VerbosityLevel::typeOnly()),
72+
$rightType->describe(VerbosityLevel::typeOnly())
73+
))->identifier('cmp.hasBool')->build()];
74+
}
75+
76+
return [];
77+
}
78+
79+
private function containsBoolean(Type $type): bool
80+
{
81+
if ($type->isBoolean()->yes()) {
82+
return true;
83+
}
84+
85+
if ($type instanceof UnionType) {
86+
foreach ($type->getTypes() as $inUnionType) {
87+
if ($inUnionType->isBoolean()->yes()) {
88+
return true;
89+
}
90+
}
91+
}
92+
93+
return false;
94+
}
95+
96+
/**
97+
* @phpstan-assert-if-true BinaryOpSpaceship $node
98+
*/
99+
private function nodeIsSpaceship(Expr $node): bool
100+
{
101+
return $node instanceof BinaryOpSpaceship;
102+
}
103+
104+
/**
105+
* @phpstan-assert-if-true BinaryOpGreater|BinaryOpGreaterOrEqual $node
106+
*/
107+
private function nodeIsGreater(Expr $node): bool
108+
{
109+
return $node instanceof BinaryOpGreater || $node instanceof BinaryOpGreaterOrEqual;
110+
}
111+
112+
/**
113+
* @phpstan-assert-if-true BinaryOpSmaller|BinaryOpSmallerOrEqual $node
114+
*/
115+
private function nodeIsSmaller(Expr $node): bool
116+
{
117+
return $node instanceof BinaryOpSmaller || $node instanceof BinaryOpSmallerOrEqual;
118+
}
119+
120+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Operators;
4+
5+
use PHPStan\Rules\Rule;
6+
use PHPStan\Testing\RuleTestCase;
7+
8+
/**
9+
* @extends RuleTestCase<OperandsIncompatibleGreaterSmallerRule>
10+
*/
11+
class OperandsIncompatibleGreaterSmallerRuleTest extends RuleTestCase
12+
{
13+
14+
protected function getRule(): Rule
15+
{
16+
return new OperandsIncompatibleGreaterSmallerRule(
17+
true
18+
);
19+
}
20+
21+
public function testRule(): void
22+
{
23+
$this->analyse([__DIR__ . '/data/greater-smaller.php'], [
24+
[
25+
'Comparison operator ">" between bool and bool is not allowed.',
26+
24,
27+
],
28+
[
29+
'Comparison operator ">" between int and bool is not allowed.',
30+
25,
31+
],
32+
[
33+
'Comparison operator ">" between int and int|false is not allowed.',
34+
26,
35+
],
36+
[
37+
'Comparison operator "<=" between bool|int and int is not allowed.',
38+
29,
39+
],
40+
[
41+
'Comparison operator "<=" between bool|int and string is not allowed.',
42+
30,
43+
],
44+
[
45+
'Comparison operator "<=" between int|false and int is not allowed.',
46+
32,
47+
],
48+
[
49+
'Comparison operator "<=" between int|false and string is not allowed.',
50+
33,
51+
],
52+
[
53+
'Comparison operator "<=>" between int and int|false is not allowed.',
54+
40,
55+
],
56+
[
57+
'Comparison operator "<=>" between int|false and int is not allowed.',
58+
41,
59+
],
60+
]);
61+
}
62+
63+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<?php
2+
3+
namespace Operators;
4+
5+
use stdClass;
6+
7+
$int = 123;
8+
$string = '123';
9+
$object = new stdClass();
10+
$object2 = new stdClass();
11+
12+
/** @var bool $boolean */
13+
$boolean = foob1();
14+
15+
/** @var bool $boolean2 */
16+
$boolean2 = foob2();
17+
18+
/** @var int|false $intOrFalse */
19+
$intOrFalse = foo();
20+
21+
/** @var int|bool $intOrBoolean */
22+
$intOrBoolean = foo2();
23+
24+
$boolean > $boolean2;
25+
$int > $boolean;
26+
$int > $intOrFalse;
27+
28+
$int <= $intOrBoolean;
29+
$intOrBoolean <= $int;
30+
$intOrBoolean <= $string;
31+
32+
$intOrFalse <= $int;
33+
$intOrFalse <= $string;
34+
35+
$object <= $object2;
36+
37+
for ($i = 0; $i < $intOrFalse; $i++) {
38+
}
39+
40+
$int <=> $intOrFalse;
41+
$intOrFalse <=> $int;
42+
43+
$int < $int;
44+
45+
preg_match_all('~\d+~', 'foo', $matches) > 0;
46+
47+
filesize('foo') > 0;
48+
0 < filesize('foo');
49+
50+
$intOrFalse >= $int;
51+

0 commit comments

Comments
 (0)
Failed to load comments.