Skip to content

Commit

Permalink
Check methods in first-class callables
Browse files Browse the repository at this point in the history
  • Loading branch information
ondrejmirtes committed Nov 17, 2021
1 parent cf79769 commit c106a9d
Show file tree
Hide file tree
Showing 5 changed files with 261 additions and 5 deletions.
7 changes: 2 additions & 5 deletions conf/config.level0.neon
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -86,11 +88,6 @@ services:
arguments:
checkFunctionNameCase: %checkFunctionNameCase%

-
class: PHPStan\Rules\Methods\CallMethodsRule
tags:
- phpstan.rules.rule

-
class: PHPStan\Rules\Methods\CallStaticMethodsRule
tags:
Expand Down
68 changes: 68 additions & 0 deletions src/Rules/Methods/MethodCallableRule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules\Methods;

use PhpParser\Node;
use PHPStan\Analyser\Scope;
use PHPStan\Internal\SprintfHelper;
use PHPStan\Node\MethodCallableNode;
use PHPStan\Php\PhpVersion;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleErrorBuilder;

/**
* @implements Rule<MethodCallableNode>
*/
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;
}

}
90 changes: 90 additions & 0 deletions tests/PHPStan/Rules/Methods/MethodCallableRuleTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules\Methods;

use PHPStan\Php\PhpVersion;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleLevelHelper;
use PHPStan\Testing\RuleTestCase;

/**
* @extends RuleTestCase<MethodCallableRule>
*/
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,
],
]);
}

}
13 changes: 13 additions & 0 deletions tests/PHPStan/Rules/Methods/data/method-callable-not-supported.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php // lint >= 8.1

namespace MethodCallableNotSupported;

class Foo
{

public function doFoo(): void
{
$this->doFoo(...);
}

}
88 changes: 88 additions & 0 deletions tests/PHPStan/Rules/Methods/data/method-callable.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
<?php // lint >= 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(...);
}

}

0 comments on commit c106a9d

Please sign in to comment.