Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions config/set/phpunit/phpunit60.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ services:
PHPUnit_:
# exclude this class, since it has no namespaced replacement
- 'PHPUnit_Framework_MockObject_MockObject'
Rector\PHPUnit\Rector\ClassMethod\AddDoesNotPerformAssertionToNonAssertingTestRector: ~
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
use Rector\Rector\AbstractRector;
use Rector\RectorDefinition\CodeSample;
use Rector\RectorDefinition\RectorDefinition;
use Rector\Reflection\FunctionReflectionResolver;
use ReflectionFunction;

/**
Expand All @@ -29,17 +28,9 @@ final class RemoveDefaultArgumentValueRector extends AbstractRector
*/
private $parsedNodesByType;

/**
* @var FunctionReflectionResolver
*/
private $functionReflectionResolver;

public function __construct(
ParsedNodesByType $parsedNodesByType,
FunctionReflectionResolver $functionReflectionResolver
) {
public function __construct(ParsedNodesByType $parsedNodesByType)
{
$this->parsedNodesByType = $parsedNodesByType;
$this->functionReflectionResolver = $functionReflectionResolver;
}

public function getDefinition(): RectorDefinition
Expand Down Expand Up @@ -245,12 +236,18 @@ private function shouldSkip(Node $node): bool
return false;
}

// skip native functions, hard to analyze without stubs (stubs would make working with IDE non-practical)
$functionName = $this->getName($node);
if (! is_string($functionName)) {
$functionName = $this->getName($node->name);
if ($functionName === null) {
return false;
}

return $this->functionReflectionResolver->isPhpNativeFunction($functionName);
if (! function_exists($functionName)) {
return false;
}

$reflectionFunction = new ReflectionFunction($functionName);

// skip native functions, hard to analyze without stubs (stubs would make working with IDE non-practical)
return $reflectionFunction->isInternal();
}
}
3 changes: 0 additions & 3 deletions packages/NodeTypeResolver/config/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,3 @@ services:

PHPStan\Analyser\ScopeFactory:
factory: ['@Rector\NodeTypeResolver\DependencyInjection\PHPStanServicesFactory', 'createScopeFactory']

PHPStan\Reflection\SignatureMap\SignatureMapProvider:
factory: ['@Rector\NodeTypeResolver\DependencyInjection\PHPStanServicesFactory', 'createSignatureMapProvider']
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
use PHPStan\Analyser\TypeSpecifier;
use PHPStan\Broker\Broker;
use PHPStan\DependencyInjection\ContainerFactory;
use PHPStan\Reflection\SignatureMap\SignatureMapProvider;

final class PHPStanServicesFactory
{
Expand Down Expand Up @@ -50,11 +49,6 @@ public function createTypeSpecifier(): TypeSpecifier
return $this->container->getByType(TypeSpecifier::class);
}

public function createSignatureMapProvider(): SignatureMapProvider
{
return $this->container->getByType(SignatureMapProvider::class);
}

