Skip to content

Commit f314ac7

Browse files
authored
Autoremove: warn about kept excluded usages (#175)
1 parent f68525a commit f314ac7

File tree

13 files changed

+255
-92
lines changed

13 files changed

+255
-92
lines changed

.ecrc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
22
"Exclude": [
3-
"^tests/Rule/data/debug/expected_output.txt"
3+
"output.txt$"
44
]
55
}

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,12 @@ class UserFacade
280280
```
281281

282282
- If you are excluding tests usages (see above), this will not cause the related tests to be removed alongside.
283+
- But you will see all those kept usages in output (with links to your IDE if you set up `editorUrl` [parameter](https://phpstan.org/user-guide/output-format#opening-file-in-an-editor))
284+
285+
```txt
286+
• Removed method UserFacade::deadMethod
287+
! Excluded usage at tests/User/UserFacadeTest.php:241 left intact
288+
```
283289

284290

285291
## Calls over unknown types

rules.neon

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,14 @@ services:
66
class: ShipMonk\PHPStan\DeadCode\Hierarchy\ClassHierarchy
77
-
88
class: ShipMonk\PHPStan\DeadCode\Transformer\FileSystem
9+
-
10+
class: ShipMonk\PHPStan\DeadCode\Output\OutputEnhancer
11+
arguments:
12+
editorUrl: %editorUrl%
913

1014
-
1115
class: ShipMonk\PHPStan\DeadCode\Debug\DebugUsagePrinter
1216
arguments:
13-
editorUrl: %editorUrl%
1417
mixedExcluderEnabled: %shipmonkDeadCode.usageExcluders.usageOverMixed.enabled%
1518

1619
-

src/Debug/DebugUsagePrinter.php

Lines changed: 8 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
use LogicException;
66
use PHPStan\Command\Output;
77
use PHPStan\DependencyInjection\Container;
8-
use PHPStan\File\RelativePathHelper;
98
use PHPStan\Reflection\ClassReflection;
109
use PHPStan\Reflection\ReflectionProvider;
1110
use ReflectionException;
@@ -15,7 +14,7 @@
1514
use ShipMonk\PHPStan\DeadCode\Graph\ClassMemberUsage;
1615
use ShipMonk\PHPStan\DeadCode\Graph\ClassMethodRef;
1716
use ShipMonk\PHPStan\DeadCode\Graph\CollectedUsage;
18-
use ShipMonk\PHPStan\DeadCode\Graph\UsageOrigin;
17+
use ShipMonk\PHPStan\DeadCode\Output\OutputEnhancer;
1918
use function array_map;
2019
use function array_sum;
2120
use function array_unique;
@@ -27,18 +26,15 @@
2726
use function reset;
2827
use function sprintf;
2928
use function str_repeat;
30-
use function str_replace;
3129
use function strpos;
3230

3331
class DebugUsagePrinter
3432
{
3533

36-
private RelativePathHelper $relativePathHelper;
34+
private OutputEnhancer $outputEnhancer;
3735

3836
private ReflectionProvider $reflectionProvider;
3937

40-
private ?string $editorUrl;
41-
4238
/**
4339
* memberKey => usage info
4440
*
@@ -50,16 +46,14 @@ class DebugUsagePrinter
5046

5147
public function __construct(
5248
Container $container,
53-
RelativePathHelper $relativePathHelper,
49+
OutputEnhancer $outputEnhancer,
5450
ReflectionProvider $reflectionProvider,
55-
?string $editorUrl,
5651
bool $mixedExcluderEnabled
5752
)
5853
{
59-
$this->relativePathHelper = $relativePathHelper;
54+
$this->outputEnhancer = $outputEnhancer;
6055
$this->reflectionProvider = $reflectionProvider;
6156
$this->mixedExcluderEnabled = $mixedExcluderEnabled;
62-
$this->editorUrl = $editorUrl;
6357
$this->debugMembers = $this->buildDebugMemberKeys(
6458
// @phpstan-ignore offsetAccess.nonOffsetAccessible, offsetAccess.nonOffsetAccessible, missingType.checkedException, argument.type
6559
$container->getParameter('shipmonkDeadCode')['debug']['usagesOf'], // prevents https://github.com/phpstan/phpstan/issues/12740
@@ -119,7 +113,7 @@ private function getExampleCaller(array $usages): ?string
119113
$origin = $usage->getUsage()->getOrigin();
120114

121115
if ($origin->getFile() !== null) {
122-
return $this->getOriginReference($origin);
116+
return $this->outputEnhancer->getOriginReference($origin);
123117
}
124118
}
125119

@@ -148,7 +142,7 @@ public function printDebugMemberUsages(Output $output, array $analysedClasses):
148142

149143
foreach ($debugMember['eliminationPath'] as $fragmentKey => $fragmentUsages) {
150144
if ($depth === 1) {
151-
$entrypoint = $this->getOriginReference($fragmentUsages[0]->getOrigin(), false);
145+
$entrypoint = $this->outputEnhancer->getOriginReference($fragmentUsages[0]->getOrigin(), false);
152146
$output->writeLineFormatted(sprintf('| <fg=gray>entry</> <fg=white>%s</>', $entrypoint));
153147
}
154148

@@ -160,7 +154,7 @@ public function printDebugMemberUsages(Output $output, array $analysedClasses):
160154

161155
$pathFragment = $nextFragmentFirstUsageOrigin === null
162156
? $this->prettyMemberKey($fragmentKey)
163-
: $this->getOriginLink($nextFragmentFirstUsageOrigin, $this->prettyMemberKey($fragmentKey));
157+
: $this->outputEnhancer->getOriginLink($nextFragmentFirstUsageOrigin, $this->prettyMemberKey($fragmentKey));
164158

165159
$output->writeLineFormatted(sprintf('| %s<fg=white>%s</>', $indent, $pathFragment));
166160

@@ -185,7 +179,7 @@ public function printDebugMemberUsages(Output $output, array $analysedClasses):
185179

186180
foreach ($debugMember['usages'] as $collectedUsage) {
187181
$origin = $collectedUsage->getUsage()->getOrigin();
188-
$output->writeFormatted(sprintf('| • <fg=white>%s</>', $this->getOriginReference($origin)));
182+
$output->writeFormatted(sprintf('| • <fg=white>%s</>', $this->outputEnhancer->getOriginReference($origin)));
189183

190184
if ($collectedUsage->isExcluded()) {
191185
$output->writeFormatted(sprintf(' - <fg=yellow>excluded by %s excluder</>', $collectedUsage->getExcludedBy()));
@@ -214,59 +208,6 @@ private function prettyMemberKey(string $memberKey): string
214208
return $replaced;
215209
}
216210

217-
private function getOriginReference(UsageOrigin $origin, bool $preferFileLine = true): string
218-
{
219-
$file = $origin->getFile();
220-
$line = $origin->getLine();
221-
222-
if ($file !== null && $line !== null) {
223-
$relativeFile = $this->relativePathHelper->getRelativePath($file);
224-
225-
$title = $origin->getClassName() !== null && $origin->getMethodName() !== null && !$preferFileLine
226-
? sprintf('%s::%s:%d', $origin->getClassName(), $origin->getMethodName(), $line)
227-
: sprintf('%s:%s', $relativeFile, $line);
228-
229-
if ($this->editorUrl === null) {
230-
return $title;
231-
}
232-
233-
return sprintf(
234-
'<href=%s>%s</>',
235-
str_replace(['%file%', '%relFile%', '%line%'], [$file, $relativeFile, (string) $line], $this->editorUrl),
236-
$title,
237-
);
238-
}
239-
240-
if ($origin->getProvider() !== null) {
241-
$note = $origin->getNote() !== null ? " ({$origin->getNote()})" : '';
242-
return 'virtual usage from ' . $origin->getProvider() . $note;
243-
}
244-
245-
throw new LogicException('Unknown state of usage origin');
246-
}
247-
248-
private function getOriginLink(UsageOrigin $origin, string $title): string
249-
{
250-
$file = $origin->getFile();
251-
$line = $origin->getLine();
252-
253-
if ($line !== null) {
254-
$title = sprintf('%s:%s', $title, $line);
255-
}
256-
257-
if ($this->editorUrl !== null && $file !== null && $line !== null) {
258-
$relativeFile = $this->relativePathHelper->getRelativePath($file);
259-
260-
return sprintf(
261-
'<href=%s>%s</>',
262-
str_replace(['%file%', '%relFile%', '%line%'], [$file, $relativeFile, (string) $line], $this->editorUrl),
263-
$title,
264-
);
265-
}
266-
267-
return $title;
268-
}
269-
270211
/**
271212
* @param list<string> $alternativeKeys
272213
*/

src/Error/BlackMember.php

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
use LogicException;
66
use ShipMonk\PHPStan\DeadCode\Graph\ClassConstantRef;
77
use ShipMonk\PHPStan\DeadCode\Graph\ClassMemberRef;
8+
use ShipMonk\PHPStan\DeadCode\Graph\ClassMemberUsage;
9+
use ShipMonk\PHPStan\DeadCode\Graph\CollectedUsage;
810
use ShipMonk\PHPStan\DeadCode\Rule\DeadCodeRule;
911
use function array_keys;
1012
use function count;
@@ -20,9 +22,9 @@ final class BlackMember
2022
private int $line;
2123

2224
/**
23-
* @var array<string, true>
25+
* @var array<string, list<ClassMemberUsage>>
2426
*/
25-
private array $excluders = [];
27+
private array $excludedUsages = [];
2628

2729
public function __construct(
2830
ClassMemberRef $member,
@@ -58,9 +60,15 @@ public function getLine(): int
5860
return $this->line;
5961
}
6062

61-
public function markHasExcludedUsage(string $excludedBy): void
63+
public function addExcludedUsage(CollectedUsage $excludedUsage): void
6264
{
63-
$this->excluders[$excludedBy] = true;
65+
if (!$excludedUsage->isExcluded()) {
66+
throw new LogicException('Given usage is not excluded!');
67+
}
68+
69+
$excludedBy = $excludedUsage->getExcludedBy();
70+
71+
$this->excludedUsages[$excludedBy][] = $excludedUsage->getUsage();
6472
}
6573

6674
public function getErrorIdentifier(): string
@@ -72,14 +80,30 @@ public function getErrorIdentifier(): string
7280

7381
public function getExclusionMessage(): string
7482
{
75-
if (count($this->excluders) === 0) {
83+
if (count($this->excludedUsages) === 0) {
7684
return '';
7785
}
7886

79-
$excluderNames = implode(', ', array_keys($this->excluders));
80-
$plural = count($this->excluders) > 1 ? 's' : '';
87+
$excluderNames = implode(', ', array_keys($this->excludedUsages));
88+
$plural = count($this->excludedUsages) > 1 ? 's' : '';
8189

8290
return " (all usages excluded by {$excluderNames} excluder{$plural})";
8391
}
8492

93+
/**
94+
* @return list<ClassMemberUsage>
95+
*/
96+
public function getExcludedUsages(): array
97+
{
98+
$result = [];
99+
100+
foreach ($this->excludedUsages as $usages) {
101+
foreach ($usages as $usage) {
102+
$result[] = $usage;
103+
}
104+
}
105+
106+
return $result;
107+
}
108+
85109
}

src/Formatter/RemoveDeadCodeFormatter.php

Lines changed: 61 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,29 @@
55
use PHPStan\Command\AnalysisResult;
66
use PHPStan\Command\ErrorFormatter\ErrorFormatter;
77
use PHPStan\Command\Output;
8+
use ShipMonk\PHPStan\DeadCode\Graph\ClassMemberUsage;
9+
use ShipMonk\PHPStan\DeadCode\Graph\UsageOrigin;
10+
use ShipMonk\PHPStan\DeadCode\Output\OutputEnhancer;
811
use ShipMonk\PHPStan\DeadCode\Rule\DeadCodeRule;
912
use ShipMonk\PHPStan\DeadCode\Transformer\FileSystem;
1013
use ShipMonk\PHPStan\DeadCode\Transformer\RemoveDeadCodeTransformer;
14+
use function array_keys;
1115
use function count;
1216

1317
class RemoveDeadCodeFormatter implements ErrorFormatter
1418
{
1519

1620
private FileSystem $fileSystem;
1721

18-
public function __construct(FileSystem $fileSystem)
22+
private OutputEnhancer $outputEnhancer;
23+
24+
public function __construct(
25+
FileSystem $fileSystem,
26+
OutputEnhancer $outputEnhancer
27+
)
1928
{
2029
$this->fileSystem = $fileSystem;
30+
$this->outputEnhancer = $outputEnhancer;
2131
}
2232

2333
public function formatErrors(
@@ -37,6 +47,7 @@ public function formatErrors(
3747
return 1;
3848
}
3949

50+
/** @var array<string, array<string, array<string, list<ClassMemberUsage>>>> $deadMembersByFiles file => [identifier => [key => excludedUsages[]]] */
4051
$deadMembersByFiles = [];
4152

4253
foreach ($analysisResult->getFileSpecificErrors() as $fileSpecificError) {
@@ -47,32 +58,74 @@ public function formatErrors(
4758
continue;
4859
}
4960

50-
/** @var array<string, array{file: string}> $metadata */
61+
/** @var array<string, array{file: string, excludedUsages: list<ClassMemberUsage>}> $metadata */
5162
$metadata = $fileSpecificError->getMetadata();
5263
$type = $fileSpecificError->getIdentifier();
5364

54-
foreach ($metadata as $key => $data) {
55-
$deadMembersByFiles[$data['file']][$type][] = $key;
65+
foreach ($metadata as $memberKey => $data) {
66+
$deadMembersByFiles[$data['file']][$type][$memberKey] = $data['excludedUsages'];
5667
}
5768
}
5869

59-
$count = 0;
70+
$membersCount = 0;
71+
$filesCount = count($deadMembersByFiles);
6072

6173
foreach ($deadMembersByFiles as $file => $deadMembersByType) {
74+
/** @var array<string, list<ClassMemberUsage>> $deadConstants */
6275
$deadConstants = $deadMembersByType[DeadCodeRule::IDENTIFIER_CONSTANT] ?? [];
76+
/** @var array<string, list<ClassMemberUsage>> $deadMethods */
6377
$deadMethods = $deadMembersByType[DeadCodeRule::IDENTIFIER_METHOD] ?? [];
6478

65-
$count += count($deadConstants) + count($deadMethods);
79+
$membersCount += count($deadConstants) + count($deadMethods);
6680

67-
$transformer = new RemoveDeadCodeTransformer($deadMethods, $deadConstants);
81+
$transformer = new RemoveDeadCodeTransformer(array_keys($deadMethods), array_keys($deadConstants));
6882
$oldCode = $this->fileSystem->read($file);
6983
$newCode = $transformer->transformCode($oldCode);
7084
$this->fileSystem->write($file, $newCode);
85+
86+
foreach ($deadConstants as $constant => $excludedUsages) {
87+
$output->writeLineFormatted(" • Removed constant <fg=white>$constant</>");
88+
$this->printExcludedUsages($output, $excludedUsages);
89+
}
90+
91+
foreach ($deadMethods as $method => $excludedUsages) {
92+
$output->writeLineFormatted(" • Removed method <fg=white>$method</>");
93+
$this->printExcludedUsages($output, $excludedUsages);
94+
}
7195
}
7296

73-
$output->writeLineFormatted('Removed ' . $count . ' dead methods in ' . count($deadMembersByFiles) . ' files.');
97+
$memberPlural = $membersCount === 1 ? '' : 's';
98+
$filePlural = $filesCount === 1 ? '' : 's';
99+
100+
$output->writeLineFormatted('');
101+
$output->writeLineFormatted("Removed $membersCount dead member$memberPlural in $filesCount file$filePlural.");
74102

75103
return 0;
76104
}
77105

106+
/**
107+
* @param list<ClassMemberUsage> $excludedUsages
108+
*/
109+
private function printExcludedUsages(Output $output, array $excludedUsages): void
110+
{
111+
foreach ($excludedUsages as $excludedUsage) {
112+
$originLink = $this->getOriginLink($excludedUsage->getOrigin());
113+
114+
if ($originLink === null) {
115+
continue;
116+
}
117+
118+
$output->writeLineFormatted(" ! Excluded usage at {$originLink} left intact");
119+
}
120+
}
121+
122+
private function getOriginLink(UsageOrigin $origin): ?string
123+
{
124+
if ($origin->getFile() === null || $origin->getLine() === null) {
125+
return null;
126+
}
127+
128+
return $this->outputEnhancer->getOriginReference($origin);
129+
}
130+
78131
}

0 commit comments

Comments
 (0)