Skip to content

Commit 923ac93

Browse files
authored
Support dynamic method calls (#52)
1 parent 5b1adcb commit 923ac93

File tree

5 files changed

+66
-41
lines changed

5 files changed

+66
-41
lines changed

README.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,6 @@ class MyEntrypointProvider implements EntrypointProvider
5050
- Only method calls are detected
5151
- Including static methods, trait methods, interface methods, first class callables, etc.
5252
- Any calls on mixed types are not detected, e.g. `$unknownClass->method()`
53-
- Expression method calls are not detected, e.g. `$this->$methodName()`
5453
- Anonymous classes are ignored
5554
- Does not check magic methods
5655
- No transitive check is performed (dead method called only from dead method)

src/Collector/MethodCallCollector.php

Lines changed: 32 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,13 @@
1111
use PhpParser\Node\Expr\New_;
1212
use PhpParser\Node\Expr\NullsafeMethodCall;
1313
use PhpParser\Node\Expr\StaticCall;
14-
use PhpParser\Node\Identifier;
1514
use PhpParser\Node\Name;
1615
use PHPStan\Analyser\Scope;
1716
use PHPStan\Collectors\Collector;
1817
use PHPStan\Node\ClassMethodsNode;
1918
use PHPStan\Node\MethodCallableNode;
2019
use PHPStan\Node\StaticMethodCallableNode;
2120
use PHPStan\Reflection\ClassReflection;
22-
use PHPStan\Reflection\ReflectionProvider;
2321
use PHPStan\Type\Type;
2422
use PHPStan\Type\TypeCombinator;
2523
use ShipMonk\PHPStan\DeadCode\Crate\Call;
@@ -31,18 +29,11 @@
3129
class MethodCallCollector implements Collector
3230
{
3331

34-
private ReflectionProvider $reflectionProvider;
35-
3632
/**
3733
* @var list<Call>
3834
*/
3935
private array $callsBuffer = [];
4036

41-
public function __construct(ReflectionProvider $reflectionProvider)
42-
{
43-
$this->reflectionProvider = $reflectionProvider;
44-
}
45-
4637
public function getNodeType(): string
4738
{
4839
return Node::class;
@@ -104,7 +95,7 @@ private function registerMethodCall(
10495
Scope $scope
10596
): void
10697
{
107-
$methodName = $this->getMethodName($methodCall);
98+
$methodNames = $this->getMethodName($methodCall, $scope);
10899

109100
if ($methodCall instanceof New_) {
110101
if ($methodCall->class instanceof Expr) {
@@ -123,13 +114,15 @@ private function registerMethodCall(
123114
$possibleDescendantCall = true;
124115
}
125116

126-
if ($methodName === null) {
127-
return;
128-
}
117+
foreach ($methodNames as $methodName) {
118+
foreach ($this->getReflectionsWithMethod($callerType, $methodName) as $classWithMethod) {
119+
if (!$classWithMethod->hasMethod($methodName)) {
120+
continue;
121+
}
129122

130-
foreach ($this->getReflectionsWithMethod($callerType, $methodName) as $classWithMethod) {
131-
$className = $classWithMethod->getMethod($methodName, $scope)->getDeclaringClass()->getName();
132-
$this->callsBuffer[] = new Call($className, $methodName, $possibleDescendantCall);
123+
$className = $classWithMethod->getMethod($methodName, $scope)->getDeclaringClass()->getName();
124+
$this->callsBuffer[] = new Call($className, $methodName, $possibleDescendantCall);
125+
}
133126
}
134127
}
135128

@@ -138,33 +131,26 @@ private function registerStaticCall(
138131
Scope $scope
139132
): void
140133
{
141-
$methodName = $this->getMethodName($staticCall);
142-
143-
if ($methodName === null) {
144-
return;
145-
}
134+
$methodNames = $this->getMethodName($staticCall, $scope);
146135

147136
if ($staticCall->class instanceof Expr) {
148137
$callerType = $scope->getType($staticCall->class);
149-
$classReflections = $this->getReflectionsWithMethod($callerType, $methodName);
150138
$possibleDescendantCall = true;
151139

152140
} else {
153-
$className = $scope->resolveName($staticCall->class);
141+
$callerType = $scope->resolveTypeByName($staticCall->class);
154142
$possibleDescendantCall = $staticCall->class->toString() === 'static';
155-
156-
if ($this->reflectionProvider->hasClass($className)) {
157-
$classReflections = [
158-
$this->reflectionProvider->getClass($className),
159-
];
160-
} else {
161-
$classReflections = [];
162-
}
163143
}
164144

165-
foreach ($classReflections as $classWithMethod) {
166-
$className = $classWithMethod->getMethod($methodName, $scope)->getDeclaringClass()->getName();
167-
$this->callsBuffer[] = new Call($className, $methodName, $possibleDescendantCall);
145+
foreach ($methodNames as $methodName) {
146+
foreach ($this->getReflectionsWithMethod($callerType, $methodName) as $classReflection) {
147+
if (!$classReflection->hasMethod($methodName)) {
148+
continue;
149+
}
150+
151+
$className = $classReflection->getMethod($methodName, $scope)->getDeclaringClass()->getName();
152+
$this->callsBuffer[] = new Call($className, $methodName, $possibleDescendantCall);
153+
}
168154
}
169155
}
170156

@@ -200,18 +186,25 @@ private function registerAttribute(Attribute $node, Scope $scope): void
200186

201187
/**
202188
* @param NullsafeMethodCall|MethodCall|StaticCall|New_ $call
189+
* @return list<string>
203190
*/
204-
private function getMethodName(CallLike $call): ?string
191+
private function getMethodName(CallLike $call, Scope $scope): array
205192
{
206193
if ($call instanceof New_) {
207-
return '__construct';
194+
return ['__construct'];
208195
}
209196

210-
if (!$call->name instanceof Identifier) {
211-
return null;
197+
if ($call->name instanceof Expr) {
198+
$possibleMethodNames = [];
199+
200+
foreach ($scope->getType($call->name)->getConstantStrings() as $constantString) {
201+
$possibleMethodNames[] = $constantString->getValue();
202+
}
203+
204+
return $possibleMethodNames;
212205
}
213206

214-
return $call->name->toString();
207+
return [$call->name->toString()];
215208
}
216209

217210
/**

tests/Rule/DeadMethodRuleTest.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ protected function getCollectors(): array
5555
return [
5656
new ClassDefinitionCollector(),
5757
new MethodDefinitionCollector(),
58-
new MethodCallCollector(self::getContainer()->getByType(ReflectionProvider::class)),
58+
new MethodCallCollector(),
5959
];
6060
}
6161

@@ -105,8 +105,10 @@ public static function provideFiles(): iterable
105105
yield 'parent-call-3' => [__DIR__ . '/data/DeadMethodRule/parent-call-3.php'];
106106
yield 'parent-call-4' => [__DIR__ . '/data/DeadMethodRule/parent-call-4.php'];
107107
yield 'attribute' => [__DIR__ . '/data/DeadMethodRule/attribute.php'];
108+
yield 'dynamic-method' => [__DIR__ . '/data/DeadMethodRule/dynamic-method.php'];
108109
yield 'call-on-class-string' => [__DIR__ . '/data/DeadMethodRule/class-string.php'];
109110
yield 'array-map-1' => [__DIR__ . '/data/DeadMethodRule/array-map-1.php'];
111+
yield 'unknown-class' => [__DIR__ . '/data/DeadMethodRule/unknown-class.php'];
110112
yield 'provider-default' => [__DIR__ . '/data/DeadMethodRule/providers/default.php'];
111113
yield 'provider-symfony' => [__DIR__ . '/data/DeadMethodRule/providers/symfony.php', 80_000];
112114
yield 'provider-phpunit' => [__DIR__ . '/data/DeadMethodRule/providers/phpunit.php', 80_000];
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php
2+
3+
namespace DynamicMethod;
4+
5+
class Test {
6+
7+
public static function a(): void {}
8+
public static function b(): void {}
9+
10+
public function c(): void {}
11+
public function d(): void {}
12+
}
13+
14+
/**
15+
* @param 'a'|'b' $method
16+
*/
17+
function test(string $method): void {
18+
Test::$method();
19+
}
20+
21+
/**
22+
* @param 'c'|'d'|'e' $method
23+
*/
24+
function test2(Test $test, string $method): void {
25+
$test->$method();
26+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<?php
2+
3+
namespace UnknownClass;
4+
5+
Unknown::notFailing();

0 commit comments

Comments
 (0)