public function createScopeFactory(): ScopeFactory
{
return $this->container->getByType(ScopeFactory::class);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,316 @@
<?php

declare(strict_types=1);

namespace Rector\PHPUnit\Rector\ClassMethod;

use Nette\Utils\Strings;
use PhpParser\Comment\Doc;
use PhpParser\Node;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Expr\StaticCall;
use PhpParser\Node\Stmt\ClassMethod;
use PHPStan\PhpDocParser\Ast\PhpDoc\GenericTagValueNode;
use Rector\BetterPhpDocParser\Attributes\Ast\PhpDoc\AttributeAwarePhpDocTagNode;
use Rector\BetterPhpDocParser\PhpDocInfo\PhpDocInfo;
use Rector\FileSystemRector\Parser\FileInfoParser;
use Rector\NodeContainer\ParsedNodesByType;
use Rector\NodeTypeResolver\PhpDoc\NodeAnalyzer\DocBlockManipulator;
use Rector\Rector\AbstractPHPUnitRector;
use Rector\RectorDefinition\CodeSample;
use Rector\RectorDefinition\RectorDefinition;
use Rector\Reflection\ClassMethodReflectionFactory;
use Symplify\PackageBuilder\FileSystem\SmartFileInfo;

/**
* @see https://phpunit.readthedocs.io/en/7.3/annotations.html#doesnotperformassertions
* @see https://github.com/sebastianbergmann/phpunit/issues/2484
*
* @see \Rector\PHPUnit\Tests\Rector\ClassMethod\AddDoesNotPerformAssertionToNonAssertingTestRector\AddDoesNotPerformAssertionToNonAssertingTestRectorTest
*/
final class AddDoesNotPerformAssertionToNonAssertingTestRector extends AbstractPHPUnitRector
{
/**
* @var ParsedNodesByType
*/
private $parsedNodesByType;

/**
* @var FileInfoParser
*/
private $fileInfoParser;

/**
* @var ClassMethod[][]|null[][]
*/
private $analyzedMethodsInFileName = [];

/**
* @var bool[]
*/
private $containsAssertCallByClassMethod = [];

/**
* @var ClassMethodReflectionFactory
*/
private $classMethodReflectionFactory;

public function __construct(
DocBlockManipulator $docBlockManipulator,
ParsedNodesByType $parsedNodesByType,
FileInfoParser $fileInfoParser,
ClassMethodReflectionFactory $classMethodReflectionFactory
) {
$this->docBlockManipulator = $docBlockManipulator;
$this->parsedNodesByType = $parsedNodesByType;
$this->fileInfoParser = $fileInfoParser;
$this->classMethodReflectionFactory = $classMethodReflectionFactory;
}

public function getDefinition(): RectorDefinition
{
return new RectorDefinition('Tests without assertion will have @doesNotPerformAssertion ', [
new CodeSample(
<<<'PHP'
class SomeClass extends PHPUnit\Framework\TestCase
{
public function test()
{
$nothing = 5;
}
}
PHP
,
<<<'PHP'
class SomeClass extends PHPUnit\Framework\TestCase
{
/**
* @doesNotPerformAssertion
*/
public function test()
{
$nothing = 5;
}
}
PHP
),
]);
}

/**
* @return string[]
*/
public function getNodeTypes(): array
{
return [ClassMethod::class];
}

/**
* @param ClassMethod $node
*/
public function refactor(Node $node): ?Node
{
if ($this->shouldSkipClassMethod($node)) {
return null;
}

$this->addDoesNotPerformAssertion($node);

return $node;
}

private function addDoesNotPerformAssertion(ClassMethod $classMethod): void
{
// A. create new doc
$doc = $classMethod->getDocComment();
if ($doc === null) {
$text = sprintf('/**%s * @doesNotPerformAssertion%s */', PHP_EOL, PHP_EOL);
$classMethod->setDocComment(new Doc($text));
return;
}

// B. extend current doc
/** @var PhpDocInfo $phpDocInfo */
$phpDocInfo = $this->getPhpDocInfo($classMethod);
$phpDocNode = $phpDocInfo->getPhpDocNode();
$phpDocNode->children[] = new AttributeAwarePhpDocTagNode('@doesNotPerformAssertion', new GenericTagValueNode(
''
));

$this->docBlockManipulator->updateNodeWithPhpDocInfo($classMethod, $phpDocInfo);
}

private function shouldSkipClassMethod(ClassMethod $classMethod): bool
{
if (! $this->isInTestClass($classMethod)) {
return true;
}

if (! $this->isTestClassMethod($classMethod)) {
return true;
}

if ($classMethod->getDocComment()) {
$text = $classMethod->getDocComment();
if (Strings::match($text->getText(), '#@expectedException\b#')) {
return true;
}
}

if ($this->containsAssertCall($classMethod)) {
return true;
}

return false;
}

private function containsAssertCall(ClassMethod $classMethod): bool
{
$cacheHash = md5($this->print($classMethod));
if (isset($this->containsAssertCallByClassMethod[$cacheHash])) {
return $this->containsAssertCallByClassMethod[$cacheHash];
}

// A. try "->assert" shallow search first for performance
$hasDirectAssertCall = (bool) $this->hasDirectAssertCall($classMethod);
if ($hasDirectAssertCall) {
$this->containsAssertCallByClassMethod[$cacheHash] = $hasDirectAssertCall;
return $hasDirectAssertCall;
}

// B. look for nested calls
$hasNestedAssertCall = $this->hasNestedAssertCall($classMethod);
$this->containsAssertCallByClassMethod[$cacheHash] = $hasNestedAssertCall;

return $hasNestedAssertCall;
}

private function findClassMethodInFile(string $fileName, string $methodName): ?ClassMethod
{
// skip already anayzed method to prevent cycling
if (isset($this->analyzedMethodsInFileName[$fileName][$methodName])) {
return $this->analyzedMethodsInFileName[$fileName][$methodName];
}

$smartFileInfo = new SmartFileInfo($fileName);
$examinedMethodNodes = $this->fileInfoParser->parseFileInfoToNodesAndDecorate($smartFileInfo);

/** @var ClassMethod|null $examinedClassMethod */
$examinedClassMethod = $this->betterNodeFinder->findFirst(
$examinedMethodNodes,
function (Node $node) use ($methodName): bool {
if (! $node instanceof ClassMethod) {
return false;
}

return $this->isName($node, $methodName);
}
);

$this->analyzedMethodsInFileName[$fileName][$methodName] = $examinedClassMethod;

return $examinedClassMethod;
}

/**
* @param MethodCall|StaticCall $node
*/
private function findClassMethod(Node $node): ?ClassMethod
{
if ($node instanceof MethodCall) {
$classMethod = $this->parsedNodesByType->findClassMethodByMethodCall($node);
if ($classMethod) {
return $classMethod;
}
} elseif ($node instanceof StaticCall) {
$classMethod = $this->parsedNodesByType->findClassMethodByStaticCall($node);
if ($classMethod) {
return $classMethod;
}
}

// in 3rd-party code
return $this->findClassMethodByParsingReflection($node);
}

/**
* @param MethodCall|StaticCall $node
*/
private function findClassMethodByParsingReflection(Node $node): ?ClassMethod
{
$methodName = $this->getName($node->name);
if ($methodName === null) {
return null;
}

if ($node instanceof MethodCall) {
$objectType = $this->getObjectType($node->var);
} else {
// StaticCall
$objectType = $this->getObjectType($node->class);
}

$reflectionMethod = $this->classMethodReflectionFactory->createFromPHPStanTypeAndMethodName(
$objectType,
$methodName
);

if ($reflectionMethod === null) {
return null;
}

$fileName = $reflectionMethod->getFileName();
if ($fileName === false || ! file_exists($fileName)) {
return null;
}

return $this->findClassMethodInFile($fileName, $methodName);
}

private function hasDirectAssertCall(ClassMethod $classMethod): bool
{
return (bool) $this->betterNodeFinder->findFirst((array) $classMethod->stmts, function (Node $node): bool {
if (! $node instanceof MethodCall && ! $node instanceof StaticCall) {
return false;
}

if ($this->isName($node->name, 'assert*')) {
return true;
}

// expectException(...)
if ($this->isName($node->name, 'expectException*')) {
return true;
}

return false;
});
}

private function hasNestedAssertCall(ClassMethod $classMethod): bool
{
$currentClassMethod = $classMethod;

// over and over the same method :/
return (bool) $this->betterNodeFinder->findFirst((array) $classMethod->stmts, function (Node $node) use (
$currentClassMethod
): bool {
if (! $node instanceof MethodCall && ! $node instanceof StaticCall) {
return false;
}

$classMethod = $this->findClassMethod($node);

// skip circular self calls
if ($currentClassMethod === $classMethod) {
return false;
}

if ($classMethod) {
return $this->containsAssertCall($classMethod);
}

return false;
});
}
}
Loading