Skip to content

Commit 8a22661

Browse files
TomasVotrubaclaudeactions-user
authored
fix: skip StaticCallToMethodCallRector when parent declares final __construct (#8001)
* 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 * [ci-review] Rector Rectify --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: GitHub Action <actions@github.com>
1 parent 749a175 commit 8a22661

9 files changed

Lines changed: 99 additions & 12 deletions

File tree

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
namespace Rector\Tests\Transform\Rector\StaticCall\StaticCallToMethodCallRector\Fixture;
4+
5+
use Illuminate\Support\Facades\App;
6+
use Illuminate\Http\Request;
7+
use Illuminate\Http\Resources\MissingValue;
8+
use Rector\Tests\Transform\Rector\StaticCall\StaticCallToMethodCallRector\Source\ResourceWithFinalConstruct;
9+
10+
class SkipWhenParentHasFinalConstruct extends ResourceWithFinalConstruct
11+
{
12+
public function toArray(
13+
Request $request,
14+
): array {
15+
return [
16+
'user_id' => $this->user_id ?? App::get(MissingValue::class),
17+
];
18+
}
19+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Rector\Tests\Transform\Rector\StaticCall\StaticCallToMethodCallRector\Source;
6+
7+
class ResourceWithFinalConstruct
8+
{
9+
public $resource;
10+
11+
final public function __construct($resource)
12+
{
13+
$this->resource = $resource;
14+
}
15+
}

rules/CodeQuality/Rector/CallLike/AddNameToBooleanArgumentRector.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
namespace Rector\CodeQuality\Rector\CallLike;
66

7+
use PhpParser\Node\Expr;
78
use PhpParser\Node;
89
use PhpParser\Node\Expr\CallLike;
910
use Rector\NodeAnalyzer\CallLikeArgumentNameAdder;
@@ -58,7 +59,7 @@ public function refactor(Node $node): ?Node
5859
{
5960
return $this->callLikeArgumentNameAdder->addNamesToArgs(
6061
$node,
61-
fn ($expr): bool => $this->valueResolver->isTrueOrFalse($expr),
62+
fn (Expr $expr): bool => $this->valueResolver->isTrueOrFalse($expr),
6263
);
6364
}
6465

rules/CodeQuality/Rector/CallLike/AddNameToNullArgumentRector.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
namespace Rector\CodeQuality\Rector\CallLike;
66

7+
use PhpParser\Node\Expr;
78
use PhpParser\Node;
89
use PhpParser\Node\Expr\CallLike;
910
use Rector\NodeAnalyzer\CallLikeArgumentNameAdder;
@@ -58,7 +59,7 @@ public function refactor(Node $node): ?Node
5859
{
5960
return $this->callLikeArgumentNameAdder->addNamesToArgs(
6061
$node,
61-
fn ($expr): bool => $this->valueResolver->isNull($expr),
62+
fn (Expr $expr): bool => $this->valueResolver->isNull($expr),
6263
);
6364
}
6465

rules/Transform/NodeAnalyzer/FuncCallStaticCallToMethodCallAnalyzer.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ public function matchTypeProvidingExpr(
3636
Class_ $class,
3737
ClassMethod $classMethod,
3838
ObjectType $objectType,
39-
): MethodCall | PropertyFetch | Variable {
39+
): MethodCall | PropertyFetch | Variable | null {
4040
$expr = $this->typeProvidingExprFromClassResolver->resolveTypeProvidingExprFromClass(
4141
$class,
4242
$classMethod,
@@ -51,6 +51,11 @@ public function matchTypeProvidingExpr(
5151
return $expr;
5252
}
5353

54+
// Cannot add constructor dependency when nearest parent constructor is final
55+
if ($this->classDependencyManipulator->hasFinalParentConstructor($class)) {
56+
return null;
57+
}
58+
5459
$propertyName = $this->propertyNaming->fqnToVariableName($objectType);
5560
$this->classDependencyManipulator->addConstructorDependency(
5661
$class,

rules/Transform/Rector/FuncCall/FuncCallToMethodCallRector.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,10 @@ public function refactor(Node $node): ?Node
114114
$funcNameToMethodCallName->getNewObjectType(),
115115
);
116116

117+
if ($expr === null) {
118+
return null;
119+
}
120+
117121
$hasChanged = true;
118122

119123
return $this->nodeFactory->createMethodCall(

rules/Transform/Rector/StaticCall/StaticCallToMethodCallRector.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,10 @@ public function refactor(Node $node): ?Node
127127
$staticCallToMethodCall->getClassObjectType(),
128128
);
129129

130+
if ($expr === null) {
131+
return null;
132+
}
133+
130134
$methodName = $this->getMethodName($node, $staticCallToMethodCall);
131135

132136
$hasChanged = true;

src/NodeAnalyzer/CallLikeArgumentNameAdder.php

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
namespace Rector\NodeAnalyzer;
66

7+
use PhpParser\Node\Expr;
78
use PhpParser\Node\Arg;
89
use PhpParser\Node\Expr\CallLike;
910
use PhpParser\Node\Identifier;
@@ -26,22 +27,22 @@ public function __construct(
2627
* argument whose value satisfies $shouldNameArgValue. All subsequent positional
2728
* arguments receive names too (required by PHP named-arg semantics).
2829
*
29-
* @param callable(\PhpParser\Node\Expr): bool $shouldNameArgValue
30+
* @param callable(Expr):bool $shouldNameArgValue
3031
*/
31-
public function addNamesToArgs(CallLike $node, callable $shouldNameArgValue): ?CallLike
32+
public function addNamesToArgs(CallLike $callLike, callable $shouldNameArgValue): ?CallLike
3233
{
33-
if ($this->shouldSkip($node)) {
34+
if ($this->shouldSkip($callLike)) {
3435
return null;
3536
}
3637

37-
$reflection = $this->reflectionResolver->resolveFunctionLikeReflectionFromCall($node);
38+
$reflection = $this->reflectionResolver->resolveFunctionLikeReflectionFromCall($callLike);
3839
if (! $reflection instanceof FunctionReflection && ! $reflection instanceof MethodReflection) {
3940
return null;
4041
}
4142

42-
$scope = ScopeFetcher::fetch($node);
43-
$args = $node->getArgs();
44-
$parameters = ParametersAcceptorSelectorVariantsWrapper::select($reflection, $node, $scope)
43+
$scope = ScopeFetcher::fetch($callLike);
44+
$args = $callLike->getArgs();
45+
$parameters = ParametersAcceptorSelectorVariantsWrapper::select($reflection, $callLike, $scope)
4546
->getParameters();
4647

4748
$position = $this->resolveFirstPositionToName($args, $parameters, $shouldNameArgValue);
@@ -70,7 +71,7 @@ public function addNamesToArgs(CallLike $node, callable $shouldNameArgValue): ?C
7071
return null;
7172
}
7273

73-
return $node;
74+
return $callLike;
7475
}
7576

7677
private function shouldSkip(CallLike $callLike): bool
@@ -96,7 +97,7 @@ private function shouldSkip(CallLike $callLike): bool
9697
/**
9798
* @param Arg[] $args
9899
* @param ParameterReflection[] $parameters
99-
* @param callable(\PhpParser\Node\Expr): bool $shouldNameArgValue
100+
* @param callable(Expr):bool $shouldNameArgValue
100101
*/
101102
private function resolveFirstPositionToName(array $args, array $parameters, callable $shouldNameArgValue): ?int
102103
{

src/NodeManipulator/ClassDependencyManipulator.php

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,43 @@ public function addStmtsToConstructorIfNotThereYet(Class_ $class, array $stmts):
189189
$classMethod->stmts = array_merge($stmts, (array) $classMethod->stmts);
190190
}
191191

192+
public function hasFinalParentConstructor(Class_ $class): bool
193+
{
194+
if ($class->getMethod(MethodName::CONSTRUCT) instanceof ClassMethod) {
195+
return false;
196+
}
197+
198+
$classReflection = $this->reflectionResolver->resolveClassReflection($class);
199+
if (! $classReflection instanceof ClassReflection) {
200+
return false;
201+
}
202+
203+
$ancestors = array_filter(
204+
$classReflection->getAncestors(),
205+
static fn (ClassReflection $ancestor): bool => $ancestor->getName() !== $classReflection->getName()
206+
);
207+
208+
foreach ($ancestors as $ancestor) {
209+
if (! $ancestor->hasNativeMethod(MethodName::CONSTRUCT)) {
210+
continue;
211+
}
212+
213+
$parentClass = $this->astResolver->resolveClassFromClassReflection($ancestor);
214+
if (! $parentClass instanceof ClassLike) {
215+
continue;
216+
}
217+
218+
$parentConstructorMethod = $parentClass->getMethod(MethodName::CONSTRUCT);
219+
if (! $parentConstructorMethod instanceof ClassMethod) {
220+
continue;
221+
}
222+
223+
return $parentConstructorMethod->isFinal();
224+
}
225+
226+
return false;
227+
}
228+
192229
private function resolveConstruct(Class_ $class): ?ClassMethod
193230
{
194231
$constructorMethod = $class->getMethod(MethodName::CONSTRUCT);

0 commit comments

Comments
 (0)