Skip to content

Commit

Permalink
Check incompatible operand values for greater/smaller operands (boole…
Browse files Browse the repository at this point in the history
…an value for greater / smaller comparison is not allowed).
  • Loading branch information
kamil-zacek committed Jun 27, 2023
1 parent b7edb14 commit 7b569cc
Show file tree
Hide file tree
Showing 4 changed files with 243 additions and 0 deletions.
9 changes: 9 additions & 0 deletions rules.neon
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ parameters:
allRules: true
disallowedLooseComparison: [%strictRules.allRules%, %featureToggles.bleedingEdge%]
booleansInConditions: %strictRules.allRules%
boolOperandsInGreaterSmallerOperators: %strictRules.allRules%
uselessCast: %strictRules.allRules%
requireParentConstructorCall: %strictRules.allRules%
disallowedConstructs: %strictRules.allRules%
Expand All @@ -35,6 +36,7 @@ parametersSchema:
allRules: anyOf(bool(), arrayOf(bool())),
disallowedLooseComparison: anyOf(bool(), arrayOf(bool())),
booleansInConditions: anyOf(bool(), arrayOf(bool()))
boolOperandsInGreaterSmallerOperators: anyOf(bool(), arrayOf(bool()))
uselessCast: anyOf(bool(), arrayOf(bool()))
requireParentConstructorCall: anyOf(bool(), arrayOf(bool()))
disallowedConstructs: anyOf(bool(), arrayOf(bool()))
Expand Down Expand Up @@ -102,6 +104,8 @@ conditionalTags:
phpstan.rules.rule: %strictRules.numericOperandsInArithmeticOperators%
PHPStan\Rules\Operators\OperandsInArithmeticSubtractionRule:
phpstan.rules.rule: %strictRules.numericOperandsInArithmeticOperators%
PHPStan\Rules\Operators\OperandsIncompatibleGreaterSmallerRule:
phpstan.rules.rule: %strictRules.boolOperandsInGreaterSmallerOperators%
PHPStan\Rules\StrictCalls\DynamicCallOnStaticMethodsRule:
phpstan.rules.rule: %strictRules.strictCalls%
PHPStan\Rules\StrictCalls\DynamicCallOnStaticMethodsCallableRule:
Expand Down Expand Up @@ -236,6 +240,11 @@ services:
arguments:
bleedingEdge: %featureToggles.bleedingEdge%

-
class: PHPStan\Rules\Operators\OperandsIncompatibleGreaterSmallerRule
arguments:
bleedingEdge: %featureToggles.bleedingEdge%

-
class: PHPStan\Rules\StrictCalls\DynamicCallOnStaticMethodsRule

Expand Down
120 changes: 120 additions & 0 deletions src/Rules/Operators/OperandsIncompatibleGreaterSmallerRule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules\Operators;

use PhpParser\Node;
use PhpParser\Node\Expr;
use PhpParser\Node\Expr\BinaryOp\Greater as BinaryOpGreater;
use PhpParser\Node\Expr\BinaryOp\GreaterOrEqual as BinaryOpGreaterOrEqual;
use PhpParser\Node\Expr\BinaryOp\Smaller as BinaryOpSmaller;
use PhpParser\Node\Expr\BinaryOp\SmallerOrEqual as BinaryOpSmallerOrEqual;
use PhpParser\Node\Expr\BinaryOp\Spaceship as BinaryOpSpaceship;
use PHPStan\Analyser\Scope;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleErrorBuilder;
use PHPStan\Type\Type;
use PHPStan\Type\UnionType;
use PHPStan\Type\VerbosityLevel;
use function sprintf;

