Skip to content

Commit f0360fa

Browse files
committed
Elimination path to contain exact usage
1 parent c89e094 commit f0360fa

File tree

2 files changed

+65
-33
lines changed

2 files changed

+65
-33
lines changed

src/Debug/DebugUsagePrinter.php

Lines changed: 49 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,20 @@
99
use ShipMonk\PHPStan\DeadCode\Enum\MemberType;
1010
use ShipMonk\PHPStan\DeadCode\Error\BlackMember;
1111
use ShipMonk\PHPStan\DeadCode\Graph\ClassConstantRef;
12+
use ShipMonk\PHPStan\DeadCode\Graph\ClassMemberUsage;
1213
use ShipMonk\PHPStan\DeadCode\Graph\ClassMethodRef;
1314
use ShipMonk\PHPStan\DeadCode\Graph\CollectedUsage;
1415
use ShipMonk\PHPStan\DeadCode\Graph\UsageOrigin;
1516
use function array_map;
1617
use function array_sum;
1718
use function count;
1819
use function explode;
20+
use function ltrim;
21+
use function next;
22+
use function preg_replace;
23+
use function reset;
1924
use function sprintf;
25+
use function str_repeat;
2026
use function str_replace;
2127
use function strpos;
2228

@@ -34,7 +40,7 @@ class DebugUsagePrinter
3440
/**
3541
* memberKey => usage info
3642
*
37-
* @var array<string, array{usages?: list<CollectedUsage>, eliminationPath?: list<string>, neverReported?: string}>
43+
* @var array<string, array{usages?: list<CollectedUsage>, eliminationPath?: array<string, list<ClassMemberUsage>>, neverReported?: string}>
3844
*/
3945
private array $debugMembers;
4046

@@ -124,21 +130,28 @@ public function printDebugMemberUsages(Output $output): void
124130
$output->writeLineFormatted("\n<fg=red>Usage debugging information:</>");
125131

126132
foreach ($this->debugMembers as $memberKey => $debugMember) {
127-
$output->writeLineFormatted(sprintf("\n<fg=cyan>%s</>", $memberKey));
133+
$output->writeLineFormatted(sprintf("\n<fg=cyan>%s</>", $this->prettyMemberKey($memberKey)));
128134

129135
if (isset($debugMember['eliminationPath'])) {
130-
$output->writeLineFormatted("|\n| Elimination path:");
136+
$output->writeLineFormatted("|\n| <fg=green>Elimination path:</>");
137+
$depth = 0;
138+
139+
foreach ($debugMember['eliminationPath'] as $fragmentKey => $fragmentUsages) {
140+
$indent = $depth === 0 ? '<fg=gray>entrypoint</> ' : ' ' . str_repeat(' ', $depth) . '<fg=gray>calls</> ';
141+
$nextFragmentUsages = next($debugMember['eliminationPath']);
142+
$nextFragmentFirstUsage = $nextFragmentUsages !== false ? reset($nextFragmentUsages) : null;
143+
$nextFragmentFirstUsageOrigin = $nextFragmentFirstUsage instanceof ClassMemberUsage ? $nextFragmentFirstUsage->getOrigin() : null;
144+
145+
if ($nextFragmentFirstUsageOrigin === null) {
146+
$output->writeLineFormatted(sprintf('| %s<fg=white>%s</>', $indent, $this->prettyMemberKey($fragmentKey)));
147+
} else {
148+
$output->writeLineFormatted(sprintf('| %s<fg=white>%s</> (%s)', $indent, $this->prettyMemberKey($fragmentKey), $this->getOriginReference($nextFragmentFirstUsageOrigin)));
149+
}
131150

132-
foreach ($debugMember['eliminationPath'] as $index => $eliminationPath) {
133-
$entrypoint = $index === 0 ? '(entrypoint)' : '';
134-
$output->writeLineFormatted(sprintf('| -> <fg=white>%s</> %s', $eliminationPath, $entrypoint));
151+
$depth++;
135152
}
136153
}
137154

138-
if (isset($debugMember['neverReported'])) {
139-
$output->writeLineFormatted(sprintf("|\n| <fg=yellow>Is never reported as dead: %s</>", $debugMember['neverReported']));
140-
}
141-
142155
if (isset($debugMember['usages'])) {
143156
$output->writeLineFormatted(sprintf("|\n| <fg=green>Found %d usages:</>", count($debugMember['usages'])));
144157

@@ -152,12 +165,27 @@ public function printDebugMemberUsages(Output $output): void
152165

153166
$output->writeLineFormatted('');
154167
}
168+
} elseif (isset($debugMember['neverReported'])) {
169+
$output->writeLineFormatted(sprintf("|\n| <fg=yellow>Is never reported as dead: %s</>", $debugMember['neverReported']));
170+
} else {
171+
$output->writeLineFormatted("|\n| <fg=yellow>No usages found</>");
155172
}
156173

157174
$output->writeLineFormatted('');
158175
}
159176
}
160177

