Skip to content

Commit e12d55f

Browse files
committed
Rules to accomodate first-class callables
1 parent 23ea5ed commit e12d55f

11 files changed

+322
-1
lines changed

composer.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
],
88
"require": {
99
"php": "^7.1 || ^8.0",
10-
"phpstan/phpstan": "^1.0"
10+
"phpstan/phpstan": "^1.2.0"
1111
},
1212
"require-dev": {
1313
"nikic/php-parser": "^4.13.0",

rules.neon

+3
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,13 @@ rules:
3838
- PHPStan\Rules\Operators\OperandsInArithmeticMultiplicationRule
3939
- PHPStan\Rules\Operators\OperandsInArithmeticSubtractionRule
4040
- PHPStan\Rules\StrictCalls\DynamicCallOnStaticMethodsRule
41+
- PHPStan\Rules\StrictCalls\DynamicCallOnStaticMethodsCallableRule
4142
- PHPStan\Rules\StrictCalls\StrictFunctionCallsRule
4243
- PHPStan\Rules\SwitchConditions\MatchingTypeInSwitchCaseConditionRule
4344
- PHPStan\Rules\VariableVariables\VariableMethodCallRule
45+
- PHPStan\Rules\VariableVariables\VariableMethodCallableRule
4446
- PHPStan\Rules\VariableVariables\VariableStaticMethodCallRule
47+
- PHPStan\Rules\VariableVariables\VariableStaticMethodCallableRule
4548
- PHPStan\Rules\VariableVariables\VariableStaticPropertyFetchRule
4649
- PHPStan\Rules\VariableVariables\VariableVariablesRule
4750

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\StrictCalls;
4+
5+
use PhpParser\Node;
6+
use PHPStan\Analyser\Scope;
7+
use PHPStan\Node\MethodCallableNode;
8+
use PHPStan\Rules\RuleLevelHelper;
9+
use PHPStan\Type\ErrorType;
10+
use PHPStan\Type\Type;
11+
12+
/**
13+
* @implements \PHPStan\Rules\Rule<MethodCallableNode>
14+
*/
15+
class DynamicCallOnStaticMethodsCallableRule implements \PHPStan\Rules\Rule
16+
{
17+
18+
/** @var \PHPStan\Rules\RuleLevelHelper */
19+
private $ruleLevelHelper;
20+
21+
public function __construct(RuleLevelHelper $ruleLevelHelper)
22+
{
23+
$this->ruleLevelHelper = $ruleLevelHelper;
24+
}
25+
26+
public function getNodeType(): string
27+
{
28+
return MethodCallableNode::class;
29+
}
30+
31+
public function processNode(Node $node, Scope $scope): array
32+
{
33+
if (!$node->getName() instanceof Node\Identifier) {
34+
return [];
35+
}
36+
37+
$name = $node->getName()->name;
38+
$type = $this->ruleLevelHelper->findTypeToCheck(
39+
$scope,
40+
$node->getVar(),
41+
'',
42+
function (Type $type) use ($name): bool {
43+
return $type->canCallMethods()->yes() && $type->hasMethod($name)->yes();
44+
}
45+
)->getType();
46+
47+
if ($type instanceof ErrorType || !$type->canCallMethods()->yes() || !$type->hasMethod($name)->yes()) {
48+
return [];
49+
}
50+
51+
$methodReflection = $type->getMethod($name, $scope);
52+
if ($methodReflection->isStatic()) {
53+
return [sprintf(
54+
'Dynamic call to static method %s::%s().',
55+
$methodReflection->getDeclaringClass()->getDisplayName(),
56+
$methodReflection->getName()
57+
)];
58+
}
59+
60+
return [];
61+
}
62+
63+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\VariableVariables;
4+
5+
use PhpParser\Node;
6+
use PHPStan\Analyser\Scope;
7+
use PHPStan\Node\MethodCallableNode;
8+
use PHPStan\Rules\Rule;
9+
use PHPStan\Type\VerbosityLevel;
10+
11+
/**
12+
* @implements Rule<MethodCallableNode>
13+
*/
14+
class VariableMethodCallableRule implements Rule
15+
{
16+
17+
public function getNodeType(): string
18+
{
19+
return MethodCallableNode::class;
20+
}
21+
22+
public function processNode(Node $node, Scope $scope): array
23+
{
24+
if ($node->getName() instanceof Node\Identifier) {
25+
return [];
26+
}
27+
28+
return [
29+
sprintf(
30+
'Variable method call on %s.',
31+
$scope->getType($node->getVar())->describe(VerbosityLevel::typeOnly())
32+
),
33+
];
34+
}
35+
36+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\VariableVariables;
4+
5+
use PhpParser\Node;
6+
use PHPStan\Analyser\Scope;
7+
use PHPStan\Node\StaticMethodCallableNode;
8+
use PHPStan\Rules\Rule;
9+
use PHPStan\Type\VerbosityLevel;
10+
11+
/**
12+
* @implements Rule<StaticMethodCallableNode>
13+
*/
14+
class VariableStaticMethodCallableRule implements Rule
15+
{
16+
17+
public function getNodeType(): string
18+
{
19+
return StaticMethodCallableNode::class;
20+
}
21+
22+
public function processNode(Node $node, Scope $scope): array
23+
{
24+
if ($node->getName() instanceof Node\Identifier) {
25+
return [];
26+
}
27+
28+
if ($node->getClass() instanceof Node\Name) {
29+
$methodCalledOn = $scope->resolveName($node->getClass());
30+
} else {
31+
$methodCalledOn = $scope->getType($node->getClass())->describe(VerbosityLevel::typeOnly());
32+
}
33+
34+
return [
35+
sprintf(
36+
'Variable static method call on %s.',
37+
$methodCalledOn
38+
),
39+
];
40+
}
41+
42+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\StrictCalls;
4+
5+
use PHPStan\Rules\Rule;
6+
use PHPStan\Rules\RuleLevelHelper;
7+
8+
/**
9+
* @extends \PHPStan\Testing\RuleTestCase<DynamicCallOnStaticMethodsCallableRule>
10+
*/
11+
class DynamicCallOnStaticMethodsCallableRuleTest extends \PHPStan\Testing\RuleTestCase
12+
{
13+
14+
protected function getRule(): Rule
15+
{
16+
return new DynamicCallOnStaticMethodsCallableRule(self::getContainer()->getByType(RuleLevelHelper::class));
17+
}
18+
19+
public function testRule(): void
20+
{
21+
if (PHP_VERSION_ID < 80100) {
22+
self::markTestSkipped('Test requires PHP 8.1.');
23+
}
24+
25+
$this->analyse([__DIR__ . '/data/dynamic-calls-on-static-methods-callables.php'], [
26+
[
27+
'Dynamic call to static method StrictCallsCallables\ClassWithStaticMethod::foo().',
28+
14,
29+
],
30+
[
31+
'Dynamic call to static method StrictCallsCallables\ClassWithStaticMethod::foo().',
32+
21,
33+
],
34+
[
35+
'Dynamic call to static method StrictCallsCallables\ClassUsingTrait::foo().',
36+
34,
37+
],
38+
[
39+
'Dynamic call to static method StrictCallsCallables\ClassUsingTrait::foo().',
40+
46,
41+
],
42+
]);
43+
}
44+
45+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<?php // lint >= 8.1
2+
3+
namespace StrictCallsCallables;
4+
5+
class ClassWithStaticMethod
6+
{
7+
public static function foo()
8+
{
9+
10+
}
11+
12+
public function bar()
13+
{
14+
$this->foo(...);
15+
$this->bar(...);
16+
}
17+
}
18+
19+
function () {
20+
$classWithStaticMethod = new ClassWithStaticMethod();
21+
$classWithStaticMethod->foo(...);
22+
$classWithStaticMethod->bar(...);
23+
};
24+
25+
trait TraitWithStaticMethod
26+
{
27+
public static function foo()
28+
{
29+
30+
}
31+
32+
public function bar()
33+
{
34+
$this->foo(...);
35+
$this->bar(...);
36+
}
37+
}
38+
39+
class ClassUsingTrait
40+
{
41+
use TraitWithStaticMethod;
42+
}
43+
44+
function () {
45+
$classUsingTrait = new ClassUsingTrait();
46+
$classUsingTrait->foo(...);
47+
$classUsingTrait->bar(...);
48+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\VariableVariables;
4+
5+
use PHPStan\Rules\Rule;
6+
use PHPStan\Testing\RuleTestCase;
7+
8+
/**
9+
* @extends RuleTestCase<VariableMethodCallableRule>
10+
*/
11+
class VariableMethodCallableRuleTest extends RuleTestCase
12+
{
13+
14+
protected function getRule(): Rule
15+
{
16+
return new VariableMethodCallableRule();
17+
}
18+
19+
public function testRule(): void
20+
{
21+
if (PHP_VERSION_ID < 80100) {
22+
self::markTestSkipped('Test requires PHP 8.1.');
23+
}
24+
25+
$this->analyse([__DIR__ . '/data/methods-callables.php'], [
26+
[
27+
'Variable method call on stdClass.',
28+
7,
29+
],
30+
]);
31+
}
32+
33+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\VariableVariables;
4+
5+
use PHPStan\Rules\Rule;
6+
use PHPStan\Testing\RuleTestCase;
7+
8+
/**
9+
* @extends RuleTestCase<VariableStaticMethodCallableRule>
10+
*/
11+
class VariableStaticMethodCallableRuleTest extends RuleTestCase
12+
{
13+
14+
protected function getRule(): Rule
15+
{
16+
return new VariableStaticMethodCallableRule();
17+
}
18+
19+
public function testRule(): void
20+
{
21+
$this->analyse([__DIR__ . '/data/staticMethods-callables.php'], [
22+
[
23+
'Variable static method call on Foo.',
24+
7,
25+
],
26+
[
27+
'Variable static method call on stdClass.',
28+
9,
29+
],
30+
]);
31+
}
32+
33+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?php // lint >= 8.1
2+
3+
function (stdClass $std) {
4+
$std->foo(...);
5+
6+
$foo = 'bar';
7+
$std->$foo(...);
8+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php // lint >= 8.1
2+
3+
function (stdClass $std) {
4+
Foo::doFoo(...);
5+
6+
$foo = 'doBar';
7+
Foo::$foo(...);
8+
9+
$std::$foo(...);
10+
};

0 commit comments

Comments
 (0)