[PSR4] Add NormalizeNamespaceByPSR4ComposerAutoloadRector

This commit is contained in:
Tomas Votruba 2019-08-18 12:56:56 +02:00
parent f9773142b2
commit 9b9ce531b2
8 changed files with 377 additions and 1 deletions

View File

@ -61,6 +61,7 @@
"Rector\\Sensio\\": "packages/Sensio/src", "Rector\\Sensio\\": "packages/Sensio/src",
"Rector\\Sylius\\": "packages/Sylius/src", "Rector\\Sylius\\": "packages/Sylius/src",
"Rector\\PHPStan\\": "packages/PHPStan/src", "Rector\\PHPStan\\": "packages/PHPStan/src",
"Rector\\PSR4\\": "packages/PSR4/src",
"Rector\\PHPUnit\\": "packages/PHPUnit/src", "Rector\\PHPUnit\\": "packages/PHPUnit/src",
"Rector\\Twig\\": "packages/Twig/src", "Rector\\Twig\\": "packages/Twig/src",
"Rector\\TypeDeclaration\\": "packages/TypeDeclaration/src", "Rector\\TypeDeclaration\\": "packages/TypeDeclaration/src",

View File

@ -0,0 +1,2 @@
services:
Rector\PSR4\Rector\Namespace_\NormalizeNamespaceByPSR4ComposerAutoloadRector: ~

View File

