Skip to content

Commit 6ebf236

Browse files
committed
Bleeding edge level 4 - ConstantLooseComparisonRule
1 parent 49b8b26 commit 6ebf236

File tree

6 files changed

+151
-0
lines changed

6 files changed

+151
-0
lines changed

conf/bleedingEdge.neon

+1
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,4 @@ parameters:
1212
illegalConstructorMethodCall: true
1313
disableCheckMissingIterableValueType: true
1414
strictUnnecessaryNullsafePropertyFetch: true
15+
looseComparison: true

conf/config.level4.neon

+9
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ rules:
1919
- PHPStan\Rules\TooWideTypehints\TooWideClosureReturnTypehintRule
2020
- PHPStan\Rules\TooWideTypehints\TooWideFunctionReturnTypehintRule
2121

22+
conditionalTags:
23+
PHPStan\Rules\Comparison\ConstantLooseComparisonRule:
24+
phpstan.rules.rule: %featureToggles.looseComparison%
25+
2226
parameters:
2327
checkAdvancedIsset: true
2428

@@ -120,6 +124,11 @@ services:
120124
tags:
121125
- phpstan.rules.rule
122126

127+
-
128+
class: PHPStan\Rules\Comparison\ConstantLooseComparisonRule
129+
arguments:
130+
checkAlwaysTrueLooseComparison: %checkAlwaysTrueLooseComparison%
131+
123132
-
124133
class: PHPStan\Rules\Comparison\TernaryOperatorConstantConditionRule
125134
arguments:

conf/config.neon

+4
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,14 @@ parameters:
3636
illegalConstructorMethodCall: false
3737
disableCheckMissingIterableValueType: false
3838
strictUnnecessaryNullsafePropertyFetch: false
39+
looseComparison: false
3940
fileExtensions:
4041
- php
4142
checkAdvancedIsset: false
4243
checkAlwaysTrueCheckTypeFunctionCall: false
4344
checkAlwaysTrueInstanceof: false
4445
checkAlwaysTrueStrictComparison: false
46+
checkAlwaysTrueLooseComparison: false
4547
checkClassCaseSensitivity: false
4648
checkExplicitMixed: false
4749
checkFunctionArgumentTypes: false
@@ -228,12 +230,14 @@ parametersSchema:
228230
illegalConstructorMethodCall: bool(),
229231
disableCheckMissingIterableValueType: bool(),
230232
strictUnnecessaryNullsafePropertyFetch: bool(),
233+
looseComparison: bool()
231234
])
232235
fileExtensions: listOf(string())
233236
checkAdvancedIsset: bool()
234237
checkAlwaysTrueCheckTypeFunctionCall: bool()
235238
checkAlwaysTrueInstanceof: bool()
236239
checkAlwaysTrueStrictComparison: bool()
240+
checkAlwaysTrueLooseComparison: bool()
237241
checkClassCaseSensitivity: bool()
238242
checkExplicitMixed: bool()
239243
checkFunctionArgumentTypes: bool()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Comparison;
4+
5+
use PhpParser\Node;
6+
use PHPStan\Analyser\Scope;
7+
use PHPStan\Rules\Rule;
8+
use PHPStan\Rules\RuleErrorBuilder;
9+
use PHPStan\Type\Constant\ConstantBooleanType;
10+
use PHPStan\Type\VerbosityLevel;
11+
use function sprintf;
12+
13+
/**
14+
* @implements Rule<Node\Expr\BinaryOp>
15+
*/
16+
class ConstantLooseComparisonRule implements Rule
17+
{
18+
19+
public function __construct(private bool $checkAlwaysTrueLooseComparison)
20+
{
21+
}
22+
23+
public function getNodeType(): string
24+
{
25+
return Node\Expr\BinaryOp::class;
26+
}
27+
28+
public function processNode(Node $node, Scope $scope): array
29+
{
30+
if (!$node instanceof Node\Expr\BinaryOp\Equal && !$node instanceof Node\Expr\BinaryOp\NotEqual) {
31+
return [];
32+
}
33+
34+
$nodeType = $scope->getType($node);
35+
if (!$nodeType instanceof ConstantBooleanType) {
36+
return [];
37+
}
38+
39+
$leftType = $scope->getType($node->left);
40+
$rightType = $scope->getType($node->right);
41+
42+
if (!$nodeType->getValue()) {
43+
return [
44+
RuleErrorBuilder::message(sprintf(
45+
'Loose comparison using %s between %s and %s will always evaluate to false.',
46+
$node instanceof Node\Expr\BinaryOp\Equal ? '==' : '!=',
47+
$leftType->describe(VerbosityLevel::value()),
48+
$rightType->describe(VerbosityLevel::value()),
49+
))->build(),
50+
];
51+
} elseif ($this->checkAlwaysTrueLooseComparison) {
52+
return [
53+
RuleErrorBuilder::message(sprintf(
54+
'Loose comparison using %s between %s and %s will always evaluate to true.',
55+
$node instanceof Node\Expr\BinaryOp\Equal ? '==' : '!=',
56+
$leftType->describe(VerbosityLevel::value()),
57+
$rightType->describe(VerbosityLevel::value()),
58+
))->build(),
59+
];
60+
}
61+
62+
return [];
63+
}
64+
65+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Comparison;
4+
5+
use PHPStan\Rules\Rule;
6+
use PHPStan\Testing\RuleTestCase;
7+
8+
/**
9+
* @extends RuleTestCase<ConstantLooseComparisonRule>
10+
*/
11+
class ConstantLooseComparisonRuleTest extends RuleTestCase
12+
{
13+
14+
private bool $checkAlwaysTrueStrictComparison;
15+
16+
protected function getRule(): Rule
17+
{
18+
return new ConstantLooseComparisonRule($this->checkAlwaysTrueStrictComparison);
19+
}
20+
21+
public function testRule(): void
22+
{
23+
$this->checkAlwaysTrueStrictComparison = false;
24+
$this->analyse([__DIR__ . '/data/loose-comparison.php'], [
25+
[
26+
"Loose comparison using == between 0 and '1' will always evaluate to false.",
27+
20,
28+
],
29+
]);
30+
}
31+
32+
public function testRuleAlwaysTrue(): void
33+
{
34+
$this->checkAlwaysTrueStrictComparison = true;
35+
$this->analyse([__DIR__ . '/data/loose-comparison.php'], [
36+
[
37+
"Loose comparison using == between 0 and '0' will always evaluate to true.",
38+
16,
39+
],
40+
[
41+
"Loose comparison using == between 0 and '1' will always evaluate to false.",
42+
20,
43+
],
44+
]);
45+
}
46+
47+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php
2+
3+
namespace ConstantLooseComparison;
4+
5+
class Foo
6+
{
7+
8+
public function doFoo(string $s, string $i): void
9+
{
10+
if ($s == $i) {
11+
12+
}
13+
if ($s != $i) {
14+
15+
}
16+
if (0 == "0") {
17+
18+
}
19+
20+
if (0 == "1") {
21+
22+
}
23+
}
24+
25+
}

0 commit comments

Comments
 (0)