Skip to content

Commit b42ceb6

Browse files
committed
Check static methods in first-class callables
1 parent 480c516 commit b42ceb6

File tree

5 files changed

+251
-0
lines changed

5 files changed

+251
-0
lines changed

conf/config.level0.neon

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ rules:
5454
- PHPStan\Rules\Methods\MethodCallableRule
5555
- PHPStan\Rules\Methods\MissingMethodImplementationRule
5656
- PHPStan\Rules\Methods\MethodAttributesRule
57+
- PHPStan\Rules\Methods\StaticMethodCallableRule
5758
- PHPStan\Rules\Operators\InvalidAssignVarRule
5859
- PHPStan\Rules\Properties\AccessPropertiesInAssignRule
5960
- PHPStan\Rules\Properties\AccessStaticPropertiesInAssignRule
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Methods;
4+
5+
use PhpParser\Node;
6+
use PHPStan\Analyser\Scope;
7+
use PHPStan\Internal\SprintfHelper;
8+
use PHPStan\Node\StaticMethodCallableNode;
9+
use PHPStan\Php\PhpVersion;
10+
use PHPStan\Rules\Rule;
11+
use PHPStan\Rules\RuleErrorBuilder;
12+
13+
/**
14+
* @implements Rule<StaticMethodCallableNode>
15+
*/
16+
class StaticMethodCallableRule implements Rule
17+
{
18+
19+
private StaticMethodCallCheck $methodCallCheck;
20+
21+
private PhpVersion $phpVersion;
22+
23+
public function __construct(StaticMethodCallCheck $methodCallCheck, PhpVersion $phpVersion)
24+
{
25+
$this->methodCallCheck = $methodCallCheck;
26+
$this->phpVersion = $phpVersion;
27+
}
28+
29+
public function getNodeType(): string
30+
{
31+
return StaticMethodCallableNode::class;
32+
}
33+
34+
public function processNode(Node $node, Scope $scope): array
35+
{
36+
if (!$this->phpVersion->supportsFirstClassCallables()) {
37+
return [
38+
RuleErrorBuilder::message('First-class callables are supported only on PHP 8.1 and later.')
39+
->nonIgnorable()
40+
->build(),
41+
];
42+
}
43+
44+
$methodName = $node->getName();
45+
if (!$methodName instanceof Node\Identifier) {
46+
return [];
47+
}
48+
49+
$methodNameName = $methodName->toString();
50+
51+
[$errors, $methodReflection] = $this->methodCallCheck->check($scope, $methodNameName, $node->getClass());
52+
if ($methodReflection === null) {
53+
return $errors;
54+
}
55+
56+
$declaringClass = $methodReflection->getDeclaringClass();
57+
if ($declaringClass->hasNativeMethod($methodNameName)) {
58+
return $errors;
59+
}
60+
61+
$messagesMethodName = SprintfHelper::escapeFormatString($declaringClass->getDisplayName() . '::' . $methodReflection->getName() . '()');
62+
63+
$errors[] = RuleErrorBuilder::message(sprintf('Creating callable from a non-native static method %s.', $messagesMethodName))->build();
64+
65+
return $errors;
66+
}
67+
68+
}
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Methods;
4+
5+
use PHPStan\Php\PhpVersion;
6+
use PHPStan\Rules\ClassCaseSensitivityCheck;
7+
use PHPStan\Rules\Rule;
8+
use PHPStan\Rules\RuleLevelHelper;
9+
use PHPStan\Testing\RuleTestCase;
10+
11+
/**
12+
* @extends RuleTestCase<StaticMethodCallableRule>
13+
*/
14+
class StaticMethodCallableRuleTest extends RuleTestCase
15+
{
16+
17+
/** @var int */
18+
private $phpVersion = PHP_VERSION_ID;
19+
20+
protected function getRule(): Rule
21+
{
22+
$reflectionProvider = $this->createReflectionProvider();
23+
$ruleLevelHelper = new RuleLevelHelper($reflectionProvider, true, false, true, false);
24+
25+
return new StaticMethodCallableRule(
26+
new StaticMethodCallCheck($reflectionProvider, $ruleLevelHelper, new ClassCaseSensitivityCheck($reflectionProvider, true), true, true),
27+
new PhpVersion($this->phpVersion)
28+
);
29+
}
30+
31+
public function testNotSupportedOnOlderVersions(): void
32+
{
33+
if (PHP_VERSION_ID >= 80100) {
34+
self::markTestSkipped('Test runs on PHP < 8.1.');
35+
}
36+
if (!self::$useStaticReflectionProvider) {
37+
self::markTestSkipped('Test requires static reflection.');
38+
}
39+
40+
$this->analyse([__DIR__ . '/data/static-method-callable-not-supported.php'], [
41+
[
42+
'First-class callables are supported only on PHP 8.1 and later.',
43+
10,
44+
],
45+
]);
46+
}
47+
48+
public function testRule(): void
49+
{
50+
if (PHP_VERSION_ID < 80100) {
51+
self::markTestSkipped('Test requires PHP 8.1.');
52+
}
53+
54+
$this->analyse([__DIR__ . '/data/static-method-callable.php'], [
55+
[
56+
'Call to static method StaticMethodCallable\Foo::doFoo() with incorrect case: dofoo',
57+
11,
58+
],
59+
[
60+
'Call to static method doFoo() on an unknown class StaticMethodCallable\Nonexistent.',
61+
12,
62+
'Learn more at https://phpstan.org/user-guide/discovering-symbols',
63+
],
64+
[
65+
'Call to an undefined static method StaticMethodCallable\Foo::nonexistent().',
66+
13,
67+
],
68+
[
69+
'Static call to instance method StaticMethodCallable\Foo::doBar().',
70+
14,
71+
],
72+
[
73+
'Call to private static method doBar() of class StaticMethodCallable\Bar.',
74+
15,
75+
],
76+
[
77+
'Cannot call abstract static method StaticMethodCallable\Bar::doBaz().',
78+
16,
79+
],
80+
[
81+
'Call to static method doFoo() on an unknown class StaticMethodCallable\Nonexistent.',
82+
21,
83+
'Learn more at https://phpstan.org/user-guide/discovering-symbols',
84+
],
85+
[
86+
'Cannot call static method doFoo() on int.',
87+
22,
88+
],
89+
[
90+
'Creating callable from a non-native static method StaticMethodCallable\Lorem::doBar().',
91+
47,
92+
],
93+
[
94+
'Creating callable from a non-native static method StaticMethodCallable\Ipsum::doBar().',
95+
66,
96+
],
97+
]);
98+
}
99+
100+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php // lint >= 8.1
2+
3+
namespace StaticMethodCallableNotSupported;
4+
5+
class Foo
6+
{
7+
8+
public static function doFoo(): void
9+
{
10+
self::doFoo(...);
11+
}
12+
13+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
<?php // lint >= 8.1
2+
3+
namespace StaticMethodCallable;
4+
5+
class Foo
6+
{
7+
8+
public static function doFoo()
9+
{
10+
self::doFoo(...);
11+
self::dofoo(...);
12+
Nonexistent::doFoo(...);
13+
self::nonexistent(...);
14+
self::doBar(...);
15+
Bar::doBar(...);
16+
Bar::doBaz(...);
17+
}
18+
19+
public function doBar(Nonexistent $n, int $i)
20+
{
21+
$n::doFoo(...);
22+
$i::doFoo(...);
23+
}
24+
25+
}
26+
27+
abstract class Bar
28+
{
29+
30+
private static function doBar()
31+
{
32+
33+
}
34+
35+
abstract public static function doBaz();
36+
37+
}
38+
39+
/**
40+
* @method static void doBar()
41+
*/
42+
class Lorem
43+
{
44+
45+
public function doFoo()
46+
{
47+
self::doBar(...);
48+
}
49+
50+
public function __call($name, $arguments)
51+
{
52+
53+
}
54+
55+
56+
}
57+
58+
/**
59+
* @method static void doBar()
60+
*/
61+
class Ipsum
62+
{
63+
64+
public function doFoo()
65+
{
66+
self::doBar(...);
67+
}
68+
69+
}

0 commit comments

Comments
 (0)