diff --git a/config/level/code-quality/code-quality.yaml b/config/level/code-quality/code-quality.yaml
index d7fef79e5be..fb22d3a423a 100644
--- a/config/level/code-quality/code-quality.yaml
+++ b/config/level/code-quality/code-quality.yaml
@@ -35,3 +35,4 @@ services:
Rector\CodeQuality\Rector\LogicalAnd\AndAssignsToSeparateLinesRector: ~
Rector\CodeQuality\Rector\For_\ForToForeachRector: ~
Rector\CodeQuality\Rector\FuncCall\CompactToVariablesRector: ~
+ Rector\CodeQuality\Rector\Class_\CompleteDynamicPropertiesRector: ~
diff --git a/docs/AllRectorsOverview.md b/docs/AllRectorsOverview.md
index 3561f2a037b..e0a30fc5edb 100644
--- a/docs/AllRectorsOverview.md
+++ b/docs/AllRectorsOverview.md
@@ -1,4 +1,4 @@
-# All 299 Rectors Overview
+# All 300 Rectors Overview
- [Projects](#projects)
- [General](#general)
@@ -769,6 +769,28 @@ Joins concat of 2 strings
+### `CompleteDynamicPropertiesRector`
+
+- class: `Rector\CodeQuality\Rector\Class_\CompleteDynamicPropertiesRector`
+
+Add missing dynamic properties
+
+```diff
+ class SomeClass
+ {
++ /**
++ * @var int
++ */
++ public $value;
+ public function set()
+ {
+ $this->value = 5;
+ }
+ }
+```
+
+
+
## CodingStyle
### `SplitDoubleAssignRector`
diff --git a/packages/CodeQuality/src/Rector/Class_/CompleteDynamicPropertiesRector.php b/packages/CodeQuality/src/Rector/Class_/CompleteDynamicPropertiesRector.php
new file mode 100644
index 00000000000..0e3d0949ee7
--- /dev/null
+++ b/packages/CodeQuality/src/Rector/Class_/CompleteDynamicPropertiesRector.php
@@ -0,0 +1,189 @@
+callableNodeTraverser = $callableNodeTraverser;
+ $this->typeToStringResolver = $typeToStringResolver;
+ $this->docBlockManipulator = $docBlockManipulator;
+ }
+
+ public function getDefinition(): RectorDefinition
+ {
+ return new RectorDefinition('Add missing dynamic properties', [
+ new CodeSample(
+ <<<'CODE_SAMPLE'
+class SomeClass
+{
+ public function set()
+ {
+ $this->value = 5;
+ }
+}
+CODE_SAMPLE
+ ,
+ <<<'CODE_SAMPLE'
+class SomeClass
+{
+ /**
+ * @var int
+ */
+ public $value;
+ public function set()
+ {
+ $this->value = 5;
+ }
+}
+CODE_SAMPLE
+ ),
+ ]);
+ }
+
+ /**
+ * @return string[]
+ */
+ public function getNodeTypes(): array
+ {
+ return [Class_::class];
+ }
+
+ /**
+ * @param Class_ $node
+ */
+ public function refactor(Node $node): ?Node
+ {
+ $fetchedLocalPropertyNameToTypes = $this->resolveFetchedLocalPropertyNameToTypes($node);
+
+ $propertyNames = [];
+ $this->callableNodeTraverser->traverseNodesWithCallable($node->stmts, function (Node $node) use (
+ &$propertyNames
+ ) {
+ if (! $node instanceof Property) {
+ return null;
+ }
+
+ $propertyNames[] = $this->getName($node);
+ });
+
+ $fetchedLocalPropertyNames = array_keys($fetchedLocalPropertyNameToTypes);
+ $propertiesToComplete = array_diff($fetchedLocalPropertyNames, $propertyNames);
+
+ $newProperties = $this->createNewProperties($fetchedLocalPropertyNameToTypes, $propertiesToComplete);
+
+ $node->stmts = array_merge_recursive($newProperties, $node->stmts);
+
+ return $node;
+ }
+
+ /**
+ * @param string[][][] $fetchedLocalPropertyNameToTypes
+ * @param string[] $propertiesToComplete
+ * @return Property[]
+ */
+ private function createNewProperties(array $fetchedLocalPropertyNameToTypes, array $propertiesToComplete): array
+ {
+ $newProperties = [];
+ foreach ($fetchedLocalPropertyNameToTypes as $propertyName => $propertyTypes) {
+ if (! in_array($propertyName, $propertiesToComplete, true)) {
+ continue;
+ }
+
+ $propertyTypes = Arrays::flatten($propertyTypes);
+ $propertyTypesAsString = implode('|', $propertyTypes);
+
+ $propertyBuilder = $this->builderFactory->property($propertyName)
+ ->makePublic();
+
+ if ($this->isAtLeastPhpVersion('7.4') && count($propertyTypes) === 1) {
+ $newProperty = $propertyBuilder->setType($propertyTypes[0])
+ ->getNode();
+ } else {
+ $newProperty = $propertyBuilder->getNode();
+ $this->docBlockManipulator->changeVarTag($newProperty, $propertyTypesAsString);
+ }
+
+ $newProperties[] = $newProperty;
+ }
+ return $newProperties;
+ }
+
+ /**
+ * @return string[][][]
+ */
+ private function resolveFetchedLocalPropertyNameToTypes(Class_ $class): array
+ {
+ $fetchedLocalPropertyNameToTypes = [];
+
+ $this->callableNodeTraverser->traverseNodesWithCallable($class->stmts, function (Node $node) use (
+ &$fetchedLocalPropertyNameToTypes
+ ) {
+ if (! $node instanceof PropertyFetch) {
+ return null;
+ }
+
+ if (! $this->isName($node->var, 'this')) {
+ return null;
+ }
+
+ $parentNode = $node->getAttribute(AttributeKey::PARENT_NODE);
+
+ // fallback type
+ $propertyFetchType = ['mixed'];
+
+ // possible get type
+ if ($parentNode instanceof Assign) {
+ $assignedValueStaticType = $this->getStaticType($parentNode->expr);
+ if ($assignedValueStaticType) {
+ $propertyFetchType = $this->typeToStringResolver->resolve($assignedValueStaticType);
+ }
+ }
+
+ /** @var string $propertyName */
+ $propertyName = $this->getName($node->name);
+
+ $fetchedLocalPropertyNameToTypes[$propertyName][] = $propertyFetchType;
+ });
+
+ return $fetchedLocalPropertyNameToTypes;
+ }
+}
diff --git a/packages/CodeQuality/tests/Rector/Class_/CompleteDynamicPropertiesRector/CompleteDynamicPropertiesRectorTest.php b/packages/CodeQuality/tests/Rector/Class_/CompleteDynamicPropertiesRector/CompleteDynamicPropertiesRectorTest.php
new file mode 100644
index 00000000000..14e02a926b0
--- /dev/null
+++ b/packages/CodeQuality/tests/Rector/Class_/CompleteDynamicPropertiesRector/CompleteDynamicPropertiesRectorTest.php
@@ -0,0 +1,23 @@
+doTestFiles([
+ __DIR__ . '/Fixture/fixture.php.inc',
+ __DIR__ . '/Fixture/multiple_types.php.inc',
+ __DIR__ . '/Fixture/skip_defined.php.inc',
+ ]);
+ }
+
+ protected function getRectorClass(): string
+ {
+ return CompleteDynamicPropertiesRector::class;
+ }
+}
diff --git a/packages/CodeQuality/tests/Rector/Class_/CompleteDynamicPropertiesRector/Fixture/fixture.php.inc b/packages/CodeQuality/tests/Rector/Class_/CompleteDynamicPropertiesRector/Fixture/fixture.php.inc
new file mode 100644
index 00000000000..b36a4b6212c
--- /dev/null
+++ b/packages/CodeQuality/tests/Rector/Class_/CompleteDynamicPropertiesRector/Fixture/fixture.php.inc
@@ -0,0 +1,28 @@
+value = 5;
+ }
+}
+
+?>
+-----
+value = 5;
+ }
+}
+
+?>
diff --git a/packages/CodeQuality/tests/Rector/Class_/CompleteDynamicPropertiesRector/Fixture/multiple_types.php.inc b/packages/CodeQuality/tests/Rector/Class_/CompleteDynamicPropertiesRector/Fixture/multiple_types.php.inc
new file mode 100644
index 00000000000..570b3fb2621
--- /dev/null
+++ b/packages/CodeQuality/tests/Rector/Class_/CompleteDynamicPropertiesRector/Fixture/multiple_types.php.inc
@@ -0,0 +1,35 @@
+value = 5;
+
+ $this->value = 'hey';
+ }
+}
+
+?>
+-----
+value = 5;
+
+ $this->value = 'hey';
+ }
+}
+
+?>
diff --git a/packages/CodeQuality/tests/Rector/Class_/CompleteDynamicPropertiesRector/Fixture/skip_defined.php.inc b/packages/CodeQuality/tests/Rector/Class_/CompleteDynamicPropertiesRector/Fixture/skip_defined.php.inc
new file mode 100644
index 00000000000..12e1a3aa583
--- /dev/null
+++ b/packages/CodeQuality/tests/Rector/Class_/CompleteDynamicPropertiesRector/Fixture/skip_defined.php.inc
@@ -0,0 +1,12 @@
+value = 5;
+ }
+}
diff --git a/packages/Php/src/Rector/Property/TypedPropertyRector.php b/packages/Php/src/Rector/Property/TypedPropertyRector.php
index 90903c9749e..437d47e3ead 100644
--- a/packages/Php/src/Rector/Property/TypedPropertyRector.php
+++ b/packages/Php/src/Rector/Property/TypedPropertyRector.php
@@ -96,6 +96,7 @@ CODE_SAMPLE
return null;
}
+ // type is already set → skip
if ($node->type !== null) {
return null;
}