diff --git a/rules-tests/Privatization/Rector/ClassMethod/PrivatizeFinalClassMethodRector/Fixture/keep_magic_parent_called_method.php.inc b/rules-tests/Privatization/Rector/ClassMethod/PrivatizeFinalClassMethodRector/Fixture/keep_magic_parent_called_method.php.inc new file mode 100644 index 00000000000..07c14c62fa7 --- /dev/null +++ b/rules-tests/Privatization/Rector/ClassMethod/PrivatizeFinalClassMethodRector/Fixture/keep_magic_parent_called_method.php.inc @@ -0,0 +1,13 @@ +{'p'} method + protected function pFileNode($fileNode) + { + } +} diff --git a/rules/Privatization/Guard/ParentClassMagicCallGuard.php b/rules/Privatization/Guard/ParentClassMagicCallGuard.php new file mode 100644 index 00000000000..b3fb3eb1930 --- /dev/null +++ b/rules/Privatization/Guard/ParentClassMagicCallGuard.php @@ -0,0 +1,88 @@ + + */ + private array $cachedContainsByClassName = []; + + public function __construct( + private readonly NodeNameResolver $nodeNameResolver, + private readonly AstResolver $astResolver, + private readonly BetterNodeFinder $betterNodeFinder + ) { + foreach (self::KNOWN_DYNAMIC_CALL_CLASSES as $knownDynamicCallClass) { + $this->cachedContainsByClassName[$knownDynamicCallClass] = true; + } + } + + /** + * E.g. parent class has $this->{$magicName} call that might call the protected method + * If we make it private, it will break the code + */ + public function containsParentClassMagicCall(Class_ $class): bool + { + if (! $class->extends instanceof Name) { + return false; + } + + // cache as heavy AST parsing here + $className = $this->nodeNameResolver->getName($class); + + if (isset($this->cachedContainsByClassName[$className])) { + return $this->cachedContainsByClassName[$className]; + } + + $parentClassName = $this->nodeNameResolver->getName($class->extends); + if (isset($this->cachedContainsByClassName[$parentClassName])) { + return $this->cachedContainsByClassName[$parentClassName]; + } + + $parentClass = $this->astResolver->resolveClassFromName($parentClassName); + if (! $parentClass instanceof Class_) { + $this->cachedContainsByClassName[$parentClassName] = false; + return false; + } + + foreach ($parentClass->getMethods() as $classMethod) { + if ($classMethod->isAbstract()) { + continue; + } + + /** @var MethodCall[] $methodCalls */ + $methodCalls = $this->betterNodeFinder->findInstancesOfScoped( + (array) $classMethod->stmts, + MethodCall::class + ); + foreach ($methodCalls as $methodCall) { + if ($methodCall->name instanceof Expr) { + $this->cachedContainsByClassName[$parentClassName] = true; + return true; + } + } + } + + return $this->containsParentClassMagicCall($parentClass); + } +} diff --git a/rules/Privatization/Rector/ClassMethod/PrivatizeFinalClassMethodRector.php b/rules/Privatization/Rector/ClassMethod/PrivatizeFinalClassMethodRector.php index 9ebbac2df15..2bd4a13b4b5 100644 --- a/rules/Privatization/Rector/ClassMethod/PrivatizeFinalClassMethodRector.php +++ b/rules/Privatization/Rector/ClassMethod/PrivatizeFinalClassMethodRector.php @@ -13,6 +13,7 @@ use Rector\PHPStan\ScopeFetcher; use Rector\Privatization\Guard\LaravelModelGuard; use Rector\Privatization\Guard\OverrideByParentClassGuard; +use Rector\Privatization\Guard\ParentClassMagicCallGuard; use Rector\Privatization\NodeManipulator\VisibilityManipulator; use Rector\Privatization\VisibilityGuard\ClassMethodVisibilityGuard; use Rector\Rector\AbstractRector; @@ -30,6 +31,7 @@ public function __construct( private readonly OverrideByParentClassGuard $overrideByParentClassGuard, private readonly BetterNodeFinder $betterNodeFinder, private readonly LaravelModelGuard $laravelModelGuard, + private readonly ParentClassMagicCallGuard $parentClassMagicCallGuard, ) { } @@ -113,6 +115,10 @@ public function refactor(Node $node): ?Node continue; } + if ($this->parentClassMagicCallGuard->containsParentClassMagicCall($node)) { + continue; + } + $this->visibilityManipulator->makePrivate($classMethod); $hasChanged = true; }