This commit is contained in:
TomasVotruba 2017-07-15 19:01:21 +02:00
commit 764ce4bb08
23 changed files with 708 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/vendor
composer.lock

14
bin/bootstrap.php Normal file
View File

@ -0,0 +1,14 @@
<?php declare(strict_types=1);
/**
* This allows to load "vendor/autoload.php" both from
* "composer create-project ..." and "composer require" installation.
*/
$possibleAutoloadPaths = [__DIR__ . '/../vendor/autoload.php', __DIR__ . '/../../../vendor/autoload.php'];
foreach ($possibleAutoloadPaths as $possibleAutoloadPath) {
if (is_file($possibleAutoloadPath)) {
require_once $possibleAutoloadPath;
break;
}
}

5
bin/rector Executable file
View File

@ -0,0 +1,5 @@
#!/usr/bin/env php
<?php
require_once __DIR__ . '/rector.php';

18
bin/rector.php Normal file
View File

@ -0,0 +1,18 @@
<?php declare(strict_types=1);
use Rector\DependencyInjection\ContainerFactory;
use Symfony\Component\Console\Application;
// Performance boost
gc_disable();
// Require Composer autoload.php
require_once __DIR__ . '/bootstrap.php';
// Build DI container
$container = (new ContainerFactory())->create();
// Run Console Application
/** @var Application $application */
$application = $container->get(Application::class);
$application->run();

34
composer.json Normal file
View File

@ -0,0 +1,34 @@
{
"name": "rector/rector",
"description": "Tool that reconstructs your legacy code to modern codebase.",
"license": "MIT",
"authors": [
{ "name": "Tomas Votruba", "email": "tomas.vot@gmail.com", "homepage": "https://tomasvotruba.com" },
{ "name": "Rector Contributors", "homepage": "https://github.com/TomasVotruba/Rector/graphs/contributors" }
],
"require": {
"php": "^7.1",
"symfony/console": "^3.3",
"symfony/dependency-injection": "^3.3",
"nikic/php-parser": "^3.0",
"ocramius/code-generator-utils": "^0.4.1"
},
"require-dev": {
"phpunit/phpunit": "^6.2",
"tracy/tracy": "^2.4",
"symplify/easy-coding-standard": "^2.1"
},
"autoload": {
"psr-4": {
"Rector\\": "src"
}
},
"autoload-dev": {
"psr-4": {
"Rector\\Tests\\": "tests"
}
},
"scripts": {
"phpstan": "phpstan analyse src tests --level 7"
}
}

16
phpunit.xml Normal file
View File

@ -0,0 +1,16 @@
<phpunit
bootstrap="vendor/autoload.php"
colors="true"
verbose="true"
>
<!-- tests directories to run -->
<testsuite>
<directory>tests</directory>
</testsuite>
<!-- source to check coverage for -->
<filter>
<whitelist processUncoveredFilesFromWhitelist="true">
<directory suffix=".php">src</directory>
</whitelist>
</filter>
</phpunit>

View File

@ -0,0 +1,73 @@
<?php declare(strict_types=1);
namespace Rector\Application;
use PhpParser\Parser;
use Rector\Dispatcher\NodeDispatcher;
use Rector\Printer\CodeStyledPrinter;
use SplFileInfo;
final class FileProcessor
{
/**
* @var Parser
*/
private $parser;
/**
* @var NodeDispatcher
*/
private $nodeDispatcher;
/**
* @var CodeStyledPrinter
*/
private $codeStyledPrinter;
public function __construct(Parser $parser, CodeStyledPrinter $codeStyledPrinter, NodeDispatcher $nodeDispatcher)
{
$this->parser = $parser;
$this->nodeDispatcher = $nodeDispatcher;
$this->codeStyledPrinter = $codeStyledPrinter;
}
/**
* @var SplFileInfo[]
*/
public function processFiles(array $files): void
{
foreach ($files as $file) {
$this->processFile($file);
}
}
public function processFile(SplFileInfo $file): void
{
$fileContent = file_get_contents($file->getRealPath());
$nodes = $this->parser->parse($fileContent);
if ($nodes === null) {
return;
}
$originalNodes = $this->cloneArrayOfObjects($nodes);
foreach ($nodes as $node) {
$this->nodeDispatcher->dispatch($node);
}
$this->codeStyledPrinter->printToFile($file, $originalNodes, $nodes);
}
/**
* @param object[] $data
* @return object[]
*/
private function cloneArrayOfObjects(array $data): array
{
foreach ($data as $key => $value) {
$data[$key] = clone $value;
}
return $data;
}
}

View File

