mirror of
https://github.com/rectorphp/rector.git
synced 2025-01-18 05:48:21 +01:00
[PSR4] Add NormalizeNamespaceByPSR4ComposerAutoloadRector
This commit is contained in:
parent
f9773142b2
commit
9b9ce531b2
@ -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",
|
||||||
|
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)
|
- [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`
|
||||||
|
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