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/docs/AllRectorsOverview.md b/docs/AllRectorsOverview.md index 8df57d72d3d..3561f2a037b 100644 --- a/docs/AllRectorsOverview.md +++ b/docs/AllRectorsOverview.md @@ -1,4 +1,4 @@ -# All 298 Rectors Overview +# All 299 Rectors Overview - [Projects](#projects) - [General](#general) @@ -14,6 +14,7 @@ - [DomainDrivenDesign](#domaindrivendesign) - [Guzzle](#guzzle) - [Laravel](#laravel) +- [Legacy](#legacy) - [MysqlToMysqli](#mysqltomysqli) - [Nette](#nette) - [NetteTesterToPHPUnit](#nettetestertophpunit) @@ -1730,6 +1731,37 @@ Move help facade-like function calls to constructor injection
+## Legacy + +### `ChangeSingletonToServiceRector` + +- class: `Rector\Legacy\Rector\ClassMethod\ChangeSingletonToServiceRector` + +Change singleton class to normal class that can be registered as a service + +```diff + class SomeClass + { +- private static $instance; +- +- private function __construct() ++ public function __construct() + { +- } +- +- public static function getInstance() +- { +- if (null === static::$instance) { +- static::$instance = new static(); +- } +- +- return static::$instance; + } + } +``` + +
+ ## MysqlToMysqli ### `MysqlAssignToMysqliRector` @@ -4376,9 +4408,9 @@ Finalize every class constant that is used only locally
-### `AbstractChildlessUnusedClassesRector` +### `MakeUnusedClassesWithChildrenAbstractRector` -- class: `Rector\SOLID\Rector\Class_\AbstractChildlessUnusedClassesRector` +- class: `Rector\SOLID\Rector\Class_\MakeUnusedClassesWithChildrenAbstractRector` Classes that have no children nor are used, should have abstract @@ -4391,11 +4423,6 @@ Classes that have no children nor are used, should have abstract +abstract class PossibleAbstractClass { } - - function run() - { - return new SomeClass(); - } ```
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: ~