@ -0,0 +1,70 @@
<?php declare(strict_types=1);
namespace Rector\Console\Command;
use Nette\Utils\Finder;
use Rector\Application\FileProcessor;
use SplFileInfo;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
final class ReconstructCommand extends Command
{
/**
* @var string
*/
private const NAME = 'reconstruct';
/**
* @var FileProcessor
*/
private $fileProcessor;
public function __construct(FileProcessor $fileProcessor)
{
$this->fileProcessor = $fileProcessor;
parent::__construct();
}
/**
* @var string
*/
private const ARGUMENT_SOURCE_NAME = 'source';
protected function configure(): void
{
$this->setName(self::NAME);
$this->setDescription('Reconstruct set of your code.');
// @todo: use modular configure from ApiGen
$this->addArgument(
self::ARGUMENT_SOURCE_NAME,
InputArgument::REQUIRED | InputArgument::IS_ARRAY,
'The path(s) to be checked.'
);
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$source = $input->getArgument(self::ARGUMENT_SOURCE_NAME);
$files = $this->findPhpFilesInDirectories($source);
$this->fileProcessor->processFiles($files);
return 0;
}
/**
* @param string[] $directories
* @return SplFileInfo[] array
*/
private function findPhpFilesInDirectories(array $directories): array
{
$finder = Finder::find('*.php')
->in($directories);
return iterator_to_array($finder->getIterator());
}
}

View File

@ -0,0 +1,12 @@
<?php declare(strict_types=1);
namespace Rector\Contract\Dispatcher;
use PhpParser\Node;
interface ReconstructorInterface
{
public function isCandidate(Node $node): bool;
public function reconstruct(Node $classNode): void;
}

View File

@ -0,0 +1,45 @@
<?php declare(strict_types=1);
namespace Rector\DependencyInjection;
use Rector\DependencyInjection\CompilerPass\CollectorCompilerPass;
use Symfony\Component\Config\Loader\LoaderInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Bundle\BundleInterface;
use Symfony\Component\HttpKernel\Kernel;
final class AppKernel extends Kernel
{
public function __construct()
{
parent::__construct('dev', true);
}
public function registerContainerConfiguration(LoaderInterface $loader): void
{
$loader->load(__DIR__ . '/../config/services.yml');
}
public function getCacheDir(): string
{
return sys_get_temp_dir() . '/_rector_cache';
}
public function getLogDir(): string
{
return sys_get_temp_dir() . '/_rector_log';
}
/**
* @return BundleInterface[]
*/
public function registerBundles(): array
{
return [];
}
protected function build(ContainerBuilder $containerBuilder): void
{
$containerBuilder->addCompilerPass(new CollectorCompilerPass);
}
}

View File

@ -0,0 +1,40 @@
<?php declare(strict_types=1);
namespace Rector\DependencyInjection\CompilerPass;
use Rector\Contract\Dispatcher\ReconstructorInterface;
use Rector\Dispatcher\NodeDispatcher;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symplify\PackageBuilder\Adapter\Symfony\DependencyInjection\DefinitionCollector;
final class CollectorCompilerPass implements CompilerPassInterface
{
public function process(ContainerBuilder $containerBuilder): void
{
$this->collectCommandsToConsoleApplication($containerBuilder);
$this->collectReconstructorsToNodeDispatcher($containerBuilder);
}
private function collectCommandsToConsoleApplication(ContainerBuilder $containerBuilder): void
{
DefinitionCollector::loadCollectorWithType(
$containerBuilder,
Application::class,
Command::class,
'add'
);
}
private function collectReconstructorsToNodeDispatcher(ContainerBuilder $containerBuilder): void
{
DefinitionCollector::loadCollectorWithType(
$containerBuilder,
NodeDispatcher::class,
ReconstructorInterface::class,
'addReconstructor'
);
}
}

View File

@ -0,0 +1,16 @@
<?php declare(strict_types=1);
namespace Rector\DependencyInjection;
use Psr\Container\ContainerInterface;
final class ContainerFactory
{
public function create(): ContainerInterface
{
$appKernel = new AppKernel;
$appKernel->boot();
return $appKernel->getContainer();
}
}

View File

@ -0,0 +1,29 @@
<?php declare(strict_types=1);
namespace Rector\Dispatcher;
use PhpParser\Node;
use Rector\Contract\Dispatcher\ReconstructorInterface;
final class NodeDispatcher
{
/**
* @var ReconstructorInterface[]
*/
private $reconstructors;
public function addReconstructor(ReconstructorInterface $reconstructor): void
{
$this->reconstructors[] = $reconstructor;
}
public function dispatch(Node $node): void
{
// todo: build hash map
foreach ($this->reconstructors as $reconstructor) {
if ($reconstructor->isCandidate($node)) {
$reconstructor->reconstruct($node);
}
}
}
}

