diff --git a/packages/NodeTypeResolver/NodeScopeAndMetadataDecorator.php b/packages/NodeTypeResolver/NodeScopeAndMetadataDecorator.php index 379d2a6691d..d9246a71cd6 100644 --- a/packages/NodeTypeResolver/NodeScopeAndMetadataDecorator.php +++ b/packages/NodeTypeResolver/NodeScopeAndMetadataDecorator.php @@ -18,7 +18,7 @@ public function __construct( private readonly CloningVisitor $cloningVisitor, private readonly PHPStanNodeScopeResolver $phpStanNodeScopeResolver, private readonly NodeConnectingVisitor $nodeConnectingVisitor, - private readonly FunctionLikeParamArgPositionNodeVisitor $functionLikeParamArgPositionNodeVisitor + private readonly FunctionLikeParamArgPositionNodeVisitor $functionLikeParamArgPositionNodeVisitor, ) { } diff --git a/phpstan.neon b/phpstan.neon index 801fa94713b..7e94c30062a 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -362,6 +362,7 @@ parameters: - rules/CodeQuality/Rector/PropertyFetch/ExplicitMethodCallOverMagicGetSetRector.php - rules/Php72/Rector/Assign/ReplaceEachAssignmentWithKeyCurrentRector.php - rules/PSR4/Rector/FileWithoutNamespace/NormalizeNamespaceByPSR4ComposerAutoloadRector.php + - rules/Renaming/NodeManipulator/ClassRenamer.php - '#Method Rector\\Core\\Application\\ApplicationFileProcessor\:\:runParallel\(\) should return array\{system_errors\: array, file_diffs\: array\} but returns array#' @@ -807,3 +808,9 @@ parameters: message: '#Short ternary operator is not allowed\. Use null coalesce operator if applicable or consider using long ternary#' paths: - src/DependencyInjection/DefinitionFinder.php + + # on purpose, it is intended to be used by end users configuring the RenameClassRector rule + - + message: "#^Class constant \"CALLBACKS\" is never used outside of its class$#" + count: 1 + path: rules/Renaming/Rector/Name/RenameClassRector.php diff --git a/rules-tests/Renaming/Rector/Name/RenameClassRector/FixtureWithCallback/rename_class_with_callback.php.inc b/rules-tests/Renaming/Rector/Name/RenameClassRector/FixtureWithCallback/rename_class_with_callback.php.inc new file mode 100644 index 00000000000..b9f66b4cc12 --- /dev/null +++ b/rules-tests/Renaming/Rector/Name/RenameClassRector/FixtureWithCallback/rename_class_with_callback.php.inc @@ -0,0 +1,35 @@ + +----- + diff --git a/rules-tests/Renaming/Rector/Name/RenameClassRector/FixtureWithCallback/rename_interface_with_callback.php.inc b/rules-tests/Renaming/Rector/Name/RenameClassRector/FixtureWithCallback/rename_interface_with_callback.php.inc new file mode 100644 index 00000000000..ad84e230b1c --- /dev/null +++ b/rules-tests/Renaming/Rector/Name/RenameClassRector/FixtureWithCallback/rename_interface_with_callback.php.inc @@ -0,0 +1,27 @@ + +----- + diff --git a/rules-tests/Renaming/Rector/Name/RenameClassRector/FixtureWithCallback/skip_correctly_named_exception.php.inc b/rules-tests/Renaming/Rector/Name/RenameClassRector/FixtureWithCallback/skip_correctly_named_exception.php.inc new file mode 100644 index 00000000000..1cd7c1e83fc --- /dev/null +++ b/rules-tests/Renaming/Rector/Name/RenameClassRector/FixtureWithCallback/skip_correctly_named_exception.php.inc @@ -0,0 +1,27 @@ + +----- + diff --git a/rules-tests/Renaming/Rector/Name/RenameClassRector/FixtureWithCallback/skip_correctly_named_interface.php.inc b/rules-tests/Renaming/Rector/Name/RenameClassRector/FixtureWithCallback/skip_correctly_named_interface.php.inc new file mode 100644 index 00000000000..48e3c2e0bab --- /dev/null +++ b/rules-tests/Renaming/Rector/Name/RenameClassRector/FixtureWithCallback/skip_correctly_named_interface.php.inc @@ -0,0 +1,27 @@ + +----- + diff --git a/rules-tests/Renaming/Rector/Name/RenameClassRector/RenameClassWithCallbackRectorTest.php b/rules-tests/Renaming/Rector/Name/RenameClassRector/RenameClassWithCallbackRectorTest.php new file mode 100644 index 00000000000..f1ae0b2c48f --- /dev/null +++ b/rules-tests/Renaming/Rector/Name/RenameClassRector/RenameClassWithCallbackRectorTest.php @@ -0,0 +1,32 @@ +doTestFile($filePath); + } + + /** + * @return Iterator> + */ + public function provideData(): Iterator + { + return $this->yieldFilesFromDirectory(__DIR__ . '/FixtureWithCallback'); + } + + public function provideConfigFilePath(): string + { + return __DIR__ . '/config/callback.php'; + } +} diff --git a/rules-tests/Renaming/Rector/Name/RenameClassRector/Source/EnforceExceptionSuffixCallback.php b/rules-tests/Renaming/Rector/Name/RenameClassRector/Source/EnforceExceptionSuffixCallback.php new file mode 100644 index 00000000000..e147fab2846 --- /dev/null +++ b/rules-tests/Renaming/Rector/Name/RenameClassRector/Source/EnforceExceptionSuffixCallback.php @@ -0,0 +1,31 @@ +getName($class); + $classReflection = $reflectionProvider->getClass($fullyQualifiedClassName); + if (! $classReflection->isSubclassOf(Exception::class)) { + return null; + } + + if (!str_ends_with($fullyQualifiedClassName, 'Exception')) { + return $fullyQualifiedClassName . 'Exception'; + } + + return null; + } +} diff --git a/rules-tests/Renaming/Rector/Name/RenameClassRector/Source/EnforceInterfaceSuffixCallback.php b/rules-tests/Renaming/Rector/Name/RenameClassRector/Source/EnforceInterfaceSuffixCallback.php new file mode 100644 index 00000000000..7ebeac3b48f --- /dev/null +++ b/rules-tests/Renaming/Rector/Name/RenameClassRector/Source/EnforceInterfaceSuffixCallback.php @@ -0,0 +1,24 @@ +getName($class); + if ( + $class instanceof Interface_ && + !str_ends_with($fullyQualifiedClassName, 'Interface') + ) { + return $fullyQualifiedClassName . 'Interface'; + } + + return null; + } +} diff --git a/rules-tests/Renaming/Rector/Name/RenameClassRector/config/callback.php b/rules-tests/Renaming/Rector/Name/RenameClassRector/config/callback.php new file mode 100644 index 00000000000..51531218607 --- /dev/null +++ b/rules-tests/Renaming/Rector/Name/RenameClassRector/config/callback.php @@ -0,0 +1,18 @@ +ruleWithConfiguration(RenameClassRector::class, [ + RenameClassRector::CALLBACKS => [ + new EnforceExceptionSuffixCallback(), + new EnforceInterfaceSuffixCallback(), + ], + ]); +}; diff --git a/rules/Renaming/Helper/RenameClassCallbackHandler.php b/rules/Renaming/Helper/RenameClassCallbackHandler.php new file mode 100644 index 00000000000..6c030285727 --- /dev/null +++ b/rules/Renaming/Helper/RenameClassCallbackHandler.php @@ -0,0 +1,75 @@ + + */ + private array $oldToNewClassCallbacks = []; + + public function __construct( + private readonly RenamedClassesDataCollector $renamedClassesDataCollector, + private readonly NodeNameResolver $nodeNameResolver, + private readonly ReflectionProvider $reflectionProvider + ) { + } + + public function hasOldToNewClassCallbacks(): bool + { + return $this->oldToNewClassCallbacks !== []; + } + + /** + * @param array $oldToNewClassCallbacks + */ + public function addOldToNewClassCallbacks(array $oldToNewClassCallbacks): void + { + $this->oldToNewClassCallbacks = [...$this->oldToNewClassCallbacks, ...$oldToNewClassCallbacks]; + } + + /** + * @return array + */ + public function getOldToNewClassesFromNode(Node $node): array + { + if ($node instanceof ClassLike) { + return $this->handleClassLike($node); + } + + return []; + } + + /** + * @return array + */ + public function handleClassLike(ClassLike $node): array + { + $oldToNewClasses = []; + $className = $node->name; + if ($className === null) { + return []; + } + + foreach ($this->oldToNewClassCallbacks as $oldToNewClassCallback) { + $newClassName = $oldToNewClassCallback($node, $this->nodeNameResolver, $this->reflectionProvider); + if ($newClassName !== null) { + $fullyQualifiedClassName = (string) $this->nodeNameResolver->getName($node); + $this->renamedClassesDataCollector->addOldToNewClass($fullyQualifiedClassName, $newClassName); + $oldToNewClasses[$fullyQualifiedClassName] = $newClassName; + } + } + + return $oldToNewClasses; + } +} diff --git a/rules/Renaming/NodeManipulator/ClassRenamer.php b/rules/Renaming/NodeManipulator/ClassRenamer.php index 248da85b195..532ddf7ec59 100644 --- a/rules/Renaming/NodeManipulator/ClassRenamer.php +++ b/rules/Renaming/NodeManipulator/ClassRenamer.php @@ -35,6 +35,7 @@ use Rector\NodeTypeResolver\PhpDoc\NodeAnalyzer\DocBlockClassRenamer; use Rector\NodeTypeResolver\ValueObject\OldToNewType; use Rector\PhpDocParser\NodeTraverser\SimpleCallableNodeTraverser; +use Rector\Renaming\Helper\RenameClassCallbackHandler; use Rector\StaticTypeMapper\ValueObject\Type\FullyQualifiedObjectType; final class ClassRenamer @@ -61,6 +62,7 @@ public function __construct( private readonly NodeRemover $nodeRemover, private readonly ParameterProvider $parameterProvider, private readonly UseImportsResolver $useImportsResolver, + private readonly RenameClassCallbackHandler $renameClassCallbackHandler, ) { } @@ -69,7 +71,7 @@ public function __construct( */ public function renameNode(Node $node, array $oldToNewClasses): ?Node { - $oldToNewTypes = $this->createOldToNewTypes($oldToNewClasses); + $oldToNewTypes = $this->createOldToNewTypes($node, $oldToNewClasses); $this->refactorPhpDoc($node, $oldToNewTypes, $oldToNewClasses); if ($node instanceof Name) { @@ -441,8 +443,9 @@ private function shouldRemoveUseName(string $last, string $newNameLastName, bool * @param array $oldToNewClasses * @return OldToNewType[] */ - private function createOldToNewTypes(array $oldToNewClasses): array + private function createOldToNewTypes(Node $node, array $oldToNewClasses): array { + $oldToNewClasses = $this->resolveOldToNewClassCallbacks($node, $oldToNewClasses); $cacheKey = md5(serialize($oldToNewClasses)); if (isset($this->oldToNewTypesByCacheKey[$cacheKey])) { @@ -461,4 +464,13 @@ private function createOldToNewTypes(array $oldToNewClasses): array return $oldToNewTypes; } + + /** + * @param array $oldToNewClasses + * @return array + */ + private function resolveOldToNewClassCallbacks(Node $node, array $oldToNewClasses): array + { + return [...$oldToNewClasses, ...$this->renameClassCallbackHandler->getOldToNewClassesFromNode($node)]; + } } diff --git a/rules/Renaming/Rector/Name/RenameClassRector.php b/rules/Renaming/Rector/Name/RenameClassRector.php index eb8d18cccd8..3a7ab2cba19 100644 --- a/rules/Renaming/Rector/Name/RenameClassRector.php +++ b/rules/Renaming/Rector/Name/RenameClassRector.php @@ -18,6 +18,8 @@ use Rector\Core\Contract\Rector\ConfigurableRectorInterface; use Rector\Core\PhpParser\Node\CustomNode\FileWithoutNamespace; use Rector\Core\Rector\AbstractRector; +use Rector\NodeNameResolver\NodeNameResolver; +use Rector\Renaming\Helper\RenameClassCallbackHandler; use Rector\Renaming\NodeManipulator\ClassRenamer; use Symplify\RuleDocGenerator\ValueObject\CodeSample\ConfiguredCodeSample; use Symplify\RuleDocGenerator\ValueObject\RuleDefinition; @@ -28,10 +30,16 @@ */ final class RenameClassRector extends AbstractRector implements ConfigurableRectorInterface { + /** + * @var string + */ + public const CALLBACKS = '#callbacks#'; + public function __construct( private readonly RenamedClassesDataCollector $renamedClassesDataCollector, private readonly ClassRenamer $classRenamer, private readonly RectorConfigProvider $rectorConfigProvider, + private readonly RenameClassCallbackHandler $renameClassCallbackHandler, ) { } @@ -95,7 +103,7 @@ public function getNodeTypes(): array public function refactor(Node $node): ?Node { $oldToNewClasses = $this->renamedClassesDataCollector->getOldToNewClasses(); - if ($oldToNewClasses === []) { + if ($oldToNewClasses === [] && ! $this->renameClassCallbackHandler->hasOldToNewClassCallbacks()) { return null; } @@ -115,6 +123,15 @@ public function refactor(Node $node): ?Node */ public function configure(array $configuration): void { + $oldToNewClassCallbacks = $configuration[self::CALLBACKS] ?? []; + Assert::isArray($oldToNewClassCallbacks); + if ($oldToNewClassCallbacks !== []) { + Assert::allIsCallable($oldToNewClassCallbacks); + /** @var array $oldToNewClassCallbacks */ + $this->renameClassCallbackHandler->addOldToNewClassCallbacks($oldToNewClassCallbacks); + unset($configuration[self::CALLBACKS]); + } + Assert::allString($configuration); Assert::allString(array_keys($configuration));