Skip to content

Commit a445278

Browse files
committed
Proper hierarchy call detection
1 parent 89a38bd commit a445278

26 files changed

+532
-226
lines changed

src/Collector/MethodCallCollector.php

+26-8
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

+34-11
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,14 @@
66
use PHPStan\Analyser\Scope;
77
use PHPStan\Collectors\Collector;
88
use PHPStan\Node\InClassNode;
9-
use ShipMonk\PHPStan\DeadCode\Helper\DeadCodeHelper;
9+
use PHPStan\Reflection\ClassReflection;
10+
use ReflectionException;
11+
use ShipMonk\PHPStan\DeadCode\Crate\MethodDefinition;
12+
use function array_map;
1013
use function strpos;
1114

1215
/**
13-
* @implements Collector<InClassNode, list<array{line: int, methodKey: string, overrides: array<string, string>, traitOrigin: ?string}>>
16+
* @implements Collector<InClassNode, list<array{line: int, definition: string, overriddenDefinitions: list<string>, traitOriginDefinition: ?string}>>
1417
*/
1518
class MethodDefinitionCollector implements Collector
1619
{
@@ -22,7 +25,7 @@ public function getNodeType(): string
2225

2326
/**
2427
* @param InClassNode $node
25-
* @return list<array{line: int, methodKey: string, overrides: array<string, string>, traitOrigin: ?string}>|null
28+
* @return list<array{line: int, definition: string, overriddenDefinitions: list<string>, traitOriginDefinition: ?string}>|null
2629
*/
2730
public function processNode(
2831
Node $node,
@@ -70,11 +73,11 @@ public function processNode(
7073

7174
$className = $method->getDeclaringClass()->getName();
7275
$methodName = $method->getName();
73-
$methodKey = DeadCodeHelper::composeMethodKey($className, $methodName);
76+
$definition = new MethodDefinition($className, $methodName);
7477

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

77-
$methodOverrides = [];
80+
$overriddenDefinitions = [];
7881

7982
foreach ($reflection->getAncestors() as $ancestor) {
8083
if ($ancestor === $reflection) {
@@ -89,19 +92,39 @@ public function processNode(
8992
continue;
9093
}
9194

92-
$ancestorMethodKey = DeadCodeHelper::composeMethodKey($ancestor->getName(), $methodName);
93-
$methodOverrides[$ancestorMethodKey] = $methodKey;
95+
$overriddenDefinitions[] = new MethodDefinition($ancestor->getName(), $methodName);
9496
}
9597

9698
$result[] = [
9799
'line' => $line,
98-
'methodKey' => $methodKey,
99-
'overrides' => $methodOverrides,
100-
'traitOrigin' => $declaringTraitMethodKey,
100+
'definition' => $definition->toString(),
101+
'overriddenDefinitions' => array_map(static fn (MethodDefinition $definition) => $definition->toString(), $overriddenDefinitions),
102+
'traitOriginDefinition' => $declaringTraitDefinition !== null ? $declaringTraitDefinition->toString() : null,
101103
];
102104
}
103105

104106
return $result !== [] ? $result : null;
105107
}
106108

109+
private function getDeclaringTraitDefinition(
110+
ClassReflection $classReflection,
111+
string $methodName
112+
): ?MethodDefinition
113+
{
114+
try {
115+
$realDeclaringClass = $classReflection->getNativeReflection()
116+
->getMethod($methodName)
117+
->getBetterReflection()
118+
->getDeclaringClass();
119+
} catch (ReflectionException $e) {
120+
return null;
121+
}
122+
123+
if ($realDeclaringClass->isTrait()) {
124+
return new MethodDefinition($realDeclaringClass->getName(), $methodName);
125+
}
126+
127+
return null;
128+
}
129+
107130
}

src/Crate/Call.php

+54
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

-21
This file was deleted.

src/Crate/MethodDefinition.php

+45
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+
}

src/Helper/DeadCodeHelper.php

-63
This file was deleted.

0 commit comments

Comments
 (0)