[Legacy] Add ChangeSingletonToServiceRector

This commit is contained in:
Tomas Votruba 2019-05-26 14:43:20 +02:00
parent 3000f593eb
commit cc2b9df06d
14 changed files with 466 additions and 5 deletions

View File

@ -72,7 +72,8 @@
"Rector\\Shopware\\": "packages/Shopware/src",
"Rector\\NetteTesterToPHPUnit\\": "packages/NetteTesterToPHPUnit/src",
"Rector\\Nette\\": "packages/Nette/src",
"Rector\\SOLID\\": "packages/SOLID/src"
"Rector\\SOLID\\": "packages/SOLID/src",
"Rector\\Legacy\\": "packages/Legacy/src"
}
},
"autoload-dev": {
@ -107,7 +108,8 @@
"Rector\\Shopware\\Tests\\": "packages/Shopware/tests",
"Rector\\NetteTesterToPHPUnit\\Tests\\": "packages/NetteTesterToPHPUnit/tests",
"Rector\\Nette\\Tests\\": "packages/Nette/tests",
"Rector\\SOLID\\Tests\\": "packages/SOLID/tests"
"Rector\\SOLID\\Tests\\": "packages/SOLID/tests",
"Rector\\Legacy\\Tests\\": "packages/Legacy/tests"
},
"classmap": [
"packages/Symfony/tests/Rector/FrameworkBundle/AbstractToConstructorInjectionRectorSource",

View File

@ -100,6 +100,7 @@ parameters:
Symplify\CodingStandard\Sniffs\CleanCode\CognitiveComplexitySniff:
# tough logic
- 'packages/Legacy/src/Rector/ClassMethod/ChangeSingletonToServiceRector.php'
- 'src/Rector/Psr4/MultipleClassFileToPsr4ClassesRector.php'
- 'src/PhpParser/Node/Resolver/NameResolver.php'
- 'src/Rector/MethodBody/NormalToFluentRector.php'
@ -111,6 +112,7 @@ parameters:
- 'packages/Laravel/src/Rector/FuncCall/HelperFunctionToConstructorInjectionRector.php'
- 'packages/PhpSpecToPHPUnit/src/Rector/MethodCall/PhpSpecPromisesToPHPUnitAssertRector.php'
- 'packages/NetteTesterToPHPUnit/src/AssertManipulator.php'
- 'packages/Legacy/src/NodeAnalyzer/SingletonClassMethodAnalyzer.php'
# aliases
- 'packages/CodingStyle/src/Rector/Namespace_/ImportFullyQualifiedNamesRector.php'

View File

@ -281,6 +281,21 @@ final class CreateRectorCommand extends Command implements ContributorCommandInt
{
$content = Json::encode($json, Json::PRETTY);
$content = $this->inlineSections($content, ['keywords', 'bin']);
$content = $this->inlineAuthors($content);
FileSystem::write($filePath, $content);
}
private function inlineAuthors(string $jsonContent): string
{
$pattern = '#(?<start>"authors": \[\s+)(?<content>.*?)(?<end>\s+\](,))#ms';
$jsonContent = Strings::replace($jsonContent, $pattern, function (array $match): string {
$inlined = Strings::replace($match['content'], '#\s+#', ' ');
$inlined = trim($inlined);
$inlined = Strings::replace($inlined, '#},#', "},\n ");
return $match['start'] . $inlined . $match['end'];
});
return $jsonContent;
}
}

View File

@ -0,0 +1,8 @@
services:
_defaults:
public: true
autowire: true
Rector\Legacy\:
resource: '../src'
exclude: '../src/{Rector/**/*Rector.php}'

View File

