diff --git a/composer.json b/composer.json index 99c5d319cb6..4f3c37f248b 100644 --- a/composer.json +++ b/composer.json @@ -173,6 +173,7 @@ "Rector\\Polyfill\\Tests\\": "packages/Polyfill/tests" }, "classmap": [ + "packages/CakePHP/tests/Rector/Name/ImplicitShortClassNameUseStatementRector/Source", "packages/Symfony/tests/Rector/FrameworkBundle/AbstractToConstructorInjectionRectorSource", "packages/Symfony/tests/Rector/FrameworkBundle/ContainerGetToConstructorInjectionRector/Source", "packages/NodeTypeResolver/tests/PerNodeTypeResolver/ParamTypeResolver/Source", @@ -182,7 +183,8 @@ "tests/Rector/Namespace_/PseudoNamespaceToNamespaceRector/Source", "tests/Issues/Issue1243/Source", "packages/Autodiscovery/tests/Rector/FileSystem/MoveInterfacesToContractNamespaceDirectoryRector/Expected", - "packages/Autodiscovery/tests/Rector/FileSystem/MoveServicesBySuffixToDirectoryRector/Expected" + "packages/Autodiscovery/tests/Rector/FileSystem/MoveServicesBySuffixToDirectoryRector/Expected", + "packages/CakePHP/tests/Rector/StaticCall/AppUsesStaticCallToUseStatementRector/Source" ], "files": [ "packages/DeadCode/tests/Rector/MethodCall/RemoveDefaultArgumentValueRector/Source/UserDefined.php", diff --git a/config/set/cakephp/cakephp30.yaml b/config/set/cakephp/cakephp30.yaml new file mode 100644 index 00000000000..afaf3f9b65d --- /dev/null +++ b/config/set/cakephp/cakephp30.yaml @@ -0,0 +1,4 @@ +services: + # @see https://github.com/cakephp/upgrade/tree/master/src/Shell/Task + Rector\CakePHP\Rector\StaticCall\AppUsesStaticCallToUseStatementRector: null + Rector\CakePHP\Rector\Name\ImplicitShortClassNameUseStatementRector: null diff --git a/docs/AllRectorsOverview.md b/docs/AllRectorsOverview.md index 518d3834cd7..7015d2b1c3e 100644 --- a/docs/AllRectorsOverview.md +++ b/docs/AllRectorsOverview.md @@ -1,4 +1,4 @@ -# All 426 Rectors Overview +# All 428 Rectors Overview - [Projects](#projects) - [General](#general) @@ -178,6 +178,21 @@ Move value object to ValueObject namespace/directory ## CakePHP +### `AppUsesStaticCallToUseStatementRector` + +- class: `Rector\CakePHP\Rector\StaticCall\AppUsesStaticCallToUseStatementRector` + +Change App::uses() to use imports + +```diff +-App::uses('NotificationListener', 'Event'); ++use Event\NotificationListener; + + CakeEventManager::instance()->attach(new NotificationListener()); +``` + +
+ ### `ChangeSnakedFixtureNameToCamelRector` - class: `Rector\CakePHP\Rector\Name\ChangeSnakedFixtureNameToCamelRector` @@ -199,6 +214,23 @@ Changes $fixtues style from snake_case to CamelCase.
+### `ImplicitShortClassNameUseStatementRector` + +- class: `Rector\CakePHP\Rector\Name\ImplicitShortClassNameUseStatementRector` + +Collect implicit class names and add imports + +```diff + use App\Foo\Plugin; ++use Cake\TestSuite\Fixture\TestFixture; + + class LocationsFixture extends TestFixture implements Plugin + { + } +``` + +
+ ### `ModalToGetSetRector` - class: `Rector\CakePHP\Rector\MethodCall\ModalToGetSetRector` diff --git a/packages/CakePHP/config/config.yaml b/packages/CakePHP/config/config.yaml new file mode 100644 index 00000000000..6ab46f25833 --- /dev/null +++ b/packages/CakePHP/config/config.yaml @@ -0,0 +1,9 @@ +services: + _defaults: + autowire: true + public: true + + Rector\CakePHP\: + resource: '../src' + exclude: + - '../src/Rector/**/*Rector.php' diff --git a/packages/CakePHP/src/FullyQualifiedClassNameResolver.php b/packages/CakePHP/src/FullyQualifiedClassNameResolver.php new file mode 100644 index 00000000000..0c496fbc74a --- /dev/null +++ b/packages/CakePHP/src/FullyQualifiedClassNameResolver.php @@ -0,0 +1,63 @@ +implicitNameResolver = $implicitNameResolver; + } + + /** + * This value used to be directory + * So "/" in path should be "\" in namespace + */ + public function resolveFromPseudoNamespaceAndShortClassName(string $pseudoNamespace, string $shortClass): string + { + $pseudoNamespace = $this->normalizeFileSystemSlashes($pseudoNamespace); + + $resolvedShortClass = $this->implicitNameResolver->resolve($shortClass); + + // A. is known renamed class? + if ($resolvedShortClass !== null) { + return $resolvedShortClass; + } + + // Chop Lib out as locations moves those files to the top level. + // But only if Lib is not the last folder. + if (Strings::match($pseudoNamespace, '#\\\\Lib\\\\#')) { + $pseudoNamespace = Strings::replace($pseudoNamespace, '#\\\\Lib#'); + } + + // B. is Cake native class? + $cakePhpVersion = 'Cake\\' . $pseudoNamespace . '\\' . $shortClass; + if (class_exists($cakePhpVersion) || interface_exists($cakePhpVersion)) { + return $cakePhpVersion; + } + + // C. is not plugin nor lib custom App class? + if (Strings::contains($pseudoNamespace, '\\') && ! Strings::match($pseudoNamespace, '#(Plugin|Lib)#')) { + return 'App\\' . $pseudoNamespace . '\\' . $shortClass; + } + + return $pseudoNamespace . '\\' . $shortClass; + } + + private function normalizeFileSystemSlashes(string $pseudoNamespace): string + { + return Strings::replace($pseudoNamespace, '#(/|\.)#', '\\'); + } +} diff --git a/packages/CakePHP/src/ImplicitNameResolver.php b/packages/CakePHP/src/ImplicitNameResolver.php new file mode 100644 index 00000000000..0a5a1535c62 --- /dev/null +++ b/packages/CakePHP/src/ImplicitNameResolver.php @@ -0,0 +1,59 @@ + new for use statements that are missing + * + * @var string[] + */ + public $implicitMap = [ + 'App' => 'Cake\Core\App', + 'AppController' => 'App\Controller\AppController', + 'AppHelper' => 'App\View\Helper\AppHelper', + 'AppModel' => 'App\Model\AppModel', + 'Cache' => 'Cake\Cache\Cache', + 'CakeEventListener' => 'Cake\Event\EventListener', + 'CakeLog' => 'Cake\Log\Log', + 'CakePlugin' => 'Cake\Core\Plugin', + 'CakeTestCase' => 'Cake\TestSuite\TestCase', + 'CakeTestFixture' => 'Cake\TestSuite\Fixture\TestFixture', + 'Component' => 'Cake\Controller\Component', + 'ComponentRegistry' => 'Cake\Controller\ComponentRegistry', + 'Configure' => 'Cake\Core\Configure', + 'ConnectionManager' => 'Cake\Database\ConnectionManager', + 'Controller' => 'Cake\Controller\Controller', + 'Debugger' => 'Cake\Error\Debugger', + 'ExceptionRenderer' => 'Cake\Error\ExceptionRenderer', + 'Helper' => 'Cake\View\Helper', + 'HelperRegistry' => 'Cake\View\HelperRegistry', + 'Inflector' => 'Cake\Utility\Inflector', + 'Model' => 'Cake\Model\Model', + 'ModelBehavior' => 'Cake\Model\Behavior', + 'Object' => 'Cake\Core\Object', + 'Router' => 'Cake\Routing\Router', + 'Shell' => 'Cake\Console\Shell', + 'View' => 'Cake\View\View', + // Also apply to already renamed ones + 'Log' => 'Cake\Log\Log', + 'Plugin' => 'Cake\Core\Plugin', + 'TestCase' => 'Cake\TestSuite\TestCase', + 'TestFixture' => 'Cake\TestSuite\Fixture\TestFixture', + ]; + + /** + * This value used to be directory + * So "/" in path should be "\" in namespace + */ + public function resolve(string $shortClass): ?string + { + return $this->implicitMap[$shortClass] ?? null; + } +} diff --git a/packages/CakePHP/src/Rector/Name/ImplicitShortClassNameUseStatementRector.php b/packages/CakePHP/src/Rector/Name/ImplicitShortClassNameUseStatementRector.php new file mode 100644 index 00000000000..f8b9369ee48 --- /dev/null +++ b/packages/CakePHP/src/Rector/Name/ImplicitShortClassNameUseStatementRector.php @@ -0,0 +1,88 @@ +implicitNameResolver = $implicitNameResolver; + } + + public function getDefinition(): RectorDefinition + { + return new RectorDefinition('Collect implicit class names and add imports', [ + new CodeSample( + <<<'PHP' +use App\Foo\Plugin; + +class LocationsFixture extends TestFixture implements Plugin +{ +} +PHP +, + <<<'PHP' +use App\Foo\Plugin; +use Cake\TestSuite\Fixture\TestFixture; + +class LocationsFixture extends TestFixture implements Plugin +{ +} +PHP + + ), + ]); + } + + /** + * @return string[] + */ + public function getNodeTypes(): array + { + return [Name::class]; + } + + /** + * @param Name $node + */ + public function refactor(Node $node): ?Node + { + $parentNode = $node->getAttribute(AttributeKey::PARENT_NODE); + if (! $parentNode instanceof ClassLike && $parentNode instanceof New_) { + return null; + } + + $classShortName = $this->getName($node); + $resvoledName = $this->implicitNameResolver->resolve($classShortName); + if ($resvoledName === null) { + return null; + } + + $this->addUseType(new FullyQualifiedObjectType($resvoledName), $node); + + return null; + } +} diff --git a/packages/CakePHP/src/Rector/StaticCall/AppUsesStaticCallToUseStatementRector.php b/packages/CakePHP/src/Rector/StaticCall/AppUsesStaticCallToUseStatementRector.php new file mode 100644 index 00000000000..4760575b3c7 --- /dev/null +++ b/packages/CakePHP/src/Rector/StaticCall/AppUsesStaticCallToUseStatementRector.php @@ -0,0 +1,125 @@ +fullyQualifiedClassNameResolver = $fullyQualifiedClassNameResolver; + } + + public function getDefinition(): RectorDefinition + { + return new RectorDefinition('Change App::uses() to use imports', [ + new CodeSample( + <<<'PHP' +App::uses('NotificationListener', 'Event'); + +CakeEventManager::instance()->attach(new NotificationListener()); +PHP +, + <<<'PHP' +use Event\NotificationListener; + +CakeEventManager::instance()->attach(new NotificationListener()); +PHP + + ), + ]); + } + + /** + * @return string[] + */ + public function getNodeTypes(): array + { + return [Expression::class]; + } + + /** + * @param Expression $node + */ + public function refactor(Node $node): ?Node + { + if (! $node->expr instanceof StaticCall) { + return null; + } + + $staticCall = $node->expr; + if (! $this->isAppUses($staticCall)) { + return null; + } + + $fullyQualifiedName = $this->createFullyQualifiedNameFromAppUsesStaticCall($staticCall); + + // A. is above the class or under the namespace + $parentNode = $node->getAttribute(AttributeKey::PARENT_NODE); + if ($parentNode instanceof Namespace_ || $parentNode === null) { + return $this->createUseFromFullyQualifiedName($fullyQualifiedName); + } + + // B. is inside the code → add use import + $this->addUseType(new FullyQualifiedObjectType($fullyQualifiedName), $node); + $this->removeNode($node); + + return null; + } + + private function createFullyQualifiedNameFromAppUsesStaticCall(StaticCall $staticCall): string + { + /** @var string $shortClassName */ + $shortClassName = $this->getValue($staticCall->args[0]->value); + + /** @var string $namespaceName */ + $namespaceName = $this->getValue($staticCall->args[1]->value); + + return $this->fullyQualifiedClassNameResolver->resolveFromPseudoNamespaceAndShortClassName( + $namespaceName, + $shortClassName + ); + } + + private function createUseFromFullyQualifiedName(string $fullyQualifiedName): Use_ + { + $useUse = new UseUse(new Name($fullyQualifiedName)); + + return new Use_([$useUse]); + } + + private function isAppUses($staticCall): bool + { + if (! $this->isName($staticCall->class, 'App')) { + return false; + } + + return $this->isName($staticCall->name, 'uses'); + } +} diff --git a/packages/CakePHP/tests/Rector/Name/ImplicitShortClassNameUseStatementRector/Fixture/cakephp_controller.php.inc b/packages/CakePHP/tests/Rector/Name/ImplicitShortClassNameUseStatementRector/Fixture/cakephp_controller.php.inc new file mode 100644 index 00000000000..6b573603e1c --- /dev/null +++ b/packages/CakePHP/tests/Rector/Name/ImplicitShortClassNameUseStatementRector/Fixture/cakephp_controller.php.inc @@ -0,0 +1,24 @@ + +----- + +----- +doTestFile($file); + } + + public function provideDataForTest(): Iterator + { + return $this->yieldFilesFromDirectory(__DIR__ . '/Fixture'); + } + + protected function getRectorClass(): string + { + return ImplicitShortClassNameUseStatementRector::class; + } +} diff --git a/packages/CakePHP/tests/Rector/Name/ImplicitShortClassNameUseStatementRector/Source/AppController.php b/packages/CakePHP/tests/Rector/Name/ImplicitShortClassNameUseStatementRector/Source/AppController.php new file mode 100644 index 00000000000..00a654f0117 --- /dev/null +++ b/packages/CakePHP/tests/Rector/Name/ImplicitShortClassNameUseStatementRector/Source/AppController.php @@ -0,0 +1,8 @@ +doTestFile($file); + } + + public function provideDataForTest(): Iterator + { + return $this->yieldFilesFromDirectory(__DIR__ . '/Fixture'); + } + + protected function getRectorClass(): string + { + return AppUsesStaticCallToUseStatementRector::class; + } +} diff --git a/packages/CakePHP/tests/Rector/StaticCall/AppUsesStaticCallToUseStatementRector/Fixture/cakephp_controller.php.inc b/packages/CakePHP/tests/Rector/StaticCall/AppUsesStaticCallToUseStatementRector/Fixture/cakephp_controller.php.inc new file mode 100644 index 00000000000..4cf2c40e1a0 --- /dev/null +++ b/packages/CakePHP/tests/Rector/StaticCall/AppUsesStaticCallToUseStatementRector/Fixture/cakephp_controller.php.inc @@ -0,0 +1,23 @@ + +----- + diff --git a/packages/CakePHP/tests/Rector/StaticCall/AppUsesStaticCallToUseStatementRector/Fixture/cakephp_fixture.php.inc b/packages/CakePHP/tests/Rector/StaticCall/AppUsesStaticCallToUseStatementRector/Fixture/cakephp_fixture.php.inc new file mode 100644 index 00000000000..8889719bb14 --- /dev/null +++ b/packages/CakePHP/tests/Rector/StaticCall/AppUsesStaticCallToUseStatementRector/Fixture/cakephp_fixture.php.inc @@ -0,0 +1,35 @@ + +----- + diff --git a/packages/CakePHP/tests/Rector/StaticCall/AppUsesStaticCallToUseStatementRector/Fixture/cakephp_import_namespaces_up.php.inc b/packages/CakePHP/tests/Rector/StaticCall/AppUsesStaticCallToUseStatementRector/Fixture/cakephp_import_namespaces_up.php.inc new file mode 100644 index 00000000000..ad9401d172d --- /dev/null +++ b/packages/CakePHP/tests/Rector/StaticCall/AppUsesStaticCallToUseStatementRector/Fixture/cakephp_import_namespaces_up.php.inc @@ -0,0 +1,33 @@ + +----- + diff --git a/packages/CakePHP/tests/Rector/StaticCall/AppUsesStaticCallToUseStatementRector/Fixture/fixture.php.inc b/packages/CakePHP/tests/Rector/StaticCall/AppUsesStaticCallToUseStatementRector/Fixture/fixture.php.inc new file mode 100644 index 00000000000..17a741567c0 --- /dev/null +++ b/packages/CakePHP/tests/Rector/StaticCall/AppUsesStaticCallToUseStatementRector/Fixture/fixture.php.inc @@ -0,0 +1,31 @@ + +----- + diff --git a/packages/CakePHP/tests/Rector/StaticCall/AppUsesStaticCallToUseStatementRector/Source/App.php b/packages/CakePHP/tests/Rector/StaticCall/AppUsesStaticCallToUseStatementRector/Source/App.php new file mode 100644 index 00000000000..bd68448d575 --- /dev/null +++ b/packages/CakePHP/tests/Rector/StaticCall/AppUsesStaticCallToUseStatementRector/Source/App.php @@ -0,0 +1,13 @@ +