Skip to content

Commit c89e094

Browse files
committed
Move debug printing logic to separate class
1 parent 888d709 commit c89e094

File tree

4 files changed

+292
-251
lines changed

4 files changed

+292
-251
lines changed

rules.neon

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,13 @@ services:
1010
-
1111
class: ShipMonk\PHPStan\DeadCode\Graph\UsageOriginDetector
1212

13+
-
14+
class: ShipMonk\PHPStan\DeadCode\Debug\DebugUsagePrinter
15+
arguments:
16+
trackMixedAccess: %shipmonkDeadCode.trackMixedAccess%
17+
debugMembers: %shipmonkDeadCode.debug.usagesOf%
18+
editorUrl: %editorUrl%
19+
1320
-
1421
class: ShipMonk\PHPStan\DeadCode\Provider\VendorUsageProvider
1522
tags:
@@ -105,9 +112,6 @@ services:
105112
- phpstan.diagnoseExtension
106113
arguments:
107114
reportTransitivelyDeadMethodAsSeparateError: %shipmonkDeadCode.reportTransitivelyDeadMethodAsSeparateError%
108-
trackMixedAccess: %shipmonkDeadCode.trackMixedAccess%
109-
debugMembers: %shipmonkDeadCode.debug.usagesOf%
110-
editorUrl: %editorUrl%
111115

112116
-
113117
class: ShipMonk\PHPStan\DeadCode\Compatibility\BackwardCompatibilityChecker

src/Debug/DebugUsagePrinter.php

