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 @@
+