@ -1,4 +1,4 @@
# All 333 Rectors Overview # All 334 Rectors Overview
- [Projects](#projects) - [Projects](#projects)
- [General](#general) - [General](#general)
@ -24,6 +24,7 @@
- [PHPStan](#phpstan) - [PHPStan](#phpstan)
- [PHPUnit](#phpunit) - [PHPUnit](#phpunit)
- [PHPUnitSymfony](#phpunitsymfony) - [PHPUnitSymfony](#phpunitsymfony)
- [PSR4](#psr4)
- [Php](#php) - [Php](#php)
- [PhpParser](#phpparser) - [PhpParser](#phpparser)
- [PhpSpecToPHPUnit](#phpspectophpunit) - [PhpSpecToPHPUnit](#phpspectophpunit)
@ -3236,6 +3237,16 @@ Add response content to response code assert, so it is easier to debug
<br> <br>
## PSR4
### `NormalizeNamespaceByPSR4ComposerAutoloadRector`
- class: `Rector\PSR4\Rector\Namespace_\NormalizeNamespaceByPSR4ComposerAutoloadRector`
Changes namespace and class names to match PSR-4 in composer.json autoload section
<br>
## Php ## Php
### `AddDefaultValueForUndefinedVariableRector` ### `AddDefaultValueForUndefinedVariableRector`

View File

@ -0,0 +1,8 @@
services:
_defaults:
public: true
autowire: true
Rector\PSR4\:
resource: '../src'
exclude: '../src/{Rector/**/*Rector.php}'

View File

@ -0,0 +1,24 @@
<?php declare(strict_types=1);
namespace Rector\PSR4\Collector;
final class RenamedClassesCollector
{
/**
* @var string[]
*/
private $renamedClasses = [];
public function addClassRename(string $oldClass, string $newClass): void
{
$this->renamedClasses[$oldClass] = $newClass;
}
/**
* @return string[]
*/
public function getRenamedClasses(): array
{
return $this->renamedClasses;
}
}

View File

@ -0,0 +1,61 @@
<?php declare(strict_types=1);
namespace Rector\PSR4\Composer;
use Nette\Utils\FileSystem;
use Nette\Utils\Json;
final class PSR4AutoloadPathsProvider
{
/**
* @var string[]
*/
private $cachedComposerJsonPSR4AutoloadPaths = [];
/**
* @return string[]
*/
public function provide(): array
{
if ($this->cachedComposerJsonPSR4AutoloadPaths !== []) {
return $this->cachedComposerJsonPSR4AutoloadPaths;
}
$composerJson = $this->readFileToJsonArray($this->getComposerJsonPath());
$psr4Autoloads = array_merge(
$composerJson['autoload']['psr-4'] ?? [],
$composerJson['autoload-dev']['psr-4'] ?? []
);
$this->cachedComposerJsonPSR4AutoloadPaths = $this->removeEmptyNamespaces($psr4Autoloads);
return $this->cachedComposerJsonPSR4AutoloadPaths;
}
private function getComposerJsonPath(): string
{
// assume the project has "composer.json" in root directory
return getcwd() . '/composer.json';
}
/**
* @return mixed[]
*/
private function readFileToJsonArray(string $composerJson): array
{
$composerJsonContent = FileSystem::read($composerJson);
return Json::decode($composerJsonContent, Json::FORCE_ARRAY);
}
/**
* @param string[] $psr4Autoloads
* @return string[]
*/
private function removeEmptyNamespaces(array $psr4Autoloads): array
{
return array_filter($psr4Autoloads, function ($value): bool {
return $value !== '';
}, ARRAY_FILTER_USE_KEY);
}
}

View File

@ -0,0 +1,116 @@
<?php declare(strict_types=1);
namespace Rector\PSR4\Extension;
use Nette\Utils\FileSystem;
use Nette\Utils\Strings;
use PhpParser\Node\Arg;
use PhpParser\Node\Expr\FuncCall;
use PhpParser\Node\Name;
use PhpParser\Node\Scalar\String_;
use PhpParser\Node\Stmt\Expression;
use Rector\Contract\Extension\ReportingExtensionInterface;
use Rector\PhpParser\Printer\BetterStandardPrinter;
use Rector\PSR4\Collector\RenamedClassesCollector;
use Rector\Rector\Class_\RenameClassRector;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Yaml\Yaml;
final class RenamedClassesReportExtension implements ReportingExtensionInterface
{
/**
* @var RenamedClassesCollector
*/
private $renamedClassesCollector;
/**
* @var SymfonyStyle
*/
private $symfonyStyle;
/**
* @var BetterStandardPrinter
*/
private $betterStandardPrinter;
public function __construct(
RenamedClassesCollector $renamedClassesCollector,
SymfonyStyle $symfonyStyle,
BetterStandardPrinter $betterStandardPrinter
) {
$this->renamedClassesCollector = $renamedClassesCollector;
$this->symfonyStyle = $symfonyStyle;
$this->betterStandardPrinter = $betterStandardPrinter;
}
public function run(): void
{
if ($this->renamedClassesCollector->getRenamedClasses() === []) {
return;
}
$data = [
# rector.yaml
'services' => [
RenameClassRector::class => [
'$oldToNewClasses' => $this->renamedClassesCollector->getRenamedClasses(),
],
],
];
// 1.
$yaml = Yaml::dump($data, 5);
FileSystem::write(getcwd() . '/rename-classes-rector.yaml', $yaml);
$nodes = [];
$renamedClasses = $this->sortInterfaceFirst($this->renamedClassesCollector->getRenamedClasses());
foreach ($renamedClasses as $oldClass => $newClass) {
$classAlias = new FuncCall(new Name('class_alias'));
$classAlias->args[] = new Arg(new String_($newClass));
$classAlias->args[] = new Arg(new String_($oldClass));
$nodes[] = new Expression($classAlias);
}
// 2.
$aliasesContent = '<?php' . PHP_EOL . PHP_EOL . $this->betterStandardPrinter->print($nodes);
FileSystem::write(getcwd() . '/rename-classes-aliases.php', $aliasesContent);
// @todo shell exec!?
$this->symfonyStyle->warning(
'Run: "vendor/bin/rector process tests --config rename-classes-rector.yaml --autoload-file rename-classes-aliases.php" to finish the process'
);
}
/**
* Interfaces needs to be aliased first, as aliased class needs to have an aliased interface that implements.
* PHP will crash with not very helpful "missing interface" error otherwise.
*
* Also abstract classes and traits, as other code depens on them.
*
* @param string[] $types
* @return string[]
*/
private function sortInterfaceFirst(array $types): array
{
$interfaces = [];
$abstractClasses = [];
$traits = [];
$classes = [];
foreach ($types as $oldType => $newType) {
if (Strings::endsWith($oldType, 'Interface')) {
$interfaces[$oldType] = $newType;
} elseif (Strings::contains($oldType, 'Abstract')) {
$abstractClasses[$oldType] = $newType;
} elseif (Strings::contains($oldType, 'Trait')) {
$traits[$oldType] = $newType;
} else {
$classes[$oldType] = $newType;
}
}
return array_merge($interfaces, $traits, $abstractClasses, $classes);
}
}

View File

@ -0,0 +1,153 @@
<?php declare(strict_types=1);
namespace Rector\PSR4\Rector\Namespace_;
use Nette\Utils\Strings;
use PhpParser\Node;
use PhpParser\Node\Name;
use PhpParser\Node\Stmt\Namespace_;
use PhpParser\Node\Stmt\Use_;
use PhpParser\Node\Stmt\UseUse;
use Rector\NodeTypeResolver\Node\AttributeKey;
use Rector\PSR4\Collector\RenamedClassesCollector;
use Rector\PSR4\Composer\PSR4AutoloadPathsProvider;
use Rector\Rector\AbstractRector;
use Rector\RectorDefinition\RectorDefinition;
use Symplify\PackageBuilder\FileSystem\SmartFileInfo;
/**
* @sponsor Thanks https://spaceflow.io/ for sponsoring this rule - visit them on https://github.com/SpaceFlow-app
*/
final class NormalizeNamespaceByPSR4ComposerAutoloadRector extends AbstractRector
{
/**
* @var PSR4AutoloadPathsProvider
*/
private $psR4AutoloadPathsProvider;
/**
* @var RenamedClassesCollector
*/
private $renamedClassesCollector;
public function __construct(
PSR4AutoloadPathsProvider $psR4AutoloadPathsProvider,
RenamedClassesCollector $renamedClassesCollector
) {
$this->psR4AutoloadPathsProvider = $psR4AutoloadPathsProvider;
$this->renamedClassesCollector = $renamedClassesCollector;
}
public function getDefinition(): RectorDefinition
{
return new RectorDefinition(
'Changes namespace and class names to match PSR-4 in composer.json autoload section'
);
}
/**
* @return string[]
*/
public function getNodeTypes(): array
{
return [Namespace_::class];
}
/**
* @param Namespace_ $node
*/
public function refactor(Node $node): ?Node
{
$expectedNamespace = $this->getExpectedNamespace($node);
if ($expectedNamespace === null) {
return null;
}
$currentNamespace = $this->getName($node);
// namespace is correct → skip
if ($currentNamespace === $expectedNamespace) {
return null;
}
// change it
$node->name = new Name($expectedNamespace);
// add use import for classes from the same namespace
$newUseImports = [];
$this->traverseNodesWithCallable($node, function (Node $node) use ($currentNamespace, &$newUseImports) {
if (! $node instanceof Name) {
return null;
}
/** @var Name|null $originalName */
$originalName = $node->getAttribute('originalName');
if ($originalName instanceof Name) {
if ($currentNamespace . '\\' . $originalName->toString() === $this->getName($node)) {
// this needs to be imported
$newUseImports[] = $this->getName($node);
}
}
});
$newUseImports = array_unique($newUseImports);
if ($newUseImports) {
$useImports = $this->createUses($newUseImports);
$node->stmts = array_merge($useImports, $node->stmts);
}
/** @var SmartFileInfo $smartFileInfo */
$smartFileInfo = $node->getAttribute(AttributeKey::FILE_INFO);
$oldClassName = $currentNamespace . '\\' . $smartFileInfo->getBasenameWithoutSuffix();
$newClassName = $expectedNamespace . '\\' . $smartFileInfo->getBasenameWithoutSuffix();
$this->renamedClassesCollector->addClassRename($oldClassName, $newClassName);
// collect changed class
return $node;
}
private function getExpectedNamespace(Node $node): ?string
{
/** @var SmartFileInfo $smartFileInfo */
$smartFileInfo = $node->getAttribute(AttributeKey::FILE_INFO);
$psr4Autoloads = $this->psR4AutoloadPathsProvider->provide();
foreach ($psr4Autoloads as $namespace => $path) {
if (Strings::startsWith($smartFileInfo->getRelativeDirectoryPath(), $path)) {
return $namespace . $this->resolveExtraNamespace($smartFileInfo, $path);
}
}
return null;
}
/**
* Get the extra path that is not included in root PSR-4 namespace
*/
private function resolveExtraNamespace(SmartFileInfo $smartFileInfo, string $path): string
{
$extraNamespace = Strings::substring($smartFileInfo->getRelativeDirectoryPath(), Strings::length($path) + 1);
$extraNamespace = Strings::replace($extraNamespace, '#/#', '\\');
return trim($extraNamespace);
}
/**
* @param string[] $newUseImports
* @return Use_[]
*/
private function createUses(array $newUseImports): array
{
$uses = [];
foreach ($newUseImports as $newUseImport) {
$useUse = new UseUse(new Name($newUseImport));
$uses[] = new Use_([$useUse]);
}
return $uses;
}
}