[PHPUnit] Add AddSeeTestAnnotationRector

This commit is contained in:
Tomas Votruba 2019-09-03 10:39:34 +02:00
parent e72491feb2
commit 1a0db5e25c
9 changed files with 290 additions and 10 deletions

View File

@ -1,2 +1,3 @@
services:
Rector\PHPUnit\Rector\MethodCall\RemoveExpectAnyFromMockRector: ~
Rector\PHPUnit\Rector\Class_\AddSeeTestAnnotationRector: ~

View File

@ -0,0 +1,142 @@
<?php declare(strict_types=1);
namespace Rector\PHPUnit\Rector\Class_;
use Nette\Utils\Strings;
use PhpParser\Node;
use PhpParser\Node\Stmt\Class_;
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagNode;
use Rector\BetterPhpDocParser\Attributes\Ast\PhpDoc\AttributeAwareGenericTagValueNode;
use Rector\NodeTypeResolver\PhpDoc\NodeAnalyzer\DocBlockManipulator;
use Rector\Rector\AbstractRector;
use Rector\RectorDefinition\CodeSample;
use Rector\RectorDefinition\RectorDefinition;
final class AddSeeTestAnnotationRector extends AbstractRector
{
/**
* @var DocBlockManipulator
*/
private $docBlockManipulator;
public function __construct(DocBlockManipulator $docBlockManipulator)
{
$this->docBlockManipulator = $docBlockManipulator;
}
public function getDefinition(): RectorDefinition
{
return new RectorDefinition(
'Add @see annotation test of the class for faster jump to test. Make it FQN, so it stays in the annotation, not in the PHP source code.',
[
new CodeSample(
<<<'CODE_SAMPLE'
class SomeService
{
}
class SomeServiceTest extends \PHPUnit\Framework\TestCase
{
}
CODE_SAMPLE
,
<<<'CODE_SAMPLE'
/**
* @see \SomeServiceTest
*/
class SomeService
{
}
class SomeServiceTest extends \PHPUnit\Framework\TestCase
{
}
CODE_SAMPLE
),
]
);
}
/**
* @return string[]
*/
public function getNodeTypes(): array
{
return [Class_::class];
}
/**
* @param Class_ $node
*/
public function refactor(Node $node): ?Node
{
if ($this->shouldSkipClass($node)) {
return null;
}
/** @var string $className */
$className = $this->getName($node);
$testCaseClassName = $this->resolveTestCaseClassName($className);
if ($testCaseClassName === null) {
return null;
}
$this->docBlockManipulator->addTag($node, $this->createSeePhpDocTagNode($testCaseClassName));
return $node;
}
private function resolveTestCaseClassName(string $className): ?string
{
if (class_exists($className . 'Test')) {
return $className . 'Test';
}
$shortClassName = Strings::after($className, '\\', -1);
$testShortClassName = $shortClassName . 'Test';
$declaredClasses = get_declared_classes();
foreach ($declaredClasses as $declaredClass) {
if (Strings::endsWith($declaredClass, '\\' . $testShortClassName)) {
return $declaredClass;
}
}
return null;
}
private function createSeePhpDocTagNode(string $className): PhpDocTagNode
{
return new PhpDocTagNode('@see', new AttributeAwareGenericTagValueNode('\\' . $className));
}
private function shouldSkipClass(Class_ $class): bool
{
if ($class->isAnonymous()) {
return true;
}
$className = $this->getName($class);
if ($className === null) {
return true;
}
// is a test case
if (Strings::endsWith($className, 'Test')) {
return true;
}
// is the @see annotation already added
if ($class->getDocComment()) {
$docCommentText = $class->getDocComment()->getText();
$seeClassPattern = '#@see (.*?)' . preg_quote($className, '#') . 'Test#';
if (Strings::match($docCommentText, $seeClassPattern)) {
return true;
}
}
return false;
}
}

View File

@ -0,0 +1,25 @@
<?php declare(strict_types=1);
namespace Rector\PHPUnit\Tests\Rector\Class_\AddSeeTestAnnotationRector;
use Rector\PHPUnit\Rector\Class_\AddSeeTestAnnotationRector;
use Rector\Testing\PHPUnit\AbstractRectorTestCase;
final class AddSeeTestAnnotationRectorTest extends AbstractRectorTestCase
{
public function test(): void
{
$this->doTestFiles([
__DIR__ . '/Fixture/fixture.php.inc',
__DIR__ . '/Fixture/different_namespace.php.inc',
__DIR__ . '/Fixture/add_to_doc_block.php.inc',
// skip
__DIR__ . '/Fixture/skip_existing.php.inc',
]);
}
protected function getRectorClass(): string
{
return AddSeeTestAnnotationRector::class;
}
}

