Skip to content

Commit bf2981b

Browse files
authored
Proper hierarchy call detection (#48)
1 parent 89a38bd commit bf2981b

32 files changed

+736
-234
lines changed

src/Collector/MethodCallCollector.php

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@
2222
use PHPStan\Reflection\ReflectionProvider;
2323
use PHPStan\Type\Type;
2424
use PHPStan\Type\TypeCombinator;
25-
use ShipMonk\PHPStan\DeadCode\Helper\DeadCodeHelper;
25+
use ShipMonk\PHPStan\DeadCode\Crate\Call;
26+
use function array_map;
2627

2728
/**
2829
* @implements Collector<Node, list<string>>
@@ -33,7 +34,7 @@ class MethodCallCollector implements Collector
3334
private ReflectionProvider $reflectionProvider;
3435

3536
/**
36-
* @var list<string>
37+
* @var list<Call>
3738
*/
3839
private array $callsBuffer = [];
3940

@@ -82,7 +83,14 @@ public function processNode(
8283
if (!$scope->isInClass() || $node instanceof ClassMethodsNode) { // @phpstan-ignore-line ignore BC promise
8384
$data = $this->callsBuffer;
8485
$this->callsBuffer = [];
85-
return $data === [] ? null : $data; // collect data once per class to save memory & resultCache size
86+
87+
// collect data once per class to save memory & resultCache size
88+
return $data === []
89+
? null
90+
: array_map(
91+
static fn (Call $call): string => $call->toString(),
92+
$data,
93+
);
8694
}
8795

8896
return null;
@@ -101,15 +109,18 @@ private function registerMethodCall(
101109
if ($methodCall instanceof New_) {
102110
if ($methodCall->class instanceof Expr) {
103111
$callerType = $scope->getType($methodCall->class);
112+
$possibleDescendantCall = true;
104113

105114
} elseif ($methodCall->class instanceof Name) {
106115
$callerType = $scope->resolveTypeByName($methodCall->class);
116+
$possibleDescendantCall = $methodCall->class->toString() === 'static';
107117

108118
} else {
109119
return;
110120
}
111121
} else {
112122
$callerType = $scope->getType($methodCall->var);
123+
$possibleDescendantCall = true;
113124
}
114125

115126
if ($methodName === null) {
@@ -118,7 +129,7 @@ private function registerMethodCall(
118129

119130
foreach ($this->getReflectionsWithMethod($callerType, $methodName) as $classWithMethod) {
120131
$className = $classWithMethod->getMethod($methodName, $scope)->getDeclaringClass()->getName();
121-
$this->callsBuffer[] = DeadCodeHelper::composeMethodKey($className, $methodName);
132+
$this->callsBuffer[] = new Call($className, $methodName, $possibleDescendantCall);
122133
}
123134
}
124135

@@ -136,8 +147,11 @@ private function registerStaticCall(
136147
if ($staticCall->class instanceof Expr) {
137148
$callerType = $scope->getType($staticCall->class);
138149
$classReflections = $this->getReflectionsWithMethod($callerType, $methodName);
150+
$possibleDescendantCall = true;
151+
139152
} else {
140153
$className = $scope->resolveName($staticCall->class);
154+
$possibleDescendantCall = $staticCall->class->toString() === 'static';
141155

142156
if ($this->reflectionProvider->hasClass($className)) {
143157
$classReflections = [
@@ -150,7 +164,7 @@ private function registerStaticCall(
150164

151165
foreach ($classReflections as $classWithMethod) {
152166
$className = $classWithMethod->getMethod($methodName, $scope)->getDeclaringClass()->getName();
153-
$this->callsBuffer[] = DeadCodeHelper::composeMethodKey($className, $methodName);
167+
$this->callsBuffer[] = new Call($className, $methodName, $possibleDescendantCall);
154168
}
155169
}
156170

@@ -164,11 +178,15 @@ private function registerArrayCallable(
164178
$callableTypeAndNames = $constantArray->findTypeAndMethodNames();
165179

166180
foreach ($callableTypeAndNames as $typeAndName) {
181+
$caller = $typeAndName->getType();
167182
$methodName = $typeAndName->getMethod();
168183

169-
foreach ($this->getReflectionsWithMethod($typeAndName->getType(), $methodName) as $classWithMethod) {
184+
// currently always true, see https://github.com/phpstan/phpstan-src/pull/3372
185+
$possibleDescendantCall = !$caller->isClassStringType()->yes();
186+
187+
foreach ($this->getReflectionsWithMethod($caller, $methodName) as $classWithMethod) {
170188
$className = $classWithMethod->getMethod($methodName, $scope)->getDeclaringClass()->getName();
171-
$this->callsBuffer[] = DeadCodeHelper::composeMethodKey($className, $methodName);
189+
$this->callsBuffer[] = new Call($className, $methodName, $possibleDescendantCall);
172190
}
173191
}
174192
}
@@ -177,7 +195,7 @@ private function registerArrayCallable(
177195

178196
private function registerAttribute(Attribute $node, Scope $scope): void
179197
{
180-
$this->callsBuffer[] = DeadCodeHelper::composeMethodKey($scope->resolveName($node->name), '__construct');
198+
$this->callsBuffer[] = new Call($scope->resolveName($node->name), '__construct', false);
181199
}
182200

183201
/**

src/Collector/MethodDefinitionCollector.php

Lines changed: 65 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,20 @@
22

33
namespace ShipMonk\PHPStan\DeadCode\Collector;
44

5+
use Closure;
56
use PhpParser\Node;
67
use PHPStan\Analyser\Scope;
8+
use PHPStan\BetterReflection\Reflection\ReflectionMethod as BetterReflectionMethod;
79
use PHPStan\Collectors\Collector;
810
use PHPStan\Node\InClassNode;
9-
use ShipMonk\PHPStan\DeadCode\Helper\DeadCodeHelper;
11+
use PHPStan\Reflection\ClassReflection;
12+
use ReflectionException;
13+
use ShipMonk\PHPStan\DeadCode\Crate\MethodDefinition;
14+
use function array_map;
1015
use function strpos;
1116

1217
/**
13-
* @implements Collector<InClassNode, list<array{line: int, methodKey: string, overrides: array<string, string>, traitOrigin: ?string}>>
18+
* @implements Collector<InClassNode, list<array{line: int, definition: string, overriddenDefinitions: list<string>, traitOriginDefinition: ?string}>>
1419
*/
1520
class MethodDefinitionCollector implements Collector
1621
{
@@ -22,7 +27,7 @@ public function getNodeType(): string
2227

2328
/**
2429
* @param InClassNode $node
25-
* @return list<array{line: int, methodKey: string, overrides: array<string, string>, traitOrigin: ?string}>|null
30+
* @return list<array{line: int, definition: string, overriddenDefinitions: list<string>, traitOriginDefinition: ?string}>|null
2631
*/
2732
public function processNode(
2833
Node $node,
@@ -37,6 +42,27 @@ public function processNode(
3742
return null; // https://github.com/phpstan/phpstan/issues/8410
3843
}
3944

45+
// we need to collect even methods of traits that are always overridden
46+
foreach ($reflection->getTraits(true) as $trait) {
47+
foreach ($trait->getNativeReflection()->getMethods() as $traitMethod) {
48+
$traitLine = $traitMethod->getStartLine();
49+
$traitName = $trait->getName();
50+
$traitMethodName = $traitMethod->getName();
51+
$declaringTraitDefinition = $this->getDeclaringTraitDefinition($trait, $traitMethodName);
52+
53+
if ($traitLine === false) {
54+
continue;
55+
}
56+
57+
$result[] = [
58+
'line' => $traitLine,
59+
'definition' => (new MethodDefinition($traitName, $traitMethodName))->toString(),
60+
'overriddenDefinitions' => [],
61+
'traitOriginDefinition' => $declaringTraitDefinition !== null ? $declaringTraitDefinition->toString() : null,
62+
];
63+
}
64+
}
65+
4066
foreach ($nativeReflection->getMethods() as $method) {
4167
if ($method->isDestructor()) {
4268
continue;
@@ -70,11 +96,11 @@ public function processNode(
7096

7197
$className = $method->getDeclaringClass()->getName();
7298
$methodName = $method->getName();
73-
$methodKey = DeadCodeHelper::composeMethodKey($className, $methodName);
99+
$definition = new MethodDefinition($className, $methodName);
74100

75-
$declaringTraitMethodKey = DeadCodeHelper::getDeclaringTraitMethodKey($reflection, $methodName);
101+
$declaringTraitDefinition = $this->getDeclaringTraitDefinition($reflection, $methodName);
76102

77-
$methodOverrides = [];
103+
$overriddenDefinitions = [];
78104

79105
foreach ($reflection->getAncestors() as $ancestor) {
80106
if ($ancestor === $reflection) {
@@ -85,23 +111,47 @@ public function processNode(
85111
continue;
86112
}
87113

88-
if ($ancestor->isTrait()) {
89-
continue;
90-
}
91-
92-
$ancestorMethodKey = DeadCodeHelper::composeMethodKey($ancestor->getName(), $methodName);
93-
$methodOverrides[$ancestorMethodKey] = $methodKey;
114+
$overriddenDefinitions[] = new MethodDefinition($ancestor->getName(), $methodName);
94115
}
95116

96117
$result[] = [
97118
'line' => $line,
98-
'methodKey' => $methodKey,
99-
'overrides' => $methodOverrides,
100-
'traitOrigin' => $declaringTraitMethodKey,
119+
'definition' => $definition->toString(),
120+
'overriddenDefinitions' => array_map(static fn (MethodDefinition $definition) => $definition->toString(), $overriddenDefinitions),
121+
'traitOriginDefinition' => $declaringTraitDefinition !== null ? $declaringTraitDefinition->toString() : null,
101122
];
102123
}
103124

104125
return $result !== [] ? $result : null;
105126
}
106127

128+
private function getDeclaringTraitDefinition(
129+
ClassReflection $classReflection,
130+
string $methodName
131+
): ?MethodDefinition
132+
{
133+
try {
134+
$nativeReflectionMethod = $classReflection->getNativeReflection()->getMethod($methodName);
135+
$betterReflectionMethod = $nativeReflectionMethod->getBetterReflection();
136+
$realDeclaringClass = $betterReflectionMethod->getDeclaringClass();
137+
138+
// when trait method name is aliased, we need the original name
139+
$realName = Closure::bind(function (): string {
140+
return $this->name;
141+
}, $betterReflectionMethod, BetterReflectionMethod::class)();
142+
143+
} catch (ReflectionException $e) {
144+
return null;
145+
}
146+
147+
if ($realDeclaringClass->isTrait()) {
148+
return new MethodDefinition(
149+
$realDeclaringClass->getName(),
150+
$realName,
151+
);
152+
}
153+
154+
return null;
155+
}
156+
107157
}

src/Crate/Call.php

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace ShipMonk\PHPStan\DeadCode\Crate;
4+
5+
use LogicException;
6+
use function count;
7+
use function explode;
8+
9+
/**
10+
* @readonly
11+
*/
12+
class Call
13+
{
14+
15+
public string $className;
16+
17+
public string $methodName;
18+
19+
public bool $possibleDescendantCall;
20+
21+
public function __construct(
22+
string $className,
23+
string $methodName,
24+
bool $possibleDescendantCall
25+
)
26+
{
27+
$this->className = $className;
28+
$this->methodName = $methodName;
29+
$this->possibleDescendantCall = $possibleDescendantCall;
30+
}
31+
32+
public function getDefinition(): MethodDefinition
33+
{
34+
return new MethodDefinition($this->className, $this->methodName);
35+
}
36+
37+
public function toString(): string
38+
{
39+
return "{$this->className}::{$this->methodName}::" . ($this->possibleDescendantCall ? '1' : '');
40+
}
41+
42+
public static function fromString(string $methodKey): self
43+
{
44+
$exploded = explode('::', $methodKey);
45+
46+
if (count($exploded) !== 3) {
47+
throw new LogicException("Invalid method key: $methodKey");
48+
}
49+
50+
[$className, $methodName, $possibleDescendantCall] = $exploded;
51+
return new self($className, $methodName, $possibleDescendantCall === '1');
52+
}
53+
54+
}

src/Crate/ClassAndMethod.php

Lines changed: 0 additions & 21 deletions
This file was deleted.

src/Crate/MethodDefinition.php

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace ShipMonk\PHPStan\DeadCode\Crate;
4+
5+
use LogicException;
6+
use function count;
7+
use function explode;
8+
9+
/**
10+
* @readonly
11+
*/
12+
class MethodDefinition
13+
{
14+
15+
public string $className;
16+
17+
public string $methodName;
18+
19+
public function __construct(
20+
string $className,
21+
string $methodName
22+
)
23+
{
24+
$this->className = $className;
25+
$this->methodName = $methodName;
26+
}
27+
28+
public static function fromString(string $methodKey): self
29+
{
30+
$exploded = explode('::', $methodKey);
31+
32+
if (count($exploded) !== 2) {
33+
throw new LogicException("Invalid method key: $methodKey");
34+
}
35+
36+
[$className, $methodName] = $exploded;
37+
return new self($className, $methodName);
38+
}
39+
40+
public function toString(): string
41+
{
42+
return "{$this->className}::{$this->methodName}";
43+
}
44+
45+
}

0 commit comments

Comments
 (0)