diff --git a/src/Console/Command/CustomRuleCommand.php b/src/Console/Command/CustomRuleCommand.php index 0d91b1c6450..c51909f1e25 100644 --- a/src/Console/Command/CustomRuleCommand.php +++ b/src/Console/Command/CustomRuleCommand.php @@ -4,12 +4,10 @@ namespace Rector\Console\Command; -use DOMDocument; -use DOMElement; -use DOMXPath; -use Generator; use Nette\Utils\FileSystem; use Nette\Utils\Strings; +use PHPStan\Reflection\ReflectionProvider; +use Rector\Enum\ClassName; use Rector\Exception\ShouldNotHappenException; use Rector\FileSystem\JsonFileSystem; use Symfony\Component\Console\Command\Command; @@ -21,14 +19,9 @@ final class CustomRuleCommand extends Command { - /** - * @see https://regex101.com/r/2eP4rw/1 - * @var string - */ - private const START_WITH_10_REGEX = '#(\^10\.|>=10\.|10\.)#'; - public function __construct( private readonly SymfonyStyle $symfonyStyle, + private readonly ReflectionProvider $reflectionProvider ) { parent::__construct(); } @@ -43,10 +36,10 @@ 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")?', + 'What is the rule class name? (e.g. "LegacyCallToDbalMethodCall")?', null, - static function (string $answer): string { - if ($answer === '') { + static function (?string $answer): string { + if ($answer === '' || $answer === null) { throw new ShouldNotHappenException('Rector name cannot be empty'); } @@ -65,27 +58,25 @@ static function (string $answer): string { $finder = Finder::create() ->files() ->in(__DIR__ . '/../../../templates/custom-rule') - ->notName('__Name__Test.php'); + ->notName('__Name__Test.php.phtml'); // 0. resolve if local phpunit is at least PHPUnit 10 (which supports #[DataProvider]) // to provide annotation if not - $arePHPUnitAttributesSupported = $this->detectPHPUnitAttributeSupport(); - - if ($arePHPUnitAttributesSupported) { + if ($this->isPHPUnitAttributeSupported()) { $finder->append([ new SplFileInfo( - __DIR__ . '/../../../templates/custom-rule/utils/rector/tests/Rector/__Name__/__Name__Test.php', + __DIR__ . '/../../../templates/custom-rule/utils/rector/tests/Rector/__Name__/__Name__Test.php.phtml', 'utils/rector/tests/Rector/__Name__', - 'utils/rector/tests/Rector/__Name__/__Name__Test.php', + 'utils/rector/tests/Rector/__Name__/__Name__Test.php.phtml', ), ]); } else { // use @annotations for PHPUnit 9 and bellow $finder->append([ new SplFileInfo( - __DIR__ . '/../../../templates/custom-rules-annotations/utils/rector/tests/Rector/__Name__/__Name__Test.php', + __DIR__ . '/../../../templates/custom-rules-annotations/utils/rector/tests/Rector/__Name__/__Name__Test.php.phtml', 'utils/rector/tests/Rector/__Name__', - 'utils/rector/tests/Rector/__Name__/__Name__Test.php', + 'utils/rector/tests/Rector/__Name__/__Name__Test.php.phtml', ), ]); } @@ -97,20 +88,21 @@ static function (string $answer): string { $fileInfos = iterator_to_array($finder->getIterator()); foreach ($fileInfos as $fileInfo) { - // replace __Name__ with $rectorName + // replace "__Name__" with $rectorName $newContent = $this->replaceNameVariable($rectorName, $fileInfo->getContents()); $newFilePath = $this->replaceNameVariable($rectorName, $fileInfo->getRelativePathname()); + // remove "phtml" suffix + $newFilePath = Strings::substring($newFilePath, 0, -strlen('.phtml')); + FileSystem::write($currentDirectory . '/' . $newFilePath, $newContent, null); $generatedFilePaths[] = $newFilePath; } - $this->symfonyStyle->title('Generated files'); + $title = sprintf('Skeleton for "%s" rule was created. Now write rule logic to solve your problem', $rectorName); + $this->symfonyStyle->title($title); $this->symfonyStyle->listing($generatedFilePaths); - $this->symfonyStyle->success( - sprintf('Base for the "%s" rule was created. Now you can fill the missing parts', $rectorName) - ); // 2. update autoload-dev in composer.json $composerJsonFilePath = $currentDirectory . '/composer.json'; @@ -125,176 +117,30 @@ static function (string $answer): string { } if ($hasChanged) { - $this->symfonyStyle->success( - 'We also update composer.json autoload-dev, to load Rector rules. Now run "composer dump-autoload" to update paths' - ); + $this->symfonyStyle->writeln('We updated "composer.json" autoload-dev to load Rector rules.'); + $this->symfonyStyle->writeln('Now run "composer dump-autoload" to update paths'); JsonFileSystem::writeFile($composerJsonFilePath, $composerJson); } } + $this->symfonyStyle->newLine(1); + // 3. update phpunit.xml(.dist) to include rector test suite - $this->setupRectorTestSuite($currentDirectory); + $this->symfonyStyle->writeln('Run Rector tests via PHPUnit:'); + $this->symfonyStyle->newLine(1); + $this->symfonyStyle->writeln(' vendor/bin/phpunit utils/rector/tests'); + $this->symfonyStyle->newLine(1); return Command::SUCCESS; } - private function setupRectorTestSuite(string $currentDirectory): void - { - if (! extension_loaded('dom')) { - $this->symfonyStyle->warning( - 'The "dom" extension is not loaded. Rector could not add the rector test suite to phpunit.xml' - ); - - return; - } - - $phpunitXmlExists = file_exists($currentDirectory . '/phpunit.xml'); - $phpunitXmlDistExists = file_exists($currentDirectory . '/phpunit.xml.dist'); - - if (! $phpunitXmlExists && ! $phpunitXmlDistExists) { - $this->symfonyStyle->warning( - 'No phpunit.xml or phpunit.xml.dist found. Rector could not add the rector test suite to it' - ); - - return; - } - - $phpunitFile = $phpunitXmlExists ? 'phpunit.xml' : 'phpunit.xml.dist'; - - $phpunitFilePath = $currentDirectory . '/' . $phpunitFile; - - $domDocument = new DOMDocument('1.0'); - $domDocument->preserveWhiteSpace = false; - $domDocument->formatOutput = true; - $domDocument->loadXML(FileSystem::read($phpunitFilePath)); - - if ($this->hasRectorTestSuite($domDocument)) { - $this->symfonyStyle->success( - 'The rector test suite already exists in ' . $phpunitFilePath . ". No changes were made.\n You can run the rector tests by running: phpunit --testsuite rector" - ); - - return; - } - - $testsuitesElement = $domDocument->getElementsByTagName('testsuites') - ->item(0); - - if (! $testsuitesElement instanceof DOMElement) { - $this->symfonyStyle->warning( - 'No element found in ' . $phpunitFilePath . '. Rector could not add the rector test suite to it' - ); - - return; - } - - $phpunitXML = $this->updatePHPUnitXMLFile($domDocument, $testsuitesElement); - - FileSystem::write($phpunitFilePath, $phpunitXML, null); - - $this->symfonyStyle->success( - 'We also update ' . $phpunitFilePath . ", to add a rector test suite.\n You can run the rector tests by running: phpunit --testsuite rector" - ); - } - - private function hasRectorTestSuite(DOMDocument $domDocument): bool - { - foreach ($this->getTestSuiteElements($domDocument) as $testSuiteElement) { - foreach ($testSuiteElement->getElementsByTagName('directory') as $directoryNode) { - if (! $directoryNode instanceof DOMElement) { - continue; - } - - $name = $testSuiteElement->getAttribute('name'); - if ($name !== 'rector') { - continue; - } - - $directory = $directoryNode->textContent; - if ($directory === 'utils/rector/tests') { - return true; - } - } - } - - return false; - } - - private function updatePHPUnitXMLFile(DOMDocument $domDocument, DOMElement $testsuitesElement): string - { - $domElement = $domDocument->createElement('testsuite'); - $domElement->setAttribute('name', 'rector'); - - $rectorTestSuiteDirectory = $domDocument->createElement('directory', 'utils/rector/tests'); - $domElement->appendChild($rectorTestSuiteDirectory); - - $testsuitesElement->appendChild($domElement); - - $phpunitXML = $domDocument->saveXML(); - if ($phpunitXML === false) { - throw new ShouldNotHappenException('Could not save XML'); - } - - return $phpunitXML; - } - - /** - * @return Generator - */ - private function getTestSuiteElements(DOMDocument $domDocument): Generator - { - $domxPath = new DOMXPath($domDocument); - $testSuiteNodes = $domxPath->query('testsuites/testsuite'); - if ($testSuiteNodes === false) { - return; - } - - if ($testSuiteNodes->length === 0) { - $testSuiteNodes = $domxPath->query('testsuite'); - if ($testSuiteNodes === false) { - return; - } - } - - if ($testSuiteNodes->length === 1) { - $element = $testSuiteNodes->item(0); - - if ($element instanceof DOMElement) { - yield $element; - } - - return; - } - - foreach ($testSuiteNodes as $testSuiteNode) { - if (! $testSuiteNode instanceof DOMElement) { - continue; - } - - yield $testSuiteNode; - } - } - private function replaceNameVariable(string $rectorName, string $contents): string { return str_replace('__Name__', $rectorName, $contents); } - private function detectPHPUnitAttributeSupport(): bool + private function isPHPUnitAttributeSupported(): bool { - $composerJsonFilePath = getcwd() . '/composer.json'; - - if (! file_exists($composerJsonFilePath)) { - // be safe - return false; - } - - $composerJson = JsonFileSystem::readFilePath($composerJsonFilePath); - $phpunitVersion = $composerJson['require-dev']['phpunit/phpunit'] ?? null; - - if ($phpunitVersion === null) { - return false; - } - - return (bool) Strings::match($phpunitVersion, self::START_WITH_10_REGEX); + return $this->reflectionProvider->hasClass(ClassName::DATA_PROVIDER); } } diff --git a/src/Enum/ClassName.php b/src/Enum/ClassName.php index 5fa03f6f14a..08ba0930df2 100644 --- a/src/Enum/ClassName.php +++ b/src/Enum/ClassName.php @@ -30,4 +30,9 @@ final class ClassName * @var string */ public const DOCTRINE_ENTITY = 'Doctrine\ORM\Mapping\Entity'; + + /** + * @var string + */ + public const DATA_PROVIDER = 'PHPUnit\Framework\Attributes\DataProvider'; } diff --git a/templates/custom-rule/utils/rector/src/Rector/__Name__.php b/templates/custom-rule/utils/rector/src/Rector/__Name__.php.phtml similarity index 100% rename from templates/custom-rule/utils/rector/src/Rector/__Name__.php rename to templates/custom-rule/utils/rector/src/Rector/__Name__.php.phtml 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.phtml similarity index 100% rename from templates/custom-rule/utils/rector/tests/Rector/__Name__/Fixture/some_class.php.inc rename to templates/custom-rule/utils/rector/tests/Rector/__Name__/Fixture/some_class.php.inc.phtml diff --git a/templates/custom-rule/utils/rector/tests/Rector/__Name__/__Name__Test.php b/templates/custom-rule/utils/rector/tests/Rector/__Name__/__Name__Test.php.phtml similarity index 100% rename from templates/custom-rule/utils/rector/tests/Rector/__Name__/__Name__Test.php rename to templates/custom-rule/utils/rector/tests/Rector/__Name__/__Name__Test.php.phtml diff --git a/templates/custom-rule/utils/rector/tests/Rector/__Name__/config/configured_rule.php b/templates/custom-rule/utils/rector/tests/Rector/__Name__/config/configured_rule.php.phtml similarity index 100% rename from templates/custom-rule/utils/rector/tests/Rector/__Name__/config/configured_rule.php rename to templates/custom-rule/utils/rector/tests/Rector/__Name__/config/configured_rule.php.phtml 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.phtml similarity index 100% rename from templates/custom-rules-annotations/utils/rector/tests/Rector/__Name__/__Name__Test.php rename to templates/custom-rules-annotations/utils/rector/tests/Rector/__Name__/__Name__Test.php.phtml