Skip to content

Commit c9bf9cd

Browse files
authored
[DeadCode] Keep private method called via self::class static call (#8098)
* [DeadCode] Keep private method called via self::class static call A private method invoked through a class-string static call, e.g. self::class::sampleClass(), is not detected by the usage analyzer and would be wrongly removed. Keep such methods. * [DeadCode] Add @see phpstan reference for class-string static call detection * [DeadCode] Move phpstan @see to resolveClassStringStaticCallNames method
1 parent 010e843 commit c9bf9cd

2 files changed

Lines changed: 61 additions & 0 deletions

File tree

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php
2+
3+
namespace Rector\Tests\DeadCode\Rector\ClassMethod\RemoveUnusedPrivateMethodRector\Fixture;
4+
5+
// a private method called through a class-string static call, self::class::sampleClass(),
6+
// is not detected by the usage analyzer - keep it
7+
final class KeepSelfClassStaticCall
8+
{
9+
public function run(): object
10+
{
11+
return self::class::sampleClass();
12+
}
13+
14+
private function sampleClass(): object
15+
{
16+
return new \stdClass();
17+
}
18+
}

‎rules/DeadCode/Rector/ClassMethod/RemoveUnusedPrivateMethodRector.php‎

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,11 @@
55
namespace Rector\DeadCode\Rector\ClassMethod;
66

77
use PhpParser\Node;
8+
use PhpParser\Node\Expr\ClassConstFetch;
89
use PhpParser\Node\Expr\MethodCall;
10+
use PhpParser\Node\Expr\StaticCall;
911
use PhpParser\Node\Expr\Variable;
12+
use PhpParser\Node\Identifier;
1013
use PhpParser\Node\Stmt\Class_;
1114
use PhpParser\Node\Stmt\ClassMethod;
1215
use PHPStan\Reflection\ClassReflection;
@@ -91,6 +94,10 @@ public function refactor(Node $node): ?Node
9194

9295
$dataProviderMethodNames = $this->resolveDataProviderMethodNames($node);
9396

97+
// methods invoked via a class-string static call, e.g. self::class::sampleClass(),
98+
// are not seen by the usage analyzer
99+
$classStringCallMethodNames = $this->resolveClassStringStaticCallNames($node);
100+
94101
foreach ($node->stmts as $classStmtKey => $classStmt) {
95102
if (! $classStmt instanceof ClassMethod) {
96103
continue;
@@ -118,6 +125,10 @@ public function refactor(Node $node): ?Node
118125
continue;
119126
}
120127

128+
if ($this->isNames($classMethod, $classStringCallMethodNames)) {
129+
continue;
130+
}
131+
121132
unset($node->stmts[$classStmtKey]);
122133
$hasChanged = true;
123134
}
@@ -153,6 +164,38 @@ private function shouldSkip(ClassMethod $classMethod, ClassReflection $classRefl
153164
return $classReflection->hasMethod(MethodName::CALL);
154165
}
155166

167+
/**
168+
* Mirrors PHPStan's UnusedPrivateMethodRule fix for class-string static calls
169+
* @see https://github.com/phpstan/phpstan-src/pull/5953
170+
*
171+
* @return string[]
172+
*/
173+
private function resolveClassStringStaticCallNames(Class_ $class): array
174+
{
175+
$methodNames = [];
176+
177+
/** @var StaticCall[] $staticCalls */
178+
$staticCalls = $this->betterNodeFinder->findInstanceOf($class->stmts, StaticCall::class);
179+
foreach ($staticCalls as $staticCall) {
180+
// e.g. self::class::sampleClass() - the called class is a ::class expression
181+
if (! $staticCall->class instanceof ClassConstFetch) {
182+
continue;
183+
}
184+
185+
if (! $this->isName($staticCall->class->name, 'class')) {
186+
continue;
187+
}
188+
189+
if (! $staticCall->name instanceof Identifier) {
190+
continue;
191+
}
192+
193+
$methodNames[] = $staticCall->name->toString();
194+
}
195+
196+
return $methodNames;
197+
}
198+
156199
private function hasDynamicMethodCallOnFetchThis(ClassMethod $classMethod): bool
157200
{
158201
return (bool) $this->betterNodeFinder->findFirst(

0 commit comments

Comments
 (0)