Skip to content

Commit

Permalink
Update the CustomRuleCommand to accept options
Browse files Browse the repository at this point in the history
  • Loading branch information
GeniJaho committed Jan 16, 2025
1 parent 5ad5480 commit 0ad995e
Show file tree
Hide file tree
Showing 7 changed files with 230 additions and 35 deletions.
125 changes: 97 additions & 28 deletions src/Console/Command/CustomRuleCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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')) {
Expand Down Expand Up @@ -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);

Expand All @@ -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;
}
Expand Down Expand Up @@ -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';
Expand All @@ -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;
}
);
}
}
4 changes: 2 additions & 2 deletions templates/custom-rule/utils/rector/src/Rector/__Name__.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
<?php

namespace Rector\Tests\TypeDeclaration\Rector\__Name__\Fixture;
namespace __Tests_Namespace__\__Name__\Fixture;

// @todo fill code before

?>
-----
<?php

namespace Rector\Tests\TypeDeclaration\Rector\__Name__\Fixture;
namespace __Tests_Namespace__\__Name__\Fixture;

// @todo fill code after

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

declare(strict_types=1);

namespace Rector\Tests\TypeDeclaration\Rector\__Name__;
namespace __Tests_Namespace__\__Name__;

use PHPUnit\Framework\Attributes\DataProvider;
use Rector\Testing\PHPUnit\AbstractRectorTestCase;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
declare(strict_types=1);

use Rector\Config\RectorConfig;
use __Namespace__\__Name__;

return static function (RectorConfig $rectorConfig): void {
$rectorConfig->rule(\Utils\Rector\Rector\__Name__::class);
$rectorConfig->rule(__Name__::class);
};
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

declare(strict_types=1);

namespace Rector\Tests\TypeDeclaration\Rector\__Name__;
namespace __Tests_Namespace__\__Name__;

use Rector\Testing\PHPUnit\AbstractRectorTestCase;

Expand Down
125 changes: 125 additions & 0 deletions tests/Console/Command/CustomRuleCommandTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
<?php

namespace Rector\Tests\Console\Command;

use Nette\Utils\FileSystem;
use Rector\Console\Command\CustomRuleCommand;
use Rector\Testing\PHPUnit\AbstractLazyTestCase;
use Symfony\Component\Console\Tester\CommandTester;

final class CustomRuleCommandTest extends AbstractLazyTestCase
{
private const TEST_OUTPUT_DIRECTORY = 'tests/Console/Command/CustomRuleCommand';

private CustomRuleCommand $customRuleCommand;

protected function setUp(): void
{
$this->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);
}
}

0 comments on commit 0ad995e

Please sign in to comment.