diff --git a/build/target-repository/docs/rector_rules_overview.md b/build/target-repository/docs/rector_rules_overview.md
index e460902499e..683d94acf92 100644
--- a/build/target-repository/docs/rector_rules_overview.md
+++ b/build/target-repository/docs/rector_rules_overview.md
@@ -5510,6 +5510,8 @@ Add override attribute to overridden methods
Add type to constants
+:wrench: **configure it!**
+
- class: [`Rector\Php83\Rector\ClassConst\AddTypeToConstRector`](../rules/Php83/Rector/ClassConst/AddTypeToConstRector.php)
```diff
@@ -5522,6 +5524,22 @@ Add type to constants
+```diff
+ class ParentClass extends ChildClass
+ {
+- public const TYPE = 'some_type';
++ public const string TYPE = 'some_type';
+ public const OTHER_TYPE = 'other_type';
+ }
+
+ class ChildClass
+ {
+ public const OTHER_TYPE = 'other_type';
+ }
+```
+
+
+
### CombineHostPortLdapUriRector
Combine separated host and port on `ldap_connect()` args
diff --git a/rules-tests/Php83/Rector/ClassConst/AddTypeToConstRector/AllowHasChildRectorTest.php b/rules-tests/Php83/Rector/ClassConst/AddTypeToConstRector/AllowHasChildRectorTest.php
new file mode 100644
index 00000000000..7aa091aaa15
--- /dev/null
+++ b/rules-tests/Php83/Rector/ClassConst/AddTypeToConstRector/AllowHasChildRectorTest.php
@@ -0,0 +1,28 @@
+doTestFile($filePath);
+ }
+
+ public static function provideData(): Iterator
+ {
+ return self::yieldFilesFromDirectory(__DIR__ . '/FixtureAllowHasChild');
+ }
+
+ public function provideConfigFilePath(): string
+ {
+ return __DIR__ . '/config/allow_has_child.php';
+ }
+}
diff --git a/rules-tests/Php83/Rector/ClassConst/AddTypeToConstRector/FixtureAllowHasChild/apply_to_public_const_when_no_child_classes.php.inc b/rules-tests/Php83/Rector/ClassConst/AddTypeToConstRector/FixtureAllowHasChild/apply_to_public_const_when_no_child_classes.php.inc
new file mode 100644
index 00000000000..49ed5c43cde
--- /dev/null
+++ b/rules-tests/Php83/Rector/ClassConst/AddTypeToConstRector/FixtureAllowHasChild/apply_to_public_const_when_no_child_classes.php.inc
@@ -0,0 +1,21 @@
+
+-----
+
diff --git a/rules-tests/Php83/Rector/ClassConst/AddTypeToConstRector/FixtureAllowHasChild/apply_type_to_class_const_when_not_overridden.php.inc b/rules-tests/Php83/Rector/ClassConst/AddTypeToConstRector/FixtureAllowHasChild/apply_type_to_class_const_when_not_overridden.php.inc
new file mode 100644
index 00000000000..1bcf6fd6905
--- /dev/null
+++ b/rules-tests/Php83/Rector/ClassConst/AddTypeToConstRector/FixtureAllowHasChild/apply_type_to_class_const_when_not_overridden.php.inc
@@ -0,0 +1,39 @@
+
+-----
+
diff --git a/rules-tests/Php83/Rector/ClassConst/AddTypeToConstRector/config/allow_has_child.php b/rules-tests/Php83/Rector/ClassConst/AddTypeToConstRector/config/allow_has_child.php
new file mode 100644
index 00000000000..58d22e2c53d
--- /dev/null
+++ b/rules-tests/Php83/Rector/ClassConst/AddTypeToConstRector/config/allow_has_child.php
@@ -0,0 +1,15 @@
+ruleWithConfiguration(AddTypeToConstRector::class, [
+ AddTypeToConstRector::ALLOW_HAS_CHILD => true,
+ ]);
+
+ $rectorConfig->phpVersion(PhpVersion::PHP_83);
+};
diff --git a/rules/Php74/Rector/FuncCall/HebrevcToNl2brHebrevRector.php b/rules/Php74/Rector/FuncCall/HebrevcToNl2brHebrevRector.php
index 65ffdcb93b8..0f4cd2bbf02 100644
--- a/rules/Php74/Rector/FuncCall/HebrevcToNl2brHebrevRector.php
+++ b/rules/Php74/Rector/FuncCall/HebrevcToNl2brHebrevRector.php
@@ -65,11 +65,6 @@ public function refactor(Node $node): ?FuncCall
}
$node->name = new Name('hebrev');
- return new FuncCall(
- new Name('nl2br'),
- [
- new Arg($node)
- ]
- );
+ return new FuncCall(new Name('nl2br'), [new Arg($node)]);
}
}
diff --git a/rules/Php83/Rector/ClassConst/AddTypeToConstRector.php b/rules/Php83/Rector/ClassConst/AddTypeToConstRector.php
index b2474721540..d893913a206 100644
--- a/rules/Php83/Rector/ClassConst/AddTypeToConstRector.php
+++ b/rules/Php83/Rector/ClassConst/AddTypeToConstRector.php
@@ -20,29 +20,43 @@
use PhpParser\Node\Stmt\ClassConst;
use PHPStan\Reflection\ClassReflection;
use PHPStan\Reflection\ReflectionProvider;
+use Rector\Contract\Rector\ConfigurableRectorInterface;
+use Rector\FamilyTree\Reflection\FamilyRelationsAnalyzer;
+use Rector\PhpParser\AstResolver;
use Rector\PHPStanStaticTypeMapper\Enum\TypeKind;
use Rector\Rector\AbstractRector;
use Rector\StaticTypeMapper\StaticTypeMapper;
use Rector\ValueObject\PhpVersionFeature;
use Rector\VersionBonding\Contract\MinPhpVersionInterface;
-use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
+use Symplify\RuleDocGenerator\ValueObject\CodeSample\ConfiguredCodeSample;
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
+use Webmozart\Assert\Assert;
/**
* @see \Rector\Tests\Php83\Rector\ClassConst\AddTypeToConstRector\AddTypeToConstRectorTest
*/
-final class AddTypeToConstRector extends AbstractRector implements MinPhpVersionInterface
+final class AddTypeToConstRector extends AbstractRector implements ConfigurableRectorInterface, MinPhpVersionInterface
{
+ /**
+ * @api
+ * @var string
+ */
+ final public const ALLOW_HAS_CHILD = 'allow_has_child';
+
+ private bool $allowHasChild = false;
+
public function __construct(
private readonly ReflectionProvider $reflectionProvider,
- private readonly StaticTypeMapper $staticTypeMapper
+ private readonly StaticTypeMapper $staticTypeMapper,
+ private readonly FamilyRelationsAnalyzer $familyRelationsAnalyzer,
+ private readonly AstResolver $astResolver,
) {
}
public function getRuleDefinition(): RuleDefinition
{
return new RuleDefinition('Add type to constants', [
- new CodeSample(
+ new ConfiguredCodeSample(
<<<'CODE_SAMPLE'
final class SomeClass
{
@@ -57,6 +71,40 @@ final class SomeClass
}
CODE_SAMPLE
,
+ [
+ AddTypeToConstRector::ALLOW_HAS_CHILD => false,
+ ]
+ ),
+ new ConfiguredCodeSample(
+ <<<'CODE_SAMPLE'
+class ParentClass extends ChildClass
+{
+ public const TYPE = 'some_type';
+ public const OTHER_TYPE = 'other_type';
+}
+
+class ChildClass
+{
+ public const OTHER_TYPE = 'other_type';
+}
+CODE_SAMPLE
+ ,
+ <<<'CODE_SAMPLE'
+class ParentClass extends ChildClass
+{
+ public const string TYPE = 'some_type';
+ public const OTHER_TYPE = 'other_type';
+}
+
+class ChildClass
+{
+ public const OTHER_TYPE = 'other_type';
+}
+CODE_SAMPLE
+ ,
+ [
+ AddTypeToConstRector::ALLOW_HAS_CHILD => true,
+ ]
),
]);
}
@@ -86,6 +134,7 @@ public function refactor(Node $node): ?Class_
}
$parentClassReflections = $this->getParentReflections($className);
+ $childClassReflections = $this->getChildClassReflections($className);
$hasChanged = false;
@@ -103,7 +152,11 @@ public function refactor(Node $node): ?Class_
continue;
}
- if ($this->canBeInherited($classConst, $node)) {
+ if (! $this->allowHasChild && $this->canBeInherited($classConst, $node)) {
+ continue;
+ }
+
+ if ($this->allowHasChild && $this->isConstGuardedByChildren($constNode, $childClassReflections)) {
continue;
}
@@ -139,6 +192,16 @@ public function refactor(Node $node): ?Class_
return $node;
}
+ /**
+ * @param mixed[] $configuration
+ */
+ public function configure(array $configuration): void
+ {
+ $ignoreInheritance = $configuration[self::ALLOW_HAS_CHILD] ?? (bool) current($configuration);
+ Assert::boolean($ignoreInheritance);
+ $this->allowHasChild = $ignoreInheritance;
+ }
+
public function provideMinPhpVersion(): int
{
return PhpVersionFeature::TYPED_CLASS_CONSTANTS;
@@ -160,6 +223,29 @@ public function isConstGuardedByParents(Const_ $const, array $parentClassReflect
return false;
}
+ /**
+ * @param ClassReflection[] $childClassReflections
+ */
+ private function isConstGuardedByChildren(Const_ $const, array $childClassReflections): bool
+ {
+ foreach ($childClassReflections as $childClassReflection) {
+ $classLike = $this->astResolver->resolveClassFromClassReflection($childClassReflection);
+ if (! $classLike instanceof Class_) {
+ continue;
+ }
+
+ foreach ($classLike->getConstants() as $childClassConstants) {
+ foreach ($childClassConstants->consts as $childConst) {
+ if ($this->nodeComparator->areNodesEqual($childConst, $const)) {
+ return true;
+ }
+ }
+ }
+ }
+
+ return false;
+ }
+
private function findValueType(Expr $expr): ?Identifier
{
if ($expr instanceof UnaryPlus || $expr instanceof UnaryMinus) {
@@ -220,6 +306,20 @@ private function getParentReflections(string $className): array
$currentClassReflection !== $classReflection);
}
+ /**
+ * @return ClassReflection[]
+ */
+ private function getChildClassReflections(string $className): array
+ {
+ if (! $this->reflectionProvider->hasClass($className)) {
+ return [];
+ }
+
+ $currentClassReflection = $this->reflectionProvider->getClass($className);
+
+ return $this->familyRelationsAnalyzer->getChildrenOfClassReflection($currentClassReflection);
+ }
+
private function canBeInherited(ClassConst $classConst, Class_ $class): bool
{
return ! $class->isFinal() && ! $classConst->isPrivate() && ! $classConst->isFinal();
diff --git a/src/StaticTypeMapper/PhpParser/NameNodeMapper.php b/src/StaticTypeMapper/PhpParser/NameNodeMapper.php
index 1953b9f4de9..e81c3e908cf 100644
--- a/src/StaticTypeMapper/PhpParser/NameNodeMapper.php
+++ b/src/StaticTypeMapper/PhpParser/NameNodeMapper.php
@@ -61,7 +61,7 @@ private function expandedNamespacedName(Node $node): ?FullyQualified
return null;
}
- if (!$node->hasAttribute(AttributeKey::NAMESPACED_NAME)) {
+ if (! $node->hasAttribute(AttributeKey::NAMESPACED_NAME)) {
return null;
}