diff --git a/config/set/php84.php b/config/set/php84.php index 8bdf9646907..582a9ec5780 100644 --- a/config/set/php84.php +++ b/config/set/php84.php @@ -14,17 +14,18 @@ use Rector\Php84\Rector\Param\ExplicitNullableParamTypeRector; return static function (RectorConfig $rectorConfig): void { - $rectorConfig->rules( - [ - ExplicitNullableParamTypeRector::class, - RoundingModeEnumRector::class, - AddEscapeArgumentRector::class, - NewMethodCallWithoutParenthesesRector::class, - DeprecatedAnnotationToDeprecatedAttributeRector::class, - ForeachToArrayFindRector::class, - ForeachToArrayFindKeyRector::class, - ForeachToArrayAllRector::class, - ForeachToArrayAnyRector::class, - ] - ); + $rectorConfig->rules([ + ExplicitNullableParamTypeRector::class, + RoundingModeEnumRector::class, + AddEscapeArgumentRector::class, + NewMethodCallWithoutParenthesesRector::class, + DeprecatedAnnotationToDeprecatedAttributeRector::class, + ForeachToArrayFindRector::class, + ForeachToArrayFindKeyRector::class, + ForeachToArrayAllRector::class, + ForeachToArrayAnyRector::class, + + // optional + // \Rector\Php84\Rector\Class_\PropertyHookRector::class, + ]); }; diff --git a/rules-tests/Php84/Rector/Class_/PropertyHookRector/Fixture/skip_different_variable.php.inc b/rules-tests/Php84/Rector/Class_/PropertyHookRector/Fixture/skip_different_variable.php.inc new file mode 100644 index 00000000000..30cb25da195 --- /dev/null +++ b/rules-tests/Php84/Rector/Class_/PropertyHookRector/Fixture/skip_different_variable.php.inc @@ -0,0 +1,20 @@ +surname; + } + + public function setName(string $name): void + { + $this->surname = ucfirst($name); + } +} diff --git a/rules-tests/Php84/Rector/Class_/PropertyHookRector/Fixture/skip_get_set_magic.php.inc b/rules-tests/Php84/Rector/Class_/PropertyHookRector/Fixture/skip_get_set_magic.php.inc new file mode 100644 index 00000000000..e5b35b1204e --- /dev/null +++ b/rules-tests/Php84/Rector/Class_/PropertyHookRector/Fixture/skip_get_set_magic.php.inc @@ -0,0 +1,26 @@ +name; + } + + public function setName(string $name): void + { + $this->name = ucfirst($name); + } + + public function __get($name) + { + } + + public function __set($name, $value) + { + } +} diff --git a/rules-tests/Php84/Rector/Class_/PropertyHookRector/Fixture/skip_getter_and_readonly.php.inc b/rules-tests/Php84/Rector/Class_/PropertyHookRector/Fixture/skip_getter_and_readonly.php.inc new file mode 100644 index 00000000000..a8dd2371297 --- /dev/null +++ b/rules-tests/Php84/Rector/Class_/PropertyHookRector/Fixture/skip_getter_and_readonly.php.inc @@ -0,0 +1,18 @@ +name = $name; + } + + public function getName(): string + { + return $this->name; + } +} diff --git a/rules-tests/Php84/Rector/Class_/PropertyHookRector/Fixture/skip_method_with_attributes.php.inc b/rules-tests/Php84/Rector/Class_/PropertyHookRector/Fixture/skip_method_with_attributes.php.inc new file mode 100644 index 00000000000..4741a2e3eb7 --- /dev/null +++ b/rules-tests/Php84/Rector/Class_/PropertyHookRector/Fixture/skip_method_with_attributes.php.inc @@ -0,0 +1,22 @@ +name; + } + + #[Required] + public function setName(string $name): void + { + $this->name = ucfirst($name); + } +} diff --git a/rules-tests/Php84/Rector/Class_/PropertyHookRector/Fixture/skip_missmatching_getter_setter_names.php.inc b/rules-tests/Php84/Rector/Class_/PropertyHookRector/Fixture/skip_missmatching_getter_setter_names.php.inc new file mode 100644 index 00000000000..f9eb33fd00f --- /dev/null +++ b/rules-tests/Php84/Rector/Class_/PropertyHookRector/Fixture/skip_missmatching_getter_setter_names.php.inc @@ -0,0 +1,18 @@ +name; + } + + public function setAnotherName(string $name): void + { + $this->name = ucfirst($name); + } +} diff --git a/rules-tests/Php84/Rector/Class_/PropertyHookRector/Fixture/skip_multi_stmts_getter.php.inc b/rules-tests/Php84/Rector/Class_/PropertyHookRector/Fixture/skip_multi_stmts_getter.php.inc new file mode 100644 index 00000000000..82bf0145be7 --- /dev/null +++ b/rules-tests/Php84/Rector/Class_/PropertyHookRector/Fixture/skip_multi_stmts_getter.php.inc @@ -0,0 +1,15 @@ +name; + } +} diff --git a/rules-tests/Php84/Rector/Class_/PropertyHookRector/Fixture/skip_non_final_class_to_avoid_child_break.php.inc b/rules-tests/Php84/Rector/Class_/PropertyHookRector/Fixture/skip_non_final_class_to_avoid_child_break.php.inc new file mode 100644 index 00000000000..651b06fa9e6 --- /dev/null +++ b/rules-tests/Php84/Rector/Class_/PropertyHookRector/Fixture/skip_non_final_class_to_avoid_child_break.php.inc @@ -0,0 +1,18 @@ +name; + } + + public function setName(string $name): void + { + $this->name = ucfirst($name); + } +} diff --git a/rules-tests/Php84/Rector/Class_/PropertyHookRector/Fixture/skip_parent_contract.php.inc b/rules-tests/Php84/Rector/Class_/PropertyHookRector/Fixture/skip_parent_contract.php.inc new file mode 100644 index 00000000000..287c336b6af --- /dev/null +++ b/rules-tests/Php84/Rector/Class_/PropertyHookRector/Fixture/skip_parent_contract.php.inc @@ -0,0 +1,15 @@ +name; + } +} diff --git a/rules-tests/Php84/Rector/Class_/PropertyHookRector/Fixture/skip_readonly_class.php.inc b/rules-tests/Php84/Rector/Class_/PropertyHookRector/Fixture/skip_readonly_class.php.inc new file mode 100644 index 00000000000..4e4c98fe329 --- /dev/null +++ b/rules-tests/Php84/Rector/Class_/PropertyHookRector/Fixture/skip_readonly_class.php.inc @@ -0,0 +1,18 @@ +name; + } + + public function setName(string $name): void + { + $this->name = ucfirst($name); + } +} diff --git a/rules-tests/Php84/Rector/Class_/PropertyHookRector/Fixture/skip_readonly_getter_and_setter.php.inc b/rules-tests/Php84/Rector/Class_/PropertyHookRector/Fixture/skip_readonly_getter_and_setter.php.inc new file mode 100644 index 00000000000..2dce01eb3dc --- /dev/null +++ b/rules-tests/Php84/Rector/Class_/PropertyHookRector/Fixture/skip_readonly_getter_and_setter.php.inc @@ -0,0 +1,18 @@ +name; + } + + public function setName(string $name): void + { + $this->name = ucfirst($name); + } +} diff --git a/rules-tests/Php84/Rector/Class_/PropertyHookRector/Fixture/skip_readonly_promoted_property.php.inc b/rules-tests/Php84/Rector/Class_/PropertyHookRector/Fixture/skip_readonly_promoted_property.php.inc new file mode 100644 index 00000000000..0e1c8210636 --- /dev/null +++ b/rules-tests/Php84/Rector/Class_/PropertyHookRector/Fixture/skip_readonly_promoted_property.php.inc @@ -0,0 +1,17 @@ +value; + } +} diff --git a/rules-tests/Php84/Rector/Class_/PropertyHookRector/Fixture/some_fixture.php.inc b/rules-tests/Php84/Rector/Class_/PropertyHookRector/Fixture/some_fixture.php.inc new file mode 100644 index 00000000000..051d0840fb9 --- /dev/null +++ b/rules-tests/Php84/Rector/Class_/PropertyHookRector/Fixture/some_fixture.php.inc @@ -0,0 +1,36 @@ +name; + } + + public function setName(string $name): void + { + $this->name = ucfirst($name); + } +} + +?> +----- + $this->name; + set(string $name) { + $this->name = ucfirst($name); + } + } +} + +?> diff --git a/rules-tests/Php84/Rector/Class_/PropertyHookRector/PropertyHookRectorTest.php b/rules-tests/Php84/Rector/Class_/PropertyHookRector/PropertyHookRectorTest.php new file mode 100644 index 00000000000..9a3360ed62b --- /dev/null +++ b/rules-tests/Php84/Rector/Class_/PropertyHookRector/PropertyHookRectorTest.php @@ -0,0 +1,28 @@ +doTestFile($filePath); + } + + public static function provideData(): Iterator + { + return self::yieldFilesFromDirectory(__DIR__ . '/Fixture'); + } + + public function provideConfigFilePath(): string + { + return __DIR__ . '/config/configured_rule.php'; + } +} diff --git a/rules-tests/Php84/Rector/Class_/PropertyHookRector/Source/SomeParentContractInterface.php b/rules-tests/Php84/Rector/Class_/PropertyHookRector/Source/SomeParentContractInterface.php new file mode 100644 index 00000000000..ab4920f92cf --- /dev/null +++ b/rules-tests/Php84/Rector/Class_/PropertyHookRector/Source/SomeParentContractInterface.php @@ -0,0 +1,8 @@ +rule(PropertyHookRector::class); + + $rectorConfig->phpVersion(PhpVersion::PHP_84); +}; diff --git a/rules/CodingStyle/Rector/FunctionLike/FunctionLikeToFirstClassCallableRector.php b/rules/CodingStyle/Rector/FunctionLike/FunctionLikeToFirstClassCallableRector.php index e3bc2fd365c..e8beda1aa68 100644 --- a/rules/CodingStyle/Rector/FunctionLike/FunctionLikeToFirstClassCallableRector.php +++ b/rules/CodingStyle/Rector/FunctionLike/FunctionLikeToFirstClassCallableRector.php @@ -74,6 +74,11 @@ public function refactor(Node $node): null|CallLike return $callLike; } + public function provideMinPhpVersion(): int + { + return PhpVersionFeature::FIRST_CLASS_CALLABLE_SYNTAX; + } + private function shouldSkip( ArrowFunction|Closure $node, FuncCall|MethodCall|StaticCall $callLike, @@ -240,9 +245,4 @@ private function isChainedCall(FuncCall|MethodCall|StaticCall $callLike): bool return $callLike->var instanceof CallLike; } - - public function provideMinPhpVersion(): int - { - return PhpVersionFeature::FIRST_CLASS_CALLABLE_SYNTAX; - } } diff --git a/rules/Php74/Rector/Double/RealToFloatTypeCastRector.php b/rules/Php74/Rector/Double/RealToFloatTypeCastRector.php index f4ded2b9d84..bddccba25bc 100644 --- a/rules/Php74/Rector/Double/RealToFloatTypeCastRector.php +++ b/rules/Php74/Rector/Double/RealToFloatTypeCastRector.php @@ -4,12 +4,12 @@ namespace Rector\Php74\Rector\Double; -use Rector\Renaming\Rector\Cast\RenameCastRector; use PhpParser\Node; use PhpParser\Node\Expr\Cast\Double; use Rector\Configuration\Deprecation\Contract\DeprecatedInterface; use Rector\Exception\ShouldNotHappenException; use Rector\Rector\AbstractRector; +use Rector\Renaming\Rector\Cast\RenameCastRector; use Rector\ValueObject\PhpVersionFeature; use Rector\VersionBonding\Contract\MinPhpVersionInterface; use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample; diff --git a/rules/Php84/NodeFactory/PropertyHookFactory.php b/rules/Php84/NodeFactory/PropertyHookFactory.php new file mode 100644 index 00000000000..634574d6aaf --- /dev/null +++ b/rules/Php84/NodeFactory/PropertyHookFactory.php @@ -0,0 +1,43 @@ +name->toString(); + + if ($methodName === 'get' . ucfirst($propertyName)) { + $methodName = 'get'; + } elseif ($methodName === 'set' . ucfirst($propertyName)) { + $methodName = 'set'; + } else { + return null; + } + + Assert::notNull($classMethod->stmts); + + $soleStmt = $classMethod->stmts[0]; + + // use sole Expr + if (($soleStmt instanceof Expression || $soleStmt instanceof Return_) && $methodName !== 'set') { + $body = $soleStmt->expr; + } else { + $body = [$soleStmt]; + } + + $setterPropertyHook = new PropertyHook($methodName, $body); + $setterPropertyHook->params = $classMethod->params; + + return $setterPropertyHook; + } +} diff --git a/rules/Php84/NodeFinder/SetterAndGetterFinder.php b/rules/Php84/NodeFinder/SetterAndGetterFinder.php new file mode 100644 index 00000000000..2adcc7f15be --- /dev/null +++ b/rules/Php84/NodeFinder/SetterAndGetterFinder.php @@ -0,0 +1,68 @@ +findGetterClassMethod($class, $propertyName); + if ($getterClassMethod instanceof ClassMethod) { + $classMethods[] = $getterClassMethod; + } + + $setterClassMethod = $this->findSetterClassMethod($class, $propertyName); + if ($setterClassMethod instanceof ClassMethod) { + $classMethods[] = $setterClassMethod; + } + + return $classMethods; + } + + public function findGetterClassMethod(Class_ $class, string $propertyName): ?ClassMethod + { + foreach ($class->getMethods() as $classMethod) { + if (! $this->classMethodAndPropertyAnalyzer->hasPropertyFetchReturn($classMethod, $propertyName)) { + continue; + } + + return $classMethod; + } + + return null; + } + + public function findSetterClassMethod(Class_ $class, string $propertyName): ?ClassMethod + { + foreach ($class->getMethods() as $classMethod) { + + if ($classMethod->isMagic()) { + continue; + } + + if (! $this->classMethodAndPropertyAnalyzer->hasOnlyPropertyAssign($classMethod, $propertyName)) { + continue; + } + + return $classMethod; + } + + return null; + } +} diff --git a/rules/Php84/Rector/Class_/PropertyHookRector.php b/rules/Php84/Rector/Class_/PropertyHookRector.php new file mode 100644 index 00000000000..5cdd9f103eb --- /dev/null +++ b/rules/Php84/Rector/Class_/PropertyHookRector.php @@ -0,0 +1,176 @@ +name; + } + + public function setName(string $name): void + { + $this->name = ucfirst($name); + } +} +CODE_SAMPLE + , + <<<'CODE_SAMPLE' +final class Product +{ + public string $name + { + get => $this->name; + set($value) => $this->name = ucfirst($value); + } +} + +CODE_SAMPLE + ), + ]); + } + + public function getNodeTypes(): array + { + return [Class_::class]; + } + + /** + * @param Class_ $node + */ + public function refactor(Node $node): ?Node + { + if ($node->isReadonly()) { + return null; + } + + // avoid breaking of child class getter/setter method use + if (! $node->isFinal() && FeatureFlags::treatClassesAsFinal($node) === false) { + return null; + } + + if ($this->hasMagicGetSetMethod($node)) { + return null; + } + + // nothing to hook to + if ($node->getProperties() === []) { + return null; + } + + $classMethodsToRemove = []; + + foreach ($node->getProperties() as $property) { + $propertyName = $this->getName($property); + + if ($property->isReadonly()) { + continue; + } + + $candidateClassMethods = $this->setterAndGetterFinder->findGetterAndSetterClassMethods( + $node, + $propertyName + ); + + foreach ($candidateClassMethods as $candidateClassMethod) { + if (count((array) $candidateClassMethod->stmts) !== 1) { + continue; + } + + // skip attributed methods + if ($candidateClassMethod->attrGroups !== []) { + continue; + } + + // avoid parent contract/method override + if ($this->parentClassMethodTypeOverrideGuard->hasParentClassMethod($candidateClassMethod)) { + continue; + } + + $propertyHook = $this->propertyHookFactory->create($candidateClassMethod, $propertyName); + if (! $propertyHook instanceof PropertyHook) { + continue; + } + + if (! $property->isPublic()) { + $property->flags = Modifiers::PUBLIC; + } + + $property->hooks[] = $propertyHook; + $classMethodsToRemove[] = $candidateClassMethod; + } + } + + if ($classMethodsToRemove === []) { + return null; + } + + foreach ($node->stmts as $key => $classStmt) { + if (! $classStmt instanceof ClassMethod) { + continue; + } + + if (! in_array($classStmt, $classMethodsToRemove)) { + continue; + } + + unset($node->stmts[$key]); + } + + return $node; + } + + public function provideMinPhpVersion(): int + { + return PhpVersionFeature::PROPERTY_HOOKS; + } + + private function hasMagicGetSetMethod(Class_ $class): bool + { + $magicGetMethod = $class->getMethod(MethodName::__GET); + if ($magicGetMethod instanceof ClassMethod) { + return true; + } + + $magicSetMethod = $class->getMethod(MethodName::__SET); + return $magicSetMethod instanceof ClassMethod; + } +} diff --git a/rules/TypeDeclaration/NodeAnalyzer/ClassMethodAndPropertyAnalyzer.php b/rules/TypeDeclaration/NodeAnalyzer/ClassMethodAndPropertyAnalyzer.php index 39680d394eb..f9676efad1b 100644 --- a/rules/TypeDeclaration/NodeAnalyzer/ClassMethodAndPropertyAnalyzer.php +++ b/rules/TypeDeclaration/NodeAnalyzer/ClassMethodAndPropertyAnalyzer.php @@ -6,7 +6,6 @@ use PhpParser\Node\Expr\Assign; use PhpParser\Node\Expr\PropertyFetch; -use PhpParser\Node\Expr\Variable; use PhpParser\Node\Stmt\ClassMethod; use PhpParser\Node\Stmt\Expression; use PhpParser\Node\Stmt\Return_; @@ -59,14 +58,6 @@ public function hasOnlyPropertyAssign(ClassMethod $classMethod, string $property $assign = $onlyClassMethodStmt->expr; - if (! $assign->expr instanceof Variable) { - return false; - } - - if (! $this->nodeNameResolver->isName($assign->expr, $propertyName)) { - return false; - } - $assignVar = $assign->var; if (! $assignVar instanceof PropertyFetch) { return false; diff --git a/src/ValueObject/PhpVersionFeature.php b/src/ValueObject/PhpVersionFeature.php index 20cf7566cef..e9bc08e2a0f 100644 --- a/src/ValueObject/PhpVersionFeature.php +++ b/src/ValueObject/PhpVersionFeature.php @@ -835,6 +835,12 @@ final class PhpVersionFeature */ public const DEPRECATE_ORD_WITH_MULTIBYTE_STRING = PhpVersion::PHP_85; + /** + * @see https://wiki.php.net/rfc/property-hooks + * @var int + */ + public const PROPERTY_HOOKS = PhpVersion::PHP_84; + /** * @see https://wiki.php.net/rfc/deprecations_php_8_5#deprecate_backticks_as_an_alias_for_shell_exec * @var int