From c106a9dc2b47f11d55ba23e18f598e380799e378 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Wed, 17 Nov 2021 15:24:24 +0100 Subject: [PATCH] Check methods in first-class callables --- conf/config.level0.neon | 7 +- src/Rules/Methods/MethodCallableRule.php | 68 ++++++++++++++ .../Rules/Methods/MethodCallableRuleTest.php | 90 +++++++++++++++++++ .../data/method-callable-not-supported.php | 13 +++ .../Rules/Methods/data/method-callable.php | 88 ++++++++++++++++++ 5 files changed, 261 insertions(+), 5 deletions(-) create mode 100644 src/Rules/Methods/MethodCallableRule.php create mode 100644 tests/PHPStan/Rules/Methods/MethodCallableRuleTest.php create mode 100644 tests/PHPStan/Rules/Methods/data/method-callable-not-supported.php create mode 100644 tests/PHPStan/Rules/Methods/data/method-callable.php diff --git a/conf/config.level0.neon b/conf/config.level0.neon index 508eca2296..44187cefd8 100644 --- a/conf/config.level0.neon +++ b/conf/config.level0.neon @@ -48,7 +48,9 @@ rules: - PHPStan\Rules\Functions\ReturnNullsafeByRefRule - PHPStan\Rules\Keywords\ContinueBreakInLoopRule - PHPStan\Rules\Methods\AbstractMethodInNonAbstractClassRule + - PHPStan\Rules\Methods\CallMethodsRule - PHPStan\Rules\Methods\ExistingClassesInTypehintsRule + - PHPStan\Rules\Methods\MethodCallableRule - PHPStan\Rules\Methods\MissingMethodImplementationRule - PHPStan\Rules\Methods\MethodAttributesRule - PHPStan\Rules\Operators\InvalidAssignVarRule @@ -86,11 +88,6 @@ services: arguments: checkFunctionNameCase: %checkFunctionNameCase% - - - class: PHPStan\Rules\Methods\CallMethodsRule - tags: - - phpstan.rules.rule - - class: PHPStan\Rules\Methods\CallStaticMethodsRule tags: diff --git a/src/Rules/Methods/MethodCallableRule.php b/src/Rules/Methods/MethodCallableRule.php new file mode 100644 index 0000000000..6ca18ae43c --- /dev/null +++ b/src/Rules/Methods/MethodCallableRule.php @@ -0,0 +1,68 @@ + + */ +class MethodCallableRule implements Rule +{ + + private MethodCallCheck $methodCallCheck; + + private PhpVersion $phpVersion; + + public function __construct(MethodCallCheck $methodCallCheck, PhpVersion $phpVersion) + { + $this->methodCallCheck = $methodCallCheck; + $this->phpVersion = $phpVersion; + } + + public function getNodeType(): string + { + return MethodCallableNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!$this->phpVersion->supportsFirstClassCallables()) { + return [ + RuleErrorBuilder::message('First-class callables are supported only on PHP 8.1 and later.') + ->nonIgnorable() + ->build(), + ]; + } + + $methodName = $node->getName(); + if (!$methodName instanceof Node\Identifier) { + return []; + } + + $methodNameName = $methodName->toString(); + + [$errors, $methodReflection] = $this->methodCallCheck->check($scope, $methodNameName, $node->getVar()); + if ($methodReflection === null) { + return $errors; + } + + $declaringClass = $methodReflection->getDeclaringClass(); + if ($declaringClass->hasNativeMethod($methodNameName)) { + return $errors; + } + + $messagesMethodName = SprintfHelper::escapeFormatString($declaringClass->getDisplayName() . '::' . $methodReflection->getName() . '()'); + + $errors[] = RuleErrorBuilder::message(sprintf('Creating callable from a non-native method %s.', $messagesMethodName))->build(); + + return $errors; + } + +} diff --git a/tests/PHPStan/Rules/Methods/MethodCallableRuleTest.php b/tests/PHPStan/Rules/Methods/MethodCallableRuleTest.php new file mode 100644 index 0000000000..5c3a0f4e06 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/MethodCallableRuleTest.php @@ -0,0 +1,90 @@ + + */ +class MethodCallableRuleTest extends RuleTestCase +{ + + /** @var int */ + private $phpVersion = PHP_VERSION_ID; + + protected function getRule(): Rule + { + $reflectionProvider = $this->createReflectionProvider(); + $ruleLevelHelper = new RuleLevelHelper($reflectionProvider, true, false, true, false); + + return new MethodCallableRule( + new MethodCallCheck($reflectionProvider, $ruleLevelHelper, true, true), + new PhpVersion($this->phpVersion) + ); + } + + public function testNotSupportedOnOlderVersions(): void + { + if (PHP_VERSION_ID >= 80100) { + self::markTestSkipped('Test runs on PHP < 8.1.'); + } + if (!self::$useStaticReflectionProvider) { + self::markTestSkipped('Test requires static reflection.'); + } + + $this->analyse([__DIR__ . '/data/method-callable-not-supported.php'], [ + [ + 'First-class callables are supported only on PHP 8.1 and later.', + 10, + ], + ]); + } + + public function testRule(): void + { + if (PHP_VERSION_ID < 80100) { + self::markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/method-callable.php'], [ + [ + 'Call to method MethodCallable\Foo::doFoo() with incorrect case: dofoo', + 11, + ], + [ + 'Call to an undefined method MethodCallable\Foo::doNonexistent().', + 12, + ], + [ + 'Cannot call method doFoo() on int.', + 13, + ], + [ + 'Call to private method doBar() of class MethodCallable\Bar.', + 18, + ], + [ + 'Call to method doFoo() on an unknown class MethodCallable\Nonexistent.', + 23, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', + ], + [ + 'Call to private method doFoo() of class MethodCallable\ParentClass.', + 53, + ], + [ + 'Creating callable from a non-native method MethodCallable\Lorem::doBar().', + 66, + ], + [ + 'Creating callable from a non-native method MethodCallable\Ipsum::doBar().', + 85, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/method-callable-not-supported.php b/tests/PHPStan/Rules/Methods/data/method-callable-not-supported.php new file mode 100644 index 0000000000..3ad95fe40a --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/method-callable-not-supported.php @@ -0,0 +1,13 @@ += 8.1 + +namespace MethodCallableNotSupported; + +class Foo +{ + + public function doFoo(): void + { + $this->doFoo(...); + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/method-callable.php b/tests/PHPStan/Rules/Methods/data/method-callable.php new file mode 100644 index 0000000000..b86bca74e0 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/method-callable.php @@ -0,0 +1,88 @@ += 8.1 + +namespace MethodCallable; + +class Foo +{ + + public function doFoo(int $i): void + { + $this->doFoo(...); + $this->dofoo(...); + $this->doNonexistent(...); + $i->doFoo(...); + } + + public function doBar(Bar $bar): void + { + $bar->doBar(...); + } + + public function doBaz(Nonexistent $n): void + { + $n->doFoo(...); + } + +} + +class Bar +{ + + private function doBar() + { + + } + +} + +class ParentClass +{ + + private function doFoo() + { + + } + +} + +class ChildClass extends ParentClass +{ + + public function doBar() + { + $this->doFoo(...); + } + +} + +/** + * @method void doBar() + */ +class Lorem +{ + + public function doFoo() + { + $this->doBar(...); + } + + public function __call($name, $arguments) + { + + } + + +} + +/** + * @method void doBar() + */ +class Ipsum +{ + + public function doFoo() + { + $this->doBar(...); + } + +}