[RectorGenerator] Refactoring to testable code

This commit is contained in:
TomasVotruba 2020-07-29 17:54:52 +02:00
parent f4c741f3f9
commit 2741893bbf
26 changed files with 808 additions and 165 deletions

View File

@ -171,6 +171,7 @@
"Rector\\JMS\\Tests\\": "rules/jms/tests",
"Rector\\Laravel\\Tests\\": "rules/laravel/tests",
"Rector\\Legacy\\Tests\\": "rules/legacy/tests",
"Rector\\RectorGenerator\\Tests\\": "packages/rector-generator/tests",
"Rector\\MagicDisclosure\\Tests\\": "rules/magic-disclosure/tests",
"Rector\\MysqlToMysqli\\Tests\\": "rules/mysql-to-mysqli/tests",
"Rector\\NetteTesterToPHPUnit\\Tests\\": "rules/nette-tester-to-phpunit/tests",
@ -262,7 +263,6 @@
"scripts": {
"complete-check": [
"@check-cs",
"bin/rector validate-fixtures --ansi",
"bin/rector validate-services-in-sets --ansi",
"bin/rector validate-sets --ansi",
"phpunit",

View File

@ -6,15 +6,14 @@ namespace Rector\RectorGenerator\Command;
use Nette\Utils\Strings;
use Rector\Core\Configuration\Option;
use Rector\Core\Exception\ShouldNotHappenException;
use Rector\RectorGenerator\Composer\ComposerPackageAutoloadUpdater;
use Rector\RectorGenerator\Config\ConfigFilesystem;
use Rector\RectorGenerator\Configuration\ConfigurationFactory;
use Rector\RectorGenerator\FileSystem\TemplateFileSystem;
use Rector\RectorGenerator\Finder\TemplateFinder;
use Rector\RectorGenerator\Generator\FileGenerator;
use Rector\RectorGenerator\Guard\OverrideGuard;
use Rector\RectorGenerator\TemplateFactory;
use Rector\RectorGenerator\TemplateVariablesFactory;
use Rector\RectorGenerator\ValueObject\Configuration;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
@ -23,20 +22,9 @@ use Symplify\PackageBuilder\Console\Command\CommandNaming;
use Symplify\PackageBuilder\Console\ShellCode;
use Symplify\PackageBuilder\Parameter\ParameterProvider;
use Symplify\SmartFileSystem\SmartFileInfo;
use Symplify\SmartFileSystem\SmartFileSystem;
final class CreateCommand extends Command
{
/**
* @var string
*/
private $testCasePath;
/**
* @var string[]
*/
private $generatedFiles = [];
/**
* @var SymfonyStyle
*/
@ -62,16 +50,6 @@ final class CreateCommand extends Command
*/
private $templateFinder;
/**
* @var TemplateFileSystem
*/
private $templateFileSystem;
/**
* @var TemplateFactory
*/
private $templateFactory;
/**
* @var ConfigFilesystem
*/
@ -82,26 +60,24 @@ final class CreateCommand extends Command
*/
private $overrideGuard;
/**
* @var SmartFileSystem
*/
private $smartFileSystem;
/**
* @var ParameterProvider
*/
private $parameterProvider;
/**
* @var FileGenerator
*/
private $fileGenerator;
public function __construct(
ComposerPackageAutoloadUpdater $composerPackageAutoloadUpdater,
ConfigFilesystem $configFilesystem,
ConfigurationFactory $configurationFactory,
FileGenerator $fileGenerator,
OverrideGuard $overrideGuard,
ParameterProvider $parameterProvider,
SmartFileSystem $smartFileSystem,
SymfonyStyle $symfonyStyle,
TemplateFactory $templateFactory,
TemplateFileSystem $templateFileSystem,
TemplateFinder $templateFinder,
TemplateVariablesFactory $templateVariablesFactory
) {
@ -112,12 +88,10 @@ final class CreateCommand extends Command
$this->templateVariablesFactory = $templateVariablesFactory;
$this->composerPackageAutoloadUpdater = $composerPackageAutoloadUpdater;
$this->templateFinder = $templateFinder;
$this->templateFileSystem = $templateFileSystem;
$this->templateFactory = $templateFactory;
$this->configFilesystem = $configFilesystem;
$this->overrideGuard = $overrideGuard;
$this->smartFileSystem = $smartFileSystem;
$this->parameterProvider = $parameterProvider;
$this->fileGenerator = $fileGenerator;
}
protected function configure(): void
@ -139,10 +113,13 @@ final class CreateCommand extends Command
$templateFileInfos = $this->templateFinder->find($configuration);
$targetDirectory = getcwd();
$isUnwantedOverride = $this->overrideGuard->isUnwantedOverride(
$templateFileInfos,
$templateVariables,
$configuration
$configuration->getPackage(),
$targetDirectory
);
if ($isUnwantedOverride) {
$this->symfonyStyle->warning('No files were changed');
@ -150,55 +127,52 @@ final class CreateCommand extends Command
return ShellCode::SUCCESS;
}
$this->generateFiles($templateFileInfos, $templateVariables, $configuration);
$generatedFilePaths = $this->fileGenerator->generateFiles(
$templateFileInfos,
$templateVariables,
$configuration,
$targetDirectory
);
$testCaseFilePath = $this->resolveTestCaseFilePath($generatedFilePaths);
$this->configFilesystem->appendRectorServiceToSet($configuration, $templateVariables);
$this->printSuccess($configuration->getName());
$this->printSuccess($configuration->getName(), $generatedFilePaths, $testCaseFilePath);
return ShellCode::SUCCESS;
}
/**
* @param SmartFileInfo[] $templateFileInfos
* @param string[] $generatedFilePaths
*/
private function generateFiles(
array $templateFileInfos,
array $templateVariables,
Configuration $configuration
): void {
foreach ($templateFileInfos as $smartFileInfo) {
$destination = $this->templateFileSystem->resolveDestination(
$smartFileInfo,
$templateVariables,
$configuration
);
$content = $this->templateFactory->create($smartFileInfo->getContents(), $templateVariables);
$this->smartFileSystem->dumpFile($destination, $content);
$this->generatedFiles[] = $destination;
// is a test case?
if (Strings::endsWith($destination, 'Test.php')) {
$this->testCasePath = dirname($destination);
}
}
}
private function printSuccess(string $name): void
private function printSuccess(string $name, array $generatedFilePaths, string $testCaseFilePath): void
{
$message = sprintf('New files generated for "%s"', $name);
$message = sprintf('New files generated for "%s":', $name);
$this->symfonyStyle->title($message);
sort($this->generatedFiles);
$this->symfonyStyle->listing($this->generatedFiles);
$message = sprintf(
'Make tests green again:%svendor/bin/phpunit %s',
PHP_EOL . PHP_EOL,
$this->testCasePath
);
sort($generatedFilePaths);
$this->symfonyStyle->listing($generatedFilePaths);
$message = sprintf('Make tests green again:%svendor/bin/phpunit %s', PHP_EOL . PHP_EOL, $testCaseFilePath);
$this->symfonyStyle->success($message);
}
/**
* @param string[] $generatedFilePaths
*/
private function resolveTestCaseFilePath(array $generatedFilePaths): string
{
foreach ($generatedFilePaths as $generatedFilePath) {
if (! Strings::endsWith($generatedFilePath, 'Test.php')) {
continue;
}
$generatedFileInfo = new SmartFileInfo($generatedFilePath);
return $generatedFileInfo->getRelativeFilePathFromCwd();
}
throw new ShouldNotHappenException();
}
}

View File

@ -35,11 +35,6 @@ final class ComposerPackageAutoloadUpdater
public function processComposerAutoload(Configuration $configuration): void
{
// skip core, already autoloaded
if ($configuration->getPackage() === 'Rector') {
return;
}
$composerJsonFilePath = getcwd() . '/composer.json';
$composerJson = $this->jsonFileSystem->loadFileToJson($composerJsonFilePath);
@ -51,10 +46,10 @@ final class ComposerPackageAutoloadUpdater
// ask user
$questionText = sprintf(
'Can we update "composer.json" autoload with "%s" namespace?%s Handle it manually o therwise',
'Should we update "composer.json" autoload with "%s" namespace?',
$package->getSrcNamespace(),
PHP_EOL
);
$isConfirmed = $this->symfonyStyle->confirm($questionText);
if (! $isConfirmed) {
return;

View File

@ -20,7 +20,7 @@ final class ConfigFilesystem
/**
* @var string
*/
private const RECTOR_FQN_NAME_PATTERN = 'Rector\__Package__\Rector\__Category__\__Name__';
public const RECTOR_FQN_NAME_PATTERN = 'Rector\__Package__\Rector\__Category__\__Name__';
/**
* @var TemplateFactory

View File

@ -6,7 +6,6 @@ namespace Rector\RectorGenerator\FileSystem;
use Nette\Utils\Strings;
use Rector\RectorGenerator\Finder\TemplateFinder;
use Rector\RectorGenerator\ValueObject\Configuration;
use Rector\RectorGenerator\ValueObject\Package;
use Symplify\SmartFileSystem\SmartFileInfo;
@ -18,19 +17,22 @@ final class TemplateFileSystem
public function resolveDestination(
SmartFileInfo $smartFileInfo,
array $templateVariables,
Configuration $configuration
string $package,
string $targetDirectory
): string {
$destination = $smartFileInfo->getRelativeFilePathFromDirectory(TemplateFinder::TEMPLATES_DIRECTORY);
// normalize core package
if ($configuration->getPackage() === Package::UTILS) {
if ($package === Package::UTILS) {
// special keyword for 3rd party Rectors, not for core Github contribution
$destination = Strings::replace($destination, '#packages\/__Package__#', 'utils/rector');
}
// remove _Configured|_Extra prefix
$destination = $this->applyVariables($destination, $templateVariables);
return Strings::replace($destination, '#(__Configured|__Extra)#', '');
$destination = Strings::replace($destination, '#(__Configured|__Extra)#', '');
return $targetDirectory . DIRECTORY_SEPARATOR . $destination;
}
/**

View File

@ -0,0 +1,82 @@
<?php
declare(strict_types=1);
namespace Rector\RectorGenerator\Generator;
use Rector\RectorGenerator\FileSystem\TemplateFileSystem;
use Rector\RectorGenerator\TemplateFactory;
use Rector\RectorGenerator\ValueObject\Configuration;
use Symplify\SmartFileSystem\SmartFileInfo;
use Symplify\SmartFileSystem\SmartFileSystem;
final class FileGenerator
{
/**
* @var TemplateFileSystem
*/
private $templateFileSystem;
/**
* @var TemplateFactory
*/
private $templateFactory;
/**
* @var SmartFileSystem
*/
private $smartFileSystem;
public function __construct(
SmartFileSystem $smartFileSystem,
TemplateFactory $templateFactory,
TemplateFileSystem $templateFileSystem
) {
$this->templateFileSystem = $templateFileSystem;
$this->templateFactory = $templateFactory;
$this->smartFileSystem = $smartFileSystem;
}
/**
* @param string[] $templateVariables
* @return string[]
*/
public function generateFiles(
array $templateFileInfos,
array $templateVariables,
Configuration $configuration,
string $destinationDirectory
): array {
$generatedFilePaths = [];
foreach ($templateFileInfos as $fileInfo) {
$generatedFilePaths[] = $this->generateFileInfoWithTemplateVariables(
$fileInfo,
$templateVariables,
$configuration->getPackage(),
$destinationDirectory
);
}
return $generatedFilePaths;
}
private function generateFileInfoWithTemplateVariables(
SmartFileInfo $smartFileInfo,
array $templateVariables,
string $package,
string $targetDirectory
): string {
$targetFilePath = $this->templateFileSystem->resolveDestination(
$smartFileInfo,
$templateVariables,
$package,
$targetDirectory
);
$content = $this->templateFactory->create($smartFileInfo->getContents(), $templateVariables);
$this->smartFileSystem->dumpFile($targetFilePath, $content);
return $targetFilePath;
}
}

View File

@ -5,7 +5,6 @@ declare(strict_types=1);
namespace Rector\RectorGenerator\Guard;
use Rector\RectorGenerator\FileSystem\TemplateFileSystem;
use Rector\RectorGenerator\ValueObject\Configuration;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symplify\SmartFileSystem\SmartFileInfo;
@ -33,10 +32,11 @@ final class OverrideGuard
public function isUnwantedOverride(
array $templateFileInfos,
array $templateVariables,
Configuration $configuration
string $package,
string $targetDirectory
): bool {
foreach ($templateFileInfos as $templateFileInfo) {
if (! $this->doesFileInfoAlreadyExist($templateVariables, $configuration, $templateFileInfo)) {
if (! $this->doesFileInfoAlreadyExist($templateVariables, $package, $templateFileInfo, $targetDirectory)) {
continue;
}
@ -48,13 +48,15 @@ final class OverrideGuard
private function doesFileInfoAlreadyExist(
array $templateVariables,
Configuration $configuration,
SmartFileInfo $templateFileInfo
string $package,
SmartFileInfo $templateFileInfo,
string $targetDirectory
): bool {
$destination = $this->templateFileSystem->resolveDestination(
$templateFileInfo,
$templateVariables,
$configuration
$package,
$targetDirectory
);
return file_exists($destination);

View File

@ -5,17 +5,27 @@ declare(strict_types=1);
namespace Rector\RectorGenerator\NodeFactory;
use PhpParser\Node\Expr\Array_;
use PhpParser\Node\Expr\ArrayDimFetch;
use PhpParser\Node\Expr\BinaryOp\Coalesce;
use PhpParser\Node\Expr\ClassConstFetch;
use PhpParser\Node\Expr\Variable;
use PhpParser\Node\Identifier;
use PhpParser\Node\Name;
use PhpParser\Node\Param;
use PhpParser\Node\Stmt\ClassConst;
use PhpParser\Node\Stmt\ClassMethod;
use PhpParser\Node\Stmt\Expression;
use PhpParser\Node\Stmt\Property;
use PHPStan\PhpDocParser\Ast\PhpDoc\ParamTagValueNode;
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
use PHPStan\Type\ArrayType;
use PHPStan\Type\FloatType;
use PHPStan\Type\IntegerType;
use PHPStan\Type\MixedType;
use PHPStan\Type\StringType;
use PHPStan\Type\Type;
use Rector\Core\Exception\NotImplementedYetException;
use Rector\BetterPhpDocParser\PhpDocInfo\PhpDocInfoFactory;
use Rector\Core\Configuration\Option;
use Rector\Core\PhpParser\Node\NodeFactory;
use Rector\Core\Util\StaticRectorStrings;
use Rector\Core\ValueObject\PhpVersionFeature;
use Symplify\PackageBuilder\Parameter\ParameterProvider;
final class ConfigurationNodeFactory
{
@ -24,9 +34,21 @@ final class ConfigurationNodeFactory
*/
private $nodeFactory;
public function __construct(NodeFactory $nodeFactory)
{
/**
* @var PhpDocInfoFactory
*/
private $phpDocInfoFactory;
public function __construct(
NodeFactory $nodeFactory,
ParameterProvider $parameterProvider,
PhpDocInfoFactory $phpDocInfoFactory
) {
$this->nodeFactory = $nodeFactory;
$this->phpDocInfoFactory = $phpDocInfoFactory;
// so types are PHP 7.2 compatible
$parameterProvider->changeParameter(Option::PHP_VERSION_FEATURES, PhpVersionFeature::BEFORE_TYPED_PROPERTIES);
}
/**
@ -36,10 +58,13 @@ final class ConfigurationNodeFactory
public function createProperties(array $ruleConfiguration): array
{
$properties = [];
foreach (array_keys($ruleConfiguration) as $variable) {
$variable = ltrim($variable, '$');
foreach (array_keys($ruleConfiguration) as $constantName) {
$propertyName = StaticRectorStrings::uppercaseUnderscoreToPascalCase($constantName);
$type = new ArrayType(new MixedType(), new MixedType());
$properties[] = $this->nodeFactory->createPrivatePropertyFromNameAndType($variable, $type);
$property = $this->nodeFactory->createPrivatePropertyFromNameAndType($propertyName, $type);
$property->props[0]->default = new Array_([]);
$properties[] = $property;
}
return $properties;
@ -47,57 +72,63 @@ final class ConfigurationNodeFactory
/**
* @param array<string, mixed> $ruleConfiguration
* @return ClassConst[]
*/
public function createConstructorClassMethod(array $ruleConfiguration): ClassMethod
public function createConfigurationConstants(array $ruleConfiguration): array
{
$classMethod = $this->nodeFactory->createPublicMethod('__construct');
$classConsts = [];
$assigns = [];
$params = [];
foreach ($ruleConfiguration as $variable => $values) {
$variable = ltrim($variable, '$');
$assign = $this->nodeFactory->createPropertyAssignment($variable);
$assigns[] = new Expression($assign);
$type = $this->resolveParamType($values);
$param = $this->nodeFactory->createParamFromNameAndType($variable, $type);
if ($type instanceof ArrayType) {
// add default for fast testing property set mgaic purposes - @todo refactor for cleaner way later
$param->default = new Array_([]);
}
$params[] = $param;
foreach (array_keys($ruleConfiguration) as $constantName) {
$constantValue = strtolower($constantName);
$classConst = $this->nodeFactory->createPublicClassConst($constantName, $constantValue);
$classConsts[] = $classConst;
}
return $classConsts;
}
/**
* @param array<string, mixed> $ruleConfiguration
*/
public function createConfigureClassMethod(array $ruleConfiguration): ClassMethod
{
$classMethod = $this->nodeFactory->createPublicMethod('configure');
$classMethod->returnType = new Identifier('void');
$configurationVariable = new Variable('configuration');
$configurationParam = new Param($configurationVariable);
$configurationParam->type = new Identifier('array');
$classMethod->params[] = $configurationParam;
$assigns = [];
foreach (array_keys($ruleConfiguration) as $constantName) {
$coalesce = $this->createConstantInConfigurationCoalesce($constantName, $configurationVariable);
$propertyName = StaticRectorStrings::uppercaseUnderscoreToPascalCase($constantName);
$assign = $this->nodeFactory->createPropertyAssignmentWithExpr($propertyName, $coalesce);
$assigns[] = new Expression($assign);
}
$classMethod->params = $params;
$classMethod->stmts = $assigns;
$phpDocInfo = $this->phpDocInfoFactory->createEmpty($classMethod);
$identifierTypeNode = new IdentifierTypeNode('mixed[]');
$paramTagValueNode = new ParamTagValueNode($identifierTypeNode, false, '$configuration', '');
$phpDocInfo->addTagValueNode($paramTagValueNode);
return $classMethod;
}
/**
* @param mixed $value
*/
private function resolveParamType($value): Type
{
if (is_array($value)) {
return new ArrayType(new MixedType(), new MixedType());
}
private function createConstantInConfigurationCoalesce(
string $constantName,
Variable $configurationVariable
): Coalesce {
$classConstFetch = new ClassConstFetch(new Name('self'), $constantName);
$arrayDimFetch = new ArrayDimFetch($configurationVariable, $classConstFetch);
if (is_string($value)) {
return new StringType();
}
$emptyArray = new Array_([]);
if (is_int($value)) {
return new IntegerType();
}
if (is_float($value)) {
return new FloatType();
}
throw new NotImplementedYetException();
return new Coalesce($arrayDimFetch, $emptyArray);
}
}

View File

@ -5,13 +5,34 @@ declare(strict_types=1);
namespace Rector\RectorGenerator;
use Nette\Utils\Strings;
use PhpParser\Node\Expr\Array_;
use PhpParser\Node\Expr\ArrayItem;
use PhpParser\Node\Expr\ClassConstFetch;
use PhpParser\Node\Name;
use PhpParser\Node\Name\FullyQualified;
use Rector\Core\PhpParser\Node\NodeFactory;
use Rector\Core\PhpParser\Printer\BetterStandardPrinter;
use Rector\RectorGenerator\Config\ConfigFilesystem;
use Rector\RectorGenerator\NodeFactory\ConfigurationNodeFactory;
use Rector\RectorGenerator\ValueObject\Configuration;
final class TemplateVariablesFactory
{
/**
* @var string
*/
private const SELF = 'self';
/**
* @var string
*/
private const VARIABLE_PACKAGE = '__Package__';
/**
* @var string
*/
private const VARIABLE_PACKAGE_LOWERCASE = '__package__';
/**
* @var BetterStandardPrinter
*/
@ -27,24 +48,31 @@ final class TemplateVariablesFactory
*/
private $configurationNodeFactory;
/**
* @var TemplateFactory
*/
private $templateFactory;
public function __construct(
BetterStandardPrinter $betterStandardPrinter,
ConfigurationNodeFactory $configurationNodeFactory,
NodeFactory $nodeFactory
NodeFactory $nodeFactory,
TemplateFactory $templateFactory
) {
$this->betterStandardPrinter = $betterStandardPrinter;
$this->nodeFactory = $nodeFactory;
$this->configurationNodeFactory = $configurationNodeFactory;
$this->templateFactory = $templateFactory;
}
/**
* @return mixed[]
* @return string[]
*/
public function createFromConfiguration(Configuration $configuration): array
{
$data = [
'__Package__' => $configuration->getPackage(),
'__package__' => $configuration->getPackageDirectory(),
self::VARIABLE_PACKAGE => $configuration->getPackage(),
self::VARIABLE_PACKAGE_LOWERCASE => $configuration->getPackageDirectory(),
'__Category__' => $configuration->getCategory(),
'__Description__' => $configuration->getDescription(),
'__Name__' => $configuration->getName(),
@ -55,13 +83,28 @@ final class TemplateVariablesFactory
'__Source__' => $this->createSourceDocBlock($configuration->getSource()),
];
$rectorClass = $this->templateFactory->create(ConfigFilesystem::RECTOR_FQN_NAME_PATTERN, $data);
$data['__RectorClass_'] = $rectorClass;
if ($configuration->getRuleConfiguration() !== []) {
$data['__RuleConfiguration__'] = $this->createRuleConfiguration($configuration->getRuleConfiguration());
$data['__ConfigurationProperty__'] = $this->createConfigurationProperty(
$data['__TestRuleConfiguration__'] = $this->createRuleConfiguration(
$data['__RectorClass_'],
$configuration->getRuleConfiguration()
);
$data['__RuleConfiguration__'] = $this->createRuleConfiguration(
self::SELF,
$configuration->getRuleConfiguration()
);
$data['__ConfigurationConstructor__'] = $this->createConfigurationConstructor(
$data['__ConfigurationProperties__'] = $this->createConfigurationProperty(
$configuration->getRuleConfiguration()
);
$data['__ConfigurationConstants__'] = $this->createConfigurationConstants(
$configuration->getRuleConfiguration()
);
$data['__ConfigureClassMethod__'] = $this->createConfigureClassMethod(
$configuration->getRuleConfiguration()
);
}
@ -74,8 +117,8 @@ final class TemplateVariablesFactory
);
}
$data['__NodeTypes_Php__'] = $this->createNodeTypePhp($configuration);
$data['__NodeTypes_Doc__'] = '\\' . implode('|\\', $configuration->getNodeTypes());
$data['__NodeTypesPhp__'] = $this->createNodeTypePhp($configuration);
$data['__NodeTypesDoc__'] = '\\' . implode('|\\', $configuration->getNodeTypes());
return $data;
}
@ -124,9 +167,20 @@ final class TemplateVariablesFactory
/**
* @param mixed[] $configuration
*/
private function createRuleConfiguration(array $configuration): string
private function createRuleConfiguration(string $rectorClass, array $configuration): string
{
$array = $this->nodeFactory->createArray($configuration);
$arrayItems = [];
foreach ($configuration as $constantName => $variableConfiguration) {
if ($rectorClass === self::SELF) {
$class = new Name(self::SELF);
} else {
$class = new FullyQualified($rectorClass);
}
$classConstFetch = new ClassConstFetch($class, $constantName);
$arrayItems[] = new ArrayItem($this->nodeFactory->createArray($variableConfiguration), $classConstFetch);
}
$array = new Array_($arrayItems);
return $this->betterStandardPrinter->print($array);
}
@ -142,9 +196,18 @@ final class TemplateVariablesFactory
/**
* @param array<string, mixed> $ruleConfiguration
*/
private function createConfigurationConstructor(array $ruleConfiguration): string
private function createConfigureClassMethod(array $ruleConfiguration): string
{
$classMethod = $this->configurationNodeFactory->createConstructorClassMethod($ruleConfiguration);
$classMethod = $this->configurationNodeFactory->createConfigureClassMethod($ruleConfiguration);
return $this->betterStandardPrinter->print($classMethod);
}
/**
* @param array<string, mixed> $ruleConfiguration
*/
private function createConfigurationConstants(array $ruleConfiguration): string
{
$configurationConstants = $this->configurationNodeFactory->createConfigurationConstants($ruleConfiguration);
return $this->betterStandardPrinter->print($configurationConstants);
}
}

View File

@ -5,6 +5,7 @@ declare(strict_types=1);
namespace Rector\RectorGenerator\ValueObject;
use Nette\Utils\Strings;
use Rector\Core\Exception\ShouldNotHappenException;
use Rector\Core\Util\StaticRectorStrings;
use Symplify\SetConfigResolver\ValueObject\Set;
use Symplify\SmartFileSystem\SmartFileInfo;
@ -201,7 +202,8 @@ final class Configuration
private function setName(string $name): void
{
if (! Strings::endsWith($name, 'Rector')) {
$name .= 'Rector';
$message = sprintf('Rector name "%s" must end with "Rector"', $name);
throw new ShouldNotHappenException($message);
}
$this->name = $name;

View File

@ -5,6 +5,7 @@ declare(strict_types=1);
namespace Rector\__Package__\Rector\__Category__;
use PhpParser\Node;
use Rector\Core\Contract\Rector\ConfigurableRectorInterface;
use Rector\Core\Rector\AbstractRector;
use Rector\Core\RectorDefinition\ConfiguredCodeSample;
use Rector\Core\RectorDefinition\RectorDefinition;
@ -13,11 +14,11 @@ use Rector\Core\RectorDefinition\RectorDefinition;
__Source__
* @see \Rector\__Package__\Tests\Rector\__Category__\__Name__\__Name__Test
*/
final class __Name__ extends AbstractRector
final class __Name__ extends AbstractRector implements ConfigurableRectorInterface
{
__ConfigurationProperty__
__ConfigurationConstants__
__ConfigurationConstructor__
__ConfigurationProperties__
public function getDefinition(): RectorDefinition
{
@ -35,11 +36,11 @@ final class __Name__ extends AbstractRector
*/
public function getNodeTypes(): array
{
return __NodeTypes_Php__;
return __NodeTypesPhp__;
}
/**
* @param __NodeTypes_Doc__ $node
* @param __NodeTypesDoc__ $node
*/
public function refactor(Node $node): ?Node
{
@ -47,4 +48,6 @@ final class __Name__ extends AbstractRector
return $node;
}
__ConfigureClassMethod__
}

View File

@ -30,11 +30,11 @@ final class __Name__ extends AbstractRector
*/
public function getNodeTypes(): array
{
return __NodeTypes_Php__;
return __NodeTypesPhp__;
}
/**
* @param __NodeTypes_Doc__ $node
* @param __NodeTypesDoc__ $node
*/
public function refactor(Node $node): ?Node
{

View File

@ -26,7 +26,7 @@ final class __Name__ExtraTest extends AbstractRectorTestCase
{
return [
\Rector\__Package__\Rector\__Category__\__Name__::class =>
__RuleConfiguration__
__TestRuleConfiguration__
];
}
}

View File

@ -25,7 +25,7 @@ final class __Name__Test extends AbstractRectorTestCase
{
return [
\Rector\__Package__\Rector\__Category__\__Name__::class =>
__RuleConfiguration__
__TestRuleConfiguration__
];
}
}

View File

@ -0,0 +1,113 @@
<?php
declare(strict_types=1);
namespace Rector\RectorGenerator\Tests\PHPUnit\Behavior;
use Rector\RectorGenerator\Tests\PHPUnit\ValueObject\ExpectedAndOutputFileInfoPair;
use Symfony\Component\Finder\Finder;
use Symplify\SmartFileSystem\Finder\FinderSanitizer;
use Symplify\SmartFileSystem\SmartFileInfo;
/**
* Use only in "\PHPUnit\Framework\TestCase"
*
* Answer here
* @see https://stackoverflow.com/questions/54263109/how-to-assert-2-directories-are-identical-in-phpunit
*/
trait DirectoryAssertableTrait
{
protected function assertDirectoryEquals(string $expectedDirectory, string $outputDirectory): void
{
$expectedFileInfos = $this->findFileInfosInDirectory($expectedDirectory);
$outputFileInfos = $this->findFileInfosInDirectory($outputDirectory);
$fileInfosByRelativeFilePath = $this->groupFileInfosByRelativeFilePath(
$expectedFileInfos,
$expectedDirectory,
$outputFileInfos,
$outputDirectory
);
foreach ($fileInfosByRelativeFilePath as $relativeFilePath => $expectedAndOutputFileInfoPair) {
// output file exists
$this->assertFileExists($outputDirectory . '/' . $relativeFilePath);
if (! $expectedAndOutputFileInfoPair->doesOutputFileExist()) {
continue;
}
// they have the same content
$this->assertSame(
$expectedAndOutputFileInfoPair->getExpectedFileContent(),
$expectedAndOutputFileInfoPair->getOutputFileContent(),
$relativeFilePath
);
}
}
/**
* @return SmartFileInfo[]
*/
private function findFileInfosInDirectory(string $directory): array
{
$firstDirectoryFinder = new Finder();
$firstDirectoryFinder->files()
->in($directory);
$finderSanitizer = new FinderSanitizer();
return $finderSanitizer->sanitize($firstDirectoryFinder);
}
/**
* @param SmartFileInfo[] $expectedFileInfos
* @param SmartFileInfo[] $outputFileInfos
* @return ExpectedAndOutputFileInfoPair[]
*/
private function groupFileInfosByRelativeFilePath(
array $expectedFileInfos,
string $expectedDirectory,
array $outputFileInfos,
string $outputDirectory
): array {
$fileInfosByRelativeFilePath = [];
foreach ($expectedFileInfos as $expectedFileInfo) {
$relativeFilePath = $expectedFileInfo->getRelativeFilePathFromDirectory($expectedDirectory);
// match output file info
$outputFileInfo = $this->resolveFileInfoByRelativeFilePath(
$outputFileInfos,
$outputDirectory,
$relativeFilePath
);
$fileInfosByRelativeFilePath[$relativeFilePath] = new ExpectedAndOutputFileInfoPair(
$expectedFileInfo,
$outputFileInfo
);
}
return $fileInfosByRelativeFilePath;
}
/**
* @param SmartFileInfo[] $fileInfos
*/
private function resolveFileInfoByRelativeFilePath(
array $fileInfos,
string $directory,
string $desiredRelativeFilePath
): ?SmartFileInfo {
foreach ($fileInfos as $fileInfo) {
$relativeFilePath = $fileInfo->getRelativeFilePathFromDirectory($directory);
if ($desiredRelativeFilePath !== $relativeFilePath) {
continue;
}
return $fileInfo;
}
return null;
}
}

View File

@ -0,0 +1,55 @@
<?php
declare(strict_types=1);
namespace Rector\RectorGenerator\Tests\PHPUnit\ValueObject;
use Rector\Core\Exception\ShouldNotHappenException;
use Symplify\SmartFileSystem\SmartFileInfo;
final class ExpectedAndOutputFileInfoPair
{
/**
* @var SmartFileInfo
*/
private $expectedFileInfo;
/**
* @var SmartFileInfo|null
*/
private $outputFileInfo;
public function __construct(SmartFileInfo $expectedFileInfo, ?SmartFileInfo $outputFileInfo)
{
$this->expectedFileInfo = $expectedFileInfo;
$this->outputFileInfo = $outputFileInfo;
}
/**
* @noRector \Rector\Privatization\Rector\ClassMethod\PrivatizeLocalOnlyMethodRector
*/
public function getExpectedFileContent(): string
{
return $this->expectedFileInfo->getContents();
}
/**
* @noRector \Rector\Privatization\Rector\ClassMethod\PrivatizeLocalOnlyMethodRector
*/
public function getOutputFileContent(): string
{
if ($this->outputFileInfo === null) {
throw new ShouldNotHappenException();
}
return $this->outputFileInfo->getContents();
}
/**
* @noRector \Rector\Privatization\Rector\ClassMethod\PrivatizeLocalOnlyMethodRector
*/
public function doesOutputFileExist(): bool
{
return $this->outputFileInfo !== null;
}
}

View File

@ -0,0 +1,87 @@
<?php
declare(strict_types=1);
namespace Rector\Symfony\Rector\MethodCall;
use PhpParser\Node;
use Rector\Core\Contract\Rector\ConfigurableRectorInterface;
use Rector\Core\Rector\AbstractRector;
use Rector\Core\RectorDefinition\ConfiguredCodeSample;
use Rector\Core\RectorDefinition\RectorDefinition;
/**
* @see \Rector\Symfony\Tests\Rector\MethodCall\ChangeServiceArgumentsToMethodCallRector\ChangeServiceArgumentsToMethodCallRectorTest
*/
final class ChangeServiceArgumentsToMethodCallRector extends AbstractRector implements ConfigurableRectorInterface
{
/**
* @var string
*/
public const CLASS_TYPE_TO_METHOD_NAME = 'class_type_to_method_name';
/**
* @var mixed[]
*/
private $classTypeToMethodName = [];
public function getDefinition(): RectorDefinition
{
return new RectorDefinition('Change $service->arg(...) to $service->call(...)', [
new ConfiguredCodeSample(
<<<'PHP'
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
return static function (ContainerConfigurator $containerConfigurator): void {
$services = $containerConfigurator->services();
$services->set(SomeClass::class)
->arg('$key', 'value');
}
PHP
,
<<<'PHP'
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
return static function (ContainerConfigurator $containerConfigurator): void {
$services = $containerConfigurator->services();
$services->set(SomeClass::class)
->call('configure', [[
'$key' => 'value'
]]);
}
PHP
,
[self::CLASS_TYPE_TO_METHOD_NAME => ['SomeClass' => 'configure']]
)
]);
}
/**
* @return string[]
*/
public function getNodeTypes(): array
{
return [\PhpParser\Node\Expr\MethodCall::class];
}
/**
* @param \PhpParser\Node\Expr\MethodCall $node
*/
public function refactor(Node $node): ?Node
{
// change the node
return $node;
}
/**
* @param mixed[] $configuration
*/
public function configure(array $configuration): void
{
$this->classTypeToMethodName = $configuration[self::CLASS_TYPE_TO_METHOD_NAME] ?? [];
}
}

View File

@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace Rector\Symfony\Tests\Rector\MethodCall\ChangeServiceArgumentsToMethodCallRector;
use Rector\Core\Testing\PHPUnit\AbstractRectorTestCase;
final class ChangeServiceArgumentsToMethodCallRectorTest extends AbstractRectorTestCase
{
/**
* @dataProvider provideData()
*/
public function test(\Symplify\SmartFileSystem\SmartFileInfo $fileInfo): void
{
$this->doTestFileInfo($fileInfo);
}
public function provideData(): \Iterator
{
return $this->yieldFilesFromDirectory(__DIR__ . '/Fixture');
}
protected function getRectorsWithConfiguration(): array
{
return [
\Rector\Symfony\Rector\MethodCall\ChangeServiceArgumentsToMethodCallRector::class =>
[\Rector\Symfony\Rector\MethodCall\ChangeServiceArgumentsToMethodCallRector::CLASS_TYPE_TO_METHOD_NAME => ['SomeClass' => 'configure']]
];
}
}

View File

@ -0,0 +1,31 @@
<?php
namespace Rector\Symfony\Tests\Rector\MethodCall\ChangeServiceArgumentsToMethodCallRector\Fixture;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
return static function (ContainerConfigurator $containerConfigurator): void {
$services = $containerConfigurator->services();
$services->set(SomeClass::class)
->arg('$key', 'value');
}
?>
-----
<?php
namespace Rector\Symfony\Tests\Rector\MethodCall\ChangeServiceArgumentsToMethodCallRector\Fixture;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
return static function (ContainerConfigurator $containerConfigurator): void {
$services = $containerConfigurator->services();
$services->set(SomeClass::class)
->call('configure', [[
'$key' => 'value'
]]);
}
?>

View File

@ -0,0 +1,93 @@
<?php
declare(strict_types=1);
namespace Rector\RectorGenerator\Tests\RectorGenerator;
use Rector\Core\HttpKernel\RectorKernel;
use Rector\RectorGenerator\Configuration\ConfigurationFactory;
use Rector\RectorGenerator\Finder\TemplateFinder;
use Rector\RectorGenerator\Generator\FileGenerator;
use Rector\RectorGenerator\TemplateVariablesFactory;
use Rector\RectorGenerator\Tests\PHPUnit\Behavior\DirectoryAssertableTrait;
use Rector\RectorGenerator\Tests\RectorGenerator\Source\StaticRectorRecipeFactory;
use Rector\RectorGenerator\ValueObject\Configuration;
use Symplify\PackageBuilder\Tests\AbstractKernelTestCase;
use Symplify\SmartFileSystem\SmartFileSystem;
final class RectorGeneratorTest extends AbstractKernelTestCase
{
use DirectoryAssertableTrait;
/**
* @var string
*/
private const DESTINATION_DIRECTORY = __DIR__ . '/__temp';
/**
* @var ConfigurationFactory
*/
private $configurationFactory;
/**
* @var TemplateVariablesFactory
*/
private $templateVariablesFactory;
/**
* @var TemplateFinder
*/
private $templateFinder;
/**
* @var FileGenerator
*/
private $fileGenerator;
/**
* @var SmartFileSystem
*/
private $smartFileSystem;
protected function setUp(): void
{
$this->bootKernel(RectorKernel::class);
$this->configurationFactory = self::$container->get(ConfigurationFactory::class);
$this->templateVariablesFactory = self::$container->get(TemplateVariablesFactory::class);
$this->templateFinder = self::$container->get(TemplateFinder::class);
$this->fileGenerator = self::$container->get(FileGenerator::class);
$this->smartFileSystem = self::$container->get(SmartFileSystem::class);
}
protected function tearDown(): void
{
// cleanup temporary data
$this->smartFileSystem->remove(self::DESTINATION_DIRECTORY);
}
public function test(): void
{
$configuration = $this->createConfiguration();
$templateFileInfos = $this->templateFinder->find($configuration);
$templateVariables = $this->templateVariablesFactory->createFromConfiguration($configuration);
$this->fileGenerator->generateFiles(
$templateFileInfos,
$templateVariables,
$configuration,
self::DESTINATION_DIRECTORY
);
// @todo decouple to EasyTesting
$this->assertDirectoryEquals(__DIR__ . '/Fixture/expected', self::DESTINATION_DIRECTORY);
}
private function createConfiguration(): Configuration
{
$rectorRecipe = StaticRectorRecipeFactory::createWithConfiguration();
return $this->configurationFactory->createFromRectorRecipe($rectorRecipe);
}
}

View File

@ -0,0 +1,64 @@
<?php
declare(strict_types=1);
namespace Rector\RectorGenerator\Tests\RectorGenerator\Source;
use PhpParser\Node\Expr\MethodCall;
use Rector\RectorGenerator\ValueObject\RecipeOption;
final class StaticRectorRecipeFactory
{
public static function createWithConfiguration(): array
{
return [
RecipeOption::PACKAGE => 'Symfony',
RecipeOption::NAME => 'ChangeServiceArgumentsToMethodCallRector',
RecipeOption::NODE_TYPES => [
MethodCall::class,
],
RecipeOption::DESCRIPTION => 'Change $service->arg(...) to $service->call(...)',
RecipeOption::CODE_BEFORE => <<<'CODE_SAMPLE'
<?php
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
return static function (ContainerConfigurator $containerConfigurator): void {
$services = $containerConfigurator->services();
$services->set(SomeClass::class)
->arg('$key', 'value');
}
CODE_SAMPLE,
RecipeOption::CODE_AFTER => <<<'CODE_SAMPLE'
<?php
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
return static function (ContainerConfigurator $containerConfigurator): void {
$services = $containerConfigurator->services();
$services->set(SomeClass::class)
->call('configure', [[
'$key' => 'value'
]]);
}
CODE_SAMPLE,
// e.g. link to RFC or headline in upgrade guide, 1 or more in the list
RecipeOption::SOURCE => null,
// e.g. symfony30, target set to add this Rule to; keep null if part of core
RecipeOption::SET => null,
// OPTIONAL: only when configured
RecipeOption::RULE_CONFIGURATION => [
'CLASS_TYPE_TO_METHOD_NAME' => [
'SomeClass' => 'configure'
]
],
// OPTIONAL: extra file
RecipeOption::EXTRA_FILE_NAME => null,
RecipeOption::EXTRA_FILE_CONTENT => null,
];
}
}

View File

@ -14,6 +14,8 @@
<directory>packages/*/tests</directory>
<directory>tests</directory>
<directory>utils/*/tests</directory>
<exclude>packages/rector-generator/templates</exclude>
<exclude>packages/rector-generator/tests/RectorGenerator/Fixture</exclude>
</testsuite>
</testsuites>

View File

@ -94,6 +94,7 @@ final class RectorsFinder
$robotLoader->setTempDirectory(sys_get_temp_dir() . '/_rector_finder');
$robotLoader->acceptFiles = [$name];
$robotLoader->excludeDirectory(__DIR__ . '/../../../packages/rector-generator/tests');
$robotLoader->rebuild();
return array_keys($robotLoader->getIndexedClasses());

View File

@ -57,6 +57,12 @@ final class StaticRectorStrings
return lcfirst($string);
}
public static function uppercaseUnderscoreToPascalCase(string $input): string
{
$input = strtolower($input);
return self::underscoreToPascalCase($input);
}
public static function underscoreToCamelCase(string $input): string
{
$nameParts = explode('_', $input);

View File

@ -136,6 +136,11 @@ final class PhpVersionFeature
*/
public const BEFORE_UNION_TYPES = '7.4';
/**
* @var string
*/
public const BEFORE_TYPED_PROPERTIES = '7.3';
/**
* @see https://wiki.php.net/rfc/covariant-returns-and-contravariant-parameters
* @var string

View File

@ -77,6 +77,7 @@ final class ValidateFixtureSuffixCommand extends Command
->notPath('#TagValueNodeReprint#')
->notPath('#PhpSpecToPHPUnitRector#')
->notPath('#Source#')
->notPath('#expected#')
->notPath('DoctrineRepositoryAsService/Fixture/PostController.php')
->notPath('Namespace_/ImportFullyQualifiedNamesRector/Fixture/SharedShortName.php')
->notPath('Class_/RenameClassRector/Fixture/DuplicatedClass.php')