diff --git a/src/Rules/Comparison/BooleanAndConstantConditionRule.php b/src/Rules/Comparison/BooleanAndConstantConditionRule.php index 60e88caa52..2947e6d3ee 100644 --- a/src/Rules/Comparison/BooleanAndConstantConditionRule.php +++ b/src/Rules/Comparison/BooleanAndConstantConditionRule.php @@ -3,6 +3,7 @@ namespace PHPStan\Rules\Comparison; use PhpParser\Node; +use PhpParser\Node\Expr; use PHPStan\Analyser\CollectedDataEmitter; use PHPStan\Analyser\NodeCallbackInvoker; use PHPStan\Analyser\Scope; @@ -27,6 +28,7 @@ public function __construct( private ConstantConditionRuleHelper $helper, private PossiblyImpureTipHelper $possiblyImpureTipHelper, private ConstantConditionInTraitHelper $constantConditionInTraitHelper, + private FunctionCallConstantConditionHelper $functionCallConstantConditionHelper, #[AutowiredParameter] private bool $treatPhpDocTypesAsCertain, #[AutowiredParameter] @@ -87,16 +89,18 @@ public function processNode( } $ruleError = $errorBuilder->build(); $hasLeftOrRightError = true; - if ($isInTrait) { + if ($this->functionCallConstantConditionHelper->isTypeCheckCandidate($originalNode->left)) { + $this->functionCallConstantConditionHelper->emitFunctionCallError(self::class, $scope, $originalNode->left, $leftType->getValue(), $ruleError); + } elseif ($isInTrait) { $this->constantConditionInTraitHelper->emitError(self::class, $scope, $originalNode->left, $leftType->getValue(), $ruleError); } else { $errors[] = $ruleError; } } else { - $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $originalNode->left); + $this->emitNoError($scope, $originalNode->left); } } else { - $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $originalNode->left); + $this->emitNoError($scope, $originalNode->left); } $rightScope = $node->getRightScope(); @@ -140,16 +144,18 @@ public function processNode( } $ruleError = $errorBuilder->build(); $hasLeftOrRightError = true; - if ($isInTrait) { + if ($this->functionCallConstantConditionHelper->isTypeCheckCandidate($originalNode->right)) { + $this->functionCallConstantConditionHelper->emitFunctionCallError(self::class, $scope, $originalNode->right, $rightType->getValue(), $ruleError); + } elseif ($isInTrait) { $this->constantConditionInTraitHelper->emitError(self::class, $scope, $originalNode->right, $rightType->getValue(), $ruleError); } else { $errors[] = $ruleError; } } else { - $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $originalNode->right); + $this->emitNoError($scope, $originalNode->right); } } else { - $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $originalNode->right); + $this->emitNoError($scope, $originalNode->right); } if (count($errors) === 0 && !$hasLeftOrRightError && !$scope->isInFirstLevelStatement()) { @@ -203,4 +209,16 @@ public function processNode( return $errors; } + private function emitNoError( + Scope&NodeCallbackInvoker&CollectedDataEmitter $scope, + Expr $expr, + ): void + { + if ($this->functionCallConstantConditionHelper->isTypeCheckCandidate($expr)) { + $this->functionCallConstantConditionHelper->emitFunctionCallNoError(self::class, $scope, $expr); + } else { + $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $expr); + } + } + } diff --git a/src/Rules/Comparison/BooleanNotConstantConditionRule.php b/src/Rules/Comparison/BooleanNotConstantConditionRule.php index fe786dd3c1..0052f9f8ab 100644 --- a/src/Rules/Comparison/BooleanNotConstantConditionRule.php +++ b/src/Rules/Comparison/BooleanNotConstantConditionRule.php @@ -25,6 +25,7 @@ public function __construct( private ConstantConditionRuleHelper $helper, private PossiblyImpureTipHelper $possiblyImpureTipHelper, private ConstantConditionInTraitHelper $constantConditionInTraitHelper, + private FunctionCallConstantConditionHelper $functionCallConstantConditionHelper, #[AutowiredParameter] private bool $treatPhpDocTypesAsCertain, #[AutowiredParameter] @@ -78,6 +79,10 @@ public function processNode( $errorBuilder->identifier(sprintf('booleanNot.always%s', $exprType->getValue() ? 'False' : 'True')); $ruleError = $errorBuilder->build(); + if ($this->functionCallConstantConditionHelper->isTypeCheckCandidate($node->expr)) { + $this->functionCallConstantConditionHelper->emitFunctionCallError(self::class, $scope, $node->expr, !$exprType->getValue(), $ruleError); + return []; + } if ($scope->isInTrait()) { $this->constantConditionInTraitHelper->emitError(self::class, $scope, $node->expr, !$exprType->getValue(), $ruleError); return []; @@ -87,7 +92,11 @@ public function processNode( } } - $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $node->expr); + if ($this->functionCallConstantConditionHelper->isTypeCheckCandidate($node->expr)) { + $this->functionCallConstantConditionHelper->emitFunctionCallNoError(self::class, $scope, $node->expr); + } else { + $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $node->expr); + } return []; } diff --git a/src/Rules/Comparison/BooleanOrConstantConditionRule.php b/src/Rules/Comparison/BooleanOrConstantConditionRule.php index cc9fc93efa..a010342caa 100644 --- a/src/Rules/Comparison/BooleanOrConstantConditionRule.php +++ b/src/Rules/Comparison/BooleanOrConstantConditionRule.php @@ -3,6 +3,7 @@ namespace PHPStan\Rules\Comparison; use PhpParser\Node; +use PhpParser\Node\Expr; use PHPStan\Analyser\CollectedDataEmitter; use PHPStan\Analyser\NodeCallbackInvoker; use PHPStan\Analyser\Scope; @@ -27,6 +28,7 @@ public function __construct( private ConstantConditionRuleHelper $helper, private PossiblyImpureTipHelper $possiblyImpureTipHelper, private ConstantConditionInTraitHelper $constantConditionInTraitHelper, + private FunctionCallConstantConditionHelper $functionCallConstantConditionHelper, #[AutowiredParameter] private bool $treatPhpDocTypesAsCertain, #[AutowiredParameter] @@ -87,16 +89,18 @@ public function processNode( } $ruleError = $errorBuilder->build(); $hasLeftOrRightError = true; - if ($isInTrait) { + if ($this->functionCallConstantConditionHelper->isTypeCheckCandidate($originalNode->left)) { + $this->functionCallConstantConditionHelper->emitFunctionCallError(self::class, $scope, $originalNode->left, $leftType->getValue(), $ruleError); + } elseif ($isInTrait) { $this->constantConditionInTraitHelper->emitError(self::class, $scope, $originalNode->left, $leftType->getValue(), $ruleError); } else { $messages[] = $ruleError; } } else { - $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $originalNode->left); + $this->emitNoError($scope, $originalNode->left); } } else { - $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $originalNode->left); + $this->emitNoError($scope, $originalNode->left); } $rightScope = $node->getRightScope(); @@ -140,16 +144,18 @@ public function processNode( } $ruleError = $errorBuilder->build(); $hasLeftOrRightError = true; - if ($isInTrait) { + if ($this->functionCallConstantConditionHelper->isTypeCheckCandidate($originalNode->right)) { + $this->functionCallConstantConditionHelper->emitFunctionCallError(self::class, $scope, $originalNode->right, $rightType->getValue(), $ruleError); + } elseif ($isInTrait) { $this->constantConditionInTraitHelper->emitError(self::class, $scope, $originalNode->right, $rightType->getValue(), $ruleError); } else { $messages[] = $ruleError; } } else { - $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $originalNode->right); + $this->emitNoError($scope, $originalNode->right); } } else { - $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $originalNode->right); + $this->emitNoError($scope, $originalNode->right); } if (count($messages) === 0 && !$hasLeftOrRightError && !$scope->isInFirstLevelStatement()) { @@ -203,4 +209,16 @@ public function processNode( return $messages; } + private function emitNoError( + Scope&NodeCallbackInvoker&CollectedDataEmitter $scope, + Expr $expr, + ): void + { + if ($this->functionCallConstantConditionHelper->isTypeCheckCandidate($expr)) { + $this->functionCallConstantConditionHelper->emitFunctionCallNoError(self::class, $scope, $expr); + } else { + $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $expr); + } + } + } diff --git a/src/Rules/Comparison/ConstantConditionInTraitHelper.php b/src/Rules/Comparison/ConstantConditionInTraitHelper.php index 31f70c70cc..145c48919c 100644 --- a/src/Rules/Comparison/ConstantConditionInTraitHelper.php +++ b/src/Rules/Comparison/ConstantConditionInTraitHelper.php @@ -27,6 +27,11 @@ public function __construct( { } + private function exprString(Expr $expr): string + { + return sprintf('%s:%d', $this->exprPrinter->printExpr($expr), $expr->getStartLine()); + } + /** * @param class-string> $ruleName */ @@ -40,11 +45,10 @@ public function emitNoError( return; } - $exprString = sprintf('%s:%d', $this->exprPrinter->printExpr($expr), $expr->getStartLine()); $scope->emitCollectedData(ConstantConditionInTraitCollector::class, [ $ruleName, $scope->getTraitReflection()->getName(), - $exprString, + $this->exprString($expr), null, ]); } @@ -68,11 +72,10 @@ public function emitError( return; } - $exprString = sprintf('%s:%d', $this->exprPrinter->printExpr($expr), $expr->getStartLine()); $scope->emitCollectedData(ConstantConditionInTraitCollector::class, [ $ruleName, $scope->getTraitReflection()->getName(), - $exprString, + $this->exprString($expr), $value, $this->ruleErrorTransformer->transform($ruleError, $scope, [], $expr), ]); diff --git a/src/Rules/Comparison/ConstantConditionRuleHelper.php b/src/Rules/Comparison/ConstantConditionRuleHelper.php index 30a08f665d..5f5bb44c68 100644 --- a/src/Rules/Comparison/ConstantConditionRuleHelper.php +++ b/src/Rules/Comparison/ConstantConditionRuleHelper.php @@ -3,8 +3,6 @@ namespace PHPStan\Rules\Comparison; use PhpParser\Node\Expr; -use PhpParser\Node\Expr\FuncCall; -use PhpParser\Node\Expr\MethodCall; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredParameter; use PHPStan\DependencyInjection\AutowiredService; @@ -15,14 +13,13 @@ final class ConstantConditionRuleHelper { public function __construct( - private ImpossibleCheckTypeHelper $impossibleCheckTypeHelper, #[AutowiredParameter] private bool $treatPhpDocTypesAsCertain, ) { } - private function shouldSkip(Scope $scope, Expr $expr): bool + private function shouldSkip(Expr $expr): bool { if ( $expr instanceof Expr\BinaryOp\Equal @@ -50,25 +47,12 @@ private function shouldSkip(Scope $scope, Expr $expr): bool return true; } - if ( - ( - $expr instanceof FuncCall - || $expr instanceof MethodCall - || $expr instanceof Expr\StaticCall - ) && !$expr->isFirstClassCallable() - ) { - $isAlways = $this->impossibleCheckTypeHelper->findSpecifiedType($scope, $expr); - if ($isAlways !== null) { - return true; - } - } - return false; } public function getBooleanType(Scope $scope, Expr $expr): BooleanType { - if ($this->shouldSkip($scope, $expr)) { + if ($this->shouldSkip($expr)) { return new BooleanType(); } @@ -81,7 +65,7 @@ public function getBooleanType(Scope $scope, Expr $expr): BooleanType public function getNativeBooleanType(Scope $scope, Expr $expr): BooleanType { - if ($this->shouldSkip($scope, $expr)) { + if ($this->shouldSkip($expr)) { return new BooleanType(); } diff --git a/src/Rules/Comparison/DoWhileLoopConstantConditionRule.php b/src/Rules/Comparison/DoWhileLoopConstantConditionRule.php index bff27fcfe2..603d6613fb 100644 --- a/src/Rules/Comparison/DoWhileLoopConstantConditionRule.php +++ b/src/Rules/Comparison/DoWhileLoopConstantConditionRule.php @@ -3,6 +3,7 @@ namespace PHPStan\Rules\Comparison; use PhpParser\Node; +use PhpParser\Node\Expr; use PhpParser\Node\Scalar\Int_; use PhpParser\Node\Stmt\Break_; use PhpParser\Node\Stmt\Continue_; @@ -28,6 +29,7 @@ public function __construct( private ConstantConditionRuleHelper $helper, private PossiblyImpureTipHelper $possiblyImpureTipHelper, private ConstantConditionInTraitHelper $constantConditionInTraitHelper, + private FunctionCallConstantConditionHelper $functionCallConstantConditionHelper, #[AutowiredParameter] private bool $treatPhpDocTypesAsCertain, #[AutowiredParameter(ref: '%tips.treatPhpDocTypesAsCertain%')] @@ -44,23 +46,24 @@ public function getNodeType(): string public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataEmitter $scope): array { $exprType = $this->helper->getBooleanType($scope, $node->getCond()); + $isTypeCheckCandidate = $this->functionCallConstantConditionHelper->isTypeCheckCandidate($node->getCond()); if ($exprType instanceof ConstantBooleanType) { if ($exprType->getValue()) { if ($node->hasYield()) { - $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $node->getCond()); + $this->emitNoError($scope, $node->getCond(), $isTypeCheckCandidate); return []; } foreach ($node->getExitPoints() as $exitPoint) { $statement = $exitPoint->getStatement(); if (!$statement instanceof Continue_) { - $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $node->getCond()); + $this->emitNoError($scope, $node->getCond(), $isTypeCheckCandidate); return []; } if (!$statement->num instanceof Int_) { continue; } if ($statement->num->value > 1) { - $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $node->getCond()); + $this->emitNoError($scope, $node->getCond(), $isTypeCheckCandidate); return []; } } @@ -68,7 +71,7 @@ public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataE foreach ($node->getExitPoints() as $exitPoint) { $statement = $exitPoint->getStatement(); if ($statement instanceof Break_) { - $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $node->getCond()); + $this->emitNoError($scope, $node->getCond(), $isTypeCheckCandidate); return []; } } @@ -99,6 +102,10 @@ public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataE ->line($node->getCond()->getStartLine()) ->identifier(sprintf('doWhile.always%s', $exprType->getValue() ? 'True' : 'False')) ->build(); + if ($isTypeCheckCandidate) { + $this->functionCallConstantConditionHelper->emitFunctionCallError(self::class, $scope, $node->getCond(), $exprType->getValue(), $ruleError); + return []; + } if ($scope->isInTrait()) { $this->constantConditionInTraitHelper->emitError(self::class, $scope, $node->getCond(), $exprType->getValue(), $ruleError); return []; @@ -107,8 +114,21 @@ public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataE return [$ruleError]; } - $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $node->getCond()); + $this->emitNoError($scope, $node->getCond(), $isTypeCheckCandidate); return []; } + private function emitNoError( + Scope&NodeCallbackInvoker&CollectedDataEmitter $scope, + Expr $cond, + bool $isTypeCheckCandidate, + ): void + { + if ($isTypeCheckCandidate) { + $this->functionCallConstantConditionHelper->emitFunctionCallNoError(self::class, $scope, $cond); + } else { + $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $cond); + } + } + } diff --git a/src/Rules/Comparison/ElseIfConstantConditionRule.php b/src/Rules/Comparison/ElseIfConstantConditionRule.php index 22c19dd1ec..f4c02a62a7 100644 --- a/src/Rules/Comparison/ElseIfConstantConditionRule.php +++ b/src/Rules/Comparison/ElseIfConstantConditionRule.php @@ -25,6 +25,7 @@ public function __construct( private ConstantConditionRuleHelper $helper, private PossiblyImpureTipHelper $possiblyImpureTipHelper, private ConstantConditionInTraitHelper $constantConditionInTraitHelper, + private FunctionCallConstantConditionHelper $functionCallConstantConditionHelper, #[AutowiredParameter] private bool $treatPhpDocTypesAsCertain, #[AutowiredParameter] @@ -79,6 +80,10 @@ public function processNode( $errorBuilder->identifier(sprintf('elseif.always%s', $exprType->getValue() ? 'True' : 'False')); $ruleError = $errorBuilder->build(); + if ($this->functionCallConstantConditionHelper->isTypeCheckCandidate($node->cond)) { + $this->functionCallConstantConditionHelper->emitFunctionCallError(self::class, $scope, $node->cond, $exprType->getValue(), $ruleError); + return []; + } if ($scope->isInTrait()) { $this->constantConditionInTraitHelper->emitError(self::class, $scope, $node->cond, $exprType->getValue(), $ruleError); return []; @@ -88,7 +93,11 @@ public function processNode( } } - $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $node->cond); + if ($this->functionCallConstantConditionHelper->isTypeCheckCandidate($node->cond)) { + $this->functionCallConstantConditionHelper->emitFunctionCallNoError(self::class, $scope, $node->cond); + } else { + $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $node->cond); + } return []; } diff --git a/src/Rules/Comparison/FunctionCallConstantConditionCollector.php b/src/Rules/Comparison/FunctionCallConstantConditionCollector.php new file mode 100644 index 0000000000..fa37dab949 --- /dev/null +++ b/src/Rules/Comparison/FunctionCallConstantConditionCollector.php @@ -0,0 +1,28 @@ +>, trait-string|null, string, null}|array{class-string>, trait-string|null, string, bool, Error|array}> + */ +final class FunctionCallConstantConditionCollector implements Collector +{ + + public function getNodeType(): string + { + throw new ShouldNotHappenException(); + } + + public function processNode(Node $node, Scope $scope) + { + throw new ShouldNotHappenException(); + } + +} diff --git a/src/Rules/Comparison/FunctionCallConstantConditionHelper.php b/src/Rules/Comparison/FunctionCallConstantConditionHelper.php new file mode 100644 index 0000000000..567632db25 --- /dev/null +++ b/src/Rules/Comparison/FunctionCallConstantConditionHelper.php @@ -0,0 +1,104 @@ +exprPrinter->printExpr($expr), $expr->getStartLine()); + } + + /** + * Whether the condition is a function/method/static call that the + * ImpossibleCheckType* rules might own. For these the constant-condition + * reporting is deferred to FunctionCallConstantConditionRule, which + * deduplicates against ImpossibleCheckTypeReportedCollector markers. + */ + public function isTypeCheckCandidate(Expr $expr): bool + { + return ( + $expr instanceof FuncCall + || $expr instanceof MethodCall + || $expr instanceof StaticCall + ) && !$expr->isFirstClassCallable(); + } + + /** + * @param class-string> $ruleName + */ + public function emitFunctionCallNoError( + string $ruleName, + Scope&NodeCallbackInvoker&CollectedDataEmitter $scope, + Expr $expr, + ): void + { + $scope->emitCollectedData(FunctionCallConstantConditionCollector::class, [ + $ruleName, + $scope->isInTrait() ? $scope->getTraitReflection()->getName() : null, + $this->exprString($expr), + null, + ]); + } + + /** + * @param class-string> $ruleName + */ + public function emitFunctionCallError( + string $ruleName, + Scope&NodeCallbackInvoker&CollectedDataEmitter $scope, + Expr $expr, + bool $value, + RuleError $ruleError, + ): void + { + if ($ruleError instanceof FixableNodeRuleError) { + throw new ShouldNotHappenException('Fixable errors are not supported by FunctionCallConstantConditionHelper.'); + } + + $scope->emitCollectedData(FunctionCallConstantConditionCollector::class, [ + $ruleName, + $scope->isInTrait() ? $scope->getTraitReflection()->getName() : null, + $this->exprString($expr), + $value, + $this->ruleErrorTransformer->transform($ruleError, $scope, [], $expr), + ]); + } + + public function emitImpossibleCheckReported( + Scope&NodeCallbackInvoker&CollectedDataEmitter $scope, + Expr $expr, + ): void + { + $scope->emitCollectedData(ImpossibleCheckTypeReportedCollector::class, [ + $this->exprString($expr), + ]); + } + +} diff --git a/src/Rules/Comparison/FunctionCallConstantConditionRule.php b/src/Rules/Comparison/FunctionCallConstantConditionRule.php new file mode 100644 index 0000000000..1754bd9cdc --- /dev/null +++ b/src/Rules/Comparison/FunctionCallConstantConditionRule.php @@ -0,0 +1,124 @@ + + */ +#[RegisteredRule(level: 4)] +final class FunctionCallConstantConditionRule implements Rule +{ + + private const NULL_TRAIT_KEY = "\0null-trait"; + + public function getNodeType(): string + { + return CollectedDataNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $reportedMarkers = []; + foreach ($node->get(ImpossibleCheckTypeReportedCollector::class) as $fileData) { + foreach ($fileData as $data) { + $reportedMarkers[$data[0]] = true; + } + } + + $errorsByRuleTraitExprValue = []; + foreach ($node->get(FunctionCallConstantConditionCollector::class) as $fileData) { + foreach ($fileData as $data) { + $ruleName = $data[0]; + $traitName = $data[1]; + $traitKey = $traitName ?? self::NULL_TRAIT_KEY; + $exprString = $data[2]; + $value = $data[3]; + $valueKey = var_export($value, true); + if ($data[3] === null) { + $errorsByRuleTraitExprValue[$ruleName][$traitKey][$exprString][$valueKey][] = null; + // no error reported + continue; + } + + $error = $data[4]; + $errorsByRuleTraitExprValue[$ruleName][$traitKey][$exprString][$valueKey][] = $error; + } + } + + $transformedErrors = []; + foreach ($errorsByRuleTraitExprValue as $ruleData) { + foreach ($ruleData as $traitKey => $traitData) { + $isTrait = $traitKey !== self::NULL_TRAIT_KEY; + foreach ($traitData as $exprString => $valueData) { + if (array_key_exists($exprString, $reportedMarkers)) { + // the ImpossibleCheckType* rule owns this call site + continue; + } + + if ($isTrait && count($valueData) > 1) { + continue; + } + + $uniquedErrors = []; + foreach ($valueData as $errors) { + foreach ($errors as $errorObject) { + if ($errorObject === null) { + continue; + } + if (is_array($errorObject)) { + $errorObject = Error::decode($errorObject); + } + + $message = $errorObject->getMessage(); + $uniquedErrors[$message] = $errorObject; + } + } + + $uniquedErrors = array_values($uniquedErrors); + if (count($uniquedErrors) === 0) { + continue; + } + + if (!$isTrait) { + foreach ($uniquedErrors as $uniquedError) { + $transformedErrors[] = new TransformedRuleError($uniquedError); + } + continue; + } + + if (count($uniquedErrors) === 1) { + // report directly in trait, no "in context of" + $transformedErrors[] = new TransformedRuleError($uniquedErrors[0]->removeTraitContext()); + continue; + } + + // report each error in its context + foreach ($uniquedErrors as $uniquedError) { + $transformedErrors[] = new TransformedRuleError($uniquedError); + } + } + } + } + + return $transformedErrors; + } + +} diff --git a/src/Rules/Comparison/IfConstantConditionRule.php b/src/Rules/Comparison/IfConstantConditionRule.php index a1eb712e65..5d43388b75 100644 --- a/src/Rules/Comparison/IfConstantConditionRule.php +++ b/src/Rules/Comparison/IfConstantConditionRule.php @@ -24,6 +24,7 @@ public function __construct( private ConstantConditionRuleHelper $helper, private PossiblyImpureTipHelper $possiblyImpureTipHelper, private ConstantConditionInTraitHelper $constantConditionInTraitHelper, + private FunctionCallConstantConditionHelper $functionCallConstantConditionHelper, #[AutowiredParameter] private bool $treatPhpDocTypesAsCertain, #[AutowiredParameter(ref: '%tips.treatPhpDocTypesAsCertain%')] @@ -68,6 +69,10 @@ public function processNode( ))) ->identifier(sprintf('if.always%s', $exprType->getValue() ? 'True' : 'False')) ->line($node->cond->getStartLine())->build(); + if ($this->functionCallConstantConditionHelper->isTypeCheckCandidate($node->cond)) { + $this->functionCallConstantConditionHelper->emitFunctionCallError(self::class, $scope, $node->cond, $exprType->getValue(), $ruleError); + return []; + } if ($scope->isInTrait()) { $this->constantConditionInTraitHelper->emitError(self::class, $scope, $node->cond, $exprType->getValue(), $ruleError); return []; @@ -76,7 +81,11 @@ public function processNode( return [$ruleError]; } - $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $node->cond); + if ($this->functionCallConstantConditionHelper->isTypeCheckCandidate($node->cond)) { + $this->functionCallConstantConditionHelper->emitFunctionCallNoError(self::class, $scope, $node->cond); + } else { + $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $node->cond); + } return []; } diff --git a/src/Rules/Comparison/ImpossibleCheckTypeFunctionCallRule.php b/src/Rules/Comparison/ImpossibleCheckTypeFunctionCallRule.php index d8f6561f58..7e755e9e36 100644 --- a/src/Rules/Comparison/ImpossibleCheckTypeFunctionCallRule.php +++ b/src/Rules/Comparison/ImpossibleCheckTypeFunctionCallRule.php @@ -24,6 +24,7 @@ public function __construct( private ImpossibleCheckTypeHelper $impossibleCheckTypeHelper, private PossiblyImpureTipHelper $possiblyImpureTipHelper, private ConstantConditionInTraitHelper $constantConditionInTraitHelper, + private FunctionCallConstantConditionHelper $functionCallConstantConditionHelper, #[AutowiredParameter] private bool $treatPhpDocTypesAsCertain, #[AutowiredParameter] @@ -53,6 +54,8 @@ public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataE return []; } + $this->functionCallConstantConditionHelper->emitImpossibleCheckReported($scope, $node); + $addTip = function (RuleErrorBuilder $ruleErrorBuilder) use ($scope, $node, $reasons): RuleErrorBuilder { if ($reasons !== []) { return $this->possiblyImpureTipHelper->addTip($scope, $node, $ruleErrorBuilder->acceptsReasonsTip($reasons)); diff --git a/src/Rules/Comparison/ImpossibleCheckTypeMethodCallRule.php b/src/Rules/Comparison/ImpossibleCheckTypeMethodCallRule.php index 279ccaf573..a5b226387d 100644 --- a/src/Rules/Comparison/ImpossibleCheckTypeMethodCallRule.php +++ b/src/Rules/Comparison/ImpossibleCheckTypeMethodCallRule.php @@ -27,6 +27,7 @@ public function __construct( private ImpossibleCheckTypeHelper $impossibleCheckTypeHelper, private PossiblyImpureTipHelper $possiblyImpureTipHelper, private ConstantConditionInTraitHelper $constantConditionInTraitHelper, + private FunctionCallConstantConditionHelper $functionCallConstantConditionHelper, #[AutowiredParameter] private bool $treatPhpDocTypesAsCertain, #[AutowiredParameter] @@ -47,6 +48,7 @@ public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataE if (!$node->name instanceof Node\Identifier) { return []; } + $methodName = $node->name->name; $reasons = []; $isAlways = $this->impossibleCheckTypeHelper->findSpecifiedType($scope, $node, $reasons); @@ -55,6 +57,8 @@ public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataE return []; } + $this->functionCallConstantConditionHelper->emitImpossibleCheckReported($scope, $node); + $addTip = function (RuleErrorBuilder $ruleErrorBuilder) use ($scope, $node, $reasons): RuleErrorBuilder { if ($reasons !== []) { return $this->possiblyImpureTipHelper->addTip($scope, $node, $ruleErrorBuilder->acceptsReasonsTip($reasons)); @@ -78,7 +82,7 @@ public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataE }; if (!$isAlways) { - $method = $this->getMethod($node->var, $node->name->name, $scope); + $method = $this->getMethod($node->var, $methodName, $scope); $errorBuilder = $addTip(RuleErrorBuilder::message(sprintf( 'Call to method %s::%s()%s will always evaluate to false.', $method->getDeclaringClass()->getDisplayName(), @@ -100,7 +104,7 @@ public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataE return []; } - $method = $this->getMethod($node->var, $node->name->name, $scope); + $method = $this->getMethod($node->var, $methodName, $scope); $errorBuilder = $addTip(RuleErrorBuilder::message(sprintf( 'Call to method %s::%s()%s will always evaluate to true.', $method->getDeclaringClass()->getDisplayName(), diff --git a/src/Rules/Comparison/ImpossibleCheckTypeReportedCollector.php b/src/Rules/Comparison/ImpossibleCheckTypeReportedCollector.php new file mode 100644 index 0000000000..fb2f9a7b48 --- /dev/null +++ b/src/Rules/Comparison/ImpossibleCheckTypeReportedCollector.php @@ -0,0 +1,26 @@ + + */ +final class ImpossibleCheckTypeReportedCollector implements Collector +{ + + public function getNodeType(): string + { + throw new ShouldNotHappenException(); + } + + public function processNode(Node $node, Scope $scope) + { + throw new ShouldNotHappenException(); + } + +} diff --git a/src/Rules/Comparison/ImpossibleCheckTypeStaticMethodCallRule.php b/src/Rules/Comparison/ImpossibleCheckTypeStaticMethodCallRule.php index bbab2d9800..e1ecf70ecd 100644 --- a/src/Rules/Comparison/ImpossibleCheckTypeStaticMethodCallRule.php +++ b/src/Rules/Comparison/ImpossibleCheckTypeStaticMethodCallRule.php @@ -27,6 +27,7 @@ public function __construct( private ImpossibleCheckTypeHelper $impossibleCheckTypeHelper, private PossiblyImpureTipHelper $possiblyImpureTipHelper, private ConstantConditionInTraitHelper $constantConditionInTraitHelper, + private FunctionCallConstantConditionHelper $functionCallConstantConditionHelper, #[AutowiredParameter] private bool $treatPhpDocTypesAsCertain, #[AutowiredParameter] @@ -47,6 +48,7 @@ public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataE if (!$node->name instanceof Node\Identifier) { return []; } + $methodName = $node->name->name; $reasons = []; $isAlways = $this->impossibleCheckTypeHelper->findSpecifiedType($scope, $node, $reasons); @@ -55,6 +57,8 @@ public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataE return []; } + $this->functionCallConstantConditionHelper->emitImpossibleCheckReported($scope, $node); + $addTip = function (RuleErrorBuilder $ruleErrorBuilder) use ($scope, $node, $reasons): RuleErrorBuilder { if ($reasons !== []) { return $this->possiblyImpureTipHelper->addTip($scope, $node, $ruleErrorBuilder->acceptsReasonsTip($reasons)); @@ -78,7 +82,7 @@ public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataE }; if (!$isAlways) { - $method = $this->getMethod($node->class, $node->name->name, $scope); + $method = $this->getMethod($node->class, $methodName, $scope); $errorBuilder = $addTip(RuleErrorBuilder::message(sprintf( 'Call to static method %s::%s()%s will always evaluate to false.', @@ -101,7 +105,7 @@ public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataE return []; } - $method = $this->getMethod($node->class, $node->name->name, $scope); + $method = $this->getMethod($node->class, $methodName, $scope); $errorBuilder = $addTip(RuleErrorBuilder::message(sprintf( 'Call to static method %s::%s()%s will always evaluate to true.', $method->getDeclaringClass()->getDisplayName(), diff --git a/src/Rules/Comparison/LogicalXorConstantConditionRule.php b/src/Rules/Comparison/LogicalXorConstantConditionRule.php index 16589f2888..c4566be9ea 100644 --- a/src/Rules/Comparison/LogicalXorConstantConditionRule.php +++ b/src/Rules/Comparison/LogicalXorConstantConditionRule.php @@ -3,6 +3,7 @@ namespace PHPStan\Rules\Comparison; use PhpParser\Node; +use PhpParser\Node\Expr; use PhpParser\Node\Expr\BinaryOp\LogicalXor; use PHPStan\Analyser\CollectedDataEmitter; use PHPStan\Analyser\NodeCallbackInvoker; @@ -26,6 +27,7 @@ public function __construct( private ConstantConditionRuleHelper $helper, private PossiblyImpureTipHelper $possiblyImpureTipHelper, private ConstantConditionInTraitHelper $constantConditionInTraitHelper, + private FunctionCallConstantConditionHelper $functionCallConstantConditionHelper, #[AutowiredParameter] private bool $treatPhpDocTypesAsCertain, #[AutowiredParameter] @@ -77,16 +79,18 @@ public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataE $errorBuilder->tip('Remove remaining cases below this one and this error will disappear too.'); } $ruleError = $errorBuilder->build(); - if ($isInTrait) { + if ($this->functionCallConstantConditionHelper->isTypeCheckCandidate($node->left)) { + $this->functionCallConstantConditionHelper->emitFunctionCallError(self::class, $scope, $node->left, $leftType->getValue(), $ruleError); + } elseif ($isInTrait) { $this->constantConditionInTraitHelper->emitError(self::class, $scope, $node->left, $leftType->getValue(), $ruleError); } else { $errors[] = $ruleError; } } else { - $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $node->left); + $this->emitNoError($scope, $node->left); } } else { - $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $node->left); + $this->emitNoError($scope, $node->left); } $rightType = $this->helper->getBooleanType($scope, $node->right); @@ -124,19 +128,33 @@ public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataE $errorBuilder->tip('Remove remaining cases below this one and this error will disappear too.'); } $ruleError = $errorBuilder->build(); - if ($isInTrait) { + if ($this->functionCallConstantConditionHelper->isTypeCheckCandidate($node->right)) { + $this->functionCallConstantConditionHelper->emitFunctionCallError(self::class, $scope, $node->right, $rightType->getValue(), $ruleError); + } elseif ($isInTrait) { $this->constantConditionInTraitHelper->emitError(self::class, $scope, $node->right, $rightType->getValue(), $ruleError); } else { $errors[] = $ruleError; } } else { - $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $node->right); + $this->emitNoError($scope, $node->right); } } else { - $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $node->right); + $this->emitNoError($scope, $node->right); } return $errors; } + private function emitNoError( + Scope&NodeCallbackInvoker&CollectedDataEmitter $scope, + Expr $expr, + ): void + { + if ($this->functionCallConstantConditionHelper->isTypeCheckCandidate($expr)) { + $this->functionCallConstantConditionHelper->emitFunctionCallNoError(self::class, $scope, $expr); + } else { + $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $expr); + } + } + } diff --git a/src/Rules/Comparison/MatchExpressionRule.php b/src/Rules/Comparison/MatchExpressionRule.php index d89fe2dd87..4757493d62 100644 --- a/src/Rules/Comparison/MatchExpressionRule.php +++ b/src/Rules/Comparison/MatchExpressionRule.php @@ -3,6 +3,7 @@ namespace PHPStan\Rules\Comparison; use PhpParser\Node; +use PhpParser\Node\Expr; use PHPStan\Analyser\CollectedDataEmitter; use PHPStan\Analyser\NodeCallbackInvoker; use PHPStan\Analyser\Scope; @@ -34,6 +35,7 @@ public function __construct( private ConstantConditionRuleHelper $constantConditionRuleHelper, private PossiblyImpureTipHelper $possiblyImpureTipHelper, private ConstantConditionInTraitHelper $constantConditionInTraitHelper, + private FunctionCallConstantConditionHelper $functionCallConstantConditionHelper, #[AutowiredParameter] private bool $treatPhpDocTypesAsCertain, ) @@ -67,14 +69,16 @@ public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataE } foreach ($armConditions as $armCondition) { $armConditionScope = $armCondition->getScope(); + $rawCondition = $armCondition->getCondition(); + $isTypeCheckCandidate = $this->functionCallConstantConditionHelper->isTypeCheckCandidate($rawCondition); $armConditionExpr = new Node\Expr\BinaryOp\Identical( $matchCondition, - $armCondition->getCondition(), + $rawCondition, ); $armConditionResult = $armConditionScope->getType($armConditionExpr); if (!$armConditionResult instanceof ConstantBooleanType) { - $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $armConditionExpr); + $this->emitNoError($scope, $armConditionExpr, $rawCondition, $isTypeCheckCandidate); continue; } if ($armConditionResult->getValue()) { @@ -84,7 +88,7 @@ public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataE if (!$this->treatPhpDocTypesAsCertain) { $armConditionNativeResult = $armConditionScope->getNativeType($armConditionExpr); if (!$armConditionNativeResult instanceof ConstantBooleanType) { - $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $armConditionExpr); + $this->emitNoError($scope, $armConditionExpr, $rawCondition, $isTypeCheckCandidate); continue; } if ($armConditionNativeResult->getValue()) { @@ -93,9 +97,9 @@ public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataE } if ($matchConditionType instanceof ConstantBooleanType) { - $armConditionStandaloneResult = $this->constantConditionRuleHelper->getBooleanType($armConditionScope, $armCondition->getCondition()); + $armConditionStandaloneResult = $this->constantConditionRuleHelper->getBooleanType($armConditionScope, $rawCondition); if (!$armConditionStandaloneResult instanceof ConstantBooleanType) { - $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $armConditionExpr); + $this->emitNoError($scope, $armConditionExpr, $rawCondition, $isTypeCheckCandidate); continue; } } @@ -105,11 +109,15 @@ public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataE $errorBuilder = RuleErrorBuilder::message(sprintf( 'Match arm comparison between %s and %s is always false.', $armConditionScope->getType($matchCondition)->describe(VerbosityLevel::value()), - $armConditionScope->getType($armCondition->getCondition())->describe(VerbosityLevel::value()), + $armConditionScope->getType($rawCondition)->describe(VerbosityLevel::value()), ))->line($armLine)->identifier('match.alwaysFalse'); $this->possiblyImpureTipHelper->addTip($armConditionScope, $armConditionExpr, $errorBuilder); $ruleError = $errorBuilder->build(); - if ($scope->isInTrait()) { + if ($isTypeCheckCandidate) { + // the constant-ness of a type-check call is owned by the + // ImpossibleCheckType* rules; defer and deduplicate against them + $this->functionCallConstantConditionHelper->emitFunctionCallError(self::class, $scope, $rawCondition, false, $ruleError); + } elseif ($scope->isInTrait()) { $this->constantConditionInTraitHelper->emitError(self::class, $scope, $armConditionExpr, false, $ruleError); } else { $errors[] = $ruleError; @@ -118,14 +126,14 @@ public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataE } if ($i === $armsCount - 1) { - $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $armConditionExpr); + $this->emitNoError($scope, $armConditionExpr, $rawCondition, $isTypeCheckCandidate); continue; } $message = sprintf( 'Match arm comparison between %s and %s is always true.', $armConditionScope->getType($matchCondition)->describe(VerbosityLevel::value()), - $armConditionScope->getType($armCondition->getCondition())->describe(VerbosityLevel::value()), + $armConditionScope->getType($rawCondition)->describe(VerbosityLevel::value()), ); $errorBuilder = RuleErrorBuilder::message($message) ->line($armLine) @@ -133,7 +141,9 @@ public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataE ->tip('Remove remaining cases below this one and this error will disappear too.'); $this->possiblyImpureTipHelper->addTip($armConditionScope, $armConditionExpr, $errorBuilder); $ruleError = $errorBuilder->build(); - if ($scope->isInTrait()) { + if ($isTypeCheckCandidate) { + $this->functionCallConstantConditionHelper->emitFunctionCallError(self::class, $scope, $rawCondition, true, $ruleError); + } elseif ($scope->isInTrait()) { $this->constantConditionInTraitHelper->emitError(self::class, $scope, $armConditionExpr, true, $ruleError); } else { $errors[] = $ruleError; @@ -167,6 +177,20 @@ public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataE return $errors; } + private function emitNoError( + Scope&NodeCallbackInvoker&CollectedDataEmitter $scope, + Expr $armConditionExpr, + Expr $rawCondition, + bool $isTypeCheckCandidate, + ): void + { + if ($isTypeCheckCandidate) { + $this->functionCallConstantConditionHelper->emitFunctionCallNoError(self::class, $scope, $rawCondition); + } else { + $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $armConditionExpr); + } + } + private function isUnhandledMatchErrorCaught(Node $node): bool { $tryCatchTypes = $node->getAttribute(TryCatchTypeVisitor::ATTRIBUTE_NAME); diff --git a/src/Rules/Comparison/TernaryOperatorConstantConditionRule.php b/src/Rules/Comparison/TernaryOperatorConstantConditionRule.php index ddb606965c..d1732bcb41 100644 --- a/src/Rules/Comparison/TernaryOperatorConstantConditionRule.php +++ b/src/Rules/Comparison/TernaryOperatorConstantConditionRule.php @@ -24,6 +24,7 @@ public function __construct( private ConstantConditionRuleHelper $helper, private PossiblyImpureTipHelper $possiblyImpureTipHelper, private ConstantConditionInTraitHelper $constantConditionInTraitHelper, + private FunctionCallConstantConditionHelper $functionCallConstantConditionHelper, #[AutowiredParameter] private bool $treatPhpDocTypesAsCertain, #[AutowiredParameter(ref: '%tips.treatPhpDocTypesAsCertain%')] @@ -65,6 +66,10 @@ public function processNode( 'Ternary operator condition is always %s.', $exprType->getValue() ? 'true' : 'false', )))->identifier(sprintf('ternary.always%s', $exprType->getValue() ? 'True' : 'False'))->build(); + if ($this->functionCallConstantConditionHelper->isTypeCheckCandidate($node->cond)) { + $this->functionCallConstantConditionHelper->emitFunctionCallError(self::class, $scope, $node->cond, $exprType->getValue(), $ruleError); + return []; + } if ($scope->isInTrait()) { $this->constantConditionInTraitHelper->emitError(self::class, $scope, $node->cond, $exprType->getValue(), $ruleError); return []; @@ -73,7 +78,11 @@ public function processNode( return [$ruleError]; } - $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $node->cond); + if ($this->functionCallConstantConditionHelper->isTypeCheckCandidate($node->cond)) { + $this->functionCallConstantConditionHelper->emitFunctionCallNoError(self::class, $scope, $node->cond); + } else { + $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $node->cond); + } return []; } diff --git a/src/Rules/Comparison/WhileLoopAlwaysFalseConditionRule.php b/src/Rules/Comparison/WhileLoopAlwaysFalseConditionRule.php index d6bd7479dc..4859e0270c 100644 --- a/src/Rules/Comparison/WhileLoopAlwaysFalseConditionRule.php +++ b/src/Rules/Comparison/WhileLoopAlwaysFalseConditionRule.php @@ -24,6 +24,7 @@ public function __construct( private ConstantConditionRuleHelper $helper, private PossiblyImpureTipHelper $possiblyImpureTipHelper, private ConstantConditionInTraitHelper $constantConditionInTraitHelper, + private FunctionCallConstantConditionHelper $functionCallConstantConditionHelper, #[AutowiredParameter] private bool $treatPhpDocTypesAsCertain, #[AutowiredParameter(ref: '%tips.treatPhpDocTypesAsCertain%')] @@ -65,6 +66,10 @@ public function processNode( $ruleError = $addTip(RuleErrorBuilder::message('While loop condition is always false.'))->line($node->cond->getStartLine()) ->identifier('while.alwaysFalse') ->build(); + if ($this->functionCallConstantConditionHelper->isTypeCheckCandidate($node->cond)) { + $this->functionCallConstantConditionHelper->emitFunctionCallError(self::class, $scope, $node->cond, false, $ruleError); + return []; + } if ($scope->isInTrait()) { $this->constantConditionInTraitHelper->emitError(self::class, $scope, $node->cond, false, $ruleError); return []; @@ -73,7 +78,11 @@ public function processNode( return [$ruleError]; } - $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $node->cond); + if ($this->functionCallConstantConditionHelper->isTypeCheckCandidate($node->cond)) { + $this->functionCallConstantConditionHelper->emitFunctionCallNoError(self::class, $scope, $node->cond); + } else { + $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $node->cond); + } return []; } diff --git a/src/Rules/Comparison/WhileLoopAlwaysTrueConditionRule.php b/src/Rules/Comparison/WhileLoopAlwaysTrueConditionRule.php index ef942cfe0c..2acdc51da6 100644 --- a/src/Rules/Comparison/WhileLoopAlwaysTrueConditionRule.php +++ b/src/Rules/Comparison/WhileLoopAlwaysTrueConditionRule.php @@ -28,6 +28,7 @@ public function __construct( private ConstantConditionRuleHelper $helper, private PossiblyImpureTipHelper $possiblyImpureTipHelper, private ConstantConditionInTraitHelper $constantConditionInTraitHelper, + private FunctionCallConstantConditionHelper $functionCallConstantConditionHelper, #[AutowiredParameter] private bool $treatPhpDocTypesAsCertain, #[AutowiredParameter(ref: '%tips.treatPhpDocTypesAsCertain%')] @@ -71,16 +72,25 @@ public function processNode( } $originalNode = $node->getOriginalNode(); $exprType = $this->helper->getBooleanType($scope, $originalNode->cond); + $isTypeCheckCandidate = $this->functionCallConstantConditionHelper->isTypeCheckCandidate($originalNode->cond); if ($exprType->isTrue()->yes()) { if ($node->hasYield()) { - $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $originalNode->cond); + if ($isTypeCheckCandidate) { + $this->functionCallConstantConditionHelper->emitFunctionCallNoError(self::class, $scope, $originalNode->cond); + } else { + $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $originalNode->cond); + } return []; } $ref = $scope->getFunction() ?? $scope->getAnonymousFunctionReflection(); if ($ref !== null && $ref->getReturnType() instanceof NeverType) { - $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $originalNode->cond); + if ($isTypeCheckCandidate) { + $this->functionCallConstantConditionHelper->emitFunctionCallNoError(self::class, $scope, $originalNode->cond); + } else { + $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $originalNode->cond); + } return []; } @@ -105,6 +115,10 @@ public function processNode( $ruleError = $addTip(RuleErrorBuilder::message('While loop condition is always true.'))->line($originalNode->cond->getStartLine()) ->identifier('while.alwaysTrue') ->build(); + if ($isTypeCheckCandidate) { + $this->functionCallConstantConditionHelper->emitFunctionCallError(self::class, $scope, $originalNode->cond, true, $ruleError); + return []; + } if ($scope->isInTrait()) { $this->constantConditionInTraitHelper->emitError(self::class, $scope, $originalNode->cond, true, $ruleError); return []; @@ -113,7 +127,11 @@ public function processNode( return [$ruleError]; } - $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $originalNode->cond); + if ($isTypeCheckCandidate) { + $this->functionCallConstantConditionHelper->emitFunctionCallNoError(self::class, $scope, $originalNode->cond); + } else { + $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $originalNode->cond); + } return []; } diff --git a/tests/PHPStan/Rules/Comparison/BooleanAndConstantConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/BooleanAndConstantConditionRuleTest.php index fea58e7d61..9967649a4c 100644 --- a/tests/PHPStan/Rules/Comparison/BooleanAndConstantConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/BooleanAndConstantConditionRuleTest.php @@ -24,19 +24,55 @@ protected function getRule(): Rule return new CompositeRule([ new BooleanAndConstantConditionRule( new ConstantConditionRuleHelper( - new ImpossibleCheckTypeHelper( - self::createReflectionProvider(), - $this->getTypeSpecifier(), - $this->treatPhpDocTypesAsCertain, - ), $this->treatPhpDocTypesAsCertain, ), new PossiblyImpureTipHelper(true), self::getContainer()->getByType(ConstantConditionInTraitHelper::class), + self::getContainer()->getByType(FunctionCallConstantConditionHelper::class), $this->treatPhpDocTypesAsCertain, $this->reportAlwaysTrueInLastCondition, true, ), + new ImpossibleCheckTypeFunctionCallRule( + new ImpossibleCheckTypeHelper( + self::createReflectionProvider(), + $this->getTypeSpecifier(), + $this->treatPhpDocTypesAsCertain, + ), + new PossiblyImpureTipHelper(true), + self::getContainer()->getByType(ConstantConditionInTraitHelper::class), + self::getContainer()->getByType(FunctionCallConstantConditionHelper::class), + $this->treatPhpDocTypesAsCertain, + $this->reportAlwaysTrueInLastCondition, + true, + ), + new ImpossibleCheckTypeMethodCallRule( + new ImpossibleCheckTypeHelper( + self::createReflectionProvider(), + $this->getTypeSpecifier(), + $this->treatPhpDocTypesAsCertain, + ), + new PossiblyImpureTipHelper(true), + self::getContainer()->getByType(ConstantConditionInTraitHelper::class), + self::getContainer()->getByType(FunctionCallConstantConditionHelper::class), + $this->treatPhpDocTypesAsCertain, + $this->reportAlwaysTrueInLastCondition, + true, + ), + new ImpossibleCheckTypeStaticMethodCallRule( + new ImpossibleCheckTypeHelper( + self::createReflectionProvider(), + $this->getTypeSpecifier(), + $this->treatPhpDocTypesAsCertain, + ), + new PossiblyImpureTipHelper(true), + self::getContainer()->getByType(ConstantConditionInTraitHelper::class), + self::getContainer()->getByType(FunctionCallConstantConditionHelper::class), + $this->treatPhpDocTypesAsCertain, + $this->reportAlwaysTrueInLastCondition, + true, + ), + new FunctionCallConstantConditionRule(), new ConstantConditionInTraitRule(), ]); } @@ -354,6 +390,11 @@ public static function dataBug4969(): iterable { yield [false, []]; yield [true, [ + [ + 'Call to function is_string() with string will always evaluate to true.', + 12, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', + ], [ 'Result of && is always false.', 15, diff --git a/tests/PHPStan/Rules/Comparison/BooleanNotConstantConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/BooleanNotConstantConditionRuleTest.php index e2121b4b7f..5e5cbe633d 100644 --- a/tests/PHPStan/Rules/Comparison/BooleanNotConstantConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/BooleanNotConstantConditionRuleTest.php @@ -23,19 +23,55 @@ protected function getRule(): Rule return new CompositeRule([ new BooleanNotConstantConditionRule( new ConstantConditionRuleHelper( - new ImpossibleCheckTypeHelper( - self::createReflectionProvider(), - $this->getTypeSpecifier(), - $this->treatPhpDocTypesAsCertain, - ), $this->treatPhpDocTypesAsCertain, ), new PossiblyImpureTipHelper(true), self::getContainer()->getByType(ConstantConditionInTraitHelper::class), + self::getContainer()->getByType(FunctionCallConstantConditionHelper::class), $this->treatPhpDocTypesAsCertain, $this->reportAlwaysTrueInLastCondition, true, ), + new ImpossibleCheckTypeFunctionCallRule( + new ImpossibleCheckTypeHelper( + self::createReflectionProvider(), + $this->getTypeSpecifier(), + $this->treatPhpDocTypesAsCertain, + ), + new PossiblyImpureTipHelper(true), + self::getContainer()->getByType(ConstantConditionInTraitHelper::class), + self::getContainer()->getByType(FunctionCallConstantConditionHelper::class), + $this->treatPhpDocTypesAsCertain, + $this->reportAlwaysTrueInLastCondition, + true, + ), + new ImpossibleCheckTypeMethodCallRule( + new ImpossibleCheckTypeHelper( + self::createReflectionProvider(), + $this->getTypeSpecifier(), + $this->treatPhpDocTypesAsCertain, + ), + new PossiblyImpureTipHelper(true), + self::getContainer()->getByType(ConstantConditionInTraitHelper::class), + self::getContainer()->getByType(FunctionCallConstantConditionHelper::class), + $this->treatPhpDocTypesAsCertain, + $this->reportAlwaysTrueInLastCondition, + true, + ), + new ImpossibleCheckTypeStaticMethodCallRule( + new ImpossibleCheckTypeHelper( + self::createReflectionProvider(), + $this->getTypeSpecifier(), + $this->treatPhpDocTypesAsCertain, + ), + new PossiblyImpureTipHelper(true), + self::getContainer()->getByType(ConstantConditionInTraitHelper::class), + self::getContainer()->getByType(FunctionCallConstantConditionHelper::class), + $this->treatPhpDocTypesAsCertain, + $this->reportAlwaysTrueInLastCondition, + true, + ), + new FunctionCallConstantConditionRule(), new ConstantConditionInTraitRule(), ]); } diff --git a/tests/PHPStan/Rules/Comparison/BooleanOrConstantConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/BooleanOrConstantConditionRuleTest.php index ba4aab8050..c32ef7f10e 100644 --- a/tests/PHPStan/Rules/Comparison/BooleanOrConstantConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/BooleanOrConstantConditionRuleTest.php @@ -24,19 +24,55 @@ protected function getRule(): Rule return new CompositeRule([ new BooleanOrConstantConditionRule( new ConstantConditionRuleHelper( - new ImpossibleCheckTypeHelper( - self::createReflectionProvider(), - $this->getTypeSpecifier(), - $this->treatPhpDocTypesAsCertain, - ), $this->treatPhpDocTypesAsCertain, ), new PossiblyImpureTipHelper(true), self::getContainer()->getByType(ConstantConditionInTraitHelper::class), + self::getContainer()->getByType(FunctionCallConstantConditionHelper::class), $this->treatPhpDocTypesAsCertain, $this->reportAlwaysTrueInLastCondition, true, ), + new ImpossibleCheckTypeFunctionCallRule( + new ImpossibleCheckTypeHelper( + self::createReflectionProvider(), + $this->getTypeSpecifier(), + $this->treatPhpDocTypesAsCertain, + ), + new PossiblyImpureTipHelper(true), + self::getContainer()->getByType(ConstantConditionInTraitHelper::class), + self::getContainer()->getByType(FunctionCallConstantConditionHelper::class), + $this->treatPhpDocTypesAsCertain, + $this->reportAlwaysTrueInLastCondition, + true, + ), + new ImpossibleCheckTypeMethodCallRule( + new ImpossibleCheckTypeHelper( + self::createReflectionProvider(), + $this->getTypeSpecifier(), + $this->treatPhpDocTypesAsCertain, + ), + new PossiblyImpureTipHelper(true), + self::getContainer()->getByType(ConstantConditionInTraitHelper::class), + self::getContainer()->getByType(FunctionCallConstantConditionHelper::class), + $this->treatPhpDocTypesAsCertain, + $this->reportAlwaysTrueInLastCondition, + true, + ), + new ImpossibleCheckTypeStaticMethodCallRule( + new ImpossibleCheckTypeHelper( + self::createReflectionProvider(), + $this->getTypeSpecifier(), + $this->treatPhpDocTypesAsCertain, + ), + new PossiblyImpureTipHelper(true), + self::getContainer()->getByType(ConstantConditionInTraitHelper::class), + self::getContainer()->getByType(FunctionCallConstantConditionHelper::class), + $this->treatPhpDocTypesAsCertain, + $this->reportAlwaysTrueInLastCondition, + true, + ), + new FunctionCallConstantConditionRule(), new ConstantConditionInTraitRule(), ]); } diff --git a/tests/PHPStan/Rules/Comparison/DoWhileLoopConstantConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/DoWhileLoopConstantConditionRuleTest.php index 6a6269ae7c..bde9382cef 100644 --- a/tests/PHPStan/Rules/Comparison/DoWhileLoopConstantConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/DoWhileLoopConstantConditionRuleTest.php @@ -19,18 +19,54 @@ protected function getRule(): Rule return new CompositeRule([ new DoWhileLoopConstantConditionRule( new ConstantConditionRuleHelper( - new ImpossibleCheckTypeHelper( - self::createReflectionProvider(), - $this->getTypeSpecifier(), - $this->shouldTreatPhpDocTypesAsCertain(), - ), $this->shouldTreatPhpDocTypesAsCertain(), ), new PossiblyImpureTipHelper(true), self::getContainer()->getByType(ConstantConditionInTraitHelper::class), + self::getContainer()->getByType(FunctionCallConstantConditionHelper::class), $this->shouldTreatPhpDocTypesAsCertain(), true, ), + new ImpossibleCheckTypeFunctionCallRule( + new ImpossibleCheckTypeHelper( + self::createReflectionProvider(), + $this->getTypeSpecifier(), + $this->shouldTreatPhpDocTypesAsCertain(), + ), + new PossiblyImpureTipHelper(true), + self::getContainer()->getByType(ConstantConditionInTraitHelper::class), + self::getContainer()->getByType(FunctionCallConstantConditionHelper::class), + $this->shouldTreatPhpDocTypesAsCertain(), + true, + true, + ), + new ImpossibleCheckTypeMethodCallRule( + new ImpossibleCheckTypeHelper( + self::createReflectionProvider(), + $this->getTypeSpecifier(), + $this->shouldTreatPhpDocTypesAsCertain(), + ), + new PossiblyImpureTipHelper(true), + self::getContainer()->getByType(ConstantConditionInTraitHelper::class), + self::getContainer()->getByType(FunctionCallConstantConditionHelper::class), + $this->shouldTreatPhpDocTypesAsCertain(), + true, + true, + ), + new ImpossibleCheckTypeStaticMethodCallRule( + new ImpossibleCheckTypeHelper( + self::createReflectionProvider(), + $this->getTypeSpecifier(), + $this->shouldTreatPhpDocTypesAsCertain(), + ), + new PossiblyImpureTipHelper(true), + self::getContainer()->getByType(ConstantConditionInTraitHelper::class), + self::getContainer()->getByType(FunctionCallConstantConditionHelper::class), + $this->shouldTreatPhpDocTypesAsCertain(), + true, + true, + ), + new FunctionCallConstantConditionRule(), new ConstantConditionInTraitRule(), ]); } diff --git a/tests/PHPStan/Rules/Comparison/ElseIfConstantConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/ElseIfConstantConditionRuleTest.php index 2683e22f40..766f6d1aa7 100644 --- a/tests/PHPStan/Rules/Comparison/ElseIfConstantConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/ElseIfConstantConditionRuleTest.php @@ -24,19 +24,55 @@ protected function getRule(): Rule return new CompositeRule([ new ElseIfConstantConditionRule( new ConstantConditionRuleHelper( - new ImpossibleCheckTypeHelper( - self::createReflectionProvider(), - $this->getTypeSpecifier(), - $this->treatPhpDocTypesAsCertain, - ), $this->treatPhpDocTypesAsCertain, ), new PossiblyImpureTipHelper(true), self::getContainer()->getByType(ConstantConditionInTraitHelper::class), + self::getContainer()->getByType(FunctionCallConstantConditionHelper::class), $this->treatPhpDocTypesAsCertain, $this->reportAlwaysTrueInLastCondition, true, ), + new ImpossibleCheckTypeFunctionCallRule( + new ImpossibleCheckTypeHelper( + self::createReflectionProvider(), + $this->getTypeSpecifier(), + $this->treatPhpDocTypesAsCertain, + ), + new PossiblyImpureTipHelper(true), + self::getContainer()->getByType(ConstantConditionInTraitHelper::class), + self::getContainer()->getByType(FunctionCallConstantConditionHelper::class), + $this->treatPhpDocTypesAsCertain, + $this->reportAlwaysTrueInLastCondition, + true, + ), + new ImpossibleCheckTypeMethodCallRule( + new ImpossibleCheckTypeHelper( + self::createReflectionProvider(), + $this->getTypeSpecifier(), + $this->treatPhpDocTypesAsCertain, + ), + new PossiblyImpureTipHelper(true), + self::getContainer()->getByType(ConstantConditionInTraitHelper::class), + self::getContainer()->getByType(FunctionCallConstantConditionHelper::class), + $this->treatPhpDocTypesAsCertain, + $this->reportAlwaysTrueInLastCondition, + true, + ), + new ImpossibleCheckTypeStaticMethodCallRule( + new ImpossibleCheckTypeHelper( + self::createReflectionProvider(), + $this->getTypeSpecifier(), + $this->treatPhpDocTypesAsCertain, + ), + new PossiblyImpureTipHelper(true), + self::getContainer()->getByType(ConstantConditionInTraitHelper::class), + self::getContainer()->getByType(FunctionCallConstantConditionHelper::class), + $this->treatPhpDocTypesAsCertain, + $this->reportAlwaysTrueInLastCondition, + true, + ), + new FunctionCallConstantConditionRule(), new ConstantConditionInTraitRule(), ]); } diff --git a/tests/PHPStan/Rules/Comparison/IfConstantConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/IfConstantConditionRuleTest.php index 37a532cb07..96b20b07bd 100644 --- a/tests/PHPStan/Rules/Comparison/IfConstantConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/IfConstantConditionRuleTest.php @@ -21,18 +21,54 @@ protected function getRule(): Rule return new CompositeRule([ new IfConstantConditionRule( new ConstantConditionRuleHelper( - new ImpossibleCheckTypeHelper( - self::createReflectionProvider(), - $this->getTypeSpecifier(), - $this->treatPhpDocTypesAsCertain, - ), $this->treatPhpDocTypesAsCertain, ), new PossiblyImpureTipHelper(true), self::getContainer()->getByType(ConstantConditionInTraitHelper::class), + self::getContainer()->getByType(FunctionCallConstantConditionHelper::class), $this->treatPhpDocTypesAsCertain, true, ), + new ImpossibleCheckTypeFunctionCallRule( + new ImpossibleCheckTypeHelper( + self::createReflectionProvider(), + $this->getTypeSpecifier(), + $this->treatPhpDocTypesAsCertain, + ), + new PossiblyImpureTipHelper(true), + self::getContainer()->getByType(ConstantConditionInTraitHelper::class), + self::getContainer()->getByType(FunctionCallConstantConditionHelper::class), + $this->treatPhpDocTypesAsCertain, + true, + true, + ), + new ImpossibleCheckTypeMethodCallRule( + new ImpossibleCheckTypeHelper( + self::createReflectionProvider(), + $this->getTypeSpecifier(), + $this->treatPhpDocTypesAsCertain, + ), + new PossiblyImpureTipHelper(true), + self::getContainer()->getByType(ConstantConditionInTraitHelper::class), + self::getContainer()->getByType(FunctionCallConstantConditionHelper::class), + $this->treatPhpDocTypesAsCertain, + true, + true, + ), + new ImpossibleCheckTypeStaticMethodCallRule( + new ImpossibleCheckTypeHelper( + self::createReflectionProvider(), + $this->getTypeSpecifier(), + $this->treatPhpDocTypesAsCertain, + ), + new PossiblyImpureTipHelper(true), + self::getContainer()->getByType(ConstantConditionInTraitHelper::class), + self::getContainer()->getByType(FunctionCallConstantConditionHelper::class), + $this->treatPhpDocTypesAsCertain, + true, + true, + ), + new FunctionCallConstantConditionRule(), new ConstantConditionInTraitRule(), ]); } @@ -55,6 +91,10 @@ public function testRule(): void 'If condition is always false.', 45, ], + [ + 'Call to function is_object() with object will always evaluate to true.', + 93, + ], [ 'If condition is always true.', 96, @@ -114,6 +154,11 @@ public function testBug4043(): void { $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-4043.php'], [ + [ + 'Call to function assert() with true will always evaluate to true.', + 13, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', + ], [ 'If condition is always false.', 43, @@ -257,6 +302,22 @@ public function testBug6211(): void { $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-6211.php'], [ + [ + 'Call to function method_exists() with Bug6211\Hell and \'test\' will always evaluate to true.', + 34, + ], + [ + 'Call to function method_exists() with \'Bug6211\\\\Hell\' and \'test\' will always evaluate to true.', + 39, + ], + [ + 'Call to function method_exists() with Bug6211\Bar and \'realMethod\' will always evaluate to true.', + 62, + ], + [ + 'Call to function property_exists() with Bug6211\Baz and \'realProp\' will always evaluate to true.', + 87, + ], [ 'If condition is always true.', 93, @@ -265,10 +326,31 @@ public function testBug6211(): void 'If condition is always true.', 100, ], + [ + 'Call to function method_exists() with Bug6211\Hell and \'test\' will always evaluate to true.', + 106, + ], + [ + 'Call to function method_exists() with Bug6211\Hell and \'test\' will always evaluate to true.', + 107, + ], [ 'If condition is always true.', 114, ], + [ + 'Call to function property_exists() with Bug6211\Baz and \'realProp\' will always evaluate to true.', + 120, + ], + [ + 'Call to function property_exists() with Bug6211\Baz and \'realProp\' will always evaluate to true.', + 121, + ], + [ + 'Call to function method_exists() with class-string and \'test\' will always evaluate to true.', + 136, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', + ], ]); } @@ -289,4 +371,21 @@ public function testBug8980(): void ]); } + public function testConstantConditionFunctionCall(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/constant-condition-function-call.php'], [ + [ + // type-check call: owned by the ImpossibleCheckType rule, reported only once + 'Call to function is_int() with int will always evaluate to true.', + 13, + ], + [ + // non-type-check, always-truthy return: reported by the constant-condition rule + 'If condition is always true.', + 19, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php index 5f357bd51b..4bbb387ecb 100644 --- a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php @@ -34,6 +34,7 @@ protected function getRule(): Rule ), new PossiblyImpureTipHelper(true), self::getContainer()->getByType(ConstantConditionInTraitHelper::class), + self::getContainer()->getByType(FunctionCallConstantConditionHelper::class), $this->treatPhpDocTypesAsCertain, $this->reportAlwaysTrueInLastCondition, true, diff --git a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeGenericOverwriteRuleTest.php b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeGenericOverwriteRuleTest.php index fae1bdb6e1..b706d1569f 100644 --- a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeGenericOverwriteRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeGenericOverwriteRuleTest.php @@ -21,6 +21,7 @@ public function getRule(): Rule ), new PossiblyImpureTipHelper(true), self::getContainer()->getByType(ConstantConditionInTraitHelper::class), + self::getContainer()->getByType(FunctionCallConstantConditionHelper::class), true, false, true, diff --git a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeMethodCallRuleEqualsTest.php b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeMethodCallRuleEqualsTest.php index 6d7e5d9f2e..f2b07c46b6 100644 --- a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeMethodCallRuleEqualsTest.php +++ b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeMethodCallRuleEqualsTest.php @@ -21,6 +21,7 @@ public function getRule(): Rule ), new PossiblyImpureTipHelper(true), self::getContainer()->getByType(ConstantConditionInTraitHelper::class), + self::getContainer()->getByType(FunctionCallConstantConditionHelper::class), true, false, true, diff --git a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeMethodCallRuleTest.php b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeMethodCallRuleTest.php index 9dde818f42..27c4bbac60 100644 --- a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeMethodCallRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeMethodCallRuleTest.php @@ -30,6 +30,7 @@ public function getRule(): Rule ), new PossiblyImpureTipHelper(true), self::getContainer()->getByType(ConstantConditionInTraitHelper::class), + self::getContainer()->getByType(FunctionCallConstantConditionHelper::class), $this->treatPhpDocTypesAsCertain, $this->reportAlwaysTrueInLastCondition, true, diff --git a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeStaticMethodCallRuleTest.php b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeStaticMethodCallRuleTest.php index 1846fd0cac..1a479ec978 100644 --- a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeStaticMethodCallRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeStaticMethodCallRuleTest.php @@ -30,6 +30,7 @@ public function getRule(): Rule ), new PossiblyImpureTipHelper(true), self::getContainer()->getByType(ConstantConditionInTraitHelper::class), + self::getContainer()->getByType(FunctionCallConstantConditionHelper::class), $this->treatPhpDocTypesAsCertain, $this->reportAlwaysTrueInLastCondition, true, diff --git a/tests/PHPStan/Rules/Comparison/LogicalXorConstantConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/LogicalXorConstantConditionRuleTest.php index 158350103e..a64a549b5b 100644 --- a/tests/PHPStan/Rules/Comparison/LogicalXorConstantConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/LogicalXorConstantConditionRuleTest.php @@ -19,19 +19,55 @@ protected function getRule(): TRule return new CompositeRule([ new LogicalXorConstantConditionRule( new ConstantConditionRuleHelper( - new ImpossibleCheckTypeHelper( - self::createReflectionProvider(), - $this->getTypeSpecifier(), - $this->shouldTreatPhpDocTypesAsCertain(), - ), $this->shouldTreatPhpDocTypesAsCertain(), ), new PossiblyImpureTipHelper(true), self::getContainer()->getByType(ConstantConditionInTraitHelper::class), + self::getContainer()->getByType(FunctionCallConstantConditionHelper::class), $this->shouldTreatPhpDocTypesAsCertain(), false, true, ), + new ImpossibleCheckTypeFunctionCallRule( + new ImpossibleCheckTypeHelper( + self::createReflectionProvider(), + $this->getTypeSpecifier(), + $this->shouldTreatPhpDocTypesAsCertain(), + ), + new PossiblyImpureTipHelper(true), + self::getContainer()->getByType(ConstantConditionInTraitHelper::class), + self::getContainer()->getByType(FunctionCallConstantConditionHelper::class), + $this->shouldTreatPhpDocTypesAsCertain(), + false, + true, + ), + new ImpossibleCheckTypeMethodCallRule( + new ImpossibleCheckTypeHelper( + self::createReflectionProvider(), + $this->getTypeSpecifier(), + $this->shouldTreatPhpDocTypesAsCertain(), + ), + new PossiblyImpureTipHelper(true), + self::getContainer()->getByType(ConstantConditionInTraitHelper::class), + self::getContainer()->getByType(FunctionCallConstantConditionHelper::class), + $this->shouldTreatPhpDocTypesAsCertain(), + false, + true, + ), + new ImpossibleCheckTypeStaticMethodCallRule( + new ImpossibleCheckTypeHelper( + self::createReflectionProvider(), + $this->getTypeSpecifier(), + $this->shouldTreatPhpDocTypesAsCertain(), + ), + new PossiblyImpureTipHelper(true), + self::getContainer()->getByType(ConstantConditionInTraitHelper::class), + self::getContainer()->getByType(FunctionCallConstantConditionHelper::class), + $this->shouldTreatPhpDocTypesAsCertain(), + false, + true, + ), + new FunctionCallConstantConditionRule(), new ConstantConditionInTraitRule(), ]); } diff --git a/tests/PHPStan/Rules/Comparison/MatchExpressionRuleTest.php b/tests/PHPStan/Rules/Comparison/MatchExpressionRuleTest.php index ae37a243f9..844d145d8a 100644 --- a/tests/PHPStan/Rules/Comparison/MatchExpressionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/MatchExpressionRuleTest.php @@ -21,17 +21,53 @@ protected function getRule(): Rule return new CompositeRule([ new MatchExpressionRule( new ConstantConditionRuleHelper( - new ImpossibleCheckTypeHelper( - self::createReflectionProvider(), - $this->getTypeSpecifier(), - $this->treatPhpDocTypesAsCertain, - ), $this->treatPhpDocTypesAsCertain, ), new PossiblyImpureTipHelper(true), self::getContainer()->getByType(ConstantConditionInTraitHelper::class), + self::getContainer()->getByType(FunctionCallConstantConditionHelper::class), $this->treatPhpDocTypesAsCertain, ), + new ImpossibleCheckTypeFunctionCallRule( + new ImpossibleCheckTypeHelper( + self::createReflectionProvider(), + $this->getTypeSpecifier(), + $this->treatPhpDocTypesAsCertain, + ), + new PossiblyImpureTipHelper(true), + self::getContainer()->getByType(ConstantConditionInTraitHelper::class), + self::getContainer()->getByType(FunctionCallConstantConditionHelper::class), + $this->treatPhpDocTypesAsCertain, + true, + true, + ), + new ImpossibleCheckTypeMethodCallRule( + new ImpossibleCheckTypeHelper( + self::createReflectionProvider(), + $this->getTypeSpecifier(), + $this->treatPhpDocTypesAsCertain, + ), + new PossiblyImpureTipHelper(true), + self::getContainer()->getByType(ConstantConditionInTraitHelper::class), + self::getContainer()->getByType(FunctionCallConstantConditionHelper::class), + $this->treatPhpDocTypesAsCertain, + true, + true, + ), + new ImpossibleCheckTypeStaticMethodCallRule( + new ImpossibleCheckTypeHelper( + self::createReflectionProvider(), + $this->getTypeSpecifier(), + $this->treatPhpDocTypesAsCertain, + ), + new PossiblyImpureTipHelper(true), + self::getContainer()->getByType(ConstantConditionInTraitHelper::class), + self::getContainer()->getByType(FunctionCallConstantConditionHelper::class), + $this->treatPhpDocTypesAsCertain, + true, + true, + ), + new FunctionCallConstantConditionRule(), new ConstantConditionInTraitRule(), ]); } @@ -307,7 +343,16 @@ public function testBug8932(): void public function testBug8937(): void { $this->treatPhpDocTypesAsCertain = false; - $this->analyse([__DIR__ . '/data/bug-8937.php'], []); + $this->analyse([__DIR__ . '/data/bug-8937.php'], [ + [ + 'Call to function is_array() with array will always evaluate to true.', + 23, + ], + [ + 'Call to function is_array() with non-empty-array will always evaluate to true.', + 24, + ], + ]); } #[RequiresPhp('>= 8.0.0')] diff --git a/tests/PHPStan/Rules/Comparison/TernaryOperatorConstantConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/TernaryOperatorConstantConditionRuleTest.php index cda724eec1..dce53141a3 100644 --- a/tests/PHPStan/Rules/Comparison/TernaryOperatorConstantConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/TernaryOperatorConstantConditionRuleTest.php @@ -21,18 +21,54 @@ protected function getRule(): Rule return new CompositeRule([ new TernaryOperatorConstantConditionRule( new ConstantConditionRuleHelper( - new ImpossibleCheckTypeHelper( - self::createReflectionProvider(), - $this->getTypeSpecifier(), - $this->treatPhpDocTypesAsCertain, - ), $this->treatPhpDocTypesAsCertain, ), new PossiblyImpureTipHelper(true), self::getContainer()->getByType(ConstantConditionInTraitHelper::class), + self::getContainer()->getByType(FunctionCallConstantConditionHelper::class), $this->treatPhpDocTypesAsCertain, true, ), + new ImpossibleCheckTypeFunctionCallRule( + new ImpossibleCheckTypeHelper( + self::createReflectionProvider(), + $this->getTypeSpecifier(), + $this->treatPhpDocTypesAsCertain, + ), + new PossiblyImpureTipHelper(true), + self::getContainer()->getByType(ConstantConditionInTraitHelper::class), + self::getContainer()->getByType(FunctionCallConstantConditionHelper::class), + $this->treatPhpDocTypesAsCertain, + true, + true, + ), + new ImpossibleCheckTypeMethodCallRule( + new ImpossibleCheckTypeHelper( + self::createReflectionProvider(), + $this->getTypeSpecifier(), + $this->treatPhpDocTypesAsCertain, + ), + new PossiblyImpureTipHelper(true), + self::getContainer()->getByType(ConstantConditionInTraitHelper::class), + self::getContainer()->getByType(FunctionCallConstantConditionHelper::class), + $this->treatPhpDocTypesAsCertain, + true, + true, + ), + new ImpossibleCheckTypeStaticMethodCallRule( + new ImpossibleCheckTypeHelper( + self::createReflectionProvider(), + $this->getTypeSpecifier(), + $this->treatPhpDocTypesAsCertain, + ), + new PossiblyImpureTipHelper(true), + self::getContainer()->getByType(ConstantConditionInTraitHelper::class), + self::getContainer()->getByType(FunctionCallConstantConditionHelper::class), + $this->treatPhpDocTypesAsCertain, + true, + true, + ), + new FunctionCallConstantConditionRule(), new ConstantConditionInTraitRule(), ]); } diff --git a/tests/PHPStan/Rules/Comparison/WhileLoopAlwaysFalseConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/WhileLoopAlwaysFalseConditionRuleTest.php index 93403a0a51..e05b9254a3 100644 --- a/tests/PHPStan/Rules/Comparison/WhileLoopAlwaysFalseConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/WhileLoopAlwaysFalseConditionRuleTest.php @@ -19,18 +19,54 @@ protected function getRule(): Rule return new CompositeRule([ new WhileLoopAlwaysFalseConditionRule( new ConstantConditionRuleHelper( - new ImpossibleCheckTypeHelper( - self::createReflectionProvider(), - $this->getTypeSpecifier(), - $this->shouldTreatPhpDocTypesAsCertain(), - ), $this->shouldTreatPhpDocTypesAsCertain(), ), new PossiblyImpureTipHelper(true), self::getContainer()->getByType(ConstantConditionInTraitHelper::class), + self::getContainer()->getByType(FunctionCallConstantConditionHelper::class), $this->shouldTreatPhpDocTypesAsCertain(), true, ), + new ImpossibleCheckTypeFunctionCallRule( + new ImpossibleCheckTypeHelper( + self::createReflectionProvider(), + $this->getTypeSpecifier(), + $this->shouldTreatPhpDocTypesAsCertain(), + ), + new PossiblyImpureTipHelper(true), + self::getContainer()->getByType(ConstantConditionInTraitHelper::class), + self::getContainer()->getByType(FunctionCallConstantConditionHelper::class), + $this->shouldTreatPhpDocTypesAsCertain(), + true, + true, + ), + new ImpossibleCheckTypeMethodCallRule( + new ImpossibleCheckTypeHelper( + self::createReflectionProvider(), + $this->getTypeSpecifier(), + $this->shouldTreatPhpDocTypesAsCertain(), + ), + new PossiblyImpureTipHelper(true), + self::getContainer()->getByType(ConstantConditionInTraitHelper::class), + self::getContainer()->getByType(FunctionCallConstantConditionHelper::class), + $this->shouldTreatPhpDocTypesAsCertain(), + true, + true, + ), + new ImpossibleCheckTypeStaticMethodCallRule( + new ImpossibleCheckTypeHelper( + self::createReflectionProvider(), + $this->getTypeSpecifier(), + $this->shouldTreatPhpDocTypesAsCertain(), + ), + new PossiblyImpureTipHelper(true), + self::getContainer()->getByType(ConstantConditionInTraitHelper::class), + self::getContainer()->getByType(FunctionCallConstantConditionHelper::class), + $this->shouldTreatPhpDocTypesAsCertain(), + true, + true, + ), + new FunctionCallConstantConditionRule(), new ConstantConditionInTraitRule(), ]); } diff --git a/tests/PHPStan/Rules/Comparison/WhileLoopAlwaysTrueConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/WhileLoopAlwaysTrueConditionRuleTest.php index bb9fd1499f..4dc0cfba24 100644 --- a/tests/PHPStan/Rules/Comparison/WhileLoopAlwaysTrueConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/WhileLoopAlwaysTrueConditionRuleTest.php @@ -19,18 +19,54 @@ protected function getRule(): Rule return new CompositeRule([ new WhileLoopAlwaysTrueConditionRule( new ConstantConditionRuleHelper( - new ImpossibleCheckTypeHelper( - self::createReflectionProvider(), - $this->getTypeSpecifier(), - $this->shouldTreatPhpDocTypesAsCertain(), - ), $this->shouldTreatPhpDocTypesAsCertain(), ), new PossiblyImpureTipHelper(true), self::getContainer()->getByType(ConstantConditionInTraitHelper::class), + self::getContainer()->getByType(FunctionCallConstantConditionHelper::class), $this->shouldTreatPhpDocTypesAsCertain(), true, ), + new ImpossibleCheckTypeFunctionCallRule( + new ImpossibleCheckTypeHelper( + self::createReflectionProvider(), + $this->getTypeSpecifier(), + $this->shouldTreatPhpDocTypesAsCertain(), + ), + new PossiblyImpureTipHelper(true), + self::getContainer()->getByType(ConstantConditionInTraitHelper::class), + self::getContainer()->getByType(FunctionCallConstantConditionHelper::class), + $this->shouldTreatPhpDocTypesAsCertain(), + true, + true, + ), + new ImpossibleCheckTypeMethodCallRule( + new ImpossibleCheckTypeHelper( + self::createReflectionProvider(), + $this->getTypeSpecifier(), + $this->shouldTreatPhpDocTypesAsCertain(), + ), + new PossiblyImpureTipHelper(true), + self::getContainer()->getByType(ConstantConditionInTraitHelper::class), + self::getContainer()->getByType(FunctionCallConstantConditionHelper::class), + $this->shouldTreatPhpDocTypesAsCertain(), + true, + true, + ), + new ImpossibleCheckTypeStaticMethodCallRule( + new ImpossibleCheckTypeHelper( + self::createReflectionProvider(), + $this->getTypeSpecifier(), + $this->shouldTreatPhpDocTypesAsCertain(), + ), + new PossiblyImpureTipHelper(true), + self::getContainer()->getByType(ConstantConditionInTraitHelper::class), + self::getContainer()->getByType(FunctionCallConstantConditionHelper::class), + $this->shouldTreatPhpDocTypesAsCertain(), + true, + true, + ), + new FunctionCallConstantConditionRule(), new ConstantConditionInTraitRule(), ]); } diff --git a/tests/PHPStan/Rules/Comparison/data/constant-condition-function-call.php b/tests/PHPStan/Rules/Comparison/data/constant-condition-function-call.php new file mode 100644 index 0000000000..af99fff7e9 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/constant-condition-function-call.php @@ -0,0 +1,22 @@ +