Lines changed: 268 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,268 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace ShipMonk\PHPStan\DeadCode\Debug;
4+
5+
use LogicException;
6+
use PHPStan\Command\Output;
7+
use PHPStan\File\RelativePathHelper;
8+
use PHPStan\Reflection\ReflectionProvider;
9+
use ShipMonk\PHPStan\DeadCode\Enum\MemberType;
10+
use ShipMonk\PHPStan\DeadCode\Error\BlackMember;
11+
use ShipMonk\PHPStan\DeadCode\Graph\ClassConstantRef;
12+
use ShipMonk\PHPStan\DeadCode\Graph\ClassMethodRef;
13+
use ShipMonk\PHPStan\DeadCode\Graph\CollectedUsage;
14+
use ShipMonk\PHPStan\DeadCode\Graph\UsageOrigin;
15+
use function array_map;
16+
use function array_sum;
17+
use function count;
18+
use function explode;
19+
use function sprintf;
20+
use function str_replace;
21+
use function strpos;
22+
23+
class DebugUsagePrinter
24+
{
25+
26+
private RelativePathHelper $relativePathHelper;
27+
28+
private ReflectionProvider $reflectionProvider;
29+
30+
private bool $trackMixedAccess;
31+
32+
private ?string $editorUrl;
33+
34+
/**
35+
* memberKey => usage info
36+
*
37+
* @var array<string, array{usages?: list<CollectedUsage>, eliminationPath?: list<string>, neverReported?: string}>
38+
*/
39+
private array $debugMembers;
40+
41+
/**
42+
* @param list<string> $debugMembers
43+
*/
44+
public function __construct(
45+
RelativePathHelper $relativePathHelper,
46+
ReflectionProvider $reflectionProvider,
47+
?string $editorUrl,
48+
bool $trackMixedAccess,
49+
array $debugMembers
50+
)
51+
{
52+
$this->relativePathHelper = $relativePathHelper;
53+
$this->reflectionProvider = $reflectionProvider;
54+
$this->editorUrl = $editorUrl;
55+
$this->trackMixedAccess = $trackMixedAccess;
56+
$this->debugMembers = $this->buildDebugMemberKeys($debugMembers);
57+
}
58+
59+
/**
60+
* @param array<MemberType::*, array<string, list<CollectedUsage>>> $mixedMemberUsages
61+
*/
62+
public function printMixedMemberUsages(Output $output, array $mixedMemberUsages): void
63+
{
64+
if ($mixedMemberUsages === [] || !$output->isDebug() || !$this->trackMixedAccess) {
65+
return;
66+
}
67+
68+
$totalCount = array_sum(array_map('count', $mixedMemberUsages));
69+
$maxExamplesToShow = 20;
70+
$examplesShown = 0;
71+
$output->writeLineFormatted(sprintf('<fg=red>Found %d usages over unknown type</>:', $totalCount));
72+
73+
foreach ($mixedMemberUsages as $memberType => $collectedUsages) {
74+
foreach ($collectedUsages as $memberName => $usages) {
75+
$examplesShown++;
76+
$memberTypeString = $memberType === MemberType::METHOD ? 'method' : 'constant';
77+
$output->writeFormatted(sprintf(' • <fg=white>%s</> %s', $memberName, $memberTypeString));
78+
79+
$exampleCaller = $this->getExampleCaller($usages);
80+
81+
if ($exampleCaller !== null) {
82+
$output->writeFormatted(sprintf(', for example in <fg=white>%s</>', $exampleCaller));
83+
}
84+
85+
$output->writeLineFormatted('');
86+
87+
if ($examplesShown >= $maxExamplesToShow) {
88+
break 2;
89+
}
90+
}
91+
}
92+
93+
if ($totalCount > $maxExamplesToShow) {
94+
$output->writeLineFormatted(sprintf('... and %d more', $totalCount - $maxExamplesToShow));
95+
}
96+
97+
$output->writeLineFormatted('');
98+
$output->writeLineFormatted('Thus, any member named the same is considered used, no matter its declaring class!');
99+
$output->writeLineFormatted('');
100+
}
101+
102+
/**
103+
* @param list<CollectedUsage> $usages
104+
*/
105+
private function getExampleCaller(array $usages): ?string
106+
{
107+
foreach ($usages as $usage) {
108+
$origin = $usage->getUsage()->getOrigin();
109+
110+
if ($origin->getFile() !== null) {
111+
return $this->getOriginReference($origin);
112+
}
113+
}
114+
115+
return null;
116+
}
117+
118+
public function printDebugMemberUsages(Output $output): void
119+
{
120+
if ($this->debugMembers === [] || !$output->isDebug()) {
121+
return;
122+
}
123+
124+
$output->writeLineFormatted("\n<fg=red>Usage debugging information:</>");
125+
126+
foreach ($this->debugMembers as $memberKey => $debugMember) {
127+
$output->writeLineFormatted(sprintf("\n<fg=cyan>%s</>", $memberKey));
128+
129+
if (isset($debugMember['eliminationPath'])) {
130+
$output->writeLineFormatted("|\n| Elimination path:");
131+
132+
foreach ($debugMember['eliminationPath'] as $index => $eliminationPath) {
133+
$entrypoint = $index === 0 ? '(entrypoint)' : '';
134+
$output->writeLineFormatted(sprintf('| -> <fg=white>%s</> %s', $eliminationPath, $entrypoint));
135+
}
136+
}
137+
138+
if (isset($debugMember['neverReported'])) {
139+
$output->writeLineFormatted(sprintf("|\n| <fg=yellow>Is never reported as dead: %s</>", $debugMember['neverReported']));
140+
}
141+
142+
if (isset($debugMember['usages'])) {
143+
$output->writeLineFormatted(sprintf("|\n| <fg=green>Found %d usages:</>", count($debugMember['usages'])));
144+
145+
foreach ($debugMember['usages'] as $collectedUsage) {
146+
$origin = $collectedUsage->getUsage()->getOrigin();
147+
$output->writeFormatted(sprintf('| • <fg=white>%s</>', $this->getOriginReference($origin)));
148+
149+
if ($collectedUsage->isExcluded()) {
150+
$output->writeFormatted(sprintf(' - <fg=yellow>Excluded by %s</>', $collectedUsage->getExcludedBy()));
151+
}
152+
153+
$output->writeLineFormatted('');
154+
}
155+
}
156+
157+
$output->writeLineFormatted('');
158+
}
159+
}
160+
161+
private function getOriginReference(UsageOrigin $origin): string
162+
{
163+
$file = $origin->getFile();
164+
$line = $origin->getLine();
165+
166+
if ($file !== null && $line !== null) {
167+
$relativeFile = $this->relativePathHelper->getRelativePath($file);
168+
169+
if ($this->editorUrl === null) {
170+
return sprintf(
171+
'%s:%s',
172+
$relativeFile,
173+
$line,
174+
);
175+
}
176+
177+
return sprintf(
178+
'<href=%s>%s:%s</>',
179+
str_replace(['%file%', '%relFile%', '%line%'], [$file, $relativeFile, (string) $line], $this->editorUrl),
180+
$relativeFile,
181+
$line,
182+
);
183+
}
184+
185+
if ($origin->getReason() !== null) {
186+
return $origin->getReason();
187+
}
188+
189+
throw new LogicException('Unknown state of usage origin');
190+
}
191+
192+
public function recordUsage(CollectedUsage $collectedUsage): void
193+
{
194+
$memberKey = $collectedUsage->getUsage()->getMemberRef()->toKey();
195+
196+
if (!isset($this->debugMembers[$memberKey])) {
197+
return;
198+
}
199+
200+
$this->debugMembers[$memberKey]['usages'][] = $collectedUsage;
201+
}
202+
203+
/**
204+
* @param list<string> $usagePath
205+
*/
206+
public function markMemberAsWhite(BlackMember $blackMember, array $usagePath): void
207+
{
208+
$memberKey = $blackMember->getMember()->toKey();
209+
210+
if (!isset($this->debugMembers[$memberKey])) {
211+
return;
212+
}
213+
214+
$this->debugMembers[$memberKey]['eliminationPath'] = $usagePath;
215+
}
216+
217+
public function markMemberAsNeverReported(BlackMember $blackMember, string $reason): void
218+
{
219+
$memberKey = $blackMember->getMember()->toKey();
220+
221+
if (!isset($this->debugMembers[$memberKey])) {
222+
return;
223+
}
224+
225+
$this->debugMembers[$memberKey]['neverReported'] = $reason;
226+
}
227+
228+
/**
229+
* @param list<string> $debugMembers
230+
* @return array<string, array{usages?: list<CollectedUsage>, eliminationPath?: list<string>, neverReported?: string}>
231+
*/
232+
private function buildDebugMemberKeys(array $debugMembers): array
233+
{
234+
$result = [];
235+
236+
foreach ($debugMembers as $debugMember) {
237+
if (strpos($debugMember, '::') === false) {
238+
throw new LogicException("Invalid debug member format: $debugMember");
239+
}
240+
241+
[$class, $memberName] = explode('::', $debugMember); // @phpstan-ignore offsetAccess.notFound
242+
243+
if (!$this->reflectionProvider->hasClass($class)) {
244+
throw new LogicException("Class $class does not exist");
245+
}
246+
247+
$classReflection = $this->reflectionProvider->getClass($class);
248+
249+
if ($classReflection->hasMethod($memberName)) {
250+
$key = ClassMethodRef::buildKey($class, $memberName);
251+
252+
} elseif ($classReflection->hasConstant($memberName)) {
253+
$key = ClassConstantRef::buildKey($class, $memberName);
254+
255+
} elseif ($classReflection->hasProperty($memberName)) {
256+
throw new LogicException('Properties are not yet supported');
257+
258+
} else {
259+
throw new LogicException("Member $memberName does not exist in $class");
260+
}
261+
262+
$result[$key] = [];
263+
}
264+
265+
return $result;
266+
}
267+
268+
}

0 commit comments

Comments
 (0)