diff --git a/conf/config.level4.neon b/conf/config.level4.neon index ea1c266ad5..099cac3a6f 100644 --- a/conf/config.level4.neon +++ b/conf/config.level4.neon @@ -3,7 +3,6 @@ includes: rules: - PHPStan\Rules\Arrays\DeadForeachRule - - PHPStan\Rules\Comparison\NumberComparisonOperatorsConstantConditionRule - PHPStan\Rules\DeadCode\NoopRule - PHPStan\Rules\DeadCode\UnreachableStatementRule - PHPStan\Rules\DeadCode\UnusedPrivateConstantRule @@ -127,6 +126,13 @@ services: tags: - phpstan.rules.rule + - + class: PHPStan\Rules\Comparison\NumberComparisonOperatorsConstantConditionRule + arguments: + treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain% + tags: + - phpstan.rules.rule + - class: PHPStan\Rules\Comparison\StrictComparisonOfDifferentTypesRule arguments: diff --git a/src/Rules/Comparison/NumberComparisonOperatorsConstantConditionRule.php b/src/Rules/Comparison/NumberComparisonOperatorsConstantConditionRule.php index bf8feaaec6..107867b6ad 100644 --- a/src/Rules/Comparison/NumberComparisonOperatorsConstantConditionRule.php +++ b/src/Rules/Comparison/NumberComparisonOperatorsConstantConditionRule.php @@ -17,6 +17,12 @@ class NumberComparisonOperatorsConstantConditionRule implements Rule { + public function __construct( + private bool $treatPhpDocTypesAsCertain, + ) + { + } + public function getNodeType(): string { return BinaryOp::class; @@ -36,16 +42,29 @@ public function processNode( return []; } - $exprType = $scope->getType($node); + $exprType = $this->treatPhpDocTypesAsCertain ? $scope->getType($node) : $scope->getNativeType($node); if ($exprType instanceof ConstantBooleanType) { + $addTip = function (RuleErrorBuilder $ruleErrorBuilder) use ($scope, $node): RuleErrorBuilder { + if (!$this->treatPhpDocTypesAsCertain) { + return $ruleErrorBuilder; + } + + $booleanNativeType = $scope->getNativeType($node); + if ($booleanNativeType instanceof ConstantBooleanType) { + return $ruleErrorBuilder; + } + + return $ruleErrorBuilder->tip('Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'); + }; + return [ - RuleErrorBuilder::message(sprintf( + $addTip(RuleErrorBuilder::message(sprintf( 'Comparison operation "%s" between %s and %s is always %s.', $node->getOperatorSigil(), $scope->getType($node->left)->describe(VerbosityLevel::value()), $scope->getType($node->right)->describe(VerbosityLevel::value()), $exprType->getValue() ? 'true' : 'false', - ))->build(), + )))->build(), ]; } diff --git a/tests/PHPStan/Analyser/data/native-types.php b/tests/PHPStan/Analyser/data/native-types.php index d146298b84..3f09d923cb 100644 --- a/tests/PHPStan/Analyser/data/native-types.php +++ b/tests/PHPStan/Analyser/data/native-types.php @@ -377,3 +377,20 @@ public function doFoo(): void } } + +class PositiveInt +{ + + /** + * @param positive-int $i + * @return void + */ + public function doFoo(int $i): void + { + assertType('true', $i > 0); + assertType('false', $i <= 0); + assertNativeType('bool', $i > 0); + assertNativeType('bool', $i <= 0); + } + +} diff --git a/tests/PHPStan/Rules/Comparison/NumberComparisonOperatorsConstantConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/NumberComparisonOperatorsConstantConditionRuleTest.php index 7500fef179..15fa02685e 100644 --- a/tests/PHPStan/Rules/Comparison/NumberComparisonOperatorsConstantConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/NumberComparisonOperatorsConstantConditionRuleTest.php @@ -12,9 +12,11 @@ class NumberComparisonOperatorsConstantConditionRuleTest extends RuleTestCase { + private bool $treatPhpDocTypesAsCertain = true; + protected function getRule(): Rule { - return new NumberComparisonOperatorsConstantConditionRule(); + return new NumberComparisonOperatorsConstantConditionRule($this->treatPhpDocTypesAsCertain); } public function testBug8277(): void @@ -160,4 +162,37 @@ public function testBug8643(): void $this->analyse([__DIR__ . '/data/bug-8643.php'], []); } + public function dataTreatPhpDocTypesAsCertain(): iterable + { + yield [ + false, + [], + ]; + yield [ + true, + [ + [ + 'Comparison operation ">=" between int<1, max> and 0 is always true.', + 11, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', + ], + [ + 'Comparison operation "<" between int<1, max> and 0 is always false.', + 18, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', + ], + ], + ]; + } + + /** + * @dataProvider dataTreatPhpDocTypesAsCertain + * @param list $expectedErrors + */ + public function testTreatPhpDocTypesAsCertain(bool $treatPhpDocTypesAsCertain, array $expectedErrors): void + { + $this->treatPhpDocTypesAsCertain = $treatPhpDocTypesAsCertain; + $this->analyse([__DIR__ . '/data/number-comparison-treat.php'], $expectedErrors); + } + } diff --git a/tests/PHPStan/Rules/Comparison/data/number-comparison-treat.php b/tests/PHPStan/Rules/Comparison/data/number-comparison-treat.php new file mode 100644 index 0000000000..46c959aafc --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/number-comparison-treat.php @@ -0,0 +1,22 @@ += 0) { + } + } + + /** @param positive-int $i */ + public function sayHello2(int $i): void + { + if ($i < 0) { + } + } + +}