From 0ad995e2f35f52b04575349baf09a7add15a3512 Mon Sep 17 00:00:00 2001 From: GeniJaho Date: Thu, 16 Jan 2025 20:55:09 +0100 Subject: [PATCH] Update the CustomRuleCommand to accept options --- src/Console/Command/CustomRuleCommand.php | 125 ++++++++++++++---- .../utils/rector/src/Rector/__Name__.php | 4 +- .../__Name__/Fixture/some_class.php.inc | 4 +- .../tests/Rector/__Name__/__Name__Test.php | 2 +- .../__Name__/config/configured_rule.php | 3 +- .../tests/Rector/__Name__/__Name__Test.php | 2 +- .../Console/Command/CustomRuleCommandTest.php | 125 ++++++++++++++++++ 7 files changed, 230 insertions(+), 35 deletions(-) create mode 100644 tests/Console/Command/CustomRuleCommandTest.php diff --git a/src/Console/Command/CustomRuleCommand.php b/src/Console/Command/CustomRuleCommand.php index 0d91b1c6450..23b3cf77295 100644 --- a/src/Console/Command/CustomRuleCommand.php +++ b/src/Console/Command/CustomRuleCommand.php @@ -14,6 +14,7 @@ use Rector\FileSystem\JsonFileSystem; 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 Symfony\Component\Finder\Finder; @@ -37,22 +38,49 @@ protected function configure(): void { $this->setName('custom-rule'); $this->setDescription('Create base of local custom rule with tests'); + $this->addOption('name', null, null, 'Name of the rule class (e.g. "LegacyCallToDbalMethodCall")'); + $this->addOption( + 'with-composer-changes', + null, + InputOption::VALUE_OPTIONAL, + 'Do not modify composer.json', + true + ); + $this->addOption('with-phpunit-changes', null, InputOption::VALUE_OPTIONAL, 'Do not modify phpunit.xml', true); + $this->addOption( + 'with-rules-dir', + null, + InputOption::VALUE_OPTIONAL, + 'Where to put the rule class', + 'utils/rector/src/Rector' + ); + $this->addOption( + 'with-tests-dir', + null, + InputOption::VALUE_OPTIONAL, + 'Where to put the rule tests', + 'utils/rector/tests/Rector' + ); + $this->addOption( + 'with-rules-namespace', + null, + InputOption::VALUE_OPTIONAL, + 'What namespace to use for the rule class', + 'Utils\\Rector\\Rector' + ); + $this->addOption( + 'with-tests-namespace', + null, + InputOption::VALUE_OPTIONAL, + 'What namespace to use for the rule tests', + 'Utils\\Rector\\Tests\\Rector' + ); } protected function execute(InputInterface $input, OutputInterface $output): int { // ask for rule name - $rectorName = $this->symfonyStyle->ask( - 'What is the name of the rule class (e.g. "LegacyCallToDbalMethodCall")?', - null, - static function (string $answer): string { - if ($answer === '') { - throw new ShouldNotHappenException('Rector name cannot be empty'); - } - - return $answer; - } - ); + $rectorName = $this->getRectorName($input); // suffix with Rector by convention if (! str_ends_with((string) $rectorName, 'Rector')) { @@ -97,9 +125,15 @@ static function (string $answer): string { $fileInfos = iterator_to_array($finder->getIterator()); foreach ($fileInfos as $fileInfo) { - // replace __Name__ with $rectorName + // replace __Name__, __Namespace__, and __Test_Namespace__ in file content $newContent = $this->replaceNameVariable($rectorName, $fileInfo->getContents()); + $newContent = $this->replaceNamespaceVariable($input->getOption('with-rules-namespace'), $newContent); + $newContent = $this->replaceTestsNamespaceVariable($input->getOption('with-tests-namespace'), $newContent); + + // Replace the directory structure found in the template with the one provided by the user $newFilePath = $this->replaceNameVariable($rectorName, $fileInfo->getRelativePathname()); + $newFilePath = str_replace('utils/rector/src/Rector', $input->getOption('with-rules-dir'), $newFilePath); + $newFilePath = str_replace('utils/rector/tests/Rector', $input->getOption('with-tests-dir'), $newFilePath); FileSystem::write($currentDirectory . '/' . $newFilePath, $newContent, null); @@ -113,27 +147,31 @@ static function (string $answer): string { ); // 2. update autoload-dev in composer.json - $composerJsonFilePath = $currentDirectory . '/composer.json'; - if (file_exists($composerJsonFilePath)) { - $hasChanged = false; - $composerJson = JsonFileSystem::readFilePath($composerJsonFilePath); - - if (! isset($composerJson['autoload-dev']['psr-4']['Utils\\Rector\\'])) { - $composerJson['autoload-dev']['psr-4']['Utils\\Rector\\'] = 'utils/rector/src'; - $composerJson['autoload-dev']['psr-4']['Utils\\Rector\\Tests\\'] = 'utils/rector/tests'; - $hasChanged = true; - } + if ($input->getOption('with-composer-changes')) { + $composerJsonFilePath = $currentDirectory . '/composer.json'; + if (file_exists($composerJsonFilePath)) { + $hasChanged = false; + $composerJson = JsonFileSystem::readFilePath($composerJsonFilePath); + + if (! isset($composerJson['autoload-dev']['psr-4']['Utils\\Rector\\'])) { + $composerJson['autoload-dev']['psr-4']['Utils\\Rector\\'] = 'utils/rector/src'; + $composerJson['autoload-dev']['psr-4']['Utils\\Rector\\Tests\\'] = 'utils/rector/tests'; + $hasChanged = true; + } - if ($hasChanged) { - $this->symfonyStyle->success( - 'We also update composer.json autoload-dev, to load Rector rules. Now run "composer dump-autoload" to update paths' - ); - JsonFileSystem::writeFile($composerJsonFilePath, $composerJson); + if ($hasChanged) { + $this->symfonyStyle->success( + 'We also update composer.json autoload-dev, to load Rector rules. Now run "composer dump-autoload" to update paths' + ); + JsonFileSystem::writeFile($composerJsonFilePath, $composerJson); + } } } // 3. update phpunit.xml(.dist) to include rector test suite - $this->setupRectorTestSuite($currentDirectory); + if ($input->getOption('with-phpunit-changes')) { + $this->setupRectorTestSuite($currentDirectory); + } return Command::SUCCESS; } @@ -279,6 +317,16 @@ private function replaceNameVariable(string $rectorName, string $contents): stri return str_replace('__Name__', $rectorName, $contents); } + private function replaceNamespaceVariable(string $namespace, string $contents): string + { + return str_replace('__Namespace__', $namespace, $contents); + } + + private function replaceTestsNamespaceVariable(string $testsNamespace, string $contents): string + { + return str_replace('__Tests_Namespace__', $testsNamespace, $contents); + } + private function detectPHPUnitAttributeSupport(): bool { $composerJsonFilePath = getcwd() . '/composer.json'; @@ -297,4 +345,25 @@ private function detectPHPUnitAttributeSupport(): bool return (bool) Strings::match($phpunitVersion, self::START_WITH_10_REGEX); } + + private function getRectorName(InputInterface $input): mixed + { + $nameFromOption = $input->getOption('name'); + + if ($nameFromOption) { + return $nameFromOption; + } + + return $this->symfonyStyle->ask( + 'What is the name of the rule class (e.g. "LegacyCallToDbalMethodCall")?', + null, + static function (string $answer): string { + if ($answer === '') { + throw new ShouldNotHappenException('Rector name cannot be empty'); + } + + return $answer; + } + ); + } } diff --git a/templates/custom-rule/utils/rector/src/Rector/__Name__.php b/templates/custom-rule/utils/rector/src/Rector/__Name__.php index fbbccf28be9..4fb057ed6e6 100644 --- a/templates/custom-rule/utils/rector/src/Rector/__Name__.php +++ b/templates/custom-rule/utils/rector/src/Rector/__Name__.php @@ -2,13 +2,13 @@ declare(strict_types=1); -namespace Utils\Rector\Rector; +namespace __Namespace__; use PhpParser\Node; use Rector\Rector\AbstractRector; /** - * @see \Rector\Tests\TypeDeclaration\Rector\__Name__\__Name__Test + * @see \__Tests_Namespace__\__Name__\__Name__Test */ final class __Name__ extends AbstractRector { diff --git a/templates/custom-rule/utils/rector/tests/Rector/__Name__/Fixture/some_class.php.inc b/templates/custom-rule/utils/rector/tests/Rector/__Name__/Fixture/some_class.php.inc index 6cf9dc365f0..bf5170c166b 100644 --- a/templates/custom-rule/utils/rector/tests/Rector/__Name__/Fixture/some_class.php.inc +++ b/templates/custom-rule/utils/rector/tests/Rector/__Name__/Fixture/some_class.php.inc @@ -1,6 +1,6 @@ rule(\Utils\Rector\Rector\__Name__::class); + $rectorConfig->rule(__Name__::class); }; diff --git a/templates/custom-rules-annotations/utils/rector/tests/Rector/__Name__/__Name__Test.php b/templates/custom-rules-annotations/utils/rector/tests/Rector/__Name__/__Name__Test.php index 6cad0044274..18ec8905a5d 100644 --- a/templates/custom-rules-annotations/utils/rector/tests/Rector/__Name__/__Name__Test.php +++ b/templates/custom-rules-annotations/utils/rector/tests/Rector/__Name__/__Name__Test.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Rector\Tests\TypeDeclaration\Rector\__Name__; +namespace __Tests_Namespace__\__Name__; use Rector\Testing\PHPUnit\AbstractRectorTestCase; diff --git a/tests/Console/Command/CustomRuleCommandTest.php b/tests/Console/Command/CustomRuleCommandTest.php new file mode 100644 index 00000000000..3f86a5f79c2 --- /dev/null +++ b/tests/Console/Command/CustomRuleCommandTest.php @@ -0,0 +1,125 @@ +customRuleCommand = $this->make(CustomRuleCommand::class); + } + + public function test(): void + { + $ruleName = 'SomeCustomRuleRector'; + $rulesDirectory = self::TEST_OUTPUT_DIRECTORY . '/src/Rector/Test'; + $testsDirectory = self::TEST_OUTPUT_DIRECTORY . '/src/Rector'; + + $commandTester = new CommandTester($this->customRuleCommand); + $commandTester->execute([ + '--name' => $ruleName, + '--with-composer-changes' => false, + '--with-phpunit-changes' => false, + '--with-rules-dir' => $rulesDirectory, + '--with-tests-dir' => $testsDirectory, + ]); + + $currentDirectory = getcwd(); + $this->assertNotEmpty(FileSystem::read(sprintf( + '%s/%s/%s.php', + $currentDirectory, + $rulesDirectory, + $ruleName + ))); + $this->assertNotEmpty( + FileSystem::read(sprintf( + '%s/%s/%s/Fixture/some_class.php.inc', + $currentDirectory, + $testsDirectory, + $ruleName + )) + ); + $this->assertNotEmpty( + FileSystem::read(sprintf( + '%s/%s/%s/config/configured_rule.php', + $currentDirectory, + $testsDirectory, + $ruleName + )) + ); + $this->assertNotEmpty( + FileSystem::read(sprintf( + '%s/%s/%s/%sTest.php', + $currentDirectory, + $testsDirectory, + $ruleName, + $ruleName + )) + ); + + $this->cleanup($currentDirectory); + } + + public function testWithRulesAndTestsNamespace(): void + { + $ruleName = 'SomeCustomRuleRector'; + $ruleDirectory = self::TEST_OUTPUT_DIRECTORY . '/utils/Rector/src/Rector'; + $testsDirectory = self::TEST_OUTPUT_DIRECTORY . '/utils/Rector/tests/Rector'; + $rulesNamespace = 'RectorLaravel\Custom'; + $testsNamespace = 'RectorLaravel\Tests\Custom'; + + $commandTester = new CommandTester($this->customRuleCommand); + $commandTester->execute([ + '--name' => $ruleName, + '--with-composer-changes' => false, + '--with-phpunit-changes' => false, + '--with-rules-dir' => $ruleDirectory, + '--with-tests-dir' => $testsDirectory, + '--with-rules-namespace' => $rulesNamespace, + '--with-tests-namespace' => $testsNamespace, + ]); + + $currentDirectory = getcwd(); + $this->assertStringContainsString( + sprintf('namespace %s;', $rulesNamespace), + FileSystem::read(sprintf('%s/%s/%s.php', $currentDirectory, $ruleDirectory, $ruleName)), + ); + $this->assertStringContainsString( + sprintf('@see \\%s\\%s\\%sTest', $testsNamespace, $ruleName, $ruleName), + FileSystem::read(sprintf('%s/%s/%s.php', $currentDirectory, $ruleDirectory, $ruleName)), + ); + $this->assertStringContainsString( + sprintf('namespace %s\\%s;', $testsNamespace, $ruleName), + FileSystem::read(sprintf('%s/%s/%s/%sTest.php', $currentDirectory, $testsDirectory, $ruleName, $ruleName)), + ); + + $configFile = FileSystem::read( + sprintf('%s/%s/%s/config/configured_rule.php', $currentDirectory, $testsDirectory, $ruleName) + ); + $this->assertStringContainsString(sprintf('$rectorConfig->rule(%s::class);', $ruleName), $configFile); + $this->assertStringContainsString(sprintf('use %s\\%s;', $rulesNamespace, $ruleName), $configFile); + + $this->assertStringContainsString( + sprintf('namespace %s\\%s\\Fixture;', $testsNamespace, $ruleName), + FileSystem::read( + sprintf('%s/%s/%s/Fixture/some_class.php.inc', $currentDirectory, $testsDirectory, $ruleName) + ), + ); + + $this->cleanup($currentDirectory); + } + + private function cleanup(false|string $currentDirectory): void + { + FileSystem::delete($currentDirectory . '/' . self::TEST_OUTPUT_DIRECTORY); + } +}