@ -0,0 +1,125 @@
<?php declare(strict_types=1);
namespace Rector\Legacy\NodeAnalyzer;
use PhpParser\Node\Expr;
use PhpParser\Node\Expr\Assign;
use PhpParser\Node\Expr\BinaryOp\Identical;
use PhpParser\Node\Expr\BooleanNot;
use PhpParser\Node\Expr\New_;
use PhpParser\Node\Expr\StaticPropertyFetch;
use PhpParser\Node\Stmt\ClassMethod;
use PhpParser\Node\Stmt\Expression;
use PhpParser\Node\Stmt\If_;
use Rector\NodeTypeResolver\Node\AttributeKey;
use Rector\NodeTypeResolver\NodeTypeResolver;
use Rector\PhpParser\Node\Manipulator\ConstFetchManipulator;
use Rector\PhpParser\Printer\BetterStandardPrinter;
final class SingletonClassMethodAnalyzer
{
/**
* @var ConstFetchManipulator
*/
private $constFetchManipulator;
/**
* @var BetterStandardPrinter
*/
private $betterStandardPrinter;
/**
* @var NodeTypeResolver
*/
private $nodeTypeResolver;
public function __construct(
ConstFetchManipulator $constFetchManipulator,
BetterStandardPrinter $betterStandardPrinter,
NodeTypeResolver $nodeTypeResolver
) {
$this->constFetchManipulator = $constFetchManipulator;
$this->betterStandardPrinter = $betterStandardPrinter;
$this->nodeTypeResolver = $nodeTypeResolver;
}
/**
* Match this code:
* if (null === static::$instance) {
* static::$instance = new static();
* }
* return static::$instance;
*
* Matches "static::$instance" on success
*/
public function matchStaticPropertyFetch(ClassMethod $classMethod): ?StaticPropertyFetch
{
if (count((array) $classMethod->stmts) !== 2) {
return null;
}
if (! $classMethod->stmts[0] instanceof If_) {
return null;
}
/** @var If_ $if */
$if = $classMethod->stmts[0];
$staticPropertyFetch = $this->matchStaticPropertyFetchInIfCond($if->cond);
if (count($if->stmts) !== 1) {
return null;
}
if (! $if->stmts[0] instanceof Expression) {
return null;
}
$stmt = $if->stmts[0]->expr;
// create self and assign to static property
if (! $stmt instanceof Assign) {
return null;
}
if (! $this->betterStandardPrinter->areNodesEqual($staticPropertyFetch, $stmt->var)) {
return null;
}
if (! $stmt->expr instanceof New_) {
return null;
}
$class = $classMethod->getAttribute(AttributeKey::CLASS_NAME);
// the "self" class is created
if ($this->nodeTypeResolver->getTypes($stmt->expr->class) !== [$class]) {
return null;
}
/** @var StaticPropertyFetch $staticPropertyFetch */
return $staticPropertyFetch;
}
private function matchStaticPropertyFetchInIfCond(Expr $expr): ?StaticPropertyFetch
{
// matching: "self::$static === null"
if ($expr instanceof Identical) {
if ($this->constFetchManipulator->isNull($expr->left) && $expr->right instanceof StaticPropertyFetch) {
return $expr->right;
}
if ($this->constFetchManipulator->isNull($expr->right) && $expr->left instanceof StaticPropertyFetch) {
return $expr->left;
}
}
// matching: "! self::$static"
if ($expr instanceof BooleanNot) {
if ($expr->expr instanceof StaticPropertyFetch) {
return $expr->expr;
}
}
return null;
}
}

View File

