<repo_name>TomasVotruba/drift<file_sep>/src/PHPUnit/ClassMethod/MethodToPestTestRector.php
<?php

namespace Pest\Drift\PHPUnit\ClassMethod;

use Exception;
use Nette\Utils\Strings;
use Pest\Drift\PestCollector;
use PhpParser\Node;
use PhpParser\Node\Expr;
use PhpParser\Node\Expr\Closure;
use PhpParser\Node\Expr\FuncCall;
use PhpParser\Node\Stmt\Class_;
use PhpParser\Node\Stmt\ClassMethod;
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagNode;
use PHPUnit\Framework\TestCase;
use Rector\BetterPhpDocParser\PhpDocInfo\PhpDocInfo;
use Rector\NodeTypeResolver\Node\AttributeKey;

class MethodToPestTestRector extends AbstractClassMethodRector
{
    public ?string $type = PestCollector::TEST_METHODS;

    public function classMethodRefactor(Class_ $classNode, ClassMethod $classMethodNode): ?Node
    {
        if (!$this->isTestMethod($classMethodNode)) {
            return null;
        }

        $pestTestNode = $this->createPestTest($classMethodNode);

        $pestTestNode = $this->migratePhpDocGroup($classMethodNode, $pestTestNode);

        $pestTestNode = $this->migrateDataProvider($classMethodNode, $pestTestNode);

        $pestTestNode = $this->migrateExpectException($classMethodNode, $pestTestNode);

        $pestTestNode = $this->migrateSkipCall($classMethodNode, $pestTestNode);

        $pestTestNode = $this->migratePhpDocDepends($classMethodNode, $pestTestNode);

        return $pestTestNode;
    }

    public function isTestMethod(ClassMethod $node): bool
    {
        /** @var PhpDocInfo $phpDoc */
        $phpDoc = $node->getAttribute(AttributeKey::PHP_DOC_INFO);

        if ($phpDoc && $phpDoc->hasByName('test')) {
            return true;
        }

        return Strings::startsWith($this->getName($node), 'test');
    }

    /**
     * @return string[]|null
     */
    public function getPhpDocGroupNames(Node $node): ?array
    {
        /** @var PhpDocInfo|null $phpDocInfo */
        $phpDocInfo = $node->getAttribute(AttributeKey::PHP_DOC_INFO);
        if ($phpDocInfo === null) {
            return null;
        }

        return array_map(static function (PhpDocTagNode $tag): string {
            return (string) $tag->value;
        }, $phpDocInfo->getTagsByName('group'));
    }

    /**
     * @return string[]|null
     */
    public function getPhpDocDependsNames(Node $node): ?array
    {
        /** @var PhpDocInfo|null $phpDocInfo */
        $phpDocInfo = $node->getAttribute(AttributeKey::PHP_DOC_INFO);
        if ($phpDocInfo === null) {
            return null;
        }

        return array_map(static function (PhpDocTagNode $tag): string {
            return (string) $tag->value;
        }, $phpDocInfo->getTagsByName('depends'));
    }

    /**
     * @param $method
     * @return FuncCall
     */
    private function createPestTest($method): FuncCall
    {
        return $this->builderFactory->funcCall(
            'test',
            [
                $this->getName($method),
                new Closure([
                    'stmts' => $method->stmts,
                    'params' => $method->params,
                ]),
            ]
        );
    }

    /**
     * @param ClassMethod $method
     * @return array|null[]
     */
    private function getExpectExceptionCall(ClassMethod $method)
    {
        /** @var Node\Stmt\Expression $stmt */
        foreach ($method->getStmts() as $key => $stmt) {
            if (isset($stmt->expr) && $this->isMethodCall($stmt->expr, 'this', 'expectException')) {
                return [$key, $stmt];
            }
        }
        return [null, null];
    }

    private function migrateExpectException(ClassMethod $method, Expr $pestTestNode): Expr
    {
        [$expectExceptionCallKey, $expectExceptionCall] = $this->getExpectExceptionCall($method);
        if ($expectExceptionCall !== null) {
            // Remove expect exception call from pest test class
            $this->removeStmt($pestTestNode->args[1]->value, $expectExceptionCallKey);
            // And add pest throws chain.
            $pestTestNode = $this->createMethodCall(
                $pestTestNode,
                'throws',
                $expectExceptionCall->expr->args
            );
        }

        return $pestTestNode;
    }

    private function migrateDataProvider(ClassMethod $method, Expr $pestTestNode): Expr
    {
        $dataProvider = $this->getDataProviderName($method);
        if ($dataProvider !== null) {
            $pestTestNode = $this->createMethodCall($pestTestNode, 'with', [$dataProvider]);
        }
        return $pestTestNode;
    }

