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; }