Skip to content

Commit fc01208

Browse files
authored
Call graph (#99)
1 parent f1c2022 commit fc01208

26 files changed

+849
-109
lines changed

README.md

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,32 @@ class ApiOutputEntrypointProvider extends SimpleMethodEntrypointProvider
8989
}
9090
```
9191

92+
## Dead cycles & transitively dead methods
93+
- This library automatically detects dead cycles and transitively dead methods (methods that are only called from dead methods)
94+
- By default, it reports only the first dead method in the subtree and the rest as a tip:
95+
96+
```
97+
------ ------------------------------------------------------------------------
98+
Line src/App/Facade/UserFacade.php
99+
------ ------------------------------------------------------------------------
100+
26 Unused App\Facade\UserFacade::updateUserAddress
101+
🪪 shipmonk.deadMethod
102+
💡 Thus App\Entity\User::updateAddress is transitively also unused
103+
💡 Thus App\Entity\Address::setPostalCode is transitively also unused
104+
💡 Thus App\Entity\Address::setCountry is transitively also unused
105+
💡 Thus App\Entity\Address::setStreet is transitively also unused
106+
💡 Thus App\Entity\Address::setZip is transitively also unused
107+
------ ------------------------------------------------------------------------
108+
```
109+
110+
- If you want to report all dead methods individually, you can enable it in your `phpstan.neon.dist`:
111+
112+
```neon
113+
parameters:
114+
shipmonkDeadCode:
115+
reportTransitivelyDeadMethodAsSeparateError: true
116+
```
117+
92118
## Comparison with tomasvotruba/unused-public
93119
- You can see [detailed comparison PR](https://github.com/shipmonk-rnd/dead-code-detector/pull/53)
94120
- Basically, their analysis is less precise and less flexible. Mainly:
@@ -104,11 +130,8 @@ class ApiOutputEntrypointProvider extends SimpleMethodEntrypointProvider
104130
- Only method calls are detected so far
105131
- Including **constructors**, static methods, trait methods, interface methods, first class callables, clone, etc.
106132
- Any calls on mixed types are not detected, e.g. `$unknownClass->method()`
107-
- Anonymous classes are ignored ([PHPStan limitation](https://github.com/phpstan/phpstan/issues/8410))
108-
- Does not check most magic methods (`__get`, `__set` etc)
109-
- Call-graph not implemented so far
110-
- No transitive check is performed (dead method called only from dead method)
111-
- No dead cycles are detected (e.g. dead method calling itself)
133+
- Methods of anonymous classes are never reported as dead ([PHPStan limitation](https://github.com/phpstan/phpstan/issues/8410))
134+
- Most magic methods (e.g. `__get`, `__set` etc) are never reported as dead
112135

113136
## Contributing
114137
- Check your code by `composer check`

rules.neon

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,10 +64,13 @@ services:
6464
class: ShipMonk\PHPStan\DeadCode\Rule\DeadMethodRule
6565
tags:
6666
- phpstan.rules.rule
67+
arguments:
68+
reportTransitivelyDeadMethodAsSeparateError: %shipmonkDeadCode.reportTransitivelyDeadMethodAsSeparateError%
6769

6870

6971
parameters:
7072
shipmonkDeadCode:
73+
reportTransitivelyDeadMethodAsSeparateError: false
7174
entrypoints:
7275
vendor:
7376
enabled: true
@@ -84,6 +87,7 @@ parameters:
8487

8588
parametersSchema:
8689
shipmonkDeadCode: structure([
90+
reportTransitivelyDeadMethodAsSeparateError: bool()
8791
entrypoints: structure([
8892
vendor: structure([
8993
enabled: bool()

src/Collector/ClassDefinitionCollector.php

Lines changed: 4 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
use PhpParser\Node\Name;
88
use PhpParser\Node\Stmt\Class_;
99
use PhpParser\Node\Stmt\ClassLike;
10-
use PhpParser\Node\Stmt\ClassMethod;
1110
use PhpParser\Node\Stmt\Enum_;
1211
use PhpParser\Node\Stmt\Interface_;
1312
use PhpParser\Node\Stmt\Trait_;
@@ -16,15 +15,15 @@
1615
use PHPStan\Analyser\Scope;
1716
use PHPStan\Collectors\Collector;
1817
use ShipMonk\PHPStan\DeadCode\Crate\Kind;
18+
use ShipMonk\PHPStan\DeadCode\Crate\Visibility;
1919
use function array_fill_keys;
2020
use function array_map;
21-
use function strpos;
2221

2322
/**
2423
* @implements Collector<ClassLike, array{
2524
* kind: string,
2625
* name: string,
27-
* methods: array<string, array{line: int, abstract: bool}>,
26+
* methods: array<string, array{line: int, abstract: bool, visibility: int-mask-of<Visibility::*>}>,
2827
* parents: array<string, null>,
2928
* traits: array<string, array{excluded?: list<string>, aliases?: array<string, string>}>,
3029
* interfaces: array<string, null>,
@@ -43,7 +42,7 @@ public function getNodeType(): string
4342
* @return array{
4443
* kind: string,
4544
* name: string,
46-
* methods: array<string, array{line: int, abstract: bool}>,
45+
* methods: array<string, array{line: int, abstract: bool, visibility: int-mask-of<Visibility::*>}>,
4746
* parents: array<string, null>,
4847
* traits: array<string, array{excluded?: list<string>, aliases?: array<string, string>}>,
4948
* interfaces: array<string, null>,
@@ -64,13 +63,10 @@ public function processNode(
6463
$methods = [];
6564

6665
foreach ($node->getMethods() as $method) {
67-
if ($this->isUnsupportedMethod($node, $method)) {
68-
continue;
69-
}
70-
7166
$methods[$method->name->toString()] = [
7267
'line' => $method->getStartLine(),
7368
'abstract' => $method->isAbstract() || $node instanceof Interface_,
69+
'visibility' => $method->flags & (Visibility::PUBLIC | Visibility::PROTECTED | Visibility::PRIVATE),
7470
];
7571
}
7672

@@ -163,35 +159,6 @@ private function getTraits(ClassLike $node): array
163159
return $traits;
164160
}
165161

166-
private function isUnsupportedMethod(ClassLike $class, ClassMethod $method): bool
167-
{
168-
$methodName = $method->name->toString();
169-
170-
if ($methodName === '__destruct') {
171-
return true;
172-
}
173-
174-
if (
175-
strpos($methodName, '__') === 0
176-
&& $methodName !== '__construct'
177-
&& $methodName !== '__clone'
178-
) {
179-
return true; // magic methods like __toString, __get, __set etc
180-
}
181-
182-
if ($methodName === '__construct' && $method->isPrivate()) { // e.g. classes with "denied" instantiation
183-
return true;
184-
}
185-
186-
// abstract methods in traits make sense (not dead) only when called within the trait itself, but that is hard to detect for now, so lets ignore them completely
187-
// the difference from interface methods (or abstract methods) is that those methods can be called over the interface, but you cannot call method over trait
188-
if ($class instanceof Trait_ && $method->isAbstract()) {
189-
return true;
190-
}
191-
192-
return false;
193-
}
194-
195162
private function getKind(ClassLike $node): string
196163
{
197164
if ($node instanceof Class_) {

src/Collector/EntrypointCollector.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use PHPStan\Collectors\Collector;
88
use PHPStan\Node\InClassNode;
99
use ShipMonk\PHPStan\DeadCode\Crate\Call;
10+
use ShipMonk\PHPStan\DeadCode\Crate\Method;
1011
use ShipMonk\PHPStan\DeadCode\Provider\MethodEntrypointProvider;
1112

1213
/**
@@ -48,7 +49,12 @@ public function processNode(
4849

4950
foreach ($this->entrypointProviders as $entrypointProvider) {
5051
foreach ($entrypointProvider->getEntrypoints($node->getClassReflection()) as $entrypointMethod) {
51-
$entrypoints[] = (new Call($entrypointMethod->getDeclaringClass()->getName(), $entrypointMethod->getName(), false))->toString();
52+
$call = new Call(
53+
null,
54+
new Method($entrypointMethod->getDeclaringClass()->getName(), $entrypointMethod->getName()),
55+
false,
56+
);
57+
$entrypoints[] = $call->toString();
5258
}
5359
}
5460

src/Collector/MethodCallCollector.php

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,11 @@
1919
use PHPStan\Node\MethodCallableNode;
2020
use PHPStan\Node\StaticMethodCallableNode;
2121
use PHPStan\Reflection\ClassReflection;
22+
use PHPStan\Reflection\MethodReflection;
2223
use PHPStan\Type\Type;
2324
use PHPStan\Type\TypeCombinator;
2425
use ShipMonk\PHPStan\DeadCode\Crate\Call;
26+
use ShipMonk\PHPStan\DeadCode\Crate\Method;
2527
use function array_map;
2628

2729
/**
@@ -126,7 +128,11 @@ private function registerMethodCall(
126128
}
127129

128130
$className = $classWithMethod->getMethod($methodName, $scope)->getDeclaringClass()->getName();
129-
$this->callsBuffer[] = new Call($className, $methodName, $possibleDescendantCall);
131+
$this->callsBuffer[] = new Call(
132+
$this->getCaller($scope),
133+
new Method($className, $methodName),
134+
$possibleDescendantCall,
135+
);
130136
}
131137
}
132138
}
@@ -154,7 +160,11 @@ private function registerStaticCall(
154160
}
155161

156162
$className = $classReflection->getMethod($methodName, $scope)->getDeclaringClass()->getName();
157-
$this->callsBuffer[] = new Call($className, $methodName, $possibleDescendantCall);
163+
$this->callsBuffer[] = new Call(
164+
$this->getCaller($scope),
165+
new Method($className, $methodName),
166+
$possibleDescendantCall,
167+
);
158168
}
159169
}
160170
}
@@ -177,7 +187,11 @@ private function registerArrayCallable(
177187

178188
foreach ($this->getReflectionsWithMethod($caller, $methodName) as $classWithMethod) {
179189
$className = $classWithMethod->getMethod($methodName, $scope)->getDeclaringClass()->getName();
180-
$this->callsBuffer[] = new Call($className, $methodName, $possibleDescendantCall);
190+
$this->callsBuffer[] = new Call(
191+
$this->getCaller($scope),
192+
new Method($className, $methodName),
193+
$possibleDescendantCall,
194+
);
181195
}
182196
}
183197
}
@@ -186,7 +200,11 @@ private function registerArrayCallable(
186200

187201
private function registerAttribute(Attribute $node, Scope $scope): void
188202
{
189-
$this->callsBuffer[] = new Call($scope->resolveName($node->name), '__construct', false);
203+
$this->callsBuffer[] = new Call(
204+
null,
205+
new Method($scope->resolveName($node->name), '__construct'),
206+
false,
207+
);
190208
}
191209

192210
private function registerClone(Clone_ $node, Scope $scope): void
@@ -196,7 +214,11 @@ private function registerClone(Clone_ $node, Scope $scope): void
196214

197215
foreach ($this->getReflectionsWithMethod($callerType, $methodName) as $classWithMethod) {
198216
$className = $classWithMethod->getMethod($methodName, $scope)->getDeclaringClass()->getName();
199-
$this->callsBuffer[] = new Call($className, $methodName, true);
217+
$this->callsBuffer[] = new Call(
218+
$this->getCaller($scope),
219+
new Method($className, $methodName),
220+
true,
221+
);
200222
}
201223
}
202224

@@ -239,4 +261,17 @@ private function getReflectionsWithMethod(Type $type, string $methodName): itera
239261
}
240262
}
241263

264+
private function getCaller(Scope $scope): ?Method
265+
{
266+
if (!$scope->isInClass()) {
267+
return null;
268+
}
269+
270+
if (!$scope->getFunction() instanceof MethodReflection) {
271+
return null;
272+
}
273+
274+
return new Method($scope->getClassReflection()->getName(), $scope->getFunction()->getName());
275+
}
276+
242277
}

src/Crate/Call.php

Lines changed: 52 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,43 +7,81 @@
77
use function explode;
88

99
/**
10-
* @readonly
10+
* @immutable
1111
*/
1212
class Call
1313
{
1414

15-
public string $className;
15+
public ?Method $caller;
1616

17-
public string $methodName;
17+
public Method $callee;
1818

1919
public bool $possibleDescendantCall;
2020

2121
public function __construct(
22-
string $className,
23-
string $methodName,
22+
?Method $caller,
23+
Method $callee,
2424
bool $possibleDescendantCall
2525
)
2626
{
27-
$this->className = $className;
28-
$this->methodName = $methodName;
27+
$this->caller = $caller;
28+
$this->callee = $callee;
2929
$this->possibleDescendantCall = $possibleDescendantCall;
3030
}
3131

3232
public function toString(): string
3333
{
34-
return "{$this->className}::{$this->methodName}::" . ($this->possibleDescendantCall ? '1' : '');
34+
$callerRef = $this->caller === null ? '' : "{$this->caller->className}::{$this->caller->methodName}";
35+
$calleeRef = "{$this->callee->className}::{$this->callee->methodName}";
36+
37+
return "{$callerRef}->$calleeRef;" . ($this->possibleDescendantCall ? '1' : '');
3538
}
3639

37-
public static function fromString(string $methodKey): self
40+
public static function fromString(string $callKey): self
3841
{
39-
$exploded = explode('::', $methodKey);
42+
$split1 = explode(';', $callKey);
43+
44+
if (count($split1) !== 2) {
45+
throw new LogicException("Invalid method key: $callKey");
46+
}
47+
48+
[$edgeKey, $possibleDescendantCall] = $split1;
49+
50+
$split2 = explode('->', $edgeKey);
51+
52+
if (count($split2) !== 2) {
53+
throw new LogicException("Invalid method key: $callKey");
54+
}
55+
56+
[$callerKey, $calleeKey] = $split2;
57+
58+
$calleeSplit = explode('::', $calleeKey);
59+
60+
if (count($calleeSplit) !== 2) {
61+
throw new LogicException("Invalid method key: $callKey");
62+
}
63+
64+
[$calleeClassName, $calleeMethodName] = $calleeSplit;
65+
$callee = new Method($calleeClassName, $calleeMethodName);
66+
67+
if ($callerKey === '') {
68+
$caller = null;
69+
} else {
70+
$callerSplit = explode('::', $callerKey);
71+
72+
if (count($callerSplit) !== 2) {
73+
throw new LogicException("Invalid method key: $callKey");
74+
}
4075

41-
if (count($exploded) !== 3) {
42-
throw new LogicException("Invalid method key: $methodKey");
76+
[$callerClassName, $callerMethodName] = $callerSplit;
77+
$caller = new Method($callerClassName, $callerMethodName);
4378
}
4479

45-
[$className, $methodName, $possibleDescendantCall] = $exploded;
46-
return new self($className, $methodName, $possibleDescendantCall === '1');
80+
return new self(
81+
$caller,
82+
$callee,
83+
$possibleDescendantCall === '1',
84+
);
4785
}
4886

4987
}

src/Crate/Method.php

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace ShipMonk\PHPStan\DeadCode\Crate;
4+
5+
/**
6+
* @immutable
7+
*/
8+
class Method
9+
{
10+
11+
public string $className;
12+
13+
public string $methodName;
14+
15+
public function __construct(
16+
string $className,
17+
string $methodName
18+
)
19+
{
20+
$this->className = $className;
21+
$this->methodName = $methodName;
22+
}
23+
24+
public function toString(): string
25+
{
26+
return $this->className . '::' . $this->methodName;
27+
}
28+
29+
}

0 commit comments

Comments
 (0)