Skip to content

Commit

Permalink
Check static methods in first-class callables
Browse files Browse the repository at this point in the history
  • Loading branch information
ondrejmirtes committed Nov 18, 2021
1 parent 480c516 commit b42ceb6
Show file tree
Hide file tree
Showing 5 changed files with 251 additions and 0 deletions.
1 change: 1 addition & 0 deletions conf/config.level0.neon
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ rules:
- PHPStan\Rules\Methods\MethodCallableRule
- PHPStan\Rules\Methods\MissingMethodImplementationRule
- PHPStan\Rules\Methods\MethodAttributesRule
- PHPStan\Rules\Methods\StaticMethodCallableRule
- PHPStan\Rules\Operators\InvalidAssignVarRule
- PHPStan\Rules\Properties\AccessPropertiesInAssignRule
- PHPStan\Rules\Properties\AccessStaticPropertiesInAssignRule
Expand Down
68 changes: 68 additions & 0 deletions src/Rules/Methods/StaticMethodCallableRule.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\StaticMethodCallableNode;
use PHPStan\Php\PhpVersion;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleErrorBuilder;

/**
* @implements Rule<StaticMethodCallableNode>
*/
class StaticMethodCallableRule implements Rule
{

private StaticMethodCallCheck $methodCallCheck;

private PhpVersion $phpVersion;

public function __construct(StaticMethodCallCheck $methodCallCheck, PhpVersion $phpVersion)
{
$this->methodCallCheck = $methodCallCheck;
$this->phpVersion = $phpVersion;
}

public function getNodeType(): string
{
return StaticMethodCallableNode::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->getClass());
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 static method %s.', $messagesMethodName))->build();

return $errors;
}

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

namespace PHPStan\Rules\Methods;

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

/**
* @extends RuleTestCase<StaticMethodCallableRule>
*/
class StaticMethodCallableRuleTest 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 StaticMethodCallableRule(
new StaticMethodCallCheck($reflectionProvider, $ruleLevelHelper, new ClassCaseSensitivityCheck($reflectionProvider, true), 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/static-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/static-method-callable.php'], [
[
'Call to static method StaticMethodCallable\Foo::doFoo() with incorrect case: dofoo',
11,
],
[
'Call to static method doFoo() on an unknown class StaticMethodCallable\Nonexistent.',
12,
'Learn more at https://phpstan.org/user-guide/discovering-symbols',
],
[
'Call to an undefined static method StaticMethodCallable\Foo::nonexistent().',
13,
],
[
'Static call to instance method StaticMethodCallable\Foo::doBar().',
14,
],
[
'Call to private static method doBar() of class StaticMethodCallable\Bar.',
15,
],
[
'Cannot call abstract static method StaticMethodCallable\Bar::doBaz().',
16,
],
[
'Call to static method doFoo() on an unknown class StaticMethodCallable\Nonexistent.',
21,
'Learn more at https://phpstan.org/user-guide/discovering-symbols',
],
[
'Cannot call static method doFoo() on int.',
22,
],
[
'Creating callable from a non-native static method StaticMethodCallable\Lorem::doBar().',
47,
],
[
'Creating callable from a non-native static method StaticMethodCallable\Ipsum::doBar().',
66,
],
]);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php // lint >= 8.1

namespace StaticMethodCallableNotSupported;

class Foo
{

public static function doFoo(): void
{
self::doFoo(...);
}

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

namespace StaticMethodCallable;

class Foo
{

public static function doFoo()
{
self::doFoo(...);
self::dofoo(...);
Nonexistent::doFoo(...);
self::nonexistent(...);
self::doBar(...);
Bar::doBar(...);
Bar::doBaz(...);
}

public function doBar(Nonexistent $n, int $i)
{
$n::doFoo(...);
$i::doFoo(...);
}

}

abstract class Bar
{

private static function doBar()
{

}

abstract public static function doBaz();

}

/**
* @method static void doBar()
*/
class Lorem
{

public function doFoo()
{
self::doBar(...);
}

public function __call($name, $arguments)
{

}


}

/**
* @method static void doBar()
*/
class Ipsum
{

public function doFoo()
{
self::doBar(...);
}

}

0 comments on commit b42ceb6

Please sign in to comment.