mirror of
https://github.com/rectorphp/rector.git
synced 2025-01-17 21:38:22 +01:00
[DX] Add Interactive Mode to Generate command (#4931)
* Add Interactive Mode to Generate command * CS Fixer * Unify names * Unify names * Fixes * Fixes * Fixes * Check generated tests against the "special hack for PHPUnit" * Re-use the same input/output in the Generate Command * Add test for Interactive Mode of Generate Command * Rename test file * Use ::class instead of string * Create Finder instance where it's used * Create RectorRecipeInteractiveProvider service * Create RectorRecipeInteractiveProvider service * Bring back SymfonyStyle as DI service
This commit is contained in:
parent
8ed61a7560
commit
94a4bb7777
@ -9,7 +9,24 @@ How can we **remove repeated work** and let us focus only on `refactor()` method
|
||||
It creates a bare structured Rule.
|
||||
Don't worry, also generates a test case, which is required to contribute.
|
||||
|
||||
## How to Generate Rector rule in 3 steps?
|
||||
## How to Generate Rector rule?
|
||||
|
||||
There are two possibilities to Generate a Rector rule.
|
||||
|
||||
### Generate using Interactive Mode
|
||||
|
||||
**Important**: using this approach will generate Rector rule with placeholder Code Samples, which should be changed
|
||||
by hand to reflect what the rule does
|
||||
|
||||
1. Run Generate command in Interactive Mode
|
||||
|
||||
```bash
|
||||
vendor/bin/rector generate --interactive
|
||||
```
|
||||
|
||||
2. Provide an answer to questions asked by the command
|
||||
|
||||
### Generate using configuration file
|
||||
|
||||
1. Initialize `rector-recipe.php` config
|
||||
|
||||
|
@ -11,10 +11,14 @@ use Rector\RectorGenerator\Config\ConfigFilesystem;
|
||||
use Rector\RectorGenerator\Finder\TemplateFinder;
|
||||
use Rector\RectorGenerator\Generator\FileGenerator;
|
||||
use Rector\RectorGenerator\Guard\OverrideGuard;
|
||||
use Rector\RectorGenerator\Provider\RectorRecipeInteractiveProvider;
|
||||
use Rector\RectorGenerator\Provider\RectorRecipeProvider;
|
||||
use Rector\RectorGenerator\TemplateVariablesFactory;
|
||||
use Rector\RectorGenerator\ValueObject\RectorRecipe;
|
||||
use Rector\Testing\PHPUnit\StaticPHPUnitEnvironment;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use Symplify\PackageBuilder\Console\ShellCode;
|
||||
@ -22,6 +26,8 @@ use Symplify\SmartFileSystem\SmartFileInfo;
|
||||
|
||||
final class GenerateCommand extends Command
|
||||
{
|
||||
private const INTERACTIVE_MODE_NAME = 'interactive';
|
||||
|
||||
/**
|
||||
* @var SymfonyStyle
|
||||
*/
|
||||
@ -62,6 +68,11 @@ final class GenerateCommand extends Command
|
||||
*/
|
||||
private $rectorRecipeProvider;
|
||||
|
||||
/**
|
||||
* @var RectorRecipeInteractiveProvider
|
||||
*/
|
||||
private $rectorRecipeInteractiveProvider;
|
||||
|
||||
public function __construct(
|
||||
ComposerPackageAutoloadUpdater $composerPackageAutoloadUpdater,
|
||||
ConfigFilesystem $configFilesystem,
|
||||
@ -70,29 +81,37 @@ final class GenerateCommand extends Command
|
||||
SymfonyStyle $symfonyStyle,
|
||||
TemplateFinder $templateFinder,
|
||||
TemplateVariablesFactory $templateVariablesFactory,
|
||||
RectorRecipeProvider $rectorRecipeProvider
|
||||
RectorRecipeProvider $rectorRecipeProvider,
|
||||
RectorRecipeInteractiveProvider $rectorRecipeInteractiveProvider
|
||||
) {
|
||||
parent::__construct();
|
||||
|
||||
$this->symfonyStyle = $symfonyStyle;
|
||||
$this->templateVariablesFactory = $templateVariablesFactory;
|
||||
$this->composerPackageAutoloadUpdater = $composerPackageAutoloadUpdater;
|
||||
$this->templateFinder = $templateFinder;
|
||||
$this->configFilesystem = $configFilesystem;
|
||||
$this->overrideGuard = $overrideGuard;
|
||||
$this->symfonyStyle = $symfonyStyle;
|
||||
$this->fileGenerator = $fileGenerator;
|
||||
$this->rectorRecipeProvider = $rectorRecipeProvider;
|
||||
$this->rectorRecipeInteractiveProvider = $rectorRecipeInteractiveProvider;
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this->setAliases(['c', 'create', 'g']);
|
||||
$this->setDescription('[DEV] Create a new Rector, in a proper location, with new tests');
|
||||
$this->addOption(
|
||||
self::INTERACTIVE_MODE_NAME,
|
||||
'i',
|
||||
InputOption::VALUE_NONE,
|
||||
'Turns on Interactive Mode - Rector will be generated based on responses to questions instead of using rector-recipe.php',
|
||||
);
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$rectorRecipe = $this->rectorRecipeProvider->provide();
|
||||
$rectorRecipe = $this->getRectorRecipe($input);
|
||||
|
||||
$templateVariables = $this->templateVariablesFactory->createFromRectorRecipe($rectorRecipe);
|
||||
|
||||
@ -136,7 +155,7 @@ final class GenerateCommand extends Command
|
||||
private function resolveTestCaseDirectoryPath(array $generatedFilePaths): string
|
||||
{
|
||||
foreach ($generatedFilePaths as $generatedFilePath) {
|
||||
if (! Strings::endsWith($generatedFilePath, 'Test.php')) {
|
||||
if (! $this->isGeneratedFilePathTestCase($generatedFilePath)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -147,6 +166,16 @@ final class GenerateCommand extends Command
|
||||
throw new ShouldNotHappenException();
|
||||
}
|
||||
|
||||
private function isGeneratedFilePathTestCase(string $generatedFilePath): bool
|
||||
{
|
||||
if (Strings::endsWith($generatedFilePath, 'Test.php')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
return Strings::endsWith($generatedFilePath, 'Test.php.inc') && StaticPHPUnitEnvironment::isPHPUnitRun();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $generatedFilePaths
|
||||
*/
|
||||
@ -167,4 +196,14 @@ final class GenerateCommand extends Command
|
||||
|
||||
$this->symfonyStyle->success($message);
|
||||
}
|
||||
|
||||
private function getRectorRecipe(InputInterface $input): RectorRecipe
|
||||
{
|
||||
$isInteractive = $input->getOption(self::INTERACTIVE_MODE_NAME);
|
||||
if (! $isInteractive) {
|
||||
return $this->rectorRecipeProvider->provide();
|
||||
}
|
||||
|
||||
return $this->rectorRecipeInteractiveProvider->provide($this->symfonyStyle);
|
||||
}
|
||||
}
|
||||
|
42
packages/rector-generator/src/Provider/NodeTypesProvider.php
Normal file
42
packages/rector-generator/src/Provider/NodeTypesProvider.php
Normal file
@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Rector\RectorGenerator\Provider;
|
||||
|
||||
use ReflectionClass;
|
||||
use Symfony\Component\Finder\Finder;
|
||||
|
||||
final class NodeTypesProvider
|
||||
{
|
||||
private const PHP_PARSER_NODES_PATH = __DIR__ . '/../../../../vendor/nikic/php-parser/lib/PhpParser/Node';
|
||||
|
||||
private const PHP_PARSER_NAMESPACE = '\PhpParser\Node\\';
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function provide(): array
|
||||
{
|
||||
$finder = new Finder();
|
||||
$filesList = $finder
|
||||
->files()
|
||||
->in(self::PHP_PARSER_NODES_PATH)
|
||||
->getIterator()
|
||||
;
|
||||
|
||||
$names = [];
|
||||
foreach ($filesList as $splFileInfo) {
|
||||
$name = str_replace(['.php', '/'], ['', '\\'], $splFileInfo->getRelativePathname());
|
||||
|
||||
$reflection = new ReflectionClass(self::PHP_PARSER_NAMESPACE . $name);
|
||||
if ($reflection->isAbstract() || $reflection->isInterface()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$names[$name] = $name;
|
||||
}
|
||||
|
||||
return $names;
|
||||
}
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Rector\RectorGenerator\Provider;
|
||||
|
||||
use Rector\Core\Util\StaticRectorStrings;
|
||||
use Symfony\Component\Finder\Finder;
|
||||
|
||||
final class PackageNamesProvider
|
||||
{
|
||||
/**
|
||||
* @return array<int, string>
|
||||
*/
|
||||
public function provide(): array
|
||||
{
|
||||
$finder = new Finder();
|
||||
$directoriesList = $finder
|
||||
->directories()
|
||||
->depth(0)
|
||||
->in(__DIR__ . '/../../../../rules/')
|
||||
->getIterator()
|
||||
;
|
||||
|
||||
$names = [];
|
||||
foreach ($directoriesList as $directory) {
|
||||
$names[] = StaticRectorStrings::dashesToCamelCase($directory->getFilename());
|
||||
}
|
||||
|
||||
return $names;
|
||||
}
|
||||
}
|
@ -0,0 +1,188 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Rector\RectorGenerator\Provider;
|
||||
|
||||
use Rector\RectorGenerator\ValueObject\RectorRecipe;
|
||||
use Rector\Set\ValueObject\SetList;
|
||||
use Symfony\Component\Console\Question\ChoiceQuestion;
|
||||
use Symfony\Component\Console\Question\Question;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
final class RectorRecipeInteractiveProvider
|
||||
{
|
||||
/**
|
||||
* @var SymfonyStyle
|
||||
*/
|
||||
private $symfonyStyle;
|
||||
|
||||
/**
|
||||
* @var PackageNamesProvider
|
||||
*/
|
||||
private $packageNamesProvider;
|
||||
|
||||
/**
|
||||
* @var NodeTypesProvider
|
||||
*/
|
||||
private $nodeTypesProvider;
|
||||
|
||||
/**
|
||||
* @var SetsListProvider
|
||||
*/
|
||||
private $setsListProvider;
|
||||
|
||||
public function __construct(
|
||||
PackageNamesProvider $packageNamesProvider,
|
||||
NodeTypesProvider $nodeTypesProvider,
|
||||
SetsListProvider $setsListProvider
|
||||
) {
|
||||
$this->packageNamesProvider = $packageNamesProvider;
|
||||
$this->nodeTypesProvider = $nodeTypesProvider;
|
||||
$this->setsListProvider = $setsListProvider;
|
||||
}
|
||||
|
||||
public function provide(SymfonyStyle $symfonyStyle): RectorRecipe
|
||||
{
|
||||
$this->symfonyStyle = $symfonyStyle;
|
||||
$rectorRecipe = new RectorRecipe(
|
||||
$this->askForPackageName(),
|
||||
$this->askForRectorName(),
|
||||
$this->askForNodeTypes(),
|
||||
$this->askForRectorDescription(),
|
||||
$this->getExampleCodeBefore(),
|
||||
$this->getExampleCodeAfter(),
|
||||
);
|
||||
$rectorRecipe->setResources($this->askForResources());
|
||||
|
||||
$set = $this->askForSet();
|
||||
if ($set !== null) {
|
||||
$rectorRecipe->setSet($set);
|
||||
}
|
||||
|
||||
return $rectorRecipe;
|
||||
}
|
||||
|
||||
private function askForPackageName(): string
|
||||
{
|
||||
$question = new Question(sprintf(
|
||||
'Package name for which Rector should be created (e.g. <fg=yellow>%s</>)',
|
||||
'Naming'
|
||||
));
|
||||
$question->setAutocompleterValues($this->packageNamesProvider->provide());
|
||||
|
||||
$packageName = $this->symfonyStyle->askQuestion($question);
|
||||
|
||||
return $packageName ?? $this->askForPackageName();
|
||||
}
|
||||
|
||||
private function askForRectorName(): string
|
||||
{
|
||||
$question = sprintf(
|
||||
'Class name of the Rector to create (e.g. <fg=yellow>%s</>)',
|
||||
'RenameMethodCallRector',
|
||||
);
|
||||
$rectorName = $this->symfonyStyle->ask($question);
|
||||
|
||||
return $rectorName ?? $this->askForRectorName();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, class-string>
|
||||
*/
|
||||
private function askForNodeTypes(): array
|
||||
{
|
||||
$question = new ChoiceQuestion(sprintf(
|
||||
'For what Nodes should the Rector be run (e.g. <fg=yellow>%s</>)',
|
||||
'Expr/MethodCall',
|
||||
), $this->nodeTypesProvider->provide());
|
||||
$question->setMultiselect(true);
|
||||
|
||||
$nodeTypes = $this->symfonyStyle->askQuestion($question);
|
||||
|
||||
$classes = [];
|
||||
foreach ($nodeTypes as $nodeType) {
|
||||
/** @var class-string $class */
|
||||
$class = 'PhpParser\Node\\' . $nodeType;
|
||||
$classes[] = $class;
|
||||
}
|
||||
|
||||
return $classes;
|
||||
}
|
||||
|
||||
private function askForRectorDescription(): string
|
||||
{
|
||||
$description = $this->symfonyStyle->ask('Short description of new Rector');
|
||||
|
||||
return $description ?? $this->askForRectorDescription();
|
||||
}
|
||||
|
||||
private function getExampleCodeBefore(): string
|
||||
{
|
||||
return <<<'CODE_SAMPLE'
|
||||
<?php
|
||||
|
||||
class SomeClass
|
||||
{
|
||||
public function run()
|
||||
{
|
||||
$this->something();
|
||||
}
|
||||
}
|
||||
|
||||
CODE_SAMPLE;
|
||||
}
|
||||
|
||||
private function getExampleCodeAfter(): string
|
||||
{
|
||||
return <<<'CODE_SAMPLE'
|
||||
<?php
|
||||
|
||||
class SomeClass
|
||||
{
|
||||
public function run()
|
||||
{
|
||||
$this->somethingElse();
|
||||
}
|
||||
}
|
||||
|
||||
CODE_SAMPLE;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string>
|
||||
*/
|
||||
private function askForResources(): array
|
||||
{
|
||||
$resources = [];
|
||||
|
||||
while (true) {
|
||||
$question = sprintf(
|
||||
'Link to resource that explains why the change is needed (e.g. <fg=yellow>%s</>)',
|
||||
'https://github.com/symfony/symfony/blob/704c648ba53be38ef2b0105c97c6497744fef8d8/UPGRADE-6.0.md',
|
||||
);
|
||||
$resource = $this->symfonyStyle->ask($question);
|
||||
|
||||
if ($resource === null) {
|
||||
break;
|
||||
}
|
||||
|
||||
$resources[] = $resource;
|
||||
}
|
||||
|
||||
return $resources;
|
||||
}
|
||||
|
||||
private function askForSet(): ?string
|
||||
{
|
||||
$question = new Question(sprintf('Set to which Rector should be added (e.g. <fg=yellow>%s</>)', 'SYMFONY_52'));
|
||||
$question->setAutocompleterValues($this->setsListProvider->provide());
|
||||
|
||||
$setName = $this->symfonyStyle->askQuestion($question);
|
||||
if ($setName === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return constant(SetList::class . $setName);
|
||||
}
|
||||
}
|
22
packages/rector-generator/src/Provider/SetsListProvider.php
Normal file
22
packages/rector-generator/src/Provider/SetsListProvider.php
Normal file
@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Rector\RectorGenerator\Provider;
|
||||
|
||||
use Rector\Set\ValueObject\SetList;
|
||||
use ReflectionClass;
|
||||
|
||||
final class SetsListProvider
|
||||
{
|
||||
/**
|
||||
* @return array<int, string>
|
||||
*/
|
||||
public function provide(): array
|
||||
{
|
||||
$setListReflection = new ReflectionClass(SetList::class);
|
||||
$constants = $setListReflection->getConstants();
|
||||
|
||||
return array_keys($constants);
|
||||
}
|
||||
}
|
@ -37,6 +37,13 @@ final class StaticRectorStrings
|
||||
return false;
|
||||
}
|
||||
|
||||
public static function dashesToCamelCase(string $input): string
|
||||
{
|
||||
$tokens = explode('-', $input);
|
||||
|
||||
return implode('', array_map('ucfirst', $tokens));
|
||||
}
|
||||
|
||||
public static function camelCaseToDashes(string $input): string
|
||||
{
|
||||
return self::camelCaseToGlue($input, '-');
|
||||
|
@ -10,6 +10,20 @@ use Rector\Core\Util\StaticRectorStrings;
|
||||
|
||||
final class StaticRectorStringsTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @dataProvider provideDataForDashesToCamelCase()
|
||||
*/
|
||||
public function testDashesToCamelCase(string $content, string $expected): void
|
||||
{
|
||||
$this->assertSame($expected, StaticRectorStrings::dashesToCamelCase($content));
|
||||
}
|
||||
|
||||
public function provideDataForDashesToCamelCase(): Iterator
|
||||
{
|
||||
yield ['simple-test', 'SimpleTest'];
|
||||
yield ['easy', 'Easy'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideDataForCamelCaseToUnderscore()
|
||||
*/
|
||||
|
Loading…
x
Reference in New Issue
Block a user