From cc2b9df06d891a6a1ad15fb4a079391bb4e32d41 Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Sun, 26 May 2019 14:43:20 +0200 Subject: [PATCH] [Legacy] Add ChangeSingletonToServiceRector --- composer.json | 8 +- ecs.yaml | 2 + .../src/Command/CreateRectorCommand.php | 15 ++ packages/Legacy/config/config.yaml | 8 + .../SingletonClassMethodAnalyzer.php | 125 +++++++++++++++ .../ChangeSingletonToServiceRector.php | 151 ++++++++++++++++++ .../ChangeSingletonToServiceRectorTest.php | 24 +++ .../Fixture/fixture.php.inc | 29 ++++ .../non_empty_protected_construct.php.inc | 38 +++++ .../Fixture/protected_construct.php.inc | 33 ++++ .../Fixture/static_variable.php.inc | 29 ++++ .../ReturnTypeDeclarationRector.php | 4 +- phpstan.neon | 2 + rector.yaml | 3 + 14 files changed, 466 insertions(+), 5 deletions(-) create mode 100644 packages/Legacy/config/config.yaml create mode 100644 packages/Legacy/src/NodeAnalyzer/SingletonClassMethodAnalyzer.php create mode 100644 packages/Legacy/src/Rector/ClassMethod/ChangeSingletonToServiceRector.php create mode 100644 packages/Legacy/tests/Rector/ClassMethod/ChangeSingletonToServiceRector/ChangeSingletonToServiceRectorTest.php create mode 100644 packages/Legacy/tests/Rector/ClassMethod/ChangeSingletonToServiceRector/Fixture/fixture.php.inc create mode 100644 packages/Legacy/tests/Rector/ClassMethod/ChangeSingletonToServiceRector/Fixture/non_empty_protected_construct.php.inc create mode 100644 packages/Legacy/tests/Rector/ClassMethod/ChangeSingletonToServiceRector/Fixture/protected_construct.php.inc create mode 100644 packages/Legacy/tests/Rector/ClassMethod/ChangeSingletonToServiceRector/Fixture/static_variable.php.inc diff --git a/composer.json b/composer.json index 2a3c908efdc..561547286ed 100644 --- a/composer.json +++ b/composer.json @@ -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", @@ -159,4 +161,4 @@ "dev-master": "0.5-dev" } } -} +} \ No newline at end of file diff --git a/ecs.yaml b/ecs.yaml index 303e06a500a..698fccdc348 100644 --- a/ecs.yaml +++ b/ecs.yaml @@ -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' diff --git a/packages/ContributorTools/src/Command/CreateRectorCommand.php b/packages/ContributorTools/src/Command/CreateRectorCommand.php index ec849118d15..ea735be53b2 100644 --- a/packages/ContributorTools/src/Command/CreateRectorCommand.php +++ b/packages/ContributorTools/src/Command/CreateRectorCommand.php @@ -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 = '#(?"authors": \[\s+)(?.*?)(?\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; + } } diff --git a/packages/Legacy/config/config.yaml b/packages/Legacy/config/config.yaml new file mode 100644 index 00000000000..306f7e1f6d7 --- /dev/null +++ b/packages/Legacy/config/config.yaml @@ -0,0 +1,8 @@ +services: + _defaults: + public: true + autowire: true + + Rector\Legacy\: + resource: '../src' + exclude: '../src/{Rector/**/*Rector.php}' diff --git a/packages/Legacy/src/NodeAnalyzer/SingletonClassMethodAnalyzer.php b/packages/Legacy/src/NodeAnalyzer/SingletonClassMethodAnalyzer.php new file mode 100644 index 00000000000..0f7348d5792 --- /dev/null +++ b/packages/Legacy/src/NodeAnalyzer/SingletonClassMethodAnalyzer.php @@ -0,0 +1,125 @@ +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; + } +} diff --git a/packages/Legacy/src/Rector/ClassMethod/ChangeSingletonToServiceRector.php b/packages/Legacy/src/Rector/ClassMethod/ChangeSingletonToServiceRector.php new file mode 100644 index 00000000000..79066c98d21 --- /dev/null +++ b/packages/Legacy/src/Rector/ClassMethod/ChangeSingletonToServiceRector.php @@ -0,0 +1,151 @@ +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; + } +} diff --git a/packages/Legacy/tests/Rector/ClassMethod/ChangeSingletonToServiceRector/ChangeSingletonToServiceRectorTest.php b/packages/Legacy/tests/Rector/ClassMethod/ChangeSingletonToServiceRector/ChangeSingletonToServiceRectorTest.php new file mode 100644 index 00000000000..49fbf8e6b38 --- /dev/null +++ b/packages/Legacy/tests/Rector/ClassMethod/ChangeSingletonToServiceRector/ChangeSingletonToServiceRectorTest.php @@ -0,0 +1,24 @@ +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; + } +} diff --git a/packages/Legacy/tests/Rector/ClassMethod/ChangeSingletonToServiceRector/Fixture/fixture.php.inc b/packages/Legacy/tests/Rector/ClassMethod/ChangeSingletonToServiceRector/Fixture/fixture.php.inc new file mode 100644 index 00000000000..47739f4d0ca --- /dev/null +++ b/packages/Legacy/tests/Rector/ClassMethod/ChangeSingletonToServiceRector/Fixture/fixture.php.inc @@ -0,0 +1,29 @@ + +----- + diff --git a/packages/Legacy/tests/Rector/ClassMethod/ChangeSingletonToServiceRector/Fixture/non_empty_protected_construct.php.inc b/packages/Legacy/tests/Rector/ClassMethod/ChangeSingletonToServiceRector/Fixture/non_empty_protected_construct.php.inc new file mode 100644 index 00000000000..958bc02a975 --- /dev/null +++ b/packages/Legacy/tests/Rector/ClassMethod/ChangeSingletonToServiceRector/Fixture/non_empty_protected_construct.php.inc @@ -0,0 +1,38 @@ + +----- + diff --git a/packages/Legacy/tests/Rector/ClassMethod/ChangeSingletonToServiceRector/Fixture/protected_construct.php.inc b/packages/Legacy/tests/Rector/ClassMethod/ChangeSingletonToServiceRector/Fixture/protected_construct.php.inc new file mode 100644 index 00000000000..11571ffb42c --- /dev/null +++ b/packages/Legacy/tests/Rector/ClassMethod/ChangeSingletonToServiceRector/Fixture/protected_construct.php.inc @@ -0,0 +1,33 @@ + +----- + diff --git a/packages/Legacy/tests/Rector/ClassMethod/ChangeSingletonToServiceRector/Fixture/static_variable.php.inc b/packages/Legacy/tests/Rector/ClassMethod/ChangeSingletonToServiceRector/Fixture/static_variable.php.inc new file mode 100644 index 00000000000..6cd76bc3fe8 --- /dev/null +++ b/packages/Legacy/tests/Rector/ClassMethod/ChangeSingletonToServiceRector/Fixture/static_variable.php.inc @@ -0,0 +1,29 @@ + +----- + diff --git a/packages/TypeDeclaration/src/Rector/FunctionLike/ReturnTypeDeclarationRector.php b/packages/TypeDeclaration/src/Rector/FunctionLike/ReturnTypeDeclarationRector.php index ee949f42469..e9fe153487a 100644 --- a/packages/TypeDeclaration/src/Rector/FunctionLike/ReturnTypeDeclarationRector.php +++ b/packages/TypeDeclaration/src/Rector/FunctionLike/ReturnTypeDeclarationRector.php @@ -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; } diff --git a/phpstan.neon b/phpstan.neon index 3bfd5ed7c78..14fe2af2f99 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -169,4 +169,6 @@ parameters: - '#Cannot cast array\|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\|null but returns array#' diff --git a/rector.yaml b/rector.yaml index 94c23a7339d..5a4d0af1e24 100644 --- a/rector.yaml +++ b/rector.yaml @@ -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: ~