mirror of
https://github.com/rectorphp/rector.git
synced 2025-01-17 21:38:22 +01:00
[PSR4] Add NormalizeNamespaceByPSR4ComposerAutoloadRector
This commit is contained in:
parent
f9773142b2
commit
9b9ce531b2
@ -61,6 +61,7 @@
|
||||
"Rector\\Sensio\\": "packages/Sensio/src",
|
||||
"Rector\\Sylius\\": "packages/Sylius/src",
|
||||
"Rector\\PHPStan\\": "packages/PHPStan/src",
|
||||
"Rector\\PSR4\\": "packages/PSR4/src",
|
||||
"Rector\\PHPUnit\\": "packages/PHPUnit/src",
|
||||
"Rector\\Twig\\": "packages/Twig/src",
|
||||
"Rector\\TypeDeclaration\\": "packages/TypeDeclaration/src",
|
||||
|
2
config/set/psr-4/psr-4.yaml
Normal file
2
config/set/psr-4/psr-4.yaml
Normal file
@ -0,0 +1,2 @@
|
||||
services:
|
||||
Rector\PSR4\Rector\Namespace_\NormalizeNamespaceByPSR4ComposerAutoloadRector: ~
|
@ -1,4 +1,4 @@
|
||||
# All 333 Rectors Overview
|
||||
# All 334 Rectors Overview
|
||||
|
||||
- [Projects](#projects)
|
||||
- [General](#general)
|
||||
@ -24,6 +24,7 @@
|
||||
- [PHPStan](#phpstan)
|
||||
- [PHPUnit](#phpunit)
|
||||
- [PHPUnitSymfony](#phpunitsymfony)
|
||||
- [PSR4](#psr4)
|
||||
- [Php](#php)
|
||||
- [PhpParser](#phpparser)
|
||||
- [PhpSpecToPHPUnit](#phpspectophpunit)
|
||||
@ -3236,6 +3237,16 @@ Add response content to response code assert, so it is easier to debug
|
||||
|
||||
<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
|
||||
|
||||
### `AddDefaultValueForUndefinedVariableRector`
|
||||
|
8
packages/PSR4/config/config.yaml
Normal file
8
packages/PSR4/config/config.yaml
Normal file
@ -0,0 +1,8 @@
|
||||
services:
|
||||
_defaults:
|
||||
public: true
|
||||
autowire: true
|
||||
|
||||
Rector\PSR4\:
|
||||
resource: '../src'
|
||||
exclude: '../src/{Rector/**/*Rector.php}'
|
24
packages/PSR4/src/Collector/RenamedClassesCollector.php
Normal file
24
packages/PSR4/src/Collector/RenamedClassesCollector.php
Normal 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;
|
||||
}
|
||||
}
|
61
packages/PSR4/src/Composer/PSR4AutoloadPathsProvider.php
Normal file
61
packages/PSR4/src/Composer/PSR4AutoloadPathsProvider.php
Normal 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);
|
||||
}
|
||||
}
|
116
packages/PSR4/src/Extension/RenamedClassesReportExtension.php
Normal file
116
packages/PSR4/src/Extension/RenamedClassesReportExtension.php
Normal 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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user