diff --git a/rules-tests/Renaming/Rector/Name/RenameClassRector/Fixture/do_not_rename_extends_its_name.php.inc b/rules-tests/Renaming/Rector/Name/RenameClassRector/Fixture/do_not_rename_extends_its_name.php.inc new file mode 100644 index 00000000000..250a66e0f53 --- /dev/null +++ b/rules-tests/Renaming/Rector/Name/RenameClassRector/Fixture/do_not_rename_extends_its_name.php.inc @@ -0,0 +1,23 @@ + +----- + diff --git a/rules-tests/Renaming/Rector/Name/RenameClassRector/config/configured_rule.php b/rules-tests/Renaming/Rector/Name/RenameClassRector/config/configured_rule.php index 5db98eb39c9..576f5c9f96e 100644 --- a/rules-tests/Renaming/Rector/Name/RenameClassRector/config/configured_rule.php +++ b/rules-tests/Renaming/Rector/Name/RenameClassRector/config/configured_rule.php @@ -62,5 +62,13 @@ // test skip rename class const fetch when target rename is interface // and interface class const fetch not found 'Symfony\Component\Serializer\Normalizer\ObjectNormalizer' => 'Symfony\Component\Serializer\Normalizer\NormalizerInterface', + + /** + * // avoid error extends itself + * namespace MyNamespace; + * + * class MyMigration extends \MyNamespace\BaseMigration {} + */ + 'MyNamespace\AbstractMigration' => 'MyNamespace\BaseMigration', ]); }; diff --git a/rules/Renaming/NodeManipulator/ClassRenamer.php b/rules/Renaming/NodeManipulator/ClassRenamer.php index b6bffa3134d..90af400e6e4 100644 --- a/rules/Renaming/NodeManipulator/ClassRenamer.php +++ b/rules/Renaming/NodeManipulator/ClassRenamer.php @@ -54,14 +54,14 @@ public function renameNode(Node $node, array $oldToNewClasses, ?Scope $scope): ? // execute FullyQualified before Name on purpose so next Name check is pure Name node if ($node instanceof FullyQualified) { - return $this->refactorName($node, $oldToNewClasses); + return $this->refactorName($node, $oldToNewClasses, $scope); } // Name as parent of FullyQualified executed for fallback annotation to attribute rename to Name if ($node instanceof Name) { $phpAttributeName = $node->getAttribute(AttributeKey::PHP_ATTRIBUTE_NAME); if (is_string($phpAttributeName)) { - return $this->refactorName(new FullyQualified($phpAttributeName), $oldToNewClasses); + return $this->refactorName(new FullyQualified($phpAttributeName), $oldToNewClasses, $scope); } return null; @@ -133,8 +133,11 @@ private function shouldSkip(string $newName, FullyQualified $fullyQualified): bo /** * @param array $oldToNewClasses */ - private function refactorName(FullyQualified $fullyQualified, array $oldToNewClasses): ?FullyQualified - { + private function refactorName( + FullyQualified $fullyQualified, + array $oldToNewClasses, + ?Scope $scope + ): ?FullyQualified { if ($fullyQualified->getAttribute(AttributeKey::IS_FUNCCALL_NAME) === true) { return null; } @@ -146,7 +149,7 @@ private function refactorName(FullyQualified $fullyQualified, array $oldToNewCla return null; } - if (! $this->isClassToInterfaceValidChange($fullyQualified, $newName)) { + if (! $this->isClassToInterfaceValidChange($fullyQualified, $newName, $scope)) { return null; } @@ -202,8 +205,11 @@ private function refactorClassLike(ClassLike $classLike, array $oldToNewClasses, * - implements SomeInterface * - implements SomeClass */ - private function isClassToInterfaceValidChange(FullyQualified $fullyQualified, string $newClassName): bool - { + private function isClassToInterfaceValidChange( + FullyQualified $fullyQualified, + string $newClassName, + ?Scope $scope + ): bool { if (! $this->reflectionProvider->hasClass($newClassName)) { return true; } @@ -211,6 +217,14 @@ private function isClassToInterfaceValidChange(FullyQualified $fullyQualified, s $classReflection = $this->reflectionProvider->getClass($newClassName); // ensure new is not with interface if ($fullyQualified->getAttribute(AttributeKey::IS_NEW_INSTANCE_NAME) !== true) { + if ($fullyQualified->getAttribute(AttributeKey::IS_CLASS_EXTENDS) === true && $scope instanceof Scope) { + $currentClassReflection = $scope->getClassReflection(); + + if ($currentClassReflection instanceof ClassReflection && $currentClassReflection->getName() === $newClassName) { + return false; + } + } + return $this->isValidClassNameChange($fullyQualified, $classReflection); } diff --git a/src/NodeTypeResolver/PHPStan/Scope/PHPStanNodeScopeResolver.php b/src/NodeTypeResolver/PHPStan/Scope/PHPStanNodeScopeResolver.php index 5386a323313..895b1efe969 100644 --- a/src/NodeTypeResolver/PHPStan/Scope/PHPStanNodeScopeResolver.php +++ b/src/NodeTypeResolver/PHPStan/Scope/PHPStanNodeScopeResolver.php @@ -55,6 +55,7 @@ use PhpParser\Node\Identifier; use PhpParser\Node\IntersectionType; use PhpParser\Node\Name; +use PhpParser\Node\Name\FullyQualified; use PhpParser\Node\NullableType; use PhpParser\Node\Param; use PhpParser\Node\Stmt; @@ -168,6 +169,16 @@ public function processNodes( $mutatingScope = $this->resolveClassOrInterfaceScope($node, $mutatingScope); $node->setAttribute(AttributeKey::SCOPE, $mutatingScope); + if ($node instanceof Class_) { + if ($node->extends instanceof FullyQualified) { + $node->extends->setAttribute(AttributeKey::SCOPE, $mutatingScope); + } + + foreach ($node->implements as $implement) { + $implement->setAttribute(AttributeKey::SCOPE, $mutatingScope); + } + } + return; }