diff --git a/conf/bleedingEdge.neon b/conf/bleedingEdge.neon index bb50811751..8d087677c4 100644 --- a/conf/bleedingEdge.neon +++ b/conf/bleedingEdge.neon @@ -9,3 +9,4 @@ parameters: readComposerPhpVersion: true dateTimeInstantiation: true detectDuplicateStubFiles: true + checkLogicalAndConstantCondition: true diff --git a/conf/config.level4.neon b/conf/config.level4.neon index ef105f1a1a..2415631a53 100644 --- a/conf/config.level4.neon +++ b/conf/config.level4.neon @@ -42,6 +42,7 @@ services: class: PHPStan\Rules\Comparison\BooleanAndConstantConditionRule arguments: treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain% + checkLogicalAndConstantCondition: %featureToggles.checkLogicalAndConstantCondition% tags: - phpstan.rules.rule diff --git a/conf/config.neon b/conf/config.neon index dd7a5de245..55aa9f1efd 100644 --- a/conf/config.neon +++ b/conf/config.neon @@ -22,6 +22,7 @@ parameters: readComposerPhpVersion: false dateTimeInstantiation: false detectDuplicateStubFiles: false + checkLogicalAndConstantCondition: false fileExtensions: - php checkAlwaysTrueCheckTypeFunctionCall: false @@ -168,7 +169,8 @@ parametersSchema: unusedClassElements: bool(), readComposerPhpVersion: bool(), dateTimeInstantiation: bool(), - detectDuplicateStubFiles: bool() + detectDuplicateStubFiles: bool(), + checkLogicalAndConstantCondition: bool() ]) fileExtensions: listOf(string()) checkAlwaysTrueCheckTypeFunctionCall: bool() diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index e01bf3a4a2..44cbd2aef7 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -49,6 +49,7 @@ use PHPStan\DependencyInjection\Reflection\ClassReflectionExtensionRegistryProvider; use PHPStan\File\FileHelper; use PHPStan\File\FileReader; +use PHPStan\Node\BooleanAndNode; use PHPStan\Node\ClassConstantsNode; use PHPStan\Node\ClassMethodsNode; use PHPStan\Node\ClassPropertiesNode; @@ -1905,6 +1906,8 @@ static function () use ($scope, $expr): MutatingScope { $rightResult = $this->processExprNode($expr->right, $leftResult->getTruthyScope(), $nodeCallback, $context); $leftMergedWithRightScope = $leftResult->getScope()->mergeWith($rightResult->getScope()); + $nodeCallback(new BooleanAndNode($expr, $leftResult->getTruthyScope()), $scope); + return new ExpressionResult( $leftMergedWithRightScope, $leftResult->hasYield() || $rightResult->hasYield(), diff --git a/src/Node/BooleanAndNode.php b/src/Node/BooleanAndNode.php new file mode 100644 index 0000000000..4d6452e850 --- /dev/null +++ b/src/Node/BooleanAndNode.php @@ -0,0 +1,55 @@ +getAttributes()); + $this->originalNode = $originalNode; + $this->rightScope = $rightScope; + } + + /** + * @return BooleanAnd|LogicalAnd + */ + public function getOriginalNode() + { + return $this->originalNode; + } + + public function getRightScope(): Scope + { + return $this->rightScope; + } + + public function getType(): string + { + return 'PHPStan_Node_BooleanAndNode'; + } + + /** + * @return string[] + */ + public function getSubNodeNames(): array + { + return []; + } + +} diff --git a/src/Rules/Comparison/BooleanAndConstantConditionRule.php b/src/Rules/Comparison/BooleanAndConstantConditionRule.php index d8ef06a110..9feac9d37c 100644 --- a/src/Rules/Comparison/BooleanAndConstantConditionRule.php +++ b/src/Rules/Comparison/BooleanAndConstantConditionRule.php @@ -2,11 +2,14 @@ namespace PHPStan\Rules\Comparison; +use PhpParser\Node\Expr\BinaryOp\BooleanAnd; +use PhpParser\Node\Expr\BinaryOp\LogicalAnd; +use PHPStan\Node\BooleanAndNode; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\Constant\ConstantBooleanType; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Expr\BinaryOp\BooleanAnd> + * @implements \PHPStan\Rules\Rule */ class BooleanAndConstantConditionRule implements \PHPStan\Rules\Rule { @@ -15,18 +18,22 @@ class BooleanAndConstantConditionRule implements \PHPStan\Rules\Rule private bool $treatPhpDocTypesAsCertain; + private bool $checkLogicalAndConstantCondition; + public function __construct( ConstantConditionRuleHelper $helper, - bool $treatPhpDocTypesAsCertain + bool $treatPhpDocTypesAsCertain, + bool $checkLogicalAndConstantCondition ) { $this->helper = $helper; $this->treatPhpDocTypesAsCertain = $treatPhpDocTypesAsCertain; + $this->checkLogicalAndConstantCondition = $checkLogicalAndConstantCondition; } public function getNodeType(): string { - return \PhpParser\Node\Expr\BinaryOp\BooleanAnd::class; + return BooleanAndNode::class; } public function processNode( @@ -35,15 +42,22 @@ public function processNode( ): array { $errors = []; - $leftType = $this->helper->getBooleanType($scope, $node->left); + + /** @var BooleanAnd|LogicalAnd $originalNode */ + $originalNode = $node->getOriginalNode(); + if (!$originalNode instanceof BooleanAnd && !$this->checkLogicalAndConstantCondition) { + return []; + } + + $leftType = $this->helper->getBooleanType($scope, $originalNode->left); $tipText = 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'; if ($leftType instanceof ConstantBooleanType) { - $addTipLeft = function (RuleErrorBuilder $ruleErrorBuilder) use ($scope, $node, $tipText): RuleErrorBuilder { + $addTipLeft = function (RuleErrorBuilder $ruleErrorBuilder) use ($scope, $tipText, $originalNode): RuleErrorBuilder { if (!$this->treatPhpDocTypesAsCertain) { return $ruleErrorBuilder; } - $booleanNativeType = $this->helper->getNativeBooleanType($scope, $node->left); + $booleanNativeType = $this->helper->getNativeBooleanType($scope, $originalNode->left); if ($booleanNativeType instanceof ConstantBooleanType) { return $ruleErrorBuilder; } @@ -53,22 +67,23 @@ public function processNode( $errors[] = $addTipLeft(RuleErrorBuilder::message(sprintf( 'Left side of && is always %s.', $leftType->getValue() ? 'true' : 'false' - )))->line($node->left->getLine())->build(); + )))->line($originalNode->left->getLine())->build(); } + $rightScope = $node->getRightScope(); $rightType = $this->helper->getBooleanType( - $scope->filterByTruthyValue($node->left), - $node->right + $rightScope, + $originalNode->right ); if ($rightType instanceof ConstantBooleanType) { - $addTipRight = function (RuleErrorBuilder $ruleErrorBuilder) use ($scope, $node, $tipText): RuleErrorBuilder { + $addTipRight = function (RuleErrorBuilder $ruleErrorBuilder) use ($rightScope, $originalNode, $tipText): RuleErrorBuilder { if (!$this->treatPhpDocTypesAsCertain) { return $ruleErrorBuilder; } $booleanNativeType = $this->helper->getNativeBooleanType( - $scope->doNotTreatPhpDocTypesAsCertain()->filterByTruthyValue($node->left), - $node->right + $rightScope->doNotTreatPhpDocTypesAsCertain(), + $originalNode->right ); if ($booleanNativeType instanceof ConstantBooleanType) { return $ruleErrorBuilder; @@ -79,18 +94,18 @@ public function processNode( $errors[] = $addTipRight(RuleErrorBuilder::message(sprintf( 'Right side of && is always %s.', $rightType->getValue() ? 'true' : 'false' - )))->line($node->right->getLine())->build(); + )))->line($originalNode->right->getLine())->build(); } if (count($errors) === 0) { - $nodeType = $scope->getType($node); + $nodeType = $scope->getType($originalNode); if ($nodeType instanceof ConstantBooleanType) { - $addTip = function (RuleErrorBuilder $ruleErrorBuilder) use ($scope, $node, $tipText): RuleErrorBuilder { + $addTip = function (RuleErrorBuilder $ruleErrorBuilder) use ($scope, $originalNode, $tipText): RuleErrorBuilder { if (!$this->treatPhpDocTypesAsCertain) { return $ruleErrorBuilder; } - $booleanNativeType = $scope->doNotTreatPhpDocTypesAsCertain()->getType($node); + $booleanNativeType = $scope->doNotTreatPhpDocTypesAsCertain()->getType($originalNode); if ($booleanNativeType instanceof ConstantBooleanType) { return $ruleErrorBuilder; } diff --git a/tests/PHPStan/Rules/Comparison/BooleanAndConstantConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/BooleanAndConstantConditionRuleTest.php index b61bafc3dc..1887e70627 100644 --- a/tests/PHPStan/Rules/Comparison/BooleanAndConstantConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/BooleanAndConstantConditionRuleTest.php @@ -23,7 +23,8 @@ protected function getRule(): \PHPStan\Rules\Rule ), $this->treatPhpDocTypesAsCertain ), - $this->treatPhpDocTypesAsCertain + $this->treatPhpDocTypesAsCertain, + true ); }