[CakePHP 3.0] Add AppUsesStaticCallToUseStatementRector (#2643)

[CakePHP 3.0] Add AppUsesStaticCallToUseStatementRector
This commit is contained in:
Tomas Votruba 2020-01-11 22:19:25 +01:00 committed by GitHub
commit d73b193e5b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 651 additions and 2 deletions

View File

@ -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",

View File

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

View File

@ -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());
```
<br>
### `ChangeSnakedFixtureNameToCamelRector`
- class: `Rector\CakePHP\Rector\Name\ChangeSnakedFixtureNameToCamelRector`
@ -199,6 +214,23 @@ Changes $fixtues style from snake_case to CamelCase.
<br>
### `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
{
}
```
<br>
### `ModalToGetSetRector`
- class: `Rector\CakePHP\Rector\MethodCall\ModalToGetSetRector`

View File

@ -0,0 +1,9 @@
services:
_defaults:
autowire: true
public: true
Rector\CakePHP\:
resource: '../src'
exclude:
- '../src/Rector/**/*Rector.php'

View File

@ -0,0 +1,63 @@
<?php
declare(strict_types=1);
namespace Rector\CakePHP;
use Nette\Utils\Strings;
/**
* @inspired https://github.com/cakephp/upgrade/blob/756410c8b7d5aff9daec3fa1fe750a3858d422ac/src/Shell/Task/AppUsesTask.php
*/
final class FullyQualifiedClassNameResolver
{
/**
* @var ImplicitNameResolver
*/
private $implicitNameResolver;
public function __construct(ImplicitNameResolver $implicitNameResolver)
{
$this->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, '#(/|\.)#', '\\');
}
}

View File

@ -0,0 +1,59 @@
<?php
declare(strict_types=1);
namespace Rector\CakePHP;
/**
* @inspired https://github.com/cakephp/upgrade/blob/756410c8b7d5aff9daec3fa1fe750a3858d422ac/src/Shell/Task/AppUsesTask.php
*/
final class ImplicitNameResolver
{
/**
* A map of old => 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;
}
}

View File

@ -0,0 +1,88 @@
<?php
declare(strict_types=1);
namespace Rector\CakePHP\Rector\Name;
use PhpParser\Node;
use PhpParser\Node\Expr\New_;
use PhpParser\Node\Name;
use PhpParser\Node\Stmt\ClassLike;
use Rector\CakePHP\ImplicitNameResolver;
use Rector\NodeTypeResolver\Node\AttributeKey;
use Rector\PHPStan\Type\FullyQualifiedObjectType;
use Rector\Rector\AbstractRector;
use Rector\RectorDefinition\CodeSample;
use Rector\RectorDefinition\RectorDefinition;
/**
* @see https://github.com/cakephp/upgrade/blob/05d85c147bb1302b576b818cabb66a40462aaed0/src/Shell/Task/AppUsesTask.php#L183
*
* @see \Rector\CakePHP\Tests\Rector\Name\ImplicitShortClassNameUseStatementRector\ImplicitShortClassNameUseStatementRectorTest
*/
final class ImplicitShortClassNameUseStatementRector extends AbstractRector
{
/**
* @var ImplicitNameResolver
*/
private $implicitNameResolver;
public function __construct(ImplicitNameResolver $implicitNameResolver)
{
$this->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;
}
}

View File

@ -0,0 +1,125 @@
<?php
declare(strict_types=1);
namespace Rector\CakePHP\Rector\StaticCall;
use PhpParser\Node;
use PhpParser\Node\Expr\StaticCall;
use PhpParser\Node\Name;
use PhpParser\Node\Stmt\Expression;
use PhpParser\Node\Stmt\Namespace_;
use PhpParser\Node\Stmt\Use_;
use PhpParser\Node\Stmt\UseUse;
use Rector\CakePHP\FullyQualifiedClassNameResolver;
use Rector\NodeTypeResolver\Node\AttributeKey;
use Rector\PHPStan\Type\FullyQualifiedObjectType;
use Rector\Rector\AbstractRector;
use Rector\RectorDefinition\CodeSample;
use Rector\RectorDefinition\RectorDefinition;
/**
* @see https://github.com/cakephp/upgrade/blob/756410c8b7d5aff9daec3fa1fe750a3858d422ac/src/Shell/Task/AppUsesTask.php
* @see https://github.com/cakephp/upgrade/search?q=uses&unscoped_q=uses
*
* @see \Rector\CakePHP\Tests\Rector\StaticCall\AppUsesStaticCallToUseStatementRector\AppUsesStaticCallToUseStatementRectorTest
*/
final class AppUsesStaticCallToUseStatementRector extends AbstractRector
{
/**
* @var FullyQualifiedClassNameResolver
*/
private $fullyQualifiedClassNameResolver;
public function __construct(FullyQualifiedClassNameResolver $fullyQualifiedClassNameResolver)
{
$this->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');
}
}

View File

@ -0,0 +1,24 @@
<?php
class AppUsesImplicit extends AppController implements Controller
{
public function test()
{
$url = Router::url('xyz');
}
}
?>
-----
<?php
use App\Controller\AppController;
use Cake\Controller\Controller;
use Cake\Routing\Router;
class AppUsesImplicit extends AppController implements Controller
{
public function test()
{
$url = Router::url('xyz');
}
}

View File

@ -0,0 +1,14 @@
<?php
class LocationsFixture extends TestFixture
{
}
?>
-----
<?php
use Cake\TestSuite\Fixture\TestFixture;
class LocationsFixture extends TestFixture
{
}

View File

@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace Rector\CakePHP\Tests\Rector\Name\ImplicitShortClassNameUseStatementRector;
use Iterator;
use Rector\CakePHP\Rector\Name\ImplicitShortClassNameUseStatementRector;
use Rector\Testing\PHPUnit\AbstractRectorTestCase;
final class ImplicitShortClassNameUseStatementRectorTest extends AbstractRectorTestCase
{
/**
* @dataProvider provideDataForTest()
*/
public function test(string $file): void
{
$this->doTestFile($file);
}
public function provideDataForTest(): Iterator
{
return $this->yieldFilesFromDirectory(__DIR__ . '/Fixture');
}
protected function getRectorClass(): string
{
return ImplicitShortClassNameUseStatementRector::class;
}
}

View File

@ -0,0 +1,8 @@
<?php
declare(strict_types=1);
class AppController
{
}

View File

@ -0,0 +1,8 @@
<?php
declare(strict_types=1);
interface Controller
{
}

View File

@ -0,0 +1,8 @@
<?php
declare(strict_types=1);
class TestFixture
{
}

View File

@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace Rector\CakePHP\Tests\Rector\StaticCall\AppUsesStaticCallToUseStatementRector;
use Iterator;
use Rector\CakePHP\Rector\StaticCall\AppUsesStaticCallToUseStatementRector;
use Rector\Testing\PHPUnit\AbstractRectorTestCase;
final class AppUsesStaticCallToUseStatementRectorTest extends AbstractRectorTestCase
{
/**
* @dataProvider provideDataForTest()
*/
public function test(string $file): void
{
$this->doTestFile($file);
}
public function provideDataForTest(): Iterator
{
return $this->yieldFilesFromDirectory(__DIR__ . '/Fixture');
}
protected function getRectorClass(): string
{
return AppUsesStaticCallToUseStatementRector::class;
}
}

View File

@ -0,0 +1,23 @@
<?php
namespace Rector\CakePHP\Tests\Rector\StaticCall\AppUsesStaticCallToUseStatementRector\Fixture;
\App::uses('Component', 'Controller');
class CakeController
{
}
?>
-----
<?php
namespace Rector\CakePHP\Tests\Rector\StaticCall\AppUsesStaticCallToUseStatementRector\Fixture;
use Cake\Controller\Component;
class CakeController
{
}
?>

View File

@ -0,0 +1,35 @@
<?php
namespace Rector\CakePHP\Tests\Rector\StaticCall\AppUsesStaticCallToUseStatementRector\Fixture;
\App::uses('HttpSocket', 'Network/Http');
\App::uses('Xml', 'Utility');
\App::uses('Component', 'Controller');
\App::uses('SomeLib', 'Data.Lib');
\App::uses('CurrencyLib', 'PluginName.Lib/Currency');
\App::uses('FooShell', 'MyPlugin.Console/Command');
// https://github.com/cakephp/upgrade/blob/05d85c147bb1302b576b818cabb66a40462aaed0/tests/test_files/AppUsesAfter.php
class CakePhpFixture
{
}
?>
-----
<?php
namespace Rector\CakePHP\Tests\Rector\StaticCall\AppUsesStaticCallToUseStatementRector\Fixture;
use App\Network\Http\HttpSocket;
use Cake\Utility\Xml;
use Cake\Controller\Component;
use Data\Lib\SomeLib;
use PluginName\Currency\CurrencyLib;
use MyPlugin\Console\Command\FooShell;
// https://github.com/cakephp/upgrade/blob/05d85c147bb1302b576b818cabb66a40462aaed0/tests/test_files/AppUsesAfter.php
class CakePhpFixture
{
}
?>

View File

@ -0,0 +1,33 @@
<?php
namespace Rector\CakePHP\Tests\Rector\StaticCall\AppUsesStaticCallToUseStatementRector\Fixture;
class ImportNamespacesUp
{
public function test()
{
\App::uses('HtmlDomLib', 'Foo.Lib');
$HtmlDom = new HtmlDomLib();
\App::uses('HtmlDomLibExt', 'Foo.Lib');
$HtmlDom = new HtmlDomLibExt();
}
}
?>
-----
<?php
namespace Rector\CakePHP\Tests\Rector\StaticCall\AppUsesStaticCallToUseStatementRector\Fixture;
use Foo\Lib\HtmlDomLib;
use Foo\Lib\HtmlDomLibExt;
class ImportNamespacesUp
{
public function test()
{
$HtmlDom = new HtmlDomLib();
$HtmlDom = new HtmlDomLibExt();
}
}
?>

View File

@ -0,0 +1,31 @@
<?php
namespace Rector\CakePHP\Tests\Rector\StaticCall\AppUsesStaticCallToUseStatementRector\Fixture;
\App::uses('NotificationListener', 'Event');
class SomeClass
{
public function run()
{
$values = new NotificationListener();
}
}
?>
-----
<?php
namespace Rector\CakePHP\Tests\Rector\StaticCall\AppUsesStaticCallToUseStatementRector\Fixture;
use Event\NotificationListener;
class SomeClass
{
public function run()
{
$values = new NotificationListener();
}
}
?>

View File

@ -0,0 +1,13 @@
<?php
declare(strict_types=1);
// faking cake php App class
// see https://github.com/cakephp/cakephp/blob/2.4.1/lib/Cake/Core/App.php#L521
final class App
{
public static function uses($className, $location) {
// self::$_classMap[$className] = $location;
}
}

View File

@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace Cake\Utility;
final class Xml
{
}