From f902ae9f46a58fd00a8081a3239df514198cac7b Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 21 May 2026 17:54:56 +0000 Subject: [PATCH 1/2] fix: skip StaticCallToMethodCallRector when parent declares final __construct When the nearest ancestor constructor is `final`, PHP forbids declaring any `__construct` in the child class. Previously the rector cloned the parent's `final __construct` and inserted it into the child, producing "Cannot override final method ParentClass::__construct()". Add `ClassDependencyManipulator::hasFinalParentConstructor()` to detect this condition. `FuncCallStaticCallToMethodCallAnalyzer::matchTypeProvidingExpr()` now returns `null` (and its return type is widened to include `null`) when constructor injection is blocked by a final parent constructor. Both `StaticCallToMethodCallRector` and `FuncCallToMethodCallRector` skip the transformation when `null` is returned. Fixes #9766 https://claude.ai/code/session_019aUU1dheeuij6J55RBaYdZ --- ...ip_when_parent_has_final_construct.php.inc | 19 ++++++++++ .../Source/ResourceWithFinalConstruct.php | 15 ++++++++ ...FuncCallStaticCallToMethodCallAnalyzer.php | 7 +++- .../FuncCall/FuncCallToMethodCallRector.php | 4 ++ .../StaticCallToMethodCallRector.php | 4 ++ .../ClassDependencyManipulator.php | 37 +++++++++++++++++++ 6 files changed, 85 insertions(+), 1 deletion(-) create mode 100644 rules-tests/Transform/Rector/StaticCall/StaticCallToMethodCallRector/Fixture/skip_when_parent_has_final_construct.php.inc create mode 100644 rules-tests/Transform/Rector/StaticCall/StaticCallToMethodCallRector/Source/ResourceWithFinalConstruct.php diff --git a/rules-tests/Transform/Rector/StaticCall/StaticCallToMethodCallRector/Fixture/skip_when_parent_has_final_construct.php.inc b/rules-tests/Transform/Rector/StaticCall/StaticCallToMethodCallRector/Fixture/skip_when_parent_has_final_construct.php.inc new file mode 100644 index 00000000000..471e647b0a7 --- /dev/null +++ b/rules-tests/Transform/Rector/StaticCall/StaticCallToMethodCallRector/Fixture/skip_when_parent_has_final_construct.php.inc @@ -0,0 +1,19 @@ + $this->user_id ?? App::get(MissingValue::class), + ]; + } +} diff --git a/rules-tests/Transform/Rector/StaticCall/StaticCallToMethodCallRector/Source/ResourceWithFinalConstruct.php b/rules-tests/Transform/Rector/StaticCall/StaticCallToMethodCallRector/Source/ResourceWithFinalConstruct.php new file mode 100644 index 00000000000..045ff2ce346 --- /dev/null +++ b/rules-tests/Transform/Rector/StaticCall/StaticCallToMethodCallRector/Source/ResourceWithFinalConstruct.php @@ -0,0 +1,15 @@ +resource = $resource; + } +} diff --git a/rules/Transform/NodeAnalyzer/FuncCallStaticCallToMethodCallAnalyzer.php b/rules/Transform/NodeAnalyzer/FuncCallStaticCallToMethodCallAnalyzer.php index 2c46e0b5502..c6dec629b46 100644 --- a/rules/Transform/NodeAnalyzer/FuncCallStaticCallToMethodCallAnalyzer.php +++ b/rules/Transform/NodeAnalyzer/FuncCallStaticCallToMethodCallAnalyzer.php @@ -36,7 +36,7 @@ public function matchTypeProvidingExpr( Class_ $class, ClassMethod $classMethod, ObjectType $objectType, - ): MethodCall | PropertyFetch | Variable { + ): MethodCall | PropertyFetch | Variable | null { $expr = $this->typeProvidingExprFromClassResolver->resolveTypeProvidingExprFromClass( $class, $classMethod, @@ -51,6 +51,11 @@ public function matchTypeProvidingExpr( return $expr; } + // Cannot add constructor dependency when nearest parent constructor is final + if ($this->classDependencyManipulator->hasFinalParentConstructor($class)) { + return null; + } + $propertyName = $this->propertyNaming->fqnToVariableName($objectType); $this->classDependencyManipulator->addConstructorDependency( $class, diff --git a/rules/Transform/Rector/FuncCall/FuncCallToMethodCallRector.php b/rules/Transform/Rector/FuncCall/FuncCallToMethodCallRector.php index 08fadf2e746..c00c2c2ad50 100644 --- a/rules/Transform/Rector/FuncCall/FuncCallToMethodCallRector.php +++ b/rules/Transform/Rector/FuncCall/FuncCallToMethodCallRector.php @@ -114,6 +114,10 @@ public function refactor(Node $node): ?Node $funcNameToMethodCallName->getNewObjectType(), ); + if ($expr === null) { + return null; + } + $hasChanged = true; return $this->nodeFactory->createMethodCall( diff --git a/rules/Transform/Rector/StaticCall/StaticCallToMethodCallRector.php b/rules/Transform/Rector/StaticCall/StaticCallToMethodCallRector.php index 5b26e209e8b..06b3effcd34 100644 --- a/rules/Transform/Rector/StaticCall/StaticCallToMethodCallRector.php +++ b/rules/Transform/Rector/StaticCall/StaticCallToMethodCallRector.php @@ -127,6 +127,10 @@ public function refactor(Node $node): ?Node $staticCallToMethodCall->getClassObjectType(), ); + if ($expr === null) { + return null; + } + $methodName = $this->getMethodName($node, $staticCallToMethodCall); $hasChanged = true; diff --git a/src/NodeManipulator/ClassDependencyManipulator.php b/src/NodeManipulator/ClassDependencyManipulator.php index 9125a347977..6a699fecf38 100644 --- a/src/NodeManipulator/ClassDependencyManipulator.php +++ b/src/NodeManipulator/ClassDependencyManipulator.php @@ -189,6 +189,43 @@ public function addStmtsToConstructorIfNotThereYet(Class_ $class, array $stmts): $classMethod->stmts = array_merge($stmts, (array) $classMethod->stmts); } + public function hasFinalParentConstructor(Class_ $class): bool + { + if ($class->getMethod(MethodName::CONSTRUCT) instanceof ClassMethod) { + return false; + } + + $classReflection = $this->reflectionResolver->resolveClassReflection($class); + if (! $classReflection instanceof ClassReflection) { + return false; + } + + $ancestors = array_filter( + $classReflection->getAncestors(), + static fn (ClassReflection $ancestor): bool => $ancestor->getName() !== $classReflection->getName() + ); + + foreach ($ancestors as $ancestor) { + if (! $ancestor->hasNativeMethod(MethodName::CONSTRUCT)) { + continue; + } + + $parentClass = $this->astResolver->resolveClassFromClassReflection($ancestor); + if (! $parentClass instanceof ClassLike) { + continue; + } + + $parentConstructorMethod = $parentClass->getMethod(MethodName::CONSTRUCT); + if (! $parentConstructorMethod instanceof ClassMethod) { + continue; + } + + return $parentConstructorMethod->isFinal(); + } + + return false; + } + private function resolveConstruct(Class_ $class): ?ClassMethod { $constructorMethod = $class->getMethod(MethodName::CONSTRUCT); From 95df6bd5935c2033e32199d8b2a4dc470a5a782d Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Thu, 21 May 2026 18:17:57 +0000 Subject: [PATCH 2/2] [ci-review] Rector Rectify --- .../AddNameToBooleanArgumentRector.php | 3 ++- .../CallLike/AddNameToNullArgumentRector.php | 3 ++- .../CallLikeArgumentNameAdder.php | 19 ++++++++++--------- 3 files changed, 14 insertions(+), 11 deletions(-) diff --git a/rules/CodeQuality/Rector/CallLike/AddNameToBooleanArgumentRector.php b/rules/CodeQuality/Rector/CallLike/AddNameToBooleanArgumentRector.php index 97cb901d6a9..57b3cbb35bd 100644 --- a/rules/CodeQuality/Rector/CallLike/AddNameToBooleanArgumentRector.php +++ b/rules/CodeQuality/Rector/CallLike/AddNameToBooleanArgumentRector.php @@ -4,6 +4,7 @@ namespace Rector\CodeQuality\Rector\CallLike; +use PhpParser\Node\Expr; use PhpParser\Node; use PhpParser\Node\Expr\CallLike; use Rector\NodeAnalyzer\CallLikeArgumentNameAdder; @@ -58,7 +59,7 @@ public function refactor(Node $node): ?Node { return $this->callLikeArgumentNameAdder->addNamesToArgs( $node, - fn ($expr): bool => $this->valueResolver->isTrueOrFalse($expr), + fn (Expr $expr): bool => $this->valueResolver->isTrueOrFalse($expr), ); } diff --git a/rules/CodeQuality/Rector/CallLike/AddNameToNullArgumentRector.php b/rules/CodeQuality/Rector/CallLike/AddNameToNullArgumentRector.php index a34b136c5e0..8c4eacd1044 100644 --- a/rules/CodeQuality/Rector/CallLike/AddNameToNullArgumentRector.php +++ b/rules/CodeQuality/Rector/CallLike/AddNameToNullArgumentRector.php @@ -4,6 +4,7 @@ namespace Rector\CodeQuality\Rector\CallLike; +use PhpParser\Node\Expr; use PhpParser\Node; use PhpParser\Node\Expr\CallLike; use Rector\NodeAnalyzer\CallLikeArgumentNameAdder; @@ -58,7 +59,7 @@ public function refactor(Node $node): ?Node { return $this->callLikeArgumentNameAdder->addNamesToArgs( $node, - fn ($expr): bool => $this->valueResolver->isNull($expr), + fn (Expr $expr): bool => $this->valueResolver->isNull($expr), ); } diff --git a/src/NodeAnalyzer/CallLikeArgumentNameAdder.php b/src/NodeAnalyzer/CallLikeArgumentNameAdder.php index 503737382c4..53e1fc7ca46 100644 --- a/src/NodeAnalyzer/CallLikeArgumentNameAdder.php +++ b/src/NodeAnalyzer/CallLikeArgumentNameAdder.php @@ -4,6 +4,7 @@ namespace Rector\NodeAnalyzer; +use PhpParser\Node\Expr; use PhpParser\Node\Arg; use PhpParser\Node\Expr\CallLike; use PhpParser\Node\Identifier; @@ -26,22 +27,22 @@ public function __construct( * argument whose value satisfies $shouldNameArgValue. All subsequent positional * arguments receive names too (required by PHP named-arg semantics). * - * @param callable(\PhpParser\Node\Expr): bool $shouldNameArgValue + * @param callable(Expr):bool $shouldNameArgValue */ - public function addNamesToArgs(CallLike $node, callable $shouldNameArgValue): ?CallLike + public function addNamesToArgs(CallLike $callLike, callable $shouldNameArgValue): ?CallLike { - if ($this->shouldSkip($node)) { + if ($this->shouldSkip($callLike)) { return null; } - $reflection = $this->reflectionResolver->resolveFunctionLikeReflectionFromCall($node); + $reflection = $this->reflectionResolver->resolveFunctionLikeReflectionFromCall($callLike); if (! $reflection instanceof FunctionReflection && ! $reflection instanceof MethodReflection) { return null; } - $scope = ScopeFetcher::fetch($node); - $args = $node->getArgs(); - $parameters = ParametersAcceptorSelectorVariantsWrapper::select($reflection, $node, $scope) + $scope = ScopeFetcher::fetch($callLike); + $args = $callLike->getArgs(); + $parameters = ParametersAcceptorSelectorVariantsWrapper::select($reflection, $callLike, $scope) ->getParameters(); $position = $this->resolveFirstPositionToName($args, $parameters, $shouldNameArgValue); @@ -70,7 +71,7 @@ public function addNamesToArgs(CallLike $node, callable $shouldNameArgValue): ?C return null; } - return $node; + return $callLike; } private function shouldSkip(CallLike $callLike): bool @@ -96,7 +97,7 @@ private function shouldSkip(CallLike $callLike): bool /** * @param Arg[] $args * @param ParameterReflection[] $parameters - * @param callable(\PhpParser\Node\Expr): bool $shouldNameArgValue + * @param callable(Expr):bool $shouldNameArgValue */ private function resolveFirstPositionToName(array $args, array $parameters, callable $shouldNameArgValue): ?int {