/**
* @implements Rule<Expr>
*/
class OperandsIncompatibleGreaterSmallerRule implements Rule
{

/** @var bool */
private $bleedingEdge;

public function __construct(bool $bleedingEdge)
{
$this->bleedingEdge = $bleedingEdge;
}

public function getNodeType(): string
{
return Expr::class;
}

public function processNode(Node $node, Scope $scope): array
{
if (!$this->bleedingEdge) {
return [];
}

if (!$this->nodeIsSpaceship($node)
&& !$this->nodeIsGreater($node)
&& !$this->nodeIsSmaller($node)
) {
return [];
}

$leftType = $scope->getType($node->left);
$rightType = $scope->getType($node->right);

if ($this->nodeIsSpaceship($node) && $leftType->isBoolean()->yes() && $rightType->isBoolean()->yes()) {
return [];
}

if ($leftType->isInteger()->yes() && $this->nodeIsSmaller($node) && $this->containsBoolean($rightType)) {
return [];
}

if ($rightType->isInteger()->yes() && $this->nodeIsGreater($node) && $this->containsBoolean($leftType)) {
return [];
}

if ($this->containsBoolean($leftType) || $this->containsBoolean($rightType)) {
return [RuleErrorBuilder::message(sprintf(
'Comparison operator "%s" between %s and %s is not allowed.',
$node->getOperatorSigil(),
$leftType->describe(VerbosityLevel::typeOnly()),
$rightType->describe(VerbosityLevel::typeOnly())
))->identifier('cmp.hasBool')->build()];
}

return [];
}

private function containsBoolean(Type $type): bool
{
if ($type->isBoolean()->yes()) {
return true;
}

if ($type instanceof UnionType) {
foreach ($type->getTypes() as $inUnionType) {
if ($inUnionType->isBoolean()->yes()) {
return true;
}
}
}

return false;
}

/**
* @phpstan-assert-if-true BinaryOpSpaceship $node
*/
private function nodeIsSpaceship(Expr $node): bool
{
return $node instanceof BinaryOpSpaceship;
}

/**
* @phpstan-assert-if-true BinaryOpGreater|BinaryOpGreaterOrEqual $node
*/
private function nodeIsGreater(Expr $node): bool
{
return $node instanceof BinaryOpGreater || $node instanceof BinaryOpGreaterOrEqual;
}

/**
* @phpstan-assert-if-true BinaryOpSmaller|BinaryOpSmallerOrEqual $node
*/
private function nodeIsSmaller(Expr $node): bool
{
return $node instanceof BinaryOpSmaller || $node instanceof BinaryOpSmallerOrEqual;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules\Operators;

use PHPStan\Rules\Rule;
use PHPStan\Testing\RuleTestCase;

/**
* @extends RuleTestCase<OperandsIncompatibleGreaterSmallerRule>
*/
class OperandsIncompatibleGreaterSmallerRuleTest extends RuleTestCase
{

protected function getRule(): Rule
{
return new OperandsIncompatibleGreaterSmallerRule(
true
);
}

public function testRule(): void
{
$this->analyse([__DIR__ . '/data/greater-smaller.php'], [
[
'Comparison operator ">" between bool and bool is not allowed.',
24,
],
[
'Comparison operator ">" between int and bool is not allowed.',
25,
],
[
'Comparison operator ">" between int and int|false is not allowed.',
26,
],
[
'Comparison operator "<=" between bool|int and int is not allowed.',
29,
],
[
'Comparison operator "<=" between bool|int and string is not allowed.',
30,
],
[
'Comparison operator "<=" between int|false and int is not allowed.',
32,
],
[
'Comparison operator "<=" between int|false and string is not allowed.',
33,
],
[
'Comparison operator "<=>" between int and int|false is not allowed.',
40,
],
[
'Comparison operator "<=>" between int|false and int is not allowed.',
41,
],
]);
}

}
51 changes: 51 additions & 0 deletions tests/Rules/Operators/data/greater-smaller.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?php

namespace Operators;

use stdClass;

$int = 123;
$string = '123';
$object = new stdClass();
$object2 = new stdClass();

/** @var bool $boolean */
$boolean = foob1();

/** @var bool $boolean2 */
$boolean2 = foob2();

/** @var int|false $intOrFalse */
$intOrFalse = foo();

/** @var int|bool $intOrBoolean */
$intOrBoolean = foo2();

$boolean > $boolean2;
$int > $boolean;
$int > $intOrFalse;

$int <= $intOrBoolean;
$intOrBoolean <= $int;
$intOrBoolean <= $string;

$intOrFalse <= $int;
$intOrFalse <= $string;

$object <= $object2;

for ($i = 0; $i < $intOrFalse; $i++) {
}

$int <=> $intOrFalse;
$intOrFalse <=> $int;

$int < $int;

preg_match_all('~\d+~', 'foo', $matches) > 0;

filesize('foo') > 0;
0 < filesize('foo');

$intOrFalse >= $int;

0 comments on commit 7b569cc

Please sign in to comment.