178+
private function prettyMemberKey(string $memberKey): string
179+
{
180+
$replaced = preg_replace('/^(m|c)\//', '', $memberKey);
181+
182+
if ($replaced === null) {
183+
throw new LogicException('Failed to pretty member key ' . $memberKey);
184+
}
185+
186+
return $replaced;
187+
}
188+
161189
private function getOriginReference(UsageOrigin $origin): string
162190
{
163191
$file = $origin->getFile();
@@ -201,17 +229,17 @@ public function recordUsage(CollectedUsage $collectedUsage): void
201229
}
202230

203231
/**
204-
* @param list<string> $usagePath
232+
* @param array<string, list<ClassMemberUsage>> $eliminationPath
205233
*/
206-
public function markMemberAsWhite(BlackMember $blackMember, array $usagePath): void
234+
public function markMemberAsWhite(BlackMember $blackMember, array $eliminationPath): void
207235
{
208236
$memberKey = $blackMember->getMember()->toKey();
209237

210238
if (!isset($this->debugMembers[$memberKey])) {
211239
return;
212240
}
213241

214-
$this->debugMembers[$memberKey]['eliminationPath'] = $usagePath;
242+
$this->debugMembers[$memberKey]['eliminationPath'] = $eliminationPath;
215243
}
216244

