-
Notifications
You must be signed in to change notification settings - Fork 471
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Check methods in first-class callables
- Loading branch information
1 parent
cf79769
commit c106a9d
Showing
5 changed files
with
261 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
13
tests/PHPStan/Rules/Methods/data/method-callable-not-supported.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(...); | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(...); | ||
} | ||
|
||
} |