@ -0,0 +1,151 @@
<?php declare(strict_types=1);
namespace Rector\Legacy\Rector\ClassMethod;
use PhpParser\Node;
use PhpParser\Node\Stmt\Class_;
use PhpParser\Node\Stmt\ClassMethod;
use PhpParser\Node\Stmt\Property;
use Rector\Legacy\NodeAnalyzer\SingletonClassMethodAnalyzer;
use Rector\Rector\AbstractRector;
use Rector\RectorDefinition\CodeSample;
use Rector\RectorDefinition\RectorDefinition;
/**
* @see https://3v4l.org/lifbH
* @see https://stackoverflow.com/a/203359/1348344
* @see http://cleancode.blog/2017/07/20/how-to-avoid-many-instances-in-singleton-pattern/
*/
final class ChangeSingletonToServiceRector extends AbstractRector
{
/**
* @var SingletonClassMethodAnalyzer
*/
private $singletonClassMethodAnalyzer;
public function __construct(SingletonClassMethodAnalyzer $singletonClassMethodAnalyzer)
{
$this->singletonClassMethodAnalyzer = $singletonClassMethodAnalyzer;
}
public function getDefinition(): RectorDefinition
{
return new RectorDefinition('Change singleton class to normal class that can be registered as a service', [
new CodeSample(
<<<'CODE_SAMPLE'
class SomeClass
{
private static $instance;
private function __construct()
{
}
public static function getInstance()
{
if (null === static::$instance) {
static::$instance = new static();
}
return static::$instance;
}
}
CODE_SAMPLE
,
<<<'CODE_SAMPLE'
class SomeClass
{
public function __construct()
{
}
}
CODE_SAMPLE
),
]);
}
/**
* @return string[]
*/
public function getNodeTypes(): array
{
return [Class_::class];
}
/**
* @param Class_ $node
*/
public function refactor(Node $node): ?Node
{
if ($node->isAnonymous()) {
return null;
}
$match = $this->matchStaticPropertyFetchAndGetSingletonMethodName($node);
if ($match === null) {
return null;
}
[$singletonPropertyName, $getSingletonMethodName] = $match;
return $this->refactorClassStmts($node, $getSingletonMethodName, $singletonPropertyName);
}
/**
* @param Class_ $class
* @return string[]|null
*/
private function matchStaticPropertyFetchAndGetSingletonMethodName(Class_ $class): ?array
{
foreach ((array) $class->stmts as $classStmt) {
if ($classStmt instanceof ClassMethod) {
if (! $classStmt->isStatic()) {
continue;
}
$staticPropertyFetch = $this->singletonClassMethodAnalyzer->matchStaticPropertyFetch($classStmt);
if ($staticPropertyFetch === null) {
return null;
}
return [$this->getName($staticPropertyFetch), $this->getName($classStmt)];
}
}
return null;
}
private function refactorClassStmts(
Class_ $node,
string $getSingletonMethodName,
string $singletonPropertyName
): Class_ {
foreach ((array) $node->stmts as $key => $classStmt) {
if ($classStmt instanceof ClassMethod) {
if ($this->isName($classStmt, $getSingletonMethodName)) {
unset($node->stmts[$key]);
continue;
}
if (! $this->isNames($classStmt, ['__construct', '__clone', '__wakeup'])) {
continue;
}
if (! $classStmt->isPublic()) {
// remove non-public empty
if ($classStmt->stmts === []) {
unset($node->stmts[$key]);
} else {
$this->makePublic($classStmt);
}
}
} elseif ($classStmt instanceof Property) {
if ($this->isName($classStmt, $singletonPropertyName)) {
unset($node->stmts[$key]);
}
}
}
return $node;
}
}

View File

@ -0,0 +1,24 @@
<?php declare(strict_types=1);
namespace Rector\Legacy\Tests\Rector\ClassMethod\ChangeSingletonToServiceRector;
use Rector\Legacy\Rector\ClassMethod\ChangeSingletonToServiceRector;
use Rector\Testing\PHPUnit\AbstractRectorTestCase;
final class ChangeSingletonToServiceRectorTest extends AbstractRectorTestCase
{
public function test(): void
{
$this->doTestFiles([
__DIR__ . '/Fixture/fixture.php.inc',
__DIR__ . '/Fixture/static_variable.php.inc',
__DIR__ . '/Fixture/protected_construct.php.inc',
__DIR__ . '/Fixture/non_empty_protected_construct.php.inc',
]);
}
protected function getRectorClass(): string
{
return ChangeSingletonToServiceRector::class;
}
}

View File

@ -0,0 +1,29 @@
<?php
namespace Rector\Legacy\Tests\Rector\ClassMethod\ChangeSingletonToServiceRector\Fixture;
class SomeClass
{
private static $instance;
public static function getInstance()
{
if (null === static::$instance) {
static::$instance = new static();
}
return static::$instance;
}
}
?>
-----
<?php
namespace Rector\Legacy\Tests\Rector\ClassMethod\ChangeSingletonToServiceRector\Fixture;
class SomeClass
{
}
?>