217245
public function markMemberAsNeverReported(BlackMember $blackMember, string $reason): void
@@ -227,7 +255,7 @@ public function markMemberAsNeverReported(BlackMember $blackMember, string $reas
227255

228256
/**
229257
* @param list<string> $debugMembers
230-
* @return array<string, array{usages?: list<CollectedUsage>, eliminationPath?: list<string>, neverReported?: string}>
258+
* @return array<string, array{usages?: list<CollectedUsage>, eliminationPath?: array<string, list<ClassMemberUsage>>, neverReported?: string}>
231259
*/
232260
private function buildDebugMemberKeys(array $debugMembers): array
233261
{
@@ -239,24 +267,25 @@ private function buildDebugMemberKeys(array $debugMembers): array
239267
}
240268

241269
[$class, $memberName] = explode('::', $debugMember); // @phpstan-ignore offsetAccess.notFound
270+
$normalizedClass = ltrim($class, '\\');
242271

243-
if (!$this->reflectionProvider->hasClass($class)) {
244-
throw new LogicException("Class $class does not exist");
272+
if (!$this->reflectionProvider->hasClass($normalizedClass)) {
273+
throw new LogicException("Class $normalizedClass does not exist");
245274
}
246275

247-
$classReflection = $this->reflectionProvider->getClass($class);
276+
$classReflection = $this->reflectionProvider->getClass($normalizedClass);
248277

249278
if ($classReflection->hasMethod($memberName)) {
250-
$key = ClassMethodRef::buildKey($class, $memberName);
279+
$key = ClassMethodRef::buildKey($normalizedClass, $memberName);
251280

252281
} elseif ($classReflection->hasConstant($memberName)) {
253-
$key = ClassConstantRef::buildKey($class, $memberName);
282+
$key = ClassConstantRef::buildKey($normalizedClass, $memberName);
254283

255284
} elseif ($classReflection->hasProperty($memberName)) {
256285
throw new LogicException('Properties are not yet supported');
257286

258287
} else {
259-
throw new LogicException("Member $memberName does not exist in $class");
288+
throw new LogicException("Member $memberName does not exist in $normalizedClass");
260289
}
261290

262291
$result[$key] = [];

src/Rule/DeadCodeRule.php

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
use function array_merge;
3535
use function array_merge_recursive;
3636
use function array_slice;
37+
use function array_unique;
3738
use function array_values;
3839
use function in_array;
3940
use function ksort;
@@ -108,7 +109,7 @@ class DeadCodeRule implements Rule, DiagnoseExtension
108109
private array $mixedMemberUsages = [];
109110

110111
/**
111-
* @var array<string, list<string>> callerKey => memberUseKey[]
112+
* @var array<string, array<string, list<ClassMemberUsage>>> callerKey => array<calleeKey, usages[]>
112113
*/
113114
private array $usageGraph = [];
114115

@@ -247,7 +248,7 @@ public function processNode(
247248

248249
foreach ($alternativeMemberKeys as $alternativeMemberKey) {
249250
foreach ($alternativeOriginKeys as $alternativeOriginKey) {
250-
$this->usageGraph[$alternativeOriginKey][] = $alternativeMemberKey;
251+
$this->usageGraph[$alternativeOriginKey][$alternativeMemberKey][] = $memberUsage;
251252
}
252253

253254
if ($isWhite) {
@@ -415,6 +416,8 @@ private function getAlternativeMemberKeys(ClassMemberRef $member): array
415416
}
416417
}
417418

419+
$result = array_values(array_unique($result));
420+
418421
$this->memberAlternativesCache[$cacheKey] = $result;
419422

420423
return $result;
@@ -455,29 +458,29 @@ private function findDefinerMemberKey(
455458
}
456459

457460
/**
458-
* @param array<string, null> $visitedKeys
461+
* @param array<string, list<ClassMemberUsage>> $visited
459462
*/
460-
private function markTransitivesWhite(string $callerKey, array $visitedKeys = []): void
463+
private function markTransitivesWhite(string $callerKey, array $visited = []): void
461464
{
462-
$visitedKeys = $visitedKeys === [] ? [$callerKey => null] : $visitedKeys;
463-
$calleeKeys = $this->usageGraph[$callerKey] ?? [];
465+
$visited = $visited === [] ? [$callerKey => []] : $visited; // TODO [] init?
466+
$callees = $this->usageGraph[$callerKey] ?? [];
464467

465468
if (isset($this->blackMembers[$callerKey])) { // TODO debug why not always present
466-
$this->debugUsagePrinter->markMemberAsWhite($this->blackMembers[$callerKey], array_keys($visitedKeys));
469+
$this->debugUsagePrinter->markMemberAsWhite($this->blackMembers[$callerKey], $visited);
467470

468471
unset($this->blackMembers[$callerKey]);
469472
}
470473

471-
foreach ($calleeKeys as $calleeKey) {
472-
if (array_key_exists($calleeKey, $visitedKeys)) {
474+
foreach ($callees as $calleeKey => $usages) {
475+
if (array_key_exists($calleeKey, $visited)) {
473476
continue;
474477
}
475478

476479
if (!isset($this->blackMembers[$calleeKey])) {
477480
continue;
478481
}
479482

480-
$this->markTransitivesWhite($calleeKey, array_merge($visitedKeys, [$calleeKey => null]));
483+
$this->markTransitivesWhite($calleeKey, array_merge($visited, [$calleeKey => $usages]));
481484
}
482485
}
483486

@@ -488,11 +491,11 @@ private function markTransitivesWhite(string $callerKey, array $visitedKeys = []
488491
private function getTransitiveDeadCalls(string $callerKey, array $visitedKeys = []): array
489492
{
490493
$visitedKeys = $visitedKeys === [] ? [$callerKey => null] : $visitedKeys;
491-
$calleeKeys = $this->usageGraph[$callerKey] ?? [];
494+
$callees = $this->usageGraph[$callerKey] ?? [];
492495

493496
$result = [];
494497

495-
foreach ($calleeKeys as $calleeKey) {
498+
foreach ($callees as $calleeKey => $_) {
496499
if (array_key_exists($calleeKey, $visitedKeys)) {
497500
continue;
498501
}
@@ -528,7 +531,7 @@ private function groupDeadMembers(): array
528531
continue;
529532
}
530533

531-
foreach ($callees as $callee) {
534+
foreach ($callees as $callee => $_) {
532535
if (array_key_exists($callee, $this->blackMembers)) {
533536
$deadMethodsWithCaller[$callee] = true;
534537
}

0 commit comments

Comments
 (0)