Skip to content

Commit 689e956

Browse files
committed
Detect always truthy/falsey conditions
1 parent 254a454 commit 689e956

54 files changed

Lines changed: 1050 additions & 15 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

conf/config.level4.neon

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@ includes:
22
- config.level3.neon
33

44
rules:
5+
- PHPStan\Rules\Comparison\BooleanAndConstantConditionRule
6+
- PHPStan\Rules\Comparison\BooleanNotConstantConditionRule
7+
- PHPStan\Rules\Comparison\BooleanOrConstantConditionRule
8+
- PHPStan\Rules\Comparison\ElseIfConstantConditionRule
9+
- PHPStan\Rules\Comparison\IfConstantConditionRule
10+
- PHPStan\Rules\Comparison\TernaryOperatorConstantConditionRule
511
- PHPStan\Rules\Cast\UselessCastRule
612

713
services:

src/Analyser/NodeScopeResolver.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -507,8 +507,8 @@ private function processNode(\PhpParser\Node $node, Scope $scope, \Closure $node
507507

508508
$elseifScope = $ifScope->filterByFalseyValue($node->cond);
509509
foreach ($node->elseifs as $elseif) {
510-
$this->processNode($elseif, $scope, $nodeCallback, true);
511510
$scope = $elseifScope;
511+
$this->processNode($elseif, $scope, $nodeCallback, true);
512512
$this->processNode($elseif->cond, $scope->exitFirstLevelStatements(), $nodeCallback);
513513
$scope = $this->lookForAssigns(
514514
$scope,

src/Analyser/Scope.php

Lines changed: 78 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
use PHPStan\Type\IntegerType;
4444
use PHPStan\Type\IterableType;
4545
use PHPStan\Type\MixedType;
46+
use PHPStan\Type\NeverType;
4647
use PHPStan\Type\NonexistentParentClassType;
4748
use PHPStan\Type\NullType;
4849
use PHPStan\Type\ObjectType;
@@ -325,12 +326,7 @@ public function getType(Expr $node): Type
325326
private function resolveType(Expr $node): Type
326327
{
327328
if (
328-
$node instanceof \PhpParser\Node\Expr\BinaryOp\BooleanAnd
329-
|| $node instanceof \PhpParser\Node\Expr\BinaryOp\BooleanOr
330-
|| $node instanceof \PhpParser\Node\Expr\BinaryOp\LogicalAnd
331-
|| $node instanceof \PhpParser\Node\Expr\BinaryOp\LogicalOr
332-
|| $node instanceof \PhpParser\Node\Expr\BooleanNot
333-
|| $node instanceof \PhpParser\Node\Expr\BinaryOp\LogicalXor
329+
$node instanceof \PhpParser\Node\Expr\BinaryOp\LogicalXor
334330
|| $node instanceof Expr\BinaryOp\Greater
335331
|| $node instanceof Expr\BinaryOp\GreaterOrEqual
336332
|| $node instanceof Expr\BinaryOp\Smaller
@@ -343,6 +339,79 @@ private function resolveType(Expr $node): Type
343339
return new BooleanType();
344340
}
345341

342+
if ($node instanceof \PhpParser\Node\Expr\BooleanNot) {
343+
$exprBooleanType = $this->getType($node->expr)->toBoolean();
344+
if ($exprBooleanType instanceof ConstantBooleanType) {
345+
return new ConstantBooleanType(!$exprBooleanType->getValue());
346+
}
347+
348+
return new BooleanType();
349+
}
350+
351+
if (
352+
$node instanceof \PhpParser\Node\Expr\BinaryOp\BooleanAnd
353+
|| $node instanceof \PhpParser\Node\Expr\BinaryOp\LogicalAnd
354+
) {
355+
$leftBooleanType = $this->getType($node->left)->toBoolean();
356+
if (
357+
$leftBooleanType instanceof ConstantBooleanType
358+
&& !$leftBooleanType->getValue()
359+
) {
360+
return new ConstantBooleanType(false);
361+
}
362+
363+
$rightBooleanType = $this->filterByTruthyValue($node->left)->getType($node->right)->toBoolean();
364+
if (
365+
$rightBooleanType instanceof ConstantBooleanType
366+
&& !$rightBooleanType->getValue()
367+
) {
368+
return new ConstantBooleanType(false);
369+
}
370+
371+
if (
372+
$leftBooleanType instanceof ConstantBooleanType
373+
&& $leftBooleanType->getValue()
374+
&& $rightBooleanType instanceof ConstantBooleanType
375+
&& $rightBooleanType->getValue()
376+
) {
377+
return new ConstantBooleanType(true);
378+
}
379+
380+
return new BooleanType();
381+
}
382+
383+
if (
384+
$node instanceof \PhpParser\Node\Expr\BinaryOp\BooleanOr
385+
|| $node instanceof \PhpParser\Node\Expr\BinaryOp\LogicalOr
386+
) {
387+
$leftBooleanType = $this->getType($node->left)->toBoolean();
388+
if (
389+
$leftBooleanType instanceof ConstantBooleanType
390+
&& $leftBooleanType->getValue()
391+
) {
392+
return new ConstantBooleanType(true);
393+
}
394+
395+
$rightBooleanType = $this->filterByFalseyValue($node->left)->getType($node->right)->toBoolean();
396+
if (
397+
$rightBooleanType instanceof ConstantBooleanType
398+
&& $rightBooleanType->getValue()
399+
) {
400+
return new ConstantBooleanType(true);
401+
}
402+
403+
if (
404+
$leftBooleanType instanceof ConstantBooleanType
405+
&& !$leftBooleanType->getValue()
406+
&& $rightBooleanType instanceof ConstantBooleanType
407+
&& !$rightBooleanType->getValue()
408+
) {
409+
return new ConstantBooleanType(false);
410+
}
411+
412+
return new BooleanType();
413+
}
414+
346415
if ($node instanceof Expr\BinaryOp\Identical) {
347416
$leftType = $this->getType($node->left);
348417
$rightType = $this->getType($node->right);
@@ -390,6 +459,9 @@ private function resolveType(Expr $node): Type
390459
}
391460

392461
$expressionType = $this->getType($node->expr);
462+
if ($expressionType instanceof NeverType) {
463+
return new ConstantBooleanType(false);
464+
}
393465
$isExpressionObject = (new ObjectWithoutClassType())->isSuperTypeOf($expressionType);
394466
if (!$isExpressionObject->no() && $type instanceof StringType) {
395467
return new BooleanType();
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Comparison;
4+
5+
use PHPStan\Type\Constant\ConstantBooleanType;
6+
7+
class BooleanAndConstantConditionRule implements \PHPStan\Rules\Rule
8+
{
9+
10+
public function getNodeType(): string
11+
{
12+
return \PhpParser\Node\Expr\BinaryOp\BooleanAnd::class;
13+
}
14+
15+
/**
16+
* @param \PhpParser\Node\Expr\BinaryOp\BooleanAnd $node
17+
* @param \PHPStan\Analyser\Scope $scope
18+
* @return string[]
19+
*/
20+
public function processNode(
21+
\PhpParser\Node $node,
22+
\PHPStan\Analyser\Scope $scope
23+
): array
24+
{
25+
$messages = [];
26+
$leftType = ConstantConditionRuleHelper::getBooleanType($scope, $node->left);
27+
if ($leftType instanceof ConstantBooleanType) {
28+
$messages[] = sprintf(
29+
'Left side of && is always %s.',
30+
$leftType->getValue() ? 'true' : 'false'
31+
);
32+
}
33+
34+
$rightType = ConstantConditionRuleHelper::getBooleanType(
35+
$scope->filterByTruthyValue($node->left),
36+
$node->right
37+
);
38+
if ($rightType instanceof ConstantBooleanType) {
39+
$messages[] = sprintf(
40+
'Right side of && is always %s.',
41+
$rightType->getValue() ? 'true' : 'false'
42+
);
43+
}
44+
45+
return $messages;
46+
}
47+
48+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Comparison;
4+
5+
use PHPStan\Type\Constant\ConstantBooleanType;
6+
7+
class BooleanNotConstantConditionRule implements \PHPStan\Rules\Rule
8+
{
9+
10+
public function getNodeType(): string
11+
{
12+
return \PhpParser\Node\Expr\BooleanNot::class;
13+
}
14+
15+
/**
16+
* @param \PhpParser\Node\Expr\BooleanNot $node
17+
* @param \PHPStan\Analyser\Scope $scope
18+
* @return string[]
19+
*/
20+
public function processNode(
21+
\PhpParser\Node $node,
22+
\PHPStan\Analyser\Scope $scope
23+
): array
24+
{
25+
$exprType = ConstantConditionRuleHelper::getBooleanType($scope, $node->expr);
26+
if ($exprType instanceof ConstantBooleanType) {
27+
return [
28+
sprintf(
29+
'Negated boolean is always %s.',
30+
$exprType->getValue() ? 'false' : 'true'
31+
),
32+
];
33+
}
34+
35+
return [];
36+
}
37+
38+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Comparison;
4+
5+
use PHPStan\Type\Constant\ConstantBooleanType;
6+
7+
class BooleanOrConstantConditionRule implements \PHPStan\Rules\Rule
8+
{
9+
10+
public function getNodeType(): string
11+
{
12+
return \PhpParser\Node\Expr\BinaryOp\BooleanOr::class;
13+
}
14+
15+
/**
16+
* @param \PhpParser\Node\Expr\BinaryOp\BooleanOr $node
17+
* @param \PHPStan\Analyser\Scope $scope
18+
* @return string[]
19+
*/
20+
public function processNode(
21+
\PhpParser\Node $node,
22+
\PHPStan\Analyser\Scope $scope
23+
): array
24+
{
25+
$messages = [];
26+
$leftType = ConstantConditionRuleHelper::getBooleanType($scope, $node->left);
27+
if ($leftType instanceof ConstantBooleanType) {
28+
$messages[] = sprintf(
29+
'Left side of || is always %s.',
30+
$leftType->getValue() ? 'true' : 'false'
31+
);
32+
}
33+
34+
$rightType = ConstantConditionRuleHelper::getBooleanType(
35+
$scope->filterByFalseyValue($node->left),
36+
$node->right
37+
);
38+
if ($rightType instanceof ConstantBooleanType) {
39+
$messages[] = sprintf(
40+
'Right side of || is always %s.',
41+
$rightType->getValue() ? 'true' : 'false'
42+
);
43+
}
44+
45+
return $messages;
46+
}
47+
48+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Comparison;
4+
5+
use PhpParser\Node\Expr;
6+
use PHPStan\Analyser\Scope;
7+
use PHPStan\Type\BooleanType;
8+
9+
class ConstantConditionRuleHelper
10+
{
11+
12+
public static function getBooleanType(
13+
Scope $scope,
14+
Expr $expr
15+
): BooleanType
16+
{
17+
if (
18+
$expr instanceof Expr\Instanceof_
19+
|| $expr instanceof Expr\FuncCall
20+
|| $expr instanceof Expr\BinaryOp\Identical
21+
|| $expr instanceof Expr\BinaryOp\NotIdentical
22+
) {
23+
// already checked by different rules
24+
return new BooleanType();
25+
}
26+
27+
return $scope->getType($expr)->toBoolean();
28+
}
29+
30+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Comparison;
4+
5+
use PHPStan\Type\Constant\ConstantBooleanType;
6+
7+
class ElseIfConstantConditionRule implements \PHPStan\Rules\Rule
8+
{
9+
10+
public function getNodeType(): string
11+
{
12+
return \PhpParser\Node\Stmt\ElseIf_::class;
13+
}
14+
15+
/**
16+
* @param \PhpParser\Node\Stmt\ElseIf_ $node
17+
* @param \PHPStan\Analyser\Scope $scope
18+
* @return string[]
19+
*/
20+
public function processNode(
21+
\PhpParser\Node $node,
22+
\PHPStan\Analyser\Scope $scope
23+
): array
24+
{
25+
$exprType = ConstantConditionRuleHelper::getBooleanType($scope, $node->cond);
26+
if ($exprType instanceof ConstantBooleanType) {
27+
return [
28+
sprintf(
29+
'Elseif condition is always %s.',
30+
$exprType->getValue() ? 'true' : 'false'
31+
),
32+
];
33+
}
34+
35+
return [];
36+
}
37+
38+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Comparison;
4+
5+
use PHPStan\Type\Constant\ConstantBooleanType;
6+
7+
class IfConstantConditionRule implements \PHPStan\Rules\Rule
8+
{
9+
10+
public function getNodeType(): string
11+
{
12+
return \PhpParser\Node\Stmt\If_::class;
13+
}
14+
15+
/**
16+
* @param \PhpParser\Node\Stmt\If_ $node
17+
* @param \PHPStan\Analyser\Scope $scope
18+
* @return string[]
19+
*/
20+
public function processNode(
21+
\PhpParser\Node $node,
22+
\PHPStan\Analyser\Scope $scope
23+
): array
24+
{
25+
$exprType = ConstantConditionRuleHelper::getBooleanType($scope, $node->cond);
26+
if ($exprType instanceof ConstantBooleanType) {
27+
return [
28+
sprintf(
29+
'If condition is always %s.',
30+
$exprType->getValue() ? 'true' : 'false'
31+
),
32+
];
33+
}
34+
35+
return [];
36+
}
37+
38+
}

0 commit comments

Comments
 (0)