    private function migratePhpDocGroup(ClassMethod $method, Expr $pestTestNode): Expr
    {
        $groups = $this->getPhpDocGroupNames($method);
        if (!empty($groups)) {
            $pestTestNode = $this->createMethodCall($pestTestNode, 'group', $groups);
        }
        return $pestTestNode;
    }

    private function migratePhpDocDepends(ClassMethod $method, Expr $pestTestNode): Expr
    {
        $depends = $this->getPhpDocDependsNames($method);
        if (!empty($depends)) {
            $pestTestNode = $this->createMethodCall($pestTestNode, 'depends', $depends);
        }
        return $pestTestNode;
    }

    /**
     * @return array|null[]
     */
    private function getMarkTestSkippedCall(ClassMethod $method)
    {
        /** @var Node\Stmt\Expression $stmt */
        foreach ($method->getStmts() as $key => $stmt) {
            if (isset($stmt->expr) && $this->isMethodCall($stmt->expr, 'this', 'markTestSkipped')) {
                return [$key, $stmt];
            }
        }
        return [null, null];
    }

    private function migrateSkipCall(ClassMethod $method, Expr $pestTestNode): Expr
    {
        [$expectExceptionCallKey, $expectExceptionCall] = $this->getMarkTestSkippedCall($method);
        if ($expectExceptionCall !== null) {
            // Remove markTestSkipped call from pest test class
            $this->removeStmt($this->getPestClosure($pestTestNode), $expectExceptionCallKey);
            // And add pest skip chain.
            $pestTestNode = $this->createMethodCall(
                $pestTestNode,
                'skip',
                $expectExceptionCall->expr->args
            );
        }

        return $pestTestNode;
    }
}
<file_sep>/src/PHPUnit/Class_/CustomTestCaseToUsesRector.php
<?php

namespace Pest\Drift\PHPUnit\Class_;

use Pest\Drift\PHPUnit\AbstractPHPUnitToPestRector;
use PhpParser\Node;
use PhpParser\Node\Expr\ClassConstFetch;
use PhpParser\Node\Stmt\Class_;
use PHPUnit\Framework\TestCase;

class CustomTestCaseToUsesRector extends AbstractPHPUnitToPestRector
{
    public function getNodeTypes(): array
    {
        return [Class_::class];
    }

    /**
     * @param Class_ $node
     */
    public function refactor(Node $node): ?Node
    {
        if (!$this->isObjectType($node, TestCase::class)) {
            return null;
        }

        if ($this->canRemovePhpUnitClass($node)
            && $this->isName($node->extends, TestCase::class)
        ) {
            return null;
        }

        $newNode = $this->createFuncCall('uses', [
            new ClassConstFetch(
                $this->canRemovePhpUnitClass($node) ? $node->extends : $node->name,
                'class'
            )
        ]);

        $this->pestCollector->addUses($node, $newNode);
        return $node;
    }
}
<file_sep>/tests/fixtures/TraitA.php
<?php

namespace Pest\Drift\Testing\fixtures;

trait TraitA
{

}
<file_sep>/tests/fixtures/TraitB.php
<?php


namespace Pest\Drift\Testing\fixtures;


trait TraitB
{

}
<file_sep>/src/PHPUnit/ClassMethod/HelperMethodRector.php
<?php

namespace Pest\Drift\PHPUnit\ClassMethod;

use Pest\Drift\PHPUnit\AbstractPHPUnitToPestRector;
use PhpParser\Node;
use PhpParser\Node\Expr;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Scalar\Encapsed;
use PhpParser\Node\Stmt\Class_;
use PhpParser\Node\Stmt\ClassMethod;
use PHPUnit\Framework\TestCase;
use Rector\Core\Exception\ShouldNotHappenException;
use Rector\NodeTypeResolver\Node\AttributeKey;
use ReflectionClass;

class HelperMethodRector extends AbstractPHPUnitToPestRector
{
    public function getNodeTypes(): array
    {
        return [Class_::class];
    }

    /**
     * @param Class_ $node
     */
    public function refactor(Node $node): ?Node
    {
        if (!$this->isObjectType($node, TestCase::class)) {
            return null;
        }

        $className = $this->getName($node->namespacedName);
        $classParents = $this->getParentClasses($className);

        // Create the helper methods
        foreach ($node->getMethods() as $method) {
            // Ignore methods which other rectors already took care of.
            if ($this->isNodeRemoved($method)) {
                continue;
            }

            // Cannot convert helper methods which are part of parent classes
            // as they might be used somewhere else.
            if ($this->isMethodInParentClasses($method, $classParents)) {
                continue;
            }

            $newNode = $this->createPestHelperMethod($method);

            $this->removeNode($method);
            $this->pestCollector->addHelperMethod($node, $newNode);

            // Migrate the methods to use helper method
            foreach ($this->pestCollector->getMethodsOrdered($node) as $testMethod) {
                $this->migratePhpUnitMethodHelpers($testMethod, $method);
            }
        }


        return $node;
    }

