Skip to content

Commit 3a12724

Browse files
committed
Bleeding edge - LogicalXorConstantConditionRule
1 parent 0a3a968 commit 3a12724

8 files changed

+211
-0
lines changed

Diff for: conf/bleedingEdge.neon

+1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ parameters:
2626
unescapeStrings: true
2727
alwaysCheckTooWideReturnTypeFinalMethods: true
2828
duplicateStubs: true
29+
logicalXor: true
2930
invarianceComposition: true
3031
alwaysTrueAlwaysReported: true
3132
disableUnreachableBranchesRules: true

Diff for: conf/config.level4.neon

+8
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ conditionalTags:
2626
phpstan.collector: %featureToggles.notAnalysedTrait%
2727
PHPStan\Rules\Traits\NotAnalysedTraitRule:
2828
phpstan.rules.rule: %featureToggles.notAnalysedTrait%
29+
PHPStan\Rules\Comparison\LogicalXorConstantConditionRule:
30+
phpstan.rules.rule: %featureToggles.logicalXor%
2931

3032
parameters:
3133
checkAdvancedIsset: true
@@ -124,6 +126,12 @@ services:
124126
tags:
125127
- phpstan.rules.rule
126128

129+
-
130+
class: PHPStan\Rules\Comparison\LogicalXorConstantConditionRule
131+
arguments:
132+
treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain%
133+
reportAlwaysTrueInLastCondition: %reportAlwaysTrueInLastCondition%
134+
127135
-
128136
class: PHPStan\Rules\Comparison\MatchExpressionRule
129137
arguments:

Diff for: conf/config.neon

+1
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ parameters:
6161
unescapeStrings: false
6262
alwaysCheckTooWideReturnTypeFinalMethods: false
6363
duplicateStubs: false
64+
logicalXor: false
6465
invarianceComposition: false
6566
alwaysTrueAlwaysReported: false
6667
disableUnreachableBranchesRules: false

Diff for: conf/parametersSchema.neon

+1
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ parametersSchema:
5656
unescapeStrings: bool()
5757
alwaysCheckTooWideReturnTypeFinalMethods: bool()
5858
duplicateStubs: bool()
59+
logicalXor: bool()
5960
invarianceComposition: bool()
6061
alwaysTrueAlwaysReported: bool()
6162
disableUnreachableBranchesRules: bool()

Diff for: phpstan-baseline.neon

+5
Original file line numberDiff line numberDiff line change
@@ -511,6 +511,11 @@ parameters:
511511
count: 1
512512
path: src/Rules/Comparison/ImpossibleCheckTypeHelper.php
513513