View File

@ -0,0 +1,14 @@
<?php declare(strict_types=1);
namespace Rector\Parser;
use PhpParser\Parser;
use PhpParser\ParserFactory as NikicParserFactory;
final class ParserFactory
{
public function create(): Parser
{
return (new NikicParserFactory)->create(NikicParserFactory::PREFER_PHP7);
}
}

View File

@ -0,0 +1,34 @@
<?php declare(strict_types=1);
namespace Rector\Printer;
use PhpParser\PrettyPrinter\Standard;
use SplFileInfo;
final class CodeStyledPrinter
{
/**
* @var Standard
*/
private $prettyPrinter;
public function __construct(Standard $prettyPrinter)
{
$this->prettyPrinter = $prettyPrinter;
}
public function printToFile(SplFileInfo $file, array $originalNodes, array $newNodes): void
{
if ($originalNodes === $newNodes) {
return;
}
file_put_contents($file->getRealPath(), $this->printToString($newNodes));
// @todo: run ecs with minimal set to code look nice
}
public function printToString(array $newNodes): string
{
return '<?php ' . $this->prettyPrinter->prettyPrint($newNodes);
}
}

View File

@ -0,0 +1,101 @@
<?php declare(strict_types=1);
namespace Rector\Reconstructor\DependencyInjection;
use PhpCsFixer\DocBlock\DocBlock;
use PhpParser\Builder\Method;
use PhpParser\BuilderFactory;
use PhpParser\Comment\Doc;
use PhpParser\Node;
use PhpParser\Node\Stmt\Class_;
use PhpParser\Node\Stmt\ClassMethod;
use PhpParser\Node\Stmt\Property;
use PhpParser\Parser;
use Rector\Contract\Dispatcher\ReconstructorInterface;
final class InjectAnnotationToConstructorReconstructor implements ReconstructorInterface
{
/**
* @var BuilderFactory
*/
private $builderFactory;
/**
* @var Parser
*/
private $parser;
public function __construct(BuilderFactory $builderFactory, Parser $parser)
{
$this->builderFactory = $builderFactory;
$this->parser = $parser;
}
public function isCandidate(Node $node): bool
{
return $node instanceof Class_;
}
/**
* @param Class_|Node $classNode
*/
public function reconstruct(Node $classNode): void
{
foreach ($classNode->stmts as $classElementStatement) {
if (! $classElementStatement instanceof Property) {
continue;
}
$propertyNode = $classElementStatement;
$propertyDocBlock = $this->createDocBlock($propertyNode);
$injectAnnotations = $propertyDocBlock->getAnnotationsOfType('inject');
if (! $injectAnnotations) {
continue;
}
$propertyType = $propertyDocBlock->getAnnotationsOfType('var')[0]->getTypes()[0];
// 1. remove @inject annotation
foreach ($injectAnnotations as $injectAnnotation) {
$injectAnnotation->remove();
}
$propertyNode->setDocComment(new Doc($propertyDocBlock->getContent()));
// 2. make public property private
$propertyNode->flags = Class_::MODIFIER_PRIVATE;
$propertyName = $propertyNode->props[0]->name;
// build assignment for constructor method body
/** @var Node[] $assign */
$assign = $this->parser->parse(sprintf(
'<?php $this->%s = $%s;',
$propertyName,
$propertyName
));
$constructorMethod = $classNode->getMethod('__construct') ?: null;
/** @var ClassMethod $constructorMethod */
if ($constructorMethod) {
$constructorMethod->params[] = $this->builderFactory->param($propertyName)
->setTypeHint($propertyType)->getNode();
$constructorMethod->stmts[] = $assign[0];
} else {
/** @var Method $constructorMethod */
$constructorMethod = $this->builderFactory->method('__construct')
->makePublic()
->addParam($this->builderFactory->param($propertyName)
->setTypeHint($propertyType)
)
->addStmts($assign);
$classNode->stmts[] = $constructorMethod->getNode();
}
}
}
private function createDocBlock(Property $propertyNode): DocBlock
{
return new DocBlock($propertyNode->getDocComment());
}
}

View File