    /**
     * @param ReflectionClass[] $classParents
     */
    private function isMethodInParentClasses(ClassMethod $method, array $classParents): bool
    {
        $methodName = $this->getName($method);

        foreach ($classParents as $classParent) {
            if (!$classParent->hasMethod($methodName)) {
                continue;
            }

            return true;
        }
        return false;
    }

    private function createPestHelperMethod(ClassMethod $method): Node\Stmt\Function_
    {
        $stmts = array_map(function (Node\Stmt $stmt) {
            if (!isset($stmt->expr) || !($stmt->expr instanceof MethodCall)) {
                return $stmt;
            }

            if (!$this->isName($stmt->expr->var, 'this')) {
                return $stmt;
            }

            $stmt->expr->var = $this->createFuncCall('test');
            return $stmt;
        }, $method->getStmts());

        return $this->builderFactory->function($this->getName($method))
            ->addStmts($stmts)
            ->getNode();
    }

    private function migratePhpUnitMethodHelpers(Node $pestNode, ClassMethod $methodNode): void
    {
        $methodName = $this->getName($methodNode);

        if ($pestNode instanceof Node\Stmt\Function_) {
            $stmts = $pestNode->getStmts();
        } elseif ($pestNode instanceof Node\Stmt\Expression) {
            $stmts = $pestNode->expr->args[1]->value->stmts;
        } else {
            throw new ShouldNotHappenException("Can't this node yet.");
        }

        $this->traverseNodesWithCallable($stmts, function (Node $node) use ($methodName) {
            if (! $node instanceof MethodCall) {
                return null;
            }

            $parentNode = $node->getAttribute(AttributeKey::PARENT_NODE);

            // probably your case to skip
            // https://github.com/rectorphp/rector/blob/master/docs/nodes_overview.md#phpparsernodescalarencapsed
            if ($parentNode instanceof Encapsed) {
                return null;
            }

            if (!$this->isName($node->name, $methodName)) {
                return null;
            }

            return $this->createFuncCall($methodName);
        });
    }

    /**
     * @param string|null $className
     * @return ReflectionClass[]|void[]
     */
    private function getParentClasses(?string $className)
    {
        return array_map(function (string $classParent) {
            if (!class_exists($classParent)) {
                return;
            }
            return new ReflectionClass($classParent);
        }, class_parents($className));
    }
}
<file_sep>/tests/BaseRectorTestCase.php
<?php

namespace Pest\Drift\Testing;

use Rector\Core\Testing\PHPUnit\AbstractRectorTestCase;

abstract class BaseRectorTestCase extends AbstractRectorTestCase
{
}
<file_sep>/src/Pest/FuncCall/PestTestNamingRector.php
<?php

namespace Pest\Drift\Pest\FuncCall;

use Nette\Utils\Strings;
use PhpParser\Node;
use PhpParser\Node\Expr\FuncCall;
use PhpParser\Node\Name;
use PhpParser\Node\Scalar\String_;
use Rector\Core\Rector\AbstractRector;
use Rector\Core\RectorDefinition\CodeSample;
use Rector\Core\RectorDefinition\RectorDefinition;

class PestTestNamingRector extends AbstractRector
{
    public function getNodeTypes(): array
    {
        return [FuncCall::class];
    }

    /**
     * @param FuncCall $node
     */
    public function refactor(Node $node): ?Node
    {
        if (!$this->isName($node, 'test')) {
            return null;
        }

        if (count($node->args) < 1 || !is_string($this->getValue($node->args[0]->value))) {
            return null;
        }

        if (!Strings::startsWith($this->getValue($node->args[0]->value), 'test')) {
            return null;
        }

        $node->name = new Name('test');
        $node->args[0]->value = new String_(trim(substr($this->getValue($node->args[0]->value), 4)));

        return $node;
    }

    public function getDefinition(): RectorDefinition
    {
        return new RectorDefinition('Renames tests starting with `test` to remove the `test` prefix', [
            new CodeSample(
                <<<'PHP'
test('testStartsWithTest')->skip();
PHP,
                <<<'PHP'
test('StartsWithTest')->skip();
PHP
            ),
        ]);
    }
}
