From 9322849ee008645a9c1ea4123017553ec1c3a859 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=B4me=20Chilliet?= Date: Fri, 25 Sep 2026 12:33:27 +0200 Subject: [PATCH 1/4] feat: Add rector rule to automatically migrate to the new createInstanceWithMocks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Côme Chilliet --- src/Rector/TestCaseAutoMockRefactorRector.php | 429 ++++++++++++++++++ .../Fixture/test_fixture_fetch.php.inc | 92 ++++ .../TestCaseAutoMockRefactorRectorTest.php | 33 ++ .../config/config.php | 16 + 4 files changed, 570 insertions(+) create mode 100644 src/Rector/TestCaseAutoMockRefactorRector.php create mode 100644 tests/Rector/TestCaseAutoMockRefactorRector/Fixture/test_fixture_fetch.php.inc create mode 100644 tests/Rector/TestCaseAutoMockRefactorRector/TestCaseAutoMockRefactorRectorTest.php create mode 100644 tests/Rector/TestCaseAutoMockRefactorRector/config/config.php diff --git a/src/Rector/TestCaseAutoMockRefactorRector.php b/src/Rector/TestCaseAutoMockRefactorRector.php new file mode 100644 index 0000000..c883f26 --- /dev/null +++ b/src/Rector/TestCaseAutoMockRefactorRector.php @@ -0,0 +1,429 @@ +commentsManager = $this->createMock(ICommentsManager::class); + + $this->sorter = new CommentersSorter($this->commentsManager); + } + + #[\PHPUnit\Framework\Attributes\DataProvider(methodName: 'sortDataProvider')] + public function testSort($data): void { + $this->commentsManager->expects($this->once()) + ->method('getForObject') + ->willReturn([]); + + $workArray = $data['input']; + $this->sorter->sort($workArray, ['itemType' => 'files', 'itemId' => '24']); + + $this->assertEquals($data['expected'], $workArray); + } +} + +CODE_SAMPLE, + <<<'CODE_SAMPLE' +commentsManager = $this->createMock(ICommentsManager::class); + + $this->sorter = new CommentersSorter($this->commentsManager); + } + + #[\PHPUnit\Framework\Attributes\DataProvider(methodName: 'sortDataProvider')] + public function testSort($data): void { + $this->commentsManager->expects($this->once()) + ->method('getForObject') + ->willReturn([]); + + $workArray = $data['input']; + $this->sorter->sort($workArray, ['itemType' => 'files', 'itemId' => '24']); + + $this->assertEquals($data['expected'], $workArray); + } +} + +CODE_SAMPLE + , + [ + new TestCaseAutoMockRefactorRector(), + ], + ), + ], + ); + } + + /** + * @return array> + */ + #[Override] + public function getNodeTypes(): array + { + return [Class_::class]; + } + + /** + * @param Class_ $node + * + * @psalm-suppress MoreSpecificImplementedParamType + */ + #[Override] + public function refactor(Node $node): ?Node + { + if (!($node instanceof Class_)) { + return null; + } + + $setupMethod = $node->getMethod('setUp'); + if (!$setupMethod instanceof ClassMethod) { + return null; + } + + $constructorCall = $this->collectConstructorCall($setupMethod); + if ($constructorCall === null || !($constructorCall->class instanceof Node\Name)) { + return null; + } + + if ($constructorCall->class->getLast() . 'Test' !== (string) $node->name) { + // The test class name does not match the tested class + return null; + } + + $mockedServices = iterator_to_array($this->collectMockedServices($constructorCall)); + if ($mockedServices === []) { + return null; + } + + $mockAssignments = $this->collectMockAssignments($setupMethod, $mockedServices); + if ($mockAssignments === []) { + return null; + } + + if (count($mockAssignments) !== count($constructorCall->args)) { + // For now we skip the complicated cases + return null; + } + + $this->replaceStatement( + $setupMethod, + $constructorCall, + new MethodCall( + new Node\Expr\Variable('this'), + 'createInstanceWithMocks', + [ + new Arg(new ClassConstFetch($constructorCall->class, 'class')), + ], + ), + ); + $this->removeStatements( + $setupMethod, + $mockAssignments, + ); + $this->removePropertiesByName( + $node, + array_keys($mockedServices), + ); + $mockAssignments = array_combine( + array_map( + fn (Expression $node): string => $node->expr->var->name->name, + $mockAssignments, + ), + $mockAssignments, + ); + $this->replaceMethodCallsToMocks( + $node, + $mockAssignments, + ); + + return $node; + } + + /** + * @psalm-suppress MoreSpecificReturnType + * @psalm-suppress LessSpecificReturnStatement + */ + private function collectConstructorCall(ClassMethod $setupMethod): ?Node\Expr\New_ + { + $stmts = $setupMethod->getStmts(); + $nodeFinder = new NodeFinder(); + if ($stmts === null) { + return null; + } + + // Find first class that has name $name + return $nodeFinder->findFirst($stmts, fn (Node $node) => $node instanceof Node\Expr\New_); + } + + /** + * @return Generator + */ + private function collectMockedServices(Node\Expr\New_ $constructorCall): Generator + { + foreach ($constructorCall->args as $arg) { + if (!($arg instanceof Arg)) { + continue; + } + if (!($arg->value instanceof PropertyFetch)) { + continue; + } + if (!($arg->value->var instanceof Variable)) { + continue; + } + if (!($arg->value->var->name === 'this')) { + continue; + } + if (!($arg->value->name instanceof Node\Identifier)) { + continue; + } + yield $arg->value->name->name => $arg->value; + } + } + + /** + * @param array $constructorArgs + * + * @return list + * + * @psalm-suppress LessSpecificReturnStatement + */ + private function collectMockAssignments(ClassMethod $setupMethod, array $constructorArgs): array + { + $stmts = $setupMethod->getStmts(); + $nodeFinder = new NodeFinder(); + if ($stmts === null) { + return []; + } + + return $nodeFinder->find($stmts, fn (Node $node) => $node instanceof Expression + && $node->expr instanceof Assign + && $node->expr->var instanceof PropertyFetch + && $node->expr->var->var instanceof Variable + && $node->expr->var->var->name === 'this' + && $node->expr->expr instanceof MethodCall + && $node->expr->expr->var instanceof Variable + && $node->expr->expr->var->name === 'this' + && $node->expr->expr->name instanceof Node\Identifier + && $node->expr->expr->name->name === 'createMock' + && $node->expr->var->name instanceof Node\Identifier + && isset($constructorArgs[$node->expr->var->name->name])); + } + + private function replaceStatement(ClassMethod $node, Node $search, Node $replace): void + { + $traverser = new NodeTraverser(); + $traverser->addVisitor(new class ($search, $replace) extends NodeVisitorAbstract { + public function __construct( + private Node $search, + private Node $replace, + ) { + } + + public function leaveNode(Node $node): ?Node + { + if ($node === $this->search) { + return $this->replace; + } + + return null; + } + }); + + if ($node->stmts !== null) { + $node->stmts = $traverser->traverse($node->stmts); + } + } + + /** + * @param list $toRemove + */ + private function removeStatements(ClassMethod $node, array $toRemove): void + { + $traverser = new NodeTraverser(); + $traverser->addVisitor(new class ($toRemove) extends NodeVisitorAbstract { + /** + * @param list $toRemove + */ + public function __construct( + private array $toRemove, + ) { + } + + public function leaveNode(Node $node): ?int + { + if (in_array($node, $this->toRemove, true)) { + return NodeVisitor::REMOVE_NODE; + } + + return null; + } + }); + + $node->stmts = $traverser->traverse($node->stmts); + } + + /** + * @param list $toRemove + */ + private function removePropertiesByName(Class_ $node, array $toRemove): void + { + $traverser = new NodeTraverser(); + $traverser->addVisitor(new class ($toRemove) extends NodeVisitorAbstract { + /** + * @param list $toRemove + */ + public function __construct( + private array $toRemove, + ) { + } + + public function leaveNode(Node $node): ?int + { + if ($node instanceof Property && in_array($node->props[0]->name->name, $this->toRemove, true)) { + return NodeVisitor::REMOVE_NODE; + } + + return null; + } + }); + + $node->stmts = $traverser->traverse($node->stmts); + } + + /** + * @param array $mocks + */ + private function replaceMethodCallsToMocks(Class_ $node, array $mocks): void + { + $traverser = new NodeTraverser(); + $traverser->addVisitor(new class ($mocks) extends NodeVisitorAbstract { + /** + * @param array $mocks + */ + public function __construct( + private array $mocks, + ) { + } + + public function leaveNode(Node $node): ?Node + { + if ( + $node instanceof PropertyFetch + && $node->var instanceof Variable + && $node->var->name === 'this' + && $node->name instanceof Node\Identifier + && isset($this->mocks[$node->name->name]) + && $this->mocks[$node->name->name]->expr instanceof Assign + && $this->mocks[$node->name->name]->expr->expr instanceof MethodCall + && $this->mocks[$node->name->name]->expr->expr->args[0] instanceof Arg + && ($this->mocks[$node->name->name]->expr->expr->args[0]->value instanceof ClassConstFetch) + ) { + return new ArrayDimFetch( + new PropertyFetch( + new Variable('this'), + 'mocks', + ), + // We reuse the ClassConstFetch from the createMock args + $this->mocks[$node->name->name]->expr->expr->args[0]->value, + ); + } + + return null; + } + }); + + $node->stmts = $traverser->traverse($node->stmts); + } +} diff --git a/tests/Rector/TestCaseAutoMockRefactorRector/Fixture/test_fixture_fetch.php.inc b/tests/Rector/TestCaseAutoMockRefactorRector/Fixture/test_fixture_fetch.php.inc new file mode 100644 index 0000000..cab02bd --- /dev/null +++ b/tests/Rector/TestCaseAutoMockRefactorRector/Fixture/test_fixture_fetch.php.inc @@ -0,0 +1,92 @@ +commentsManager = $this->createMock(ICommentsManager::class); + + $this->sorter = new CommentersSorter($this->commentsManager); + } + + /** + * @param $data + */ + #[\PHPUnit\Framework\Attributes\DataProvider(methodName: 'sortDataProvider')] + public function testSort($data): void { + $this->commentsManager->expects($this->once()) + ->method('getForObject') + ->willReturn([]); + + $workArray = $data['input']; + $this->sorter->sort($workArray, ['itemType' => 'files', 'itemId' => '24']); + + $this->assertEquals($data['expected'], $workArray); + } +} + +?> +----- +sorter = $this->createInstanceWithMocks(CommentersSorter::class); + } + + /** + * @param $data + */ + #[\PHPUnit\Framework\Attributes\DataProvider(methodName: 'sortDataProvider')] + public function testSort($data): void { + $this->mocks[ICommentsManager::class]->expects($this->once()) + ->method('getForObject') + ->willReturn([]); + + $workArray = $data['input']; + $this->sorter->sort($workArray, ['itemType' => 'files', 'itemId' => '24']); + + $this->assertEquals($data['expected'], $workArray); + } +} + +?> diff --git a/tests/Rector/TestCaseAutoMockRefactorRector/TestCaseAutoMockRefactorRectorTest.php b/tests/Rector/TestCaseAutoMockRefactorRector/TestCaseAutoMockRefactorRectorTest.php new file mode 100644 index 0000000..91fef2f --- /dev/null +++ b/tests/Rector/TestCaseAutoMockRefactorRector/TestCaseAutoMockRefactorRectorTest.php @@ -0,0 +1,33 @@ +doTestFile($filePath); + } + + public static function provideData(): Iterator + { + return self::yieldFilesFromDirectory(__DIR__ . '/Fixture'); + } + + public function provideConfigFilePath(): string + { + return __DIR__ . '/config/config.php'; + } +} diff --git a/tests/Rector/TestCaseAutoMockRefactorRector/config/config.php b/tests/Rector/TestCaseAutoMockRefactorRector/config/config.php new file mode 100644 index 0000000..dee61b7 --- /dev/null +++ b/tests/Rector/TestCaseAutoMockRefactorRector/config/config.php @@ -0,0 +1,16 @@ +withRules([ + TestCaseAutoMockRefactorRector::class, + ]); From 86104b70ad3c3149e47acbd47fab1c1ed47ed8be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=B4me=20Chilliet?= Date: Fri, 25 Sep 2026 14:10:19 +0200 Subject: [PATCH 2/4] chore: Fix psalm and phpstan issues MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Côme Chilliet --- src/Rector/TestCaseAutoMockRefactorRector.php | 88 +++++++++---------- 1 file changed, 44 insertions(+), 44 deletions(-) diff --git a/src/Rector/TestCaseAutoMockRefactorRector.php b/src/Rector/TestCaseAutoMockRefactorRector.php index c883f26..8660a5e 100644 --- a/src/Rector/TestCaseAutoMockRefactorRector.php +++ b/src/Rector/TestCaseAutoMockRefactorRector.php @@ -31,9 +31,7 @@ use Symplify\RuleDocGenerator\ValueObject\CodeSample\ConfiguredCodeSample; use Symplify\RuleDocGenerator\ValueObject\RuleDefinition; -use function array_combine; use function array_keys; -use function array_map; use function count; use function in_array; use function iterator_to_array; @@ -216,25 +214,27 @@ public function refactor(Node $node): ?Node $node, array_keys($mockedServices), ); - $mockAssignments = array_combine( - array_map( - fn (Expression $node): string => $node->expr->var->name->name, - $mockAssignments, - ), - $mockAssignments, - ); + $constFetchMap = []; + foreach ($mockAssignments as $mockAssignment) { + if ( + $mockAssignment->expr instanceof Assign + && $mockAssignment->expr->var instanceof PropertyFetch + && $mockAssignment->expr->var->name instanceof Node\Identifier + && $mockAssignment->expr->expr instanceof MethodCall + && $mockAssignment->expr->expr->args[0] instanceof Arg + && $mockAssignment->expr->expr->args[0]->value instanceof ClassConstFetch + ) { + $constFetchMap[$mockAssignment->expr->var->name->name] = $mockAssignment->expr->expr->args[0]->value; + } + } $this->replaceMethodCallsToMocks( $node, - $mockAssignments, + $constFetchMap, ); return $node; } - /** - * @psalm-suppress MoreSpecificReturnType - * @psalm-suppress LessSpecificReturnStatement - */ private function collectConstructorCall(ClassMethod $setupMethod): ?Node\Expr\New_ { $stmts = $setupMethod->getStmts(); @@ -243,7 +243,9 @@ private function collectConstructorCall(ClassMethod $setupMethod): ?Node\Expr\Ne return null; } - // Find first class that has name $name + /** + * @var ?Node\Expr\New_ + */ return $nodeFinder->findFirst($stmts, fn (Node $node) => $node instanceof Node\Expr\New_); } @@ -276,8 +278,6 @@ private function collectMockedServices(Node\Expr\New_ $constructorCall): Generat * @param array $constructorArgs * * @return list - * - * @psalm-suppress LessSpecificReturnStatement */ private function collectMockAssignments(ClassMethod $setupMethod, array $constructorArgs): array { @@ -287,7 +287,10 @@ private function collectMockAssignments(ClassMethod $setupMethod, array $constru return []; } - return $nodeFinder->find($stmts, fn (Node $node) => $node instanceof Expression + /** + * @var list $exprs + */ + $exprs = $nodeFinder->find($stmts, fn (Node $node) => $node instanceof Expression && $node->expr instanceof Assign && $node->expr->var instanceof PropertyFetch && $node->expr->var->var instanceof Variable @@ -299,12 +302,13 @@ private function collectMockAssignments(ClassMethod $setupMethod, array $constru && $node->expr->expr->name->name === 'createMock' && $node->expr->var->name instanceof Node\Identifier && isset($constructorArgs[$node->expr->var->name->name])); + + return $exprs; } private function replaceStatement(ClassMethod $node, Node $search, Node $replace): void { - $traverser = new NodeTraverser(); - $traverser->addVisitor(new class ($search, $replace) extends NodeVisitorAbstract { + $this->traverseWithVisitor($node, new class ($search, $replace) extends NodeVisitorAbstract { public function __construct( private Node $search, private Node $replace, @@ -320,10 +324,6 @@ public function leaveNode(Node $node): ?Node return null; } }); - - if ($node->stmts !== null) { - $node->stmts = $traverser->traverse($node->stmts); - } } /** @@ -331,8 +331,7 @@ public function leaveNode(Node $node): ?Node */ private function removeStatements(ClassMethod $node, array $toRemove): void { - $traverser = new NodeTraverser(); - $traverser->addVisitor(new class ($toRemove) extends NodeVisitorAbstract { + $this->traverseWithVisitor($node, new class ($toRemove) extends NodeVisitorAbstract { /** * @param list $toRemove */ @@ -344,14 +343,12 @@ public function __construct( public function leaveNode(Node $node): ?int { if (in_array($node, $this->toRemove, true)) { - return NodeVisitor::REMOVE_NODE; + return NodeTraverser::REMOVE_NODE; } return null; } }); - - $node->stmts = $traverser->traverse($node->stmts); } /** @@ -359,8 +356,7 @@ public function leaveNode(Node $node): ?int */ private function removePropertiesByName(Class_ $node, array $toRemove): void { - $traverser = new NodeTraverser(); - $traverser->addVisitor(new class ($toRemove) extends NodeVisitorAbstract { + $this->traverseWithVisitor($node, new class ($toRemove) extends NodeVisitorAbstract { /** * @param list $toRemove */ @@ -372,25 +368,22 @@ public function __construct( public function leaveNode(Node $node): ?int { if ($node instanceof Property && in_array($node->props[0]->name->name, $this->toRemove, true)) { - return NodeVisitor::REMOVE_NODE; + return NodeTraverser::REMOVE_NODE; } return null; } }); - - $node->stmts = $traverser->traverse($node->stmts); } /** - * @param array $mocks + * @param array $mocks */ private function replaceMethodCallsToMocks(Class_ $node, array $mocks): void { - $traverser = new NodeTraverser(); - $traverser->addVisitor(new class ($mocks) extends NodeVisitorAbstract { + $this->traverseWithVisitor($node, new class ($mocks) extends NodeVisitorAbstract { /** - * @param array $mocks + * @param array $mocks */ public function __construct( private array $mocks, @@ -405,10 +398,6 @@ public function leaveNode(Node $node): ?Node && $node->var->name === 'this' && $node->name instanceof Node\Identifier && isset($this->mocks[$node->name->name]) - && $this->mocks[$node->name->name]->expr instanceof Assign - && $this->mocks[$node->name->name]->expr->expr instanceof MethodCall - && $this->mocks[$node->name->name]->expr->expr->args[0] instanceof Arg - && ($this->mocks[$node->name->name]->expr->expr->args[0]->value instanceof ClassConstFetch) ) { return new ArrayDimFetch( new PropertyFetch( @@ -416,14 +405,25 @@ public function leaveNode(Node $node): ?Node 'mocks', ), // We reuse the ClassConstFetch from the createMock args - $this->mocks[$node->name->name]->expr->expr->args[0]->value, + $this->mocks[$node->name->name], ); } return null; } }); + } + + private function traverseWithVisitor(Class_ | ClassMethod $node, NodeVisitor $visitor): void + { + if ($node->stmts === null) { + return; + } + $traverser = new NodeTraverser(); + $traverser->addVisitor($visitor); - $node->stmts = $traverser->traverse($node->stmts); + /** @var list $newStmts */ + $newStmts = $traverser->traverse($node->stmts); + $node->stmts = $newStmts; } } From ba7cdf59346f2bcaa744b0d3a1619f836eb11b1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=B4me=20Chilliet?= Date: Fri, 25 Sep 2026 15:33:11 +0200 Subject: [PATCH 3/4] chore: Fix code sample MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Côme Chilliet --- src/Rector/TestCaseAutoMockRefactorRector.php | 61 +++---------------- 1 file changed, 7 insertions(+), 54 deletions(-) diff --git a/src/Rector/TestCaseAutoMockRefactorRector.php b/src/Rector/TestCaseAutoMockRefactorRector.php index 8660a5e..032692c 100644 --- a/src/Rector/TestCaseAutoMockRefactorRector.php +++ b/src/Rector/TestCaseAutoMockRefactorRector.php @@ -42,27 +42,10 @@ final class TestCaseAutoMockRefactorRector extends AbstractRector public function getRuleDefinition(): RuleDefinition { return new RuleDefinition( - 'Migrate test cases to new createInstanceWithMock method', + 'Migrate test cases to new createInstanceWithMocks method', [ new ConfiguredCodeSample( <<<'CODE_SAMPLE' -sorter = new CommentersSorter($this->commentsManager); } - #[\PHPUnit\Framework\Attributes\DataProvider(methodName: 'sortDataProvider')] - public function testSort($data): void { + public function testSort(): void { $this->commentsManager->expects($this->once()) ->method('getForObject') ->willReturn([]); - $workArray = $data['input']; - $this->sorter->sort($workArray, ['itemType' => 'files', 'itemId' => '24']); - - $this->assertEquals($data['expected'], $workArray); + $this->assertEquals(true, $this->sorter->testedMethod()); } } - CODE_SAMPLE, <<<'CODE_SAMPLE' -commentsManager = $this->createMock(ICommentsManager::class); - - $this->sorter = new CommentersSorter($this->commentsManager); + $this->sorter = $this->createInstanceWithMocks(CommentersSorter::class); } - #[\PHPUnit\Framework\Attributes\DataProvider(methodName: 'sortDataProvider')] - public function testSort($data): void { - $this->commentsManager->expects($this->once()) + public function testSort(): void { + $this->mocks[ICommentsManager::class]->expects($this->once()) ->method('getForObject') ->willReturn([]); - $workArray = $data['input']; - $this->sorter->sort($workArray, ['itemType' => 'files', 'itemId' => '24']); - - $this->assertEquals($data['expected'], $workArray); + $this->assertEquals(true, $this->sorter->testedMethod()); } } - CODE_SAMPLE , [ From 978b3fa310534cd34d5480da901bfec42ce25bda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=B4me=20Chilliet?= Date: Fri, 25 Sep 2026 17:09:31 +0200 Subject: [PATCH 4/4] fix: Reorder calls to make sure mocks are not used before creation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Côme Chilliet --- src/Rector/TestCaseAutoMockRefactorRector.php | 61 +++++++++++++++++++ .../Fixture/test_fixture_fetch.php.inc | 6 ++ 2 files changed, 67 insertions(+) diff --git a/src/Rector/TestCaseAutoMockRefactorRector.php b/src/Rector/TestCaseAutoMockRefactorRector.php index 032692c..3bd1731 100644 --- a/src/Rector/TestCaseAutoMockRefactorRector.php +++ b/src/Rector/TestCaseAutoMockRefactorRector.php @@ -32,6 +32,7 @@ use Symplify\RuleDocGenerator\ValueObject\RuleDefinition; use function array_keys; +use function array_splice; use function count; use function in_array; use function iterator_to_array; @@ -118,6 +119,11 @@ public function refactor(Node $node): ?Node return null; } + if (!isset($node->extends) || ((string) $node->extends !== 'Test\TestCase')) { + // Not extending Nextcloud TestCase, skip + return null; + } + $setupMethod = $node->getMethod('setUp'); if (!$setupMethod instanceof ClassMethod) { return null; @@ -184,6 +190,7 @@ public function refactor(Node $node): ?Node $node, $constFetchMap, ); + $this->reorderSetupStatements($setupMethod); return $node; } @@ -379,4 +386,58 @@ private function traverseWithVisitor(Class_ | ClassMethod $node, NodeVisitor $vi $newStmts = $traverser->traverse($node->stmts); $node->stmts = $newStmts; } + + private function reorderSetupStatements(ClassMethod $setupMethod): void + { + /** + * @var list|null $stmts + */ + $stmts = $setupMethod->getStmts(); + $nodeFinder = new NodeFinder(); + if ($stmts === null) { + return; + } + + $firstMockUse = -1; + $firstCreateUse = -1; + foreach ($stmts as $key => $stmt) { + if ($firstMockUse < 0) { + // Search in subnodes + $mockArrayUse = $nodeFinder->findFirst( + $stmt, + fn (Node $node) => $node instanceof ArrayDimFetch + && $node->var instanceof PropertyFetch + && $node->var->var instanceof Variable + && $node->var->var->name === 'this' + && $node->var->name instanceof Node\Identifier + && $node->var->name->name === 'mocks', + ); + if ($mockArrayUse !== null) { + $firstMockUse = $key; + } + } + if ($firstCreateUse < 0) { + // Search in subnodes + $createUse = $nodeFinder->findFirst( + $stmt, + fn (Node $node) => $node instanceof MethodCall + && $node->name instanceof Node\Identifier + && $node->name->name === 'createInstanceWithMocks' + && $node->var instanceof Variable + && $node->var->name === 'this', + ); + if ($createUse !== null) { + $firstCreateUse = $key; + } + } + } + if ($firstCreateUse < 0 || $firstMockUse < 0 || $firstCreateUse < $firstMockUse) { + // No reorder needed + return; + } + // Remove create + $create = array_splice($stmts, $firstCreateUse, 1); + array_splice($stmts, $firstMockUse, 0, $create); + $setupMethod->stmts = $stmts; + } } diff --git a/tests/Rector/TestCaseAutoMockRefactorRector/Fixture/test_fixture_fetch.php.inc b/tests/Rector/TestCaseAutoMockRefactorRector/Fixture/test_fixture_fetch.php.inc index cab02bd..2313887 100644 --- a/tests/Rector/TestCaseAutoMockRefactorRector/Fixture/test_fixture_fetch.php.inc +++ b/tests/Rector/TestCaseAutoMockRefactorRector/Fixture/test_fixture_fetch.php.inc @@ -24,6 +24,9 @@ class CommentersSorterTest extends TestCase { parent::setUp(); $this->commentsManager = $this->createMock(ICommentsManager::class); + $this->commentsManager->method('m') + ->expects($this->any()) + ->willReturn(true); $this->sorter = new CommentersSorter($this->commentsManager); } @@ -71,6 +74,9 @@ class CommentersSorterTest extends TestCase { parent::setUp(); $this->sorter = $this->createInstanceWithMocks(CommentersSorter::class); + $this->mocks[ICommentsManager::class]->method('m') + ->expects($this->any()) + ->willReturn(true); } /**