diff --git a/rules-tests/CodeQuality/Rector/Class_/InlineStubPropertyToCreateStubMethodCallRector/Fixture/inline_used_in_calls.php.inc b/rules-tests/CodeQuality/Rector/Class_/InlineStubPropertyToCreateStubMethodCallRector/Fixture/inline_used_in_calls.php.inc index 8d029862c..9cc59b4cc 100644 --- a/rules-tests/CodeQuality/Rector/Class_/InlineStubPropertyToCreateStubMethodCallRector/Fixture/inline_used_in_calls.php.inc +++ b/rules-tests/CodeQuality/Rector/Class_/InlineStubPropertyToCreateStubMethodCallRector/Fixture/inline_used_in_calls.php.inc @@ -45,9 +45,10 @@ final class InlineUsedInCalls extends TestCase public function testAnother() { - $anotherObject = new NotRelevantClass($this->createStub(\Rector\PHPUnit\Tests\CodeQuality\Rector\Class_\InlineStubPropertyToCreateStubMethodCallRector\Source\NotRelevantClass::class)); + $someStub = $this->createStub(\Rector\PHPUnit\Tests\CodeQuality\Rector\Class_\InlineStubPropertyToCreateStubMethodCallRector\Source\NotRelevantClass::class); + $anotherObject = new NotRelevantClass($someStub); - $this->runThis($this->createStub(\Rector\PHPUnit\Tests\CodeQuality\Rector\Class_\InlineStubPropertyToCreateStubMethodCallRector\Source\NotRelevantClass::class)); + $this->runThis($someStub); } private function runThis($object) diff --git a/rules-tests/CodeQuality/Rector/Class_/InlineStubPropertyToCreateStubMethodCallRector/Fixture/reuse_variable_on_multiple_uses.php.inc b/rules-tests/CodeQuality/Rector/Class_/InlineStubPropertyToCreateStubMethodCallRector/Fixture/reuse_variable_on_multiple_uses.php.inc new file mode 100644 index 000000000..d00bd7aa1 --- /dev/null +++ b/rules-tests/CodeQuality/Rector/Class_/InlineStubPropertyToCreateStubMethodCallRector/Fixture/reuse_variable_on_multiple_uses.php.inc @@ -0,0 +1,51 @@ +formatter = $this->createStub(NotRelevantClass::class); + } + + public function testAnother() + { + $handler = new NotRelevantClass($this->formatter); + + $this->assertSame(spl_object_id($this->formatter), spl_object_id($handler)); + } +} + +?> +----- +createStub(\Rector\PHPUnit\Tests\CodeQuality\Rector\Class_\InlineStubPropertyToCreateStubMethodCallRector\Source\NotRelevantClass::class); + $handler = new NotRelevantClass($formatterStub); + + $this->assertSame(spl_object_id($formatterStub), spl_object_id($handler)); + } +} + +?> diff --git a/rules/CodeQuality/Rector/Class_/InlineStubPropertyToCreateStubMethodCallRector.php b/rules/CodeQuality/Rector/Class_/InlineStubPropertyToCreateStubMethodCallRector.php index d8245d585..8483ed201 100644 --- a/rules/CodeQuality/Rector/Class_/InlineStubPropertyToCreateStubMethodCallRector.php +++ b/rules/CodeQuality/Rector/Class_/InlineStubPropertyToCreateStubMethodCallRector.php @@ -19,6 +19,7 @@ use PhpParser\Node\Stmt\Expression; use PhpParser\Node\Stmt\Property; use PHPStan\Type\ObjectType; +use Rector\PhpParser\Node\BetterNodeFinder; use Rector\PhpParser\NodeFinder\PropertyFetchFinder; use Rector\PHPUnit\CodeQuality\NodeAnalyser\StubPropertyResolver; use Rector\PHPUnit\CodeQuality\NodeFinder\PropertyFetchUsageFinder; @@ -39,6 +40,7 @@ public function __construct( private readonly PropertyFetchFinder $propertyFetchFinder, private readonly PropertyFetchUsageFinder $propertyFetchUsageFinder, private readonly StubPropertyResolver $stubPropertyResolver, + private readonly BetterNodeFinder $betterNodeFinder, ) { } @@ -149,7 +151,32 @@ public function refactor(Node $node): ?Node // 3. replace property fetch calls, with createStub() $stubClassName = $propertyNamesToStubClasses[$propertyName]; - $this->traverseNodesWithCallable($node->getMethods(), function (Node $node) use ( + foreach ($node->getMethods() as $classMethod) { + $this->refactorClassMethod($classMethod, $propertyName, $stubClassName); + } + } + + if ($hasChanged === false) { + return null; + } + + return $node; + } + + private function refactorClassMethod(ClassMethod $classMethod, string $propertyName, string $stubClassName): void + { + $propertyFetches = $this->betterNodeFinder->find( + (array) $classMethod->stmts, + fn (Node $node): bool => $node instanceof PropertyFetch && $this->isName($node->name, $propertyName) + ); + + if ($propertyFetches === []) { + return; + } + + // single use → inline directly + if (count($propertyFetches) === 1) { + $this->traverseNodesWithCallable($classMethod, function (Node $node) use ( $propertyName, $stubClassName ): ?MethodCall { @@ -161,19 +188,74 @@ public function refactor(Node $node): ?Node return null; } - $classConstFetch = new ClassConstFetch(new FullyQualified($stubClassName), 'class'); - - return new MethodCall(new Variable('this'), new Identifier('createStub'), [ - new Arg($classConstFetch), - ]); + return $this->createCreateStubMethodCall($stubClassName); }); + + return; } - if ($hasChanged === false) { - return null; + // multiple uses → assign to a local variable above first use and reuse it + $variableName = $this->resolveVariableName($propertyName); + + $this->traverseNodesWithCallable($classMethod, function (Node $node) use ( + $propertyName, + $variableName + ): ?Variable { + if (! $node instanceof PropertyFetch) { + return null; + } + + if (! $this->isName($node->name, $propertyName)) { + return null; + } + + return new Variable($variableName); + }); + + $assignExpression = new Expression( + new Assign(new Variable($variableName), $this->createCreateStubMethodCall($stubClassName)) + ); + + $this->insertBeforeFirstVariableUse($classMethod, $variableName, $assignExpression); + } + + private function insertBeforeFirstVariableUse( + ClassMethod $classMethod, + string $variableName, + Expression $assignExpression + ): void { + $stmts = (array) $classMethod->stmts; + + foreach ($stmts as $key => $stmt) { + $firstVariable = $this->betterNodeFinder->findFirst( + $stmt, + fn (Node $node): bool => $node instanceof Variable && $this->isName($node, $variableName) + ); + + if (! $firstVariable instanceof Variable) { + continue; + } + + array_splice($stmts, $key, 0, [$assignExpression]); + $classMethod->stmts = $stmts; + return; } + } - return $node; + private function createCreateStubMethodCall(string $stubClassName): MethodCall + { + $classConstFetch = new ClassConstFetch(new FullyQualified($stubClassName), 'class'); + + return new MethodCall(new Variable('this'), new Identifier('createStub'), [new Arg($classConstFetch)]); + } + + private function resolveVariableName(string $propertyName): string + { + if (str_ends_with(strtolower($propertyName), 'stub')) { + return $propertyName; + } + + return $propertyName . 'Stub'; } private function removeSetupPropertyFetchByPropertyName(ClassMethod $setUpClassMethod, string $propertyName): void