@ -0,0 +1,44 @@
<?php declare(strict_types=1);
namespace Rector\Testing\Application;
use PhpParser\Node;
use PhpParser\Parser;
use Rector\Contract\Dispatcher\ReconstructorInterface;
use Rector\Printer\CodeStyledPrinter;
use SplFileInfo;
final class FileReconstructor
{
/**
* @var Parser
*/
private $parser;
/**
* @var CodeStyledPrinter
*/
private $codeStyledPrinter;
public function __construct(Parser $parser, CodeStyledPrinter $codeStyledPrinter)
{
$this->parser = $parser;
$this->codeStyledPrinter = $codeStyledPrinter;
}
public function processFileWithReconstructor(SplFileInfo $file, ReconstructorInterface $reconstructor): string
{
$fileContent = file_get_contents($file->getRealPath());
/** @var Node[] $nodes */
$nodes = $this->parser->parse($fileContent);
foreach ($nodes as $node) {
if ($reconstructor->isCandidate($node)) {
$reconstructor->reconstruct($node);
}
}
return $this->codeStyledPrinter->printToString($nodes);
}
}

20
src/config/services.yml Normal file
View File

@ -0,0 +1,20 @@
services:
_defaults:
autowire: true
autoconfigure: true
# PSR-4 autodiscovery
Rector\:
resource: '../../src'
# 3rd party services
Symfony\Component\Console\Application:
arguments:
$name: "Rector"
PhpParser\Parser:
factory: ['@Rector\Parser\ParserFactory', 'create']
PhpParser\BuilderFactory: ~
PhpParser\PrettyPrinter\Standard: ~
# $parser = (new ParserFactory)->create(ParserFactory::PREFER_PHP7);
# $statements = $parser->parse(file_get_contents(__DIR__ . '/../source/ClassWithInjects

View File

@ -0,0 +1,33 @@
<?php declare(strict_types=1);
namespace Rector\Tests;
use PHPUnit\Framework\TestCase;
use Psr\Container\ContainerInterface;
use Rector\DependencyInjection\ContainerFactory;
abstract class AbstractContainerAwareTestCase extends TestCase
{
/**
* @var ContainerInterface
*/
protected $container;
/**
* @var ContainerInterface
*/
private static $cachedContainer;
/**
* @param mixed[] $data
*/
public function __construct(?string $name = null, array $data = [], string $dataName = '')
{
if (! self::$cachedContainer) {
self::$cachedContainer = (new ContainerFactory)->create();
}
$this->container = self::$cachedContainer;
parent::__construct($name, $data, $dataName);
}
}

View File

@ -0,0 +1,36 @@
<?php declare(strict_types=1);
namespace Rector\Tests;
use Rector\Contract\Dispatcher\ReconstructorInterface;
use Rector\Testing\Application\FileReconstructor;
use SplFileInfo;
abstract class AbstractReconstructorTestCase extends AbstractContainerAwareTestCase
{
/**
* @var FileReconstructor
*/
private $fileReconstructor;
protected function setUp(): void
{
$this->fileReconstructor = $this->container->get(FileReconstructor::class);
}
protected function doTestFileMatchesExpectedContent(string $file, string $reconstructedFile): void
{
$reconstructedFileContent = $this->fileReconstructor->processFileWithReconstructor(
new SplFileInfo($file), $this->getReconstructor()
);
$this->assertStringEqualsFile($reconstructedFile, $reconstructedFileContent);
}
abstract protected function getReconstructorClass(): string;
private function getReconstructor(): ReconstructorInterface
{
return $this->container->get($this->getReconstructorClass());
}
}

View File

@ -0,0 +1,19 @@
<?php declare(strict_types=1);
namespace Rector\Tests\Reconstructor\DependencyInjection;
use Rector\Reconstructor\DependencyInjection\InjectAnnotationToConstructorReconstructor;
use Rector\Tests\AbstractReconstructorTestCase;
final class InjectAnnotationToConstructorReconstructorTest extends AbstractReconstructorTestCase
{
public function test(): void
{
$this->doTestFileMatchesExpectedContent(__DIR__ . '/wrong/wrong.php.inc', __DIR__ . '/correct/correct.php.inc');
}
protected function getReconstructorClass(): string
{
return InjectAnnotationToConstructorReconstructor::class;
}
}

View File

@ -0,0 +1,17 @@
<?php declare (strict_types=1);
class ClassWithInjects
{
/**
* @var stdClass
*/
private $property;
/**
* @var DateTimeInterface
*/
private $otherProperty;
public function __construct(stdClass $property, DateTimeInterface $otherProperty)
{
$this->property = $property;
$this->otherProperty = $otherProperty;
}
}

View File

@ -0,0 +1,16 @@
<?php declare (strict_types=1);
class ClassWithInjects
{
/**
* @var stdClass
* @inject
*/
public $property;
/**
* @var DateTimeInterface
* @inject
*/
public $otherProperty;
}