View File

@ -0,0 +1,34 @@
<?php
namespace Rector\PHPUnit\Tests\Rector\Class_\AddSeeTestAnnotationRector\Fixture;
/**
* This is here
*/
class AddToDocBlock
{
}
class AddToDocBlockTest extends \PHPUnit\Framework\TestCase
{
}
?>
-----
<?php
namespace Rector\PHPUnit\Tests\Rector\Class_\AddSeeTestAnnotationRector\Fixture;
/**
* This is here
* @see \Rector\PHPUnit\Tests\Rector\Class_\AddSeeTestAnnotationRector\Fixture\AddToDocBlockTest
*/
class AddToDocBlock
{
}
class AddToDocBlockTest extends \PHPUnit\Framework\TestCase
{
}
?>

View File

@ -0,0 +1,26 @@
<?php
namespace Rector\PHPUnit\Tests\Rector\Class_\AddSeeTestAnnotationRector\Fixture;
/**
* This is here
*/
class DifferentNamespace
{
}
?>
-----
<?php
namespace Rector\PHPUnit\Tests\Rector\Class_\AddSeeTestAnnotationRector\Fixture;
/**
* This is here
* @see \Rector\PHPUnit\Tests\Rector\Class_\AddSeeTestAnnotationRector\Source\DifferentNamespaceTest
*/
class DifferentNamespace
{
}
?>

View File

@ -0,0 +1,30 @@
<?php
namespace Rector\PHPUnit\Tests\Rector\Class_\AddSeeTestAnnotationRector\Fixture;
class SomeService
{
}
class SomeServiceTest extends \PHPUnit\Framework\TestCase
{
}
?>
-----
<?php
namespace Rector\PHPUnit\Tests\Rector\Class_\AddSeeTestAnnotationRector\Fixture;
/**
* @see \Rector\PHPUnit\Tests\Rector\Class_\AddSeeTestAnnotationRector\Fixture\SomeServiceTest
*/
class SomeService
{
}
class SomeServiceTest extends \PHPUnit\Framework\TestCase
{
}
?>

View File

@ -0,0 +1,14 @@
<?php
namespace Rector\PHPUnit\Tests\Rector\Class_\AddSeeTestAnnotationRector\Fixture;
/**
* @see \Rector\PHPUnit\Tests\Rector\Class_\AddSeeTestAnnotationRector\Fixture\SomeExistingTest
*/
class SomeExisting
{
}
class SomeExistingTest extends \PHPUnit\Framework\TestCase
{
}

View File

@ -0,0 +1,8 @@
<?php declare(strict_types=1);
namespace Rector\PHPUnit\Tests\Rector\Class_\AddSeeTestAnnotationRector\Source;
class DifferentNamespaceTest
{
}

View File

@ -75,7 +75,7 @@ final class ConfigurationFactory
'code_after',
'description',
'source',
'level',
'set',
];
if (count(array_intersect(array_keys($config), $requiredKeys)) === count($requiredKeys)) {
@ -130,33 +130,33 @@ final class ConfigurationFactory
return Strings::after($fqnNodeTypes[0], '\\', -1);
}
private function resolveSetConfig(string $level): ?string
private function resolveSetConfig(string $set): ?string
{
if ($level === '') {
if ($set === '') {
return null;
}
$fileLevel = sprintf('#^%s(\.yaml)?$#', $level);
$fileSet = sprintf('#^%s(\.yaml)?$#', $set);
$finder = Finder::create()->files()
->in($this->setsDirectory)
->name($fileLevel);
->name($fileSet);
/** @var SplFileInfo[] $fileInfos */
$fileInfos = iterator_to_array($finder->getIterator());
if (count($fileInfos) === 0) {
// assume new one is created
$match = Strings::match($level, '#\/(?<name>[a-zA-Z_-]+])#');
$match = Strings::match($set, '#\/(?<name>[a-zA-Z_-]+])#');
if (isset($match['name'])) {
return $this->setsDirectory . '/' . $match['name'] . '/' . $level;
return $this->setsDirectory . '/' . $match['name'] . '/' . $set;
}
return null;
}
/** @var SplFileInfo $foundLevelConfigFileInfo */
$foundLevelConfigFileInfo = array_pop($fileInfos);
/** @var SplFileInfo $foundSetConfigFileInfo */
$foundSetConfigFileInfo = array_pop($fileInfos);
return $foundLevelConfigFileInfo->getRealPath();
return $foundSetConfigFileInfo->getRealPath();
}
private function isNodeClassMatch(string $nodeClass, string $nodeType): bool