514+
-
515+
message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantBooleanType is error\\-prone and deprecated\\. Use Type\\:\\:isTrue\\(\\) or Type\\:\\:isFalse\\(\\) instead\\.$#"
516+
count: 4
517+
path: src/Rules/Comparison/LogicalXorConstantConditionRule.php
518+
514519
-
515520
message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantBooleanType is error\\-prone and deprecated\\. Use Type\\:\\:isTrue\\(\\) or Type\\:\\:isFalse\\(\\) instead\\.$#"
516521
count: 4
+99
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Comparison;
4+
5+
use PhpParser\Node;
6+
use PhpParser\Node\Expr\BinaryOp\LogicalXor;
7+
use PHPStan\Analyser\Scope;
8+
use PHPStan\Parser\LastConditionVisitor;
9+
use PHPStan\Rules\Rule;
10+
use PHPStan\Rules\RuleErrorBuilder;
11+
use PHPStan\Type\Constant\ConstantBooleanType;
12+
use function sprintf;
13+
14+
/**
15+
* @implements Rule<LogicalXor>
16+
*/
17+
class LogicalXorConstantConditionRule implements Rule
18+
{
19+
20+
public function __construct(
21+
private ConstantConditionRuleHelper $helper,
22+
private bool $treatPhpDocTypesAsCertain,
23+
private bool $reportAlwaysTrueInLastCondition,
24+
)
25+
{
26+
}
27+
28+
public function getNodeType(): string
29+
{
30+
return LogicalXor::class;
31+
}
32+
33+
public function processNode(Node $node, Scope $scope): array
34+
{
35+
$errors = [];
36+
$leftType = $this->helper->getBooleanType($scope, $node->left);
37+
$tipText = 'Because the type is coming from a PHPDoc, you can turn off this check by setting <fg=cyan>treatPhpDocTypesAsCertain: false</> in your <fg=cyan>%configurationFile%</>.';
38+
if ($leftType instanceof ConstantBooleanType) {
39+
$addTipLeft = function (RuleErrorBuilder $ruleErrorBuilder) use ($scope, $tipText, $node): RuleErrorBuilder {
40+
if (!$this->treatPhpDocTypesAsCertain) {
41+
return $ruleErrorBuilder;
42+
}
43+
44+
$booleanNativeType = $this->helper->getNativeBooleanType($scope, $node->left);
45+
if ($booleanNativeType instanceof ConstantBooleanType) {
46+
return $ruleErrorBuilder;
47+
}
48+
49+
return $ruleErrorBuilder->tip($tipText);
50+
};
51+
52+
$isLast = $node->getAttribute(LastConditionVisitor::ATTRIBUTE_NAME);
53+
if (!$leftType->getValue() || $isLast !== true || $this->reportAlwaysTrueInLastCondition) {
54+
$errorBuilder = $addTipLeft(RuleErrorBuilder::message(sprintf(
55+
'Left side of xor is always %s.',
56+
$leftType->getValue() ? 'true' : 'false',
57+
)))->line($node->left->getLine());
58+
if ($leftType->getValue() && $isLast === false && !$this->reportAlwaysTrueInLastCondition) {
59+
$errorBuilder->tip('Remove remaining cases below this one and this error will disappear too.');
60+
}
61+
$errors[] = $errorBuilder->build();
62+
}
63+
}
64+
65+
$rightType = $this->helper->getBooleanType($scope, $node->right);
66+
if ($rightType instanceof ConstantBooleanType && !$scope->isInFirstLevelStatement()) {
67+
$addTipRight = function (RuleErrorBuilder $ruleErrorBuilder) use ($scope, $node, $tipText): RuleErrorBuilder {
68+
if (!$this->treatPhpDocTypesAsCertain) {
69+
return $ruleErrorBuilder;
70+
}
71+
72+
$booleanNativeType = $this->helper->getNativeBooleanType(
73+
$scope,
74+
$node->right,
75+
);
76+
if ($booleanNativeType instanceof ConstantBooleanType) {
77+
return $ruleErrorBuilder;
78+
}
79+
80+
return $ruleErrorBuilder->tip($tipText);
81+
};
82+
83+
$isLast = $node->getAttribute(LastConditionVisitor::ATTRIBUTE_NAME);
84+
if (!$rightType->getValue() || $isLast !== true || $this->reportAlwaysTrueInLastCondition) {
85+
$errorBuilder = $addTipRight(RuleErrorBuilder::message(sprintf(
86+
'Right side of xor is always %s.',
87+
$rightType->getValue() ? 'true' : 'false',
88+
)))->line($node->right->getLine());
89+
if ($rightType->getValue() && $isLast === false && !$this->reportAlwaysTrueInLastCondition) {
90+
$errorBuilder->tip('Remove remaining cases below this one and this error will disappear too.');
91+
}
92+
$errors[] = $errorBuilder->build();
93+
}
94+
}
95+
96+
return $errors;
97+
}
98+
99+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Comparison;
4+
5+
use PHPStan\Rules\Rule as TRule;
6+
use PHPStan\Testing\RuleTestCase;
7+
8+
/**
9+
* @extends RuleTestCase<LogicalXorConstantConditionRule>
10+
*/
11+
class LogicalXorConstantConditionRuleTest extends RuleTestCase
12+
{
13+
14+
private bool $treatPhpDocTypesAsCertain;
15+
16+
private bool $reportAlwaysTrueInLastCondition = false;
17+
18+
protected function getRule(): TRule
19+
{
20+
return new LogicalXorConstantConditionRule(
21+
new ConstantConditionRuleHelper(
22+
new ImpossibleCheckTypeHelper(
23+
$this->createReflectionProvider(),
24+
$this->getTypeSpecifier(),
25+
[],
26+
$this->treatPhpDocTypesAsCertain,
27+
true,
28+
),
29+
$this->treatPhpDocTypesAsCertain,
30+
true,
31+
),
32+
$this->treatPhpDocTypesAsCertain,
33+
$this->reportAlwaysTrueInLastCondition,
34+
);
35+
}
36+
37+
public function testRule(): void
38+
{
39+
$tipText = 'Because the type is coming from a PHPDoc, you can turn off this check by setting <fg=cyan>treatPhpDocTypesAsCertain: false</> in your <fg=cyan>%configurationFile%</>.';
40+
$this->treatPhpDocTypesAsCertain = true;
41+
$this->analyse([__DIR__ . '/data/logical-xor.php'], [
42+
[
43+
'Left side of xor is always true.',
44+
14,
45+
],
46+
[
47+
'Right side of xor is always false.',
48+
14,
49+
],
50+
[
51+
'Left side of xor is always false.',
52+
17,
53+
],
54+
[
55+
'Right side of xor is always true.',
56+
17,
57+
],
58+
[
59+
'Left side of xor is always true.',
60+
20,
61+
$tipText,
62+
],
63+
[
64+
'Right side of xor is always true.',
65+
20,
66+
$tipText,
67+
],
68+
]);
69+
}
70+
71+
}

Diff for: tests/PHPStan/Rules/Comparison/data/logical-xor.php

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php
2+
3+
namespace ConstantLogicalXor;
4+
5+
class Foo
6+
{
7+
8+
/**
9+
* @param object $a
10+
* @param object $b
11+
*/
12+
public function doFoo($a, $b)
13+
{
14+
if (1 xor 0) {
15+
16+
}
17+
if (0 xor 1) {
18+
19+
}
20+
if ($a xor $b) {
21+
22+
}
23+
}
24+
25+
}

0 commit comments

Comments
 (0)