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: ~