diff --git a/build/target-repository/docs/rector_rules_overview.md b/build/target-repository/docs/rector_rules_overview.md index 30f82a2df57..7301dfb8cbe 100644 --- a/build/target-repository/docs/rector_rules_overview.md +++ b/build/target-repository/docs/rector_rules_overview.md @@ -1,4 +1,4 @@ -# 351 Rules Overview +# 353 Rules Overview
@@ -44,7 +44,7 @@ - [Php82](#php82) (4) -- [Php83](#php83) (1) +- [Php83](#php83) (2) - [Privatization](#privatization) (4) @@ -5263,6 +5263,22 @@ Add override attribute to overridden methods
+### AddTypeToConstRector + +Add const to type + +- class: [`Rector\Php83\Rector\ClassConst\AddTypeToConstRector`](../rules/Php83/Rector/ClassConst/AddTypeToConstRector.php) + +```diff + final class SomeClass + { +- public const TYPE = 'some_type'; ++ public const string TYPE = 'some_type'; + } +``` + +
+ ## Privatization ### FinalizeClassesWithoutChildrenRector diff --git a/config/set/php83.php b/config/set/php83.php index 474fd7d2d4f..25ded98cf97 100644 --- a/config/set/php83.php +++ b/config/set/php83.php @@ -3,8 +3,10 @@ declare(strict_types=1); use Rector\Config\RectorConfig; -use Rector\Php83\Rector\ClassMethod\AddOverrideAttributeToOverriddenMethodsRector; return static function (RectorConfig $rectorConfig): void { - $rectorConfig->rules([AddOverrideAttributeToOverriddenMethodsRector::class]); + $rectorConfig->rules([ + \Rector\Php83\Rector\ClassMethod\AddOverrideAttributeToOverriddenMethodsRector::class, + \Rector\Php83\Rector\ClassConst\AddTypeToConstRector::class, + ]); }; diff --git a/rules-tests/Php83/Rector/ClassConst/AddTypeToConstRector/AddTypeToConstRectorTest.php b/rules-tests/Php83/Rector/ClassConst/AddTypeToConstRector/AddTypeToConstRectorTest.php new file mode 100644 index 00000000000..5e5f3f3d694 --- /dev/null +++ b/rules-tests/Php83/Rector/ClassConst/AddTypeToConstRector/AddTypeToConstRectorTest.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/Php83/Rector/ClassConst/AddTypeToConstRector/Fixture/apply_type_to_class_const_when_class_final.php.inc b/rules-tests/Php83/Rector/ClassConst/AddTypeToConstRector/Fixture/apply_type_to_class_const_when_class_final.php.inc new file mode 100644 index 00000000000..d3537c54aa8 --- /dev/null +++ b/rules-tests/Php83/Rector/ClassConst/AddTypeToConstRector/Fixture/apply_type_to_class_const_when_class_final.php.inc @@ -0,0 +1,41 @@ + +----- + diff --git a/rules-tests/Php83/Rector/ClassConst/AddTypeToConstRector/Fixture/apply_type_to_class_const_when_const_protected.php.inc b/rules-tests/Php83/Rector/ClassConst/AddTypeToConstRector/Fixture/apply_type_to_class_const_when_const_protected.php.inc new file mode 100644 index 00000000000..fae0e0a1efa --- /dev/null +++ b/rules-tests/Php83/Rector/ClassConst/AddTypeToConstRector/Fixture/apply_type_to_class_const_when_const_protected.php.inc @@ -0,0 +1,41 @@ + +----- + diff --git a/rules-tests/Php83/Rector/ClassConst/AddTypeToConstRector/Fixture/skip_when_abstract_class.php.inc b/rules-tests/Php83/Rector/ClassConst/AddTypeToConstRector/Fixture/skip_when_abstract_class.php.inc new file mode 100644 index 00000000000..ea96a52a0e8 --- /dev/null +++ b/rules-tests/Php83/Rector/ClassConst/AddTypeToConstRector/Fixture/skip_when_abstract_class.php.inc @@ -0,0 +1,11 @@ + + diff --git a/rules-tests/Php83/Rector/ClassConst/AddTypeToConstRector/Fixture/skip_when_implementable_not_autoloaded.php.inc b/rules-tests/Php83/Rector/ClassConst/AddTypeToConstRector/Fixture/skip_when_implementable_not_autoloaded.php.inc new file mode 100644 index 00000000000..a62e294528f --- /dev/null +++ b/rules-tests/Php83/Rector/ClassConst/AddTypeToConstRector/Fixture/skip_when_implementable_not_autoloaded.php.inc @@ -0,0 +1,11 @@ + + diff --git a/rules-tests/Php83/Rector/ClassConst/AddTypeToConstRector/Fixture/skip_when_interface_defines_const.php.inc b/rules-tests/Php83/Rector/ClassConst/AddTypeToConstRector/Fixture/skip_when_interface_defines_const.php.inc new file mode 100644 index 00000000000..59fe0b21a7c --- /dev/null +++ b/rules-tests/Php83/Rector/ClassConst/AddTypeToConstRector/Fixture/skip_when_interface_defines_const.php.inc @@ -0,0 +1,13 @@ + + diff --git a/rules-tests/Php83/Rector/ClassConst/AddTypeToConstRector/Fixture/skip_when_operations_used.php.inc b/rules-tests/Php83/Rector/ClassConst/AddTypeToConstRector/Fixture/skip_when_operations_used.php.inc new file mode 100644 index 00000000000..266bbeb1c61 --- /dev/null +++ b/rules-tests/Php83/Rector/ClassConst/AddTypeToConstRector/Fixture/skip_when_operations_used.php.inc @@ -0,0 +1,16 @@ + diff --git a/rules-tests/Php83/Rector/ClassConst/AddTypeToConstRector/Fixture/skip_when_parent_class_const_defines_type.php.inc b/rules-tests/Php83/Rector/ClassConst/AddTypeToConstRector/Fixture/skip_when_parent_class_const_defines_type.php.inc new file mode 100644 index 00000000000..120f0f35982 --- /dev/null +++ b/rules-tests/Php83/Rector/ClassConst/AddTypeToConstRector/Fixture/skip_when_parent_class_const_defines_type.php.inc @@ -0,0 +1,13 @@ + + diff --git a/rules-tests/Php83/Rector/ClassConst/AddTypeToConstRector/Fixture/skip_when_parent_not_autoloaded.php.inc b/rules-tests/Php83/Rector/ClassConst/AddTypeToConstRector/Fixture/skip_when_parent_not_autoloaded.php.inc new file mode 100644 index 00000000000..3eb6748b9f5 --- /dev/null +++ b/rules-tests/Php83/Rector/ClassConst/AddTypeToConstRector/Fixture/skip_when_parent_not_autoloaded.php.inc @@ -0,0 +1,11 @@ + + diff --git a/rules-tests/Php83/Rector/ClassConst/AddTypeToConstRector/Fixture/skip_when_referencing_another_const.php.inc b/rules-tests/Php83/Rector/ClassConst/AddTypeToConstRector/Fixture/skip_when_referencing_another_const.php.inc new file mode 100644 index 00000000000..287664ce825 --- /dev/null +++ b/rules-tests/Php83/Rector/ClassConst/AddTypeToConstRector/Fixture/skip_when_referencing_another_const.php.inc @@ -0,0 +1,12 @@ + diff --git a/rules-tests/Php83/Rector/ClassConst/AddTypeToConstRector/Fixture/skip_when_trait_defines_const.php.inc b/rules-tests/Php83/Rector/ClassConst/AddTypeToConstRector/Fixture/skip_when_trait_defines_const.php.inc new file mode 100644 index 00000000000..21f74013583 --- /dev/null +++ b/rules-tests/Php83/Rector/ClassConst/AddTypeToConstRector/Fixture/skip_when_trait_defines_const.php.inc @@ -0,0 +1,15 @@ + + diff --git a/rules-tests/Php83/Rector/ClassConst/AddTypeToConstRector/Fixture/skip_when_trait_not_autoloaded.php.inc b/rules-tests/Php83/Rector/ClassConst/AddTypeToConstRector/Fixture/skip_when_trait_not_autoloaded.php.inc new file mode 100644 index 00000000000..2d544303501 --- /dev/null +++ b/rules-tests/Php83/Rector/ClassConst/AddTypeToConstRector/Fixture/skip_when_trait_not_autoloaded.php.inc @@ -0,0 +1,13 @@ + + diff --git a/rules-tests/Php83/Rector/ClassConst/AddTypeToConstRector/Fixture/skip_when_type_already_set.php.inc b/rules-tests/Php83/Rector/ClassConst/AddTypeToConstRector/Fixture/skip_when_type_already_set.php.inc new file mode 100644 index 00000000000..dae15e64449 --- /dev/null +++ b/rules-tests/Php83/Rector/ClassConst/AddTypeToConstRector/Fixture/skip_when_type_already_set.php.inc @@ -0,0 +1,10 @@ + diff --git a/rules-tests/Php83/Rector/ClassConst/AddTypeToConstRector/Source/ParentClass.php b/rules-tests/Php83/Rector/ClassConst/AddTypeToConstRector/Source/ParentClass.php new file mode 100644 index 00000000000..58325c04722 --- /dev/null +++ b/rules-tests/Php83/Rector/ClassConst/AddTypeToConstRector/Source/ParentClass.php @@ -0,0 +1,8 @@ +rule(\Rector\Php83\Rector\ClassConst\AddTypeToConstRector::class); + + $rectorConfig->phpVersion(PhpVersion::PHP_83); +}; diff --git a/rules/Php83/Rector/ClassConst/AddTypeToConstRector.php b/rules/Php83/Rector/ClassConst/AddTypeToConstRector.php new file mode 100644 index 00000000000..67df5df23ef --- /dev/null +++ b/rules/Php83/Rector/ClassConst/AddTypeToConstRector.php @@ -0,0 +1,236 @@ +isAbstract()) { + return null; + } + + $consts = array_filter($node->stmts, function (Node $stmt) { + return $stmt instanceof Node\Stmt\ClassConst; + }); + + if ($consts === []) { + return null; + } + + try { + $parents = $this->getParents($node); + $implementations = $this->getImplementations($node); + $traits = $this->getTraits($node); + } catch (FullyQualifiedNameNotAutoloadedException) { + return null; + } + + $changes = false; + + foreach ($consts as $const) { + // If a type is set, skip + if ($const->type !== null) { + continue; + } + + foreach ($const->consts as $constNode) { + if ($this->shouldSkipDueToInheritance($constNode, $parents, $implementations, $traits)) { + continue; + } + if ($this->canBeInheritied($const, $node)) { + continue; + } + $valueType = $this->findValueType($constNode->value); + } + + if (($valueType ?? null) === null) { + continue; + } + + $const->type = $valueType; + + $changes = true; + } + + if (! $changes) { + return null; + } + + return $node; + } + + public function provideMinPhpVersion(): int + { + return PhpVersionFeature::TYPED_CLASS_CONSTANTS; + } + + /** + * @param ClassReflection[] $parents + * @param ClassReflection[] $implementations + * @param ClassReflection[] $traits + */ + public function shouldSkipDueToInheritance( + Node\Const_ $constNode, + array $parents, + array $implementations, + array $traits, + ): bool { + foreach ([$parents, $implementations, $traits] as $inheritance) { + foreach ($inheritance as $inheritanceItem) { + if ($constNode->name->name === '') { + continue; + } + try { + $inheritanceItem->getConstant($constNode->name->name); + return true; + } catch (MissingConstantFromReflectionException) { + } + } + } + return false; + } + + private function findValueType(Node\Expr $value): ?Node\Identifier + { + if ($value instanceof Node\Scalar\String_) { + return new Node\Identifier('string'); + } + if ($value instanceof Node\Scalar\LNumber) { + return new Node\Identifier('int'); + } + if ($value instanceof Node\Scalar\DNumber) { + return new Node\Identifier('float'); + } + if ($value instanceof Node\Expr\ConstFetch && $value->name->toLowerString() !== 'null') { + return new Node\Identifier('bool'); + } + if ($value instanceof Node\Expr\ConstFetch && $value->name->toLowerString() === 'null') { + return new Node\Identifier('null'); + } + if ($value instanceof Node\Expr\Array_) { + return new Node\Identifier('array'); + } + + return null; + } + + /** + * @return ClassReflection[] + */ + private function getParents(Class_ $class): array + { + $parents = array_filter([$class->extends]); + + return array_map(function (Node\Name $name): ClassReflection { + if (! $name instanceof FullyQualified) { + throw new FullyQualifiedNameNotAutoloadedException($name); + } + + if ($this->reflectionProvider->hasClass($name->toString())) { + return $this->reflectionProvider->getClass($name->toString()); + } + + throw new FullyQualifiedNameNotAutoloadedException($name); + }, $parents); + } + + /** + * @return ClassReflection[] + */ + private function getImplementations(Class_ $class): array + { + return array_map(function (Node\Name $name): ClassReflection { + if (! $name instanceof FullyQualified) { + throw new FullyQualifiedNameNotAutoloadedException($name); + } + + if ($this->reflectionProvider->hasClass($name->toString())) { + return $this->reflectionProvider->getClass($name->toString()); + } + + throw new FullyQualifiedNameNotAutoloadedException($name); + }, $class->implements); + } + + /** + * @return ClassReflection[] + */ + private function getTraits(Class_ $node): array + { + $traits = []; + foreach ($node->getTraitUses() as $traitUse) { + $traits = [...$traits, ...$traitUse->traits]; + } + + return array_map(function (Node\Name $name): ClassReflection { + if (! $name instanceof FullyQualified) { + throw new FullyQualifiedNameNotAutoloadedException($name); + } + + if ($this->reflectionProvider->hasClass($name->toString())) { + return $this->reflectionProvider->getClass($name->toString()); + } + + throw new FullyQualifiedNameNotAutoloadedException($name); + }, $traits); + } + + private function canBeInheritied(Node\Stmt\ClassConst $constNode, Class_ $node): bool + { + return ! $node->isFinal() && ! $constNode->isPrivate(); + } +} diff --git a/src/Exception/FullyQualifiedNameNotAutoloadedException.php b/src/Exception/FullyQualifiedNameNotAutoloadedException.php new file mode 100644 index 00000000000..7adac11de3c --- /dev/null +++ b/src/Exception/FullyQualifiedNameNotAutoloadedException.php @@ -0,0 +1,17 @@ +toString())); + } +} diff --git a/src/ValueObject/PhpVersionFeature.php b/src/ValueObject/PhpVersionFeature.php index bc41c89be98..48382c5cbd5 100644 --- a/src/ValueObject/PhpVersionFeature.php +++ b/src/ValueObject/PhpVersionFeature.php @@ -649,4 +649,10 @@ final class PhpVersionFeature * @var int */ public const OVERRIDE_ATTRIBUTE = PhpVersion::PHP_83; + + /** + * @see https://wiki.php.net/rfc/typed_class_constants + * @var int + */ + public const TYPED_CLASS_CONSTANTS = PhpVersion::PHP_83; }