diff --git a/rules.neon b/rules.neon index a8f89b8..f9d3773 100644 --- a/rules.neon +++ b/rules.neon @@ -1,6 +1,8 @@ services: - class: PHPStan\Rules\Deprecations\DeprecatedClassHelper + - + class: PHPStan\Rules\Deprecations\EchoDeprecatedBinaryOpToStringRule rules: - PHPStan\Rules\Deprecations\AccessDeprecatedPropertyRule @@ -19,3 +21,7 @@ rules: - PHPStan\Rules\Deprecations\TypeHintDeprecatedInFunctionSignatureRule - PHPStan\Rules\Deprecations\UsageOfDeprecatedCastRule - PHPStan\Rules\Deprecations\UsageOfDeprecatedTraitRule + +conditionalTags: + PHPStan\Rules\Deprecations\EchoDeprecatedBinaryOpToStringRule: + phpstan.rules.rule: %featureToggles.bleedingEdge% diff --git a/src/Rules/Deprecations/EchoDeprecatedBinaryOpToStringRule.php b/src/Rules/Deprecations/EchoDeprecatedBinaryOpToStringRule.php new file mode 100644 index 0000000..4b39be9 --- /dev/null +++ b/src/Rules/Deprecations/EchoDeprecatedBinaryOpToStringRule.php @@ -0,0 +1,88 @@ + + */ +class EchoDeprecatedBinaryOpToStringRule implements \PHPStan\Rules\Rule +{ + + /** @var Broker */ + private $broker; + + public function __construct(Broker $broker) + { + $this->broker = $broker; + } + + public function getNodeType(): string + { + return BinaryOp::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (DeprecatedScopeHelper::isScopeDeprecated($scope)) { + return []; + } + + $messages = []; + $message = $this->checkExpr($node->left, $scope); + if ($message) { + $messages[] = $message; + } + $message = $this->checkExpr($node->right, $scope); + if ($message) { + $messages[] = $message; + } + + return $messages; + } + + private function checkExpr(Node\Expr $node, Scope $scope): ?string + { + $methodCalledOnType = $scope->getType($node); + $referencedClasses = TypeUtils::getDirectClassNames($methodCalledOnType); + + foreach ($referencedClasses as $referencedClass) { + try { + $classReflection = $this->broker->getClass($referencedClass); + $methodReflection = $classReflection->getNativeMethod('__toString'); + + if (!$methodReflection->isDeprecated()->yes()) { + return null; + } + + $description = $methodReflection->getDeprecatedDescription(); + if ($description === null) { + return sprintf( + 'Call to deprecated method %s() of class %s.', + $methodReflection->getName(), + $methodReflection->getDeclaringClass()->getName() + ); + } + + return sprintf( + "Call to deprecated method %s() of class %s:\n%s", + $methodReflection->getName(), + $methodReflection->getDeclaringClass()->getName(), + $description + ); + } catch (\PHPStan\Broker\ClassNotFoundException $e) { + // Other rules will notify if the class is not found + } catch (\PHPStan\Reflection\MissingMethodFromReflectionException $e) { + // Other rules will notify if the the method is not found + } + } + + return null; + } + +} diff --git a/src/Rules/Deprecations/EchoDeprecatedToStringRule.php b/src/Rules/Deprecations/EchoDeprecatedToStringRule.php new file mode 100644 index 0000000..8eb0e9b --- /dev/null +++ b/src/Rules/Deprecations/EchoDeprecatedToStringRule.php @@ -0,0 +1,115 @@ + + */ +class EchoDeprecatedToStringRule implements \PHPStan\Rules\Rule +{ + + /** @var RuleLevelHelper */ + private $ruleLevelHelper; + + public function __construct(RuleLevelHelper $ruleLevelHelper) + { + $this->ruleLevelHelper = $ruleLevelHelper; + } + + public function getNodeType(): string + { + return Echo_::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (DeprecatedScopeHelper::isScopeDeprecated($scope)) { + return []; + } + + $messages = []; + foreach ($node->exprs as $key => $expr) { + $this->deepCheckExpr($expr, $scope, $messages); + } + + return $messages; + } + + /** + * @param Node\Expr $expr + * @param Scope $scope + * @param string[] $messages + */ + private function deepCheckExpr(Node\Expr $expr, Scope $scope, array &$messages): void + { + if ($expr instanceof Node\Expr\BinaryOp) { + $this->deepCheckExpr($expr->left, $scope, $messages); + $this->deepCheckExpr($expr->right, $scope, $messages); + } elseif ($expr instanceof Node\Expr\Cast\String_ || $expr instanceof Node\Expr\Assign || $expr instanceof Node\Expr\AssignOp) { + $this->deepCheckExpr($expr->expr, $scope, $messages); + } elseif ($expr instanceof Node\Scalar\Encapsed) { + foreach ($expr->parts as $part) { + $this->deepCheckExpr($part, $scope, $messages); + } + } else { + $message = $this->checkExpr($expr, $scope); + + if ($message) { + $messages[] = $message; + } + } + } + + private function checkExpr(Node\Expr $expr, Scope $scope): ?string + { + $type = $this->ruleLevelHelper->findTypeToCheck( + $scope, + $expr, + '', + static function (Type $type): bool { + return !$type->toString() instanceof ErrorType; + } + )->getType(); + + if (!$type instanceof ObjectType) { + return null; + } + + $classReflection = $type->getClassReflection(); + + if ($classReflection === null) { + return null; + } + + $methodReflection = $classReflection->getNativeMethod('__toString'); + + if (!$methodReflection->isDeprecated()->yes()) { + return null; + } + + $description = $methodReflection->getDeprecatedDescription(); + if ($description === null) { + return sprintf( + 'Call to deprecated method %s() of class %s.', + $methodReflection->getName(), + $methodReflection->getDeclaringClass()->getName() + ); + } + + return sprintf( + "Call to deprecated method %s() of class %s:\n%s", + $methodReflection->getName(), + $methodReflection->getDeclaringClass()->getName(), + $description + ); + } + +} diff --git a/tests/Rules/Deprecations/EchoDeprecatedBinaryOpToStringRuleTest.php b/tests/Rules/Deprecations/EchoDeprecatedBinaryOpToStringRuleTest.php new file mode 100644 index 0000000..9a5e9e7 --- /dev/null +++ b/tests/Rules/Deprecations/EchoDeprecatedBinaryOpToStringRuleTest.php @@ -0,0 +1,66 @@ + + */ +class EchoDeprecatedBinaryOpToStringRuleTest extends \PHPStan\Testing\RuleTestCase +{ + + protected function getRule(): \PHPStan\Rules\Rule + { + return new EchoDeprecatedBinaryOpToStringRule($this->createBroker()); + } + + public function testDeprecatedMagicMethodToStringCall(): void + { + require_once __DIR__ . '/data/echo-deprecated-binaryop-magic-method-tostring.php'; + $this->analyse( + [__DIR__ . '/data/echo-deprecated-binaryop-magic-method-tostring.php'], + [ + [ + 'Call to deprecated method __toString() of class EchoDeprecatedBinaryOpToStringRule\MagicBar.', + 8, + ], + [ + 'Call to deprecated method __toString() of class EchoDeprecatedBinaryOpToStringRule\MagicBar.', + 9, + ], + [ + 'Call to deprecated method __toString() of class EchoDeprecatedBinaryOpToStringRule\MagicBar.', + 10, + ], + [ + 'Call to deprecated method __toString() of class EchoDeprecatedBinaryOpToStringRule\MagicBar.', + 11, + ], + [ + 'Call to deprecated method __toString() of class EchoDeprecatedBinaryOpToStringRule\MagicBar.', + 12, + ], + [ + "Call to deprecated method __toString() of class EchoDeprecatedBinaryOpToStringRule\MagicBarWithDesc:\nuse XY instead.", + 15, + ], + [ + "Call to deprecated method __toString() of class EchoDeprecatedBinaryOpToStringRule\MagicBarWithDesc:\nuse XY instead.", + 16, + ], + [ + "Call to deprecated method __toString() of class EchoDeprecatedBinaryOpToStringRule\MagicBarWithDesc:\nuse XY instead.", + 17, + ], + [ + "Call to deprecated method __toString() of class EchoDeprecatedBinaryOpToStringRule\MagicBarWithDesc:\nuse XY instead.", + 18, + ], + [ + "Call to deprecated method __toString() of class EchoDeprecatedBinaryOpToStringRule\MagicBarWithDesc:\nuse XY instead.", + 19, + ], + ] + ); + } + +} diff --git a/tests/Rules/Deprecations/EchoDeprecatedToStringRuleTest.php b/tests/Rules/Deprecations/EchoDeprecatedToStringRuleTest.php new file mode 100644 index 0000000..f13e90a --- /dev/null +++ b/tests/Rules/Deprecations/EchoDeprecatedToStringRuleTest.php @@ -0,0 +1,142 @@ + + */ +class EchoDeprecatedToStringRuleTest extends \PHPStan\Testing\RuleTestCase +{ + + protected function getRule(): \PHPStan\Rules\Rule + { + $ruleLevelHelper = new RuleLevelHelper($this->createBroker(), true, false, true); + + return new EchoDeprecatedToStringRule($ruleLevelHelper); + } + + public function testDeprecatedMagicMethodToStringCall(): void + { + require_once __DIR__ . '/data/echo-deprecated-magic-method-tostring.php'; + $this->analyse( + [__DIR__ . '/data/echo-deprecated-magic-method-tostring.php'], + [ + [ + 'Call to deprecated method __toString() of class EchoDeprecatedToStringRule\MagicBar.', + 8, + ], + [ + 'Call to deprecated method __toString() of class EchoDeprecatedToStringRule\MagicBar.', + 9, + ], + [ + 'Call to deprecated method __toString() of class EchoDeprecatedToStringRule\MagicBar.', + 10, + ], + [ + 'Call to deprecated method __toString() of class EchoDeprecatedToStringRule\MagicBar.', + 11, + ], + [ + 'Call to deprecated method __toString() of class EchoDeprecatedToStringRule\MagicBar.', + 12, + ], + [ + 'Call to deprecated method __toString() of class EchoDeprecatedToStringRule\MagicBar.', + 13, + ], + [ + 'Call to deprecated method __toString() of class EchoDeprecatedToStringRule\MagicBar.', + 14, + ], + [ + 'Call to deprecated method __toString() of class EchoDeprecatedToStringRule\MagicBar.', + 15, + ], + [ + 'Call to deprecated method __toString() of class EchoDeprecatedToStringRule\MagicBar.', + 16, + ], + [ + 'Call to deprecated method __toString() of class EchoDeprecatedToStringRule\MagicBar.', + 17, + ], + [ + 'Call to deprecated method __toString() of class EchoDeprecatedToStringRule\MagicBar.', + 18, + ], + [ + 'Call to deprecated method __toString() of class EchoDeprecatedToStringRule\MagicBar.', + 19, + ], + [ + 'Call to deprecated method __toString() of class EchoDeprecatedToStringRule\MagicBar.', + 20, + ], + [ + 'Call to deprecated method __toString() of class EchoDeprecatedToStringRule\MagicBar.', + 21, + ], + [ + "Call to deprecated method __toString() of class EchoDeprecatedToStringRule\MagicBarWithDesc:\nuse XY instead.", + 24, + ], + [ + "Call to deprecated method __toString() of class EchoDeprecatedToStringRule\MagicBarWithDesc:\nuse XY instead.", + 25, + ], + [ + "Call to deprecated method __toString() of class EchoDeprecatedToStringRule\MagicBarWithDesc:\nuse XY instead.", + 26, + ], + [ + "Call to deprecated method __toString() of class EchoDeprecatedToStringRule\MagicBarWithDesc:\nuse XY instead.", + 27, + ], + [ + "Call to deprecated method __toString() of class EchoDeprecatedToStringRule\MagicBarWithDesc:\nuse XY instead.", + 28, + ], + [ + "Call to deprecated method __toString() of class EchoDeprecatedToStringRule\MagicBarWithDesc:\nuse XY instead.", + 29, + ], + [ + "Call to deprecated method __toString() of class EchoDeprecatedToStringRule\MagicBarWithDesc:\nuse XY instead.", + 30, + ], + [ + "Call to deprecated method __toString() of class EchoDeprecatedToStringRule\MagicBarWithDesc:\nuse XY instead.", + 31, + ], + [ + "Call to deprecated method __toString() of class EchoDeprecatedToStringRule\MagicBarWithDesc:\nuse XY instead.", + 32, + ], + [ + "Call to deprecated method __toString() of class EchoDeprecatedToStringRule\MagicBarWithDesc:\nuse XY instead.", + 33, + ], + [ + "Call to deprecated method __toString() of class EchoDeprecatedToStringRule\MagicBarWithDesc:\nuse XY instead.", + 34, + ], + [ + "Call to deprecated method __toString() of class EchoDeprecatedToStringRule\MagicBarWithDesc:\nuse XY instead.", + 35, + ], + [ + "Call to deprecated method __toString() of class EchoDeprecatedToStringRule\MagicBarWithDesc:\nuse XY instead.", + 36, + ], + [ + "Call to deprecated method __toString() of class EchoDeprecatedToStringRule\MagicBarWithDesc:\nuse XY instead.", + 37, + ], + ] + ); + } + +} diff --git a/tests/Rules/Deprecations/data/echo-deprecated-binaryop-magic-method-tostring.php b/tests/Rules/Deprecations/data/echo-deprecated-binaryop-magic-method-tostring.php new file mode 100644 index 0000000..133611a --- /dev/null +++ b/tests/Rules/Deprecations/data/echo-deprecated-binaryop-magic-method-tostring.php @@ -0,0 +1,57 @@ + $bar; + + $barDesc = new MagicBarWithDesc(); + echo "" == $barDesc; + echo "" != $barDesc; + echo "" === $barDesc; + echo "" !== $barDesc; + echo "" <=> $barDesc; + + $noDeps = new NoDeprecation(); + echo "" == $noDeps; + echo "" != $noDeps; + echo "" === $noDeps; + echo "" !== $noDeps; + echo "" <=> $noDeps; +}; + +class MagicBar +{ + /** + * @deprecated + */ + public function __toString() + { + return 'a string'; + } +} + +class MagicBarWithDesc +{ + /** + * @deprecated use XY instead. + */ + public function __toString() + { + return 'a string'; + } +} + +class NoDeprecation +{ + public function __toString() + { + return 'a string'; + } +} diff --git a/tests/Rules/Deprecations/data/echo-deprecated-magic-method-tostring.php b/tests/Rules/Deprecations/data/echo-deprecated-magic-method-tostring.php new file mode 100644 index 0000000..ae9a442 --- /dev/null +++ b/tests/Rules/Deprecations/data/echo-deprecated-magic-method-tostring.php @@ -0,0 +1,89 @@ + $bar; + + $barDesc = new MagicBarWithDesc(); + echo $barDesc; + echo $barDesc . "hallo"; + echo "{$barDesc}"; + echo "la". ($barDesc."3") . "lu"; + echo "la". (string)($barDesc) . "lu"; + echo "la". ($x=$barDesc) . "lu"; + echo "la". ($x.=$barDesc) . "lu"; + echo "" ?: $barDesc; + echo $_GET['doesNotExist'] ?? $barDesc; + echo "" == $barDesc; + echo "" != $barDesc; + echo "" === $barDesc; + echo "" !== $barDesc; + echo "" <=> $barDesc; + + $noDeps = new NoDeprecation(); + echo $noDeps; + echo $noDeps . "hallo"; + echo "{$noDeps}"; + echo "la". ($noDeps."3") . "lu"; + echo "la". (string)($noDeps) . "lu"; + echo "la". ($x=$noDeps) . "lu"; + echo "la". ($x.=$noDeps) . "lu"; + echo "" ?: $noDeps; + echo $_GET['doesNotExist'] ?? $noDeps; + echo "" == $noDeps; + echo "" != $noDeps; + echo "" === $noDeps; + echo "" !== $noDeps; + echo "" <=> $noDeps; + + echo "la". "le" . "lu"; + echo "la". 5 . "lu"; + echo "la". (5+3) . "lu"; + echo "la". (5*3) . "lu"; +}; + +class MagicBar +{ + /** + * @deprecated + */ + public function __toString() + { + return 'a string'; + } +} + +class MagicBarWithDesc +{ + /** + * @deprecated use XY instead. + */ + public function __toString() + { + return 'a string'; + } +} + +class NoDeprecation +{ + public function __toString() + { + return 'a string'; + } +}