View File

@ -0,0 +1,38 @@
<?php
namespace Rector\Legacy\Tests\Rector\ClassMethod\ChangeSingletonToServiceRector\Fixture;
class NonEmptyProtectedConstruct
{
private static $instance;
public static function getInstance()
{
if (null === static::$instance) {
static::$instance = new static();
}
return static::$instance;
}
protected function __construct()
{
$bla = 1;
}
}
?>
-----
<?php
namespace Rector\Legacy\Tests\Rector\ClassMethod\ChangeSingletonToServiceRector\Fixture;
class NonEmptyProtectedConstruct
{
public function __construct()
{
$bla = 1;
}
}
?>

View File

@ -0,0 +1,33 @@
<?php
namespace Rector\Legacy\Tests\Rector\ClassMethod\ChangeSingletonToServiceRector\Fixture;
class ProtectedConstruct
{
private static $instance;
public static function getInstance()
{
if (null === static::$instance) {
static::$instance = new static();
}
return static::$instance;
}
protected function __construct()
{
}
}
?>
-----
<?php
namespace Rector\Legacy\Tests\Rector\ClassMethod\ChangeSingletonToServiceRector\Fixture;
class ProtectedConstruct
{
}
?>

View File

@ -0,0 +1,29 @@
<?php
namespace Rector\Legacy\Tests\Rector\ClassMethod\ChangeSingletonToServiceRector\Fixture;
class StaticVariable
{
private static $instance;
public static function getThis()
{
if(!self::$instance) {
self::$instance = new self;
}
return self::$instance;
}
}
?>
-----
<?php
namespace Rector\Legacy\Tests\Rector\ClassMethod\ChangeSingletonToServiceRector\Fixture;
class StaticVariable
{
}
?>

View File

@ -19,7 +19,7 @@ final class ReturnTypeDeclarationRector extends AbstractTypeDeclarationRector
/**
* @var string[]
*/
private $excludeClassMethodNames = ['__construct', '__destruct', '__clone'];
private const EXCLUDED_METHOD_NAMES = ['__construct', '__destruct', '__clone'];
public function getDefinition(): RectorDefinition
{
@ -69,7 +69,7 @@ CODE_SAMPLE
}
// skip excluded methods
if ($node instanceof ClassMethod && $this->isNames($node, $this->excludeClassMethodNames)) {
if ($node instanceof ClassMethod && $this->isNames($node, self::EXCLUDED_METHOD_NAMES)) {
return null;
}

View File

@ -169,4 +169,6 @@ parameters:
- '#Cannot cast array<string\>\|bool\|string\|null to string#'
- '#Parameter \#1 \$node of method Rector\\PhpParser\\Node\\Manipulator\\VisibilityManipulator\:\:makeAbstract\(\) expects PhpParser\\Node\\Stmt\\Class_\|PhpParser\\Node\\Stmt\\ClassMethod, PhpParser\\Node given#'
- '#Method Rector\\Legacy\\NodeAnalyzer\\SingletonClassMethodAnalyzer\:\:matchStaticPropertyFetch\(\) should return PhpParser\\Node\\Expr\\StaticPropertyFetch\|null but returns PhpParser\\Node\\Expr#'
- '#Method Rector\\Legacy\\Rector\\ClassMethod\\ChangeSingletonToServiceRector\:\:matchStaticPropertyFetchAndGetSingletonMethodName\(\) should return array<string\>\|null but returns array<int, string\|null\>#'

View File

@ -4,6 +4,8 @@ parameters:
- "/Fixtures/"
- "/Expected/"
- "/Source/"
- "/tests/" # for better performance of local changes
# autoload-buggy cases
- "*.php.inc"
# string might not exist for SplitStringClassConstantToClassConstFetchRector
@ -15,3 +17,4 @@ parameters:
php_version_features: '7.1'
services:
# Rector\CodingStyle\Rector\Namespace_\ImportFullyQualifiedNamesRector: ~