Skip to content

Commit

Permalink
Autoremove dead code (#103)
Browse files Browse the repository at this point in the history
  • Loading branch information
janedbal authored Nov 1, 2024
1 parent fc01208 commit 3ea58ca
Show file tree
Hide file tree
Showing 16 changed files with 443 additions and 28 deletions.
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,30 @@ parameters:
reportTransitivelyDeadMethodAsSeparateError: true
```

## Automatic removal of dead code
- If you are sure that the reported methods are dead, you can automatically remove them by running PHPStan with `removeDeadCode` error format:

```bash
vendor/bin/phpstan analyse --error-format removeDeadCode
```

```php
// before
class UserFacade
{
public function deadMethod(): void
{
}
}
```

```php
// after
class UserFacade
{
}
```

## Comparison with tomasvotruba/unused-public
- You can see [detailed comparison PR](https://github.com/shipmonk-rnd/dead-code-detector/pull/53)
- Basically, their analysis is less precise and less flexible. Mainly:
Expand Down
3 changes: 3 additions & 0 deletions collision-detector.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"excludePaths": ["tests/Rule/data/DeadMethodRule/removing"]
}
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
],
"require": {
"php": "^7.4 || ^8.0",
"phpstan/phpstan": "^1.11.0"
"phpstan/phpstan": "^1.11.5"
},
"require-dev": {
"doctrine/orm": "^2.19 || ^3.0",
Expand All @@ -22,6 +22,7 @@
"nette/application": "^3.1",
"nette/component-model": "^3.0",
"nette/utils": "^3.0 || ^4.0",
"nikic/php-parser": "^4.19",
"phpstan/phpstan-phpunit": "^1.1.1",
"phpstan/phpstan-strict-rules": "^1.2.3",
"phpstan/phpstan-symfony": "^1.4",
Expand Down
36 changes: 17 additions & 19 deletions composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions rules.neon
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
services:
errorFormatter.removeDeadCode:
class: ShipMonk\PHPStan\DeadCode\Formatter\RemoveDeadCodeFormatter

-
class: ShipMonk\PHPStan\DeadCode\Hierarchy\ClassHierarchy
-
class: ShipMonk\PHPStan\DeadCode\Transformer\FileSystem

-
class: ShipMonk\PHPStan\DeadCode\Provider\VendorEntrypointProvider
Expand Down
71 changes: 71 additions & 0 deletions src/Formatter/RemoveDeadCodeFormatter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<?php declare(strict_types = 1);

namespace ShipMonk\PHPStan\DeadCode\Formatter;

use PHPStan\Command\AnalysisResult;
use PHPStan\Command\ErrorFormatter\ErrorFormatter;
use PHPStan\Command\Output;
use ShipMonk\PHPStan\DeadCode\Rule\DeadMethodRule;
use ShipMonk\PHPStan\DeadCode\Transformer\FileSystem;
use ShipMonk\PHPStan\DeadCode\Transformer\RemoveMethodCodeTransformer;
use function count;

class RemoveDeadCodeFormatter implements ErrorFormatter
{

private FileSystem $fileSystem;

public function __construct(FileSystem $fileSystem)
{
$this->fileSystem = $fileSystem;
}

public function formatErrors(
AnalysisResult $analysisResult,
Output $output
): int
{
$internalErrors = $analysisResult->getInternalErrorObjects();

foreach ($internalErrors as $internalError) {
$output->writeLineFormatted('<error>' . $internalError->getMessage() . '</error>');
}

if (count($internalErrors) > 0) {
$output->writeLineFormatted('');
$output->writeLineFormatted('Fix listed internal errors first.');
return 1;
}

$deadMethodKeysByFiles = [];

foreach ($analysisResult->getFileSpecificErrors() as $fileSpecificError) {
if ($fileSpecificError->getIdentifier() !== DeadMethodRule::ERROR_IDENTIFIER) {
continue;
}

/** @var array<string, array{file: string, line: string}> $metadata */
$metadata = $fileSpecificError->getMetadata();

foreach ($metadata as $key => $data) {
$deadMethodKeysByFiles[$data['file']][] = $key;
}
}

$count = 0;

foreach ($deadMethodKeysByFiles as $file => $blackMethodKeys) {
$count += count($blackMethodKeys);

$transformer = new RemoveMethodCodeTransformer($blackMethodKeys);
$oldCode = $this->fileSystem->read($file);
$newCode = $transformer->transformCode($oldCode);
$this->fileSystem->write($file, $newCode);
}

$output->writeLineFormatted('Removed ' . $count . ' dead methods in ' . count($deadMethodKeysByFiles) . ' files.');

return 0;
}

}
11 changes: 9 additions & 2 deletions src/Rule/DeadMethodRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
class DeadMethodRule implements Rule
{

public const ERROR_IDENTIFIER = 'shipmonk.deadMethod';
private const UNSUPPORTED_MAGIC_METHODS = [
'__invoke' => null,
'__toString' => null,
Expand Down Expand Up @@ -83,7 +84,7 @@ class DeadMethodRule implements Rule

public function __construct(
ClassHierarchy $classHierarchy,
bool $reportTransitivelyDeadMethodAsSeparateError = false
bool $reportTransitivelyDeadMethodAsSeparateError
)
{
$this->classHierarchy = $classHierarchy;
Expand Down Expand Up @@ -439,16 +440,22 @@ private function buildError(
$builder = RuleErrorBuilder::message('Unused ' . $deadMethodKey)
->file($file)
->line($line)
->identifier('shipmonk.deadMethod');
->identifier(self::ERROR_IDENTIFIER);

$metadata = [];
$metadata[$deadMethodKey] = [
'file' => $file,
'line' => $line,
'transitive' => false,
];

foreach ($transitiveDeadMethodKeys as $transitiveDeadMethodKey => [$transitiveDeadMethodFile, $transitiveDeadMethodLine]) {
$builder->addTip("Thus $transitiveDeadMethodKey is transitively also unused");

$metadata[$transitiveDeadMethodKey] = [
'file' => $transitiveDeadMethodFile,
'line' => $transitiveDeadMethodLine,
'transitive' => true,
];
}

Expand Down
32 changes: 32 additions & 0 deletions src/Transformer/FileSystem.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php declare(strict_types = 1);

namespace ShipMonk\PHPStan\DeadCode\Transformer;

use LogicException;
use function file_get_contents;
use function file_put_contents;

class FileSystem
{

public function read(string $path): string
{
$contents = file_get_contents($path);

if ($contents === false) {
throw new LogicException('Could not read file: ' . $path);
}

return $contents;
}

public function write(string $path, string $content): void
{
$success = file_put_contents($path, $content);

if ($success === false) {
throw new LogicException('Could not write to file: ' . $path);
}
}

}
64 changes: 64 additions & 0 deletions src/Transformer/RemoveMethodCodeTransformer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<?php declare(strict_types = 1);

namespace ShipMonk\PHPStan\DeadCode\Transformer;

use LogicException;
use PhpParser\Lexer;
use PhpParser\NodeTraverser as PhpTraverser;
use PhpParser\NodeVisitor\CloningVisitor;
use PhpParser\Parser;
use PhpParser\Parser\Php7;
use PhpParser\PrettyPrinter\Standard as PhpPrinter;

class RemoveMethodCodeTransformer
{

private Lexer $phpLexer;

private Parser $phpParser;

private PhpTraverser $cloningTraverser;

private PhpTraverser $removingTraverser;

private PhpPrinter $phpPrinter;

/**
* @param list<string> $deadMethodKeys
*/
public function __construct(array $deadMethodKeys)
{
$this->phpLexer = new Lexer([
'usedAttributes' => [
'comments',
'startLine',
'endLine',
'startTokenPos',
'endTokenPos',
],
]);
$this->phpParser = new Php7($this->phpLexer);

$this->cloningTraverser = new PhpTraverser();
$this->cloningTraverser->addVisitor(new CloningVisitor());

$this->removingTraverser = new PhpTraverser();
$this->removingTraverser->addVisitor(new RemoveMethodVisitor($deadMethodKeys));

$this->phpPrinter = new PhpPrinter();
}

public function transformCode(string $oldCode): string
{
$oldAst = $this->phpParser->parse($oldCode);

if ($oldAst === null) {
throw new LogicException('Failed to parse the code');
}

$oldTokens = $this->phpLexer->getTokens();
$newAst = $this->removingTraverser->traverse($this->cloningTraverser->traverse($oldAst));
return $this->phpPrinter->printFormatPreserving($newAst, $oldAst, $oldTokens);
}

}
Loading

0 comments on commit 3ea58ca

Please sign in to comment.