Skip to content

Commit 780dc3f

Browse files
authored
Fix invalid file reported for non-overridden dead trait methods (#67)
1 parent 5d4d631 commit 780dc3f

File tree

6 files changed

+85
-23
lines changed

6 files changed

+85
-23
lines changed

src/Collector/MethodDefinitionCollector.php

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
use function strpos;
1717

1818
/**
19-
* @implements Collector<InClassNode, list<array{line: int, definition: string, overriddenDefinitions: list<string>, traitOriginDefinition: ?string}>>
19+
* @implements Collector<InClassNode, list<array{line: int, file: string, definition: string, overriddenDefinitions: list<string>, traitOriginDefinition: ?string}>>
2020
*/
2121
class MethodDefinitionCollector implements Collector
2222
{
@@ -28,7 +28,7 @@ public function getNodeType(): string
2828

2929
/**
3030
* @param InClassNode $node
31-
* @return list<array{line: int, definition: string, overriddenDefinitions: list<string>, traitOriginDefinition: ?string}>|null
31+
* @return list<array{line: int, file: string, definition: string, overriddenDefinitions: list<string>, traitOriginDefinition: ?string}>|null
3232
*/
3333
public function processNode(
3434
Node $node,
@@ -51,16 +51,18 @@ public function processNode(
5151
}
5252

5353
$traitLine = $traitMethod->getStartLine();
54+
$traitFile = $traitMethod->getFileName();
5455
$traitName = $trait->getName();
5556
$traitMethodName = $traitMethod->getName();
5657
$declaringTraitDefinition = $this->getDeclaringTraitDefinition($trait, $traitMethodName);
5758

58-
if ($traitLine === false) {
59+
if ($traitLine === false || $traitFile === false) {
5960
continue;
6061
}
6162

6263
$result[] = [
6364
'line' => $traitLine,
65+
'file' => $traitFile,
6466
'definition' => (new MethodDefinition($traitName, $traitMethodName))->toString(),
6567
'overriddenDefinitions' => [],
6668
'traitOriginDefinition' => $declaringTraitDefinition !== null ? $declaringTraitDefinition->toString() : null,
@@ -105,6 +107,7 @@ public function processNode(
105107

106108
$result[] = [
107109
'line' => $line,
110+
'file' => $scope->getFile(),
108111
'definition' => $definition->toString(),
109112
'overriddenDefinitions' => array_map(static fn (MethodDefinition $definition) => $definition->toString(), $overriddenDefinitions),
110113
'traitOriginDefinition' => $declaringTraitDefinition !== null ? $declaringTraitDefinition->toString() : null,

src/Rule/DeadMethodRule.php

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -89,11 +89,12 @@ public function processNode(
8989

9090
unset($classDeclarationData);
9191

92-
foreach ($methodDeclarationData as $file => $methodsInFile) {
92+
foreach ($methodDeclarationData as $methodsInFile) {
9393
foreach ($methodsInFile as $declared) {
9494
foreach ($declared as $serializedMethodDeclaration) {
9595
[
9696
'line' => $line,
97+
'file' => $file,
9798
'definition' => $definition,
9899
'overriddenDefinitions' => $overriddenDefinitions,
99100
'traitOriginDefinition' => $declaringTraitMethodKey,
@@ -241,13 +242,14 @@ private function isEntryPoint(MethodDefinition $methodDefinition): bool
241242
}
242243

243244
/**
244-
* @param array{line: int, definition: string, overriddenDefinitions: list<string>, traitOriginDefinition: string|null} $serializedMethodDeclaration
245-
* @return array{line: int, definition: MethodDefinition, overriddenDefinitions: list<MethodDefinition>, traitOriginDefinition: MethodDefinition|null}
245+
* @param array{line: int, file: string, definition: string, overriddenDefinitions: list<string>, traitOriginDefinition: string|null} $serializedMethodDeclaration
246+
* @return array{line: int, file: string, definition: MethodDefinition, overriddenDefinitions: list<MethodDefinition>, traitOriginDefinition: MethodDefinition|null}
246247
*/
247248
private function deserializeMethodDeclaration(array $serializedMethodDeclaration): array
248249
{
249250
return [
250251
'line' => $serializedMethodDeclaration['line'],
252+
'file' => $serializedMethodDeclaration['file'],
251253
'definition' => MethodDefinition::fromString($serializedMethodDeclaration['definition']),
252254
'overriddenDefinitions' => array_map(
253255
static fn (string $definition) => MethodDefinition::fromString($definition),

tests/Rule/DeadMethodRuleTest.php

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
use ShipMonk\PHPStan\DeadCode\Provider\SymfonyEntrypointProvider;
2424
use ShipMonk\PHPStan\DeadCode\Provider\VendorEntrypointProvider;
2525
use ShipMonk\PHPStan\DeadCode\Reflection\ClassHierarchy;
26+
use function is_array;
2627
use const PHP_VERSION_ID;
2728

2829
/**
@@ -60,19 +61,20 @@ protected function getCollectors(): array
6061
}
6162

6263
/**
64+
* @param string|list<string> $files
6365
* @dataProvider provideFiles
6466
*/
65-
public function testDead(string $file, ?int $lowestPhpVersion = null): void
67+
public function testDead($files, ?int $lowestPhpVersion = null): void
6668
{
6769
if ($lowestPhpVersion !== null && PHP_VERSION_ID < $lowestPhpVersion) {
6870
self::markTestSkipped('Requires PHP ' . $lowestPhpVersion);
6971
}
7072

71-
$this->analyseFile($file);
73+
$this->analyseFiles(is_array($files) ? $files : [$files]);
7274
}
7375

7476
/**
75-
* @return array<string, array{0: string, 1?: int}>
77+
* @return array<string, array{0: string|list<string>, 1?: int}>
7678
*/
7779
public static function provideFiles(): iterable
7880
{
@@ -97,6 +99,7 @@ public static function provideFiles(): iterable
9799
yield 'trait-8' => [__DIR__ . '/data/DeadMethodRule/traits-8.php'];
98100
yield 'trait-9' => [__DIR__ . '/data/DeadMethodRule/traits-9.php'];
99101
yield 'trait-10' => [__DIR__ . '/data/DeadMethodRule/traits-10.php'];
102+
yield 'trait-11' => [[__DIR__ . '/data/DeadMethodRule/traits-11-a.php', __DIR__ . '/data/DeadMethodRule/traits-11-b.php']];
100103
yield 'nullsafe' => [__DIR__ . '/data/DeadMethodRule/nullsafe.php'];
101104
yield 'dead-in-parent-1' => [__DIR__ . '/data/DeadMethodRule/dead-in-parent-1.php'];
102105
yield 'indirect-interface' => [__DIR__ . '/data/DeadMethodRule/indirect-interface.php'];

tests/Rule/RuleTestCase.php

Lines changed: 52 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
use PHPStan\Analyser\Error;
77
use PHPStan\Rules\Rule;
88
use PHPStan\Testing\RuleTestCase as OriginalRuleTestCase;
9+
use function array_diff;
910
use function array_values;
1011
use function explode;
1112
use function file_get_contents;
@@ -15,6 +16,7 @@
1516
use function preg_match;
1617
use function preg_match_all;
1718
use function preg_replace;
19+
use function sort;
1820
use function sprintf;
1921
use function trim;
2022
use function uniqid;
@@ -26,27 +28,52 @@
2628
abstract class RuleTestCase extends OriginalRuleTestCase
2729
{
2830

29-
protected function analyseFile(string $file, bool $autofix = false): void
31+
/**
32+
* @param list<string> $files
33+
*/
34+
protected function analyseFiles(array $files, bool $autofix = false): void
3035
{
31-
$analyserErrors = $this->gatherAnalyserErrors([$file]);
36+
sort($files);
37+
38+
$analyserErrors = $this->gatherAnalyserErrors($files);
3239

3340
if ($autofix === true) {
34-
$this->autofix($file, $analyserErrors);
35-
self::fail("File $file was autofixed. This setup should never remain in the codebase.");
41+
foreach ($files as $file) {
42+
$this->autofix($file, $analyserErrors);
43+
}
44+
45+
self::fail('Autofixed. This setup should never remain in the codebase.');
3646
}
3747

38-
$actualErrors = $this->processActualErrors($analyserErrors);
39-
$expectedErrors = $this->parseExpectedErrors($file);
48+
if ($analyserErrors === []) {
49+
$this->expectNotToPerformAssertions();
50+
}
51+
52+
$actualErrorsByFile = $this->processActualErrors($analyserErrors);
53+
54+
foreach ($actualErrorsByFile as $file => $actualErrors) {
55+
$expectedErrors = $this->parseExpectedErrors($file);
56+
57+
$extraErrors = array_diff($expectedErrors, $actualErrors);
58+
$missingErrors = array_diff($actualErrors, $expectedErrors);
4059

41-
self::assertSame(
42-
implode("\n", $expectedErrors) . "\n",
43-
implode("\n", $actualErrors) . "\n",
44-
);
60+
$extraErrorsString = $extraErrors === [] ? '' : "\n - Extra errors: " . implode("\n", $extraErrors);
61+
$missingErrorsString = $missingErrors === [] ? '' : "\n - Missing errors: " . implode("\n", $missingErrors);
62+
63+
self::assertSame(
64+
implode("\n", $expectedErrors) . "\n",
65+
implode("\n", $actualErrors) . "\n",
66+
sprintf(
67+
"Errors in file $file do not match. %s\n",
68+
$extraErrorsString . $missingErrorsString,
69+
),
70+
);
71+
}
4572
}
4673

4774
/**
4875
* @param list<Error> $actualErrors
49-
* @return list<string>
76+
* @return array<string, list<string>>
5077
*/
5178
protected function processActualErrors(array $actualErrors): array
5279
{
@@ -55,15 +82,22 @@ protected function processActualErrors(array $actualErrors): array
5582
foreach ($actualErrors as $error) {
5683
$usedLine = $error->getLine() ?? -1;
5784
$key = sprintf('%04d', $usedLine) . '-' . uniqid();
58-
$resultToAssert[$key] = $this->formatErrorForAssert($error->getMessage(), $usedLine);
85+
$resultToAssert[$error->getFile()][$key] = $this->formatErrorForAssert($error->getMessage(), $usedLine);
5986

6087
self::assertNotNull($error->getIdentifier(), "Missing error identifier for error: {$error->getMessage()}");
6188
self::assertStringStartsWith('shipmonk.', $error->getIdentifier(), "Unexpected error identifier for: {$error->getMessage()}");
6289
}
6390

64-
ksort($resultToAssert);
91+
$finalResult = [];
92+
93+
foreach ($resultToAssert as $file => $fileErrors) {
94+
ksort($fileErrors);
95+
$finalResult[$file] = array_values($fileErrors);
96+
}
6597

66-
return array_values($resultToAssert);
98+
ksort($finalResult);
99+
100+
return $finalResult;
67101
}
68102

69103
/**
@@ -117,6 +151,10 @@ private function autofix(string $file, array $analyserErrors): void
117151
throw new LogicException('Error without line number: ' . $analyserError->getMessage());
118152
}
119153

154+
if ($analyserError->getFile() !== $file) {
155+
continue;
156+
}
157+
120158
$errorsByLines[$line] = $analyserError;
121159
}
122160

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace DeadTrait11;
4+
5+
trait SomeTrait {
6+
protected function foo(): void {} // error: Unused DeadTrait11\SomeTrait::foo
7+
}
8+
9+
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace DeadTrait11;
4+
5+
class User {
6+
use SomeTrait;
7+
}

0 commit comments

Comments
 (0)