Skip to content

Commit

Permalink
[CodeQuality] Add AssertFuncCallToPHPUnitAssertRector (#450)
Browse files Browse the repository at this point in the history
* [CodeQuality] Add AssertFuncCallToPHPUnitAssertRector

* [ci-review] Rector Rectify

---------

Co-authored-by: GitHub Action <[email protected]>
  • Loading branch information
TomasVotruba and actions-user authored Jan 22, 2025
1 parent 289978f commit 57a5555
Show file tree
Hide file tree
Showing 11 changed files with 366 additions and 2 deletions.
1 change: 1 addition & 0 deletions config/sets/phpunit-code-quality.php
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
NarrowUnusedSetUpDefinedPropertyRector::class,

// specific asserts

AssertCompareOnCountableWithMethodToAssertCountRector::class,
AssertComparisonToSpecificMethodRector::class,
AssertNotOperatorRector::class,
Expand Down
3 changes: 1 addition & 2 deletions rector.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,13 @@

// object types
StringClassNameToClassConstantRector::class => [
__DIR__ . '/src/Rector/Class_/TestListenerToHooksRector.php',
__DIR__ . '/src/NodeAnalyzer/TestsNodeAnalyzer.php',
__DIR__ . '/config',
__DIR__ . '/src/NodeFinder/DataProviderClassMethodFinder.php',
],
])
->withPhpSets()
->withAttributesSets(all: true)
->withAttributesSets()
->withPreparedSets(
deadCode: true,
codeQuality: true,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

declare(strict_types=1);

namespace Rector\PHPUnit\Tests\CodeQuality\Rector\FuncCall\AssertFuncCallToPHPUnitAssertRector;

use Iterator;
use PHPUnit\Framework\Attributes\DataProvider;
use Rector\Testing\PHPUnit\AbstractRectorTestCase;

final class AssertFuncCallToPHPUnitAssertRectorTest extends AbstractRectorTestCase
{
#[DataProvider('provideData')]
public function test(string $filePath): void
{
$this->doTestFile($filePath);
}

public static function provideData(): Iterator
{
return self::yieldFilesFromDirectory(__DIR__ . '/Fixture');
}

public function provideConfigFilePath(): string
{
return __DIR__ . '/config/configured_rule.php';
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

namespace Rector\PHPUnit\Tests\CodeQuality\Rector\FuncCall\AssertFuncCallToPHPUnitAssertRector\Fixture;

use PHPUnit\Framework\TestCase;

final class AssertBoolInTest extends TestCase
{
public function some($response)
{
assert((bool) $response);
}
}

?>
-----
<?php

namespace Rector\PHPUnit\Tests\CodeQuality\Rector\FuncCall\AssertFuncCallToPHPUnitAssertRector\Fixture;

use PHPUnit\Framework\TestCase;

final class AssertBoolInTest extends TestCase
{
public function some($response)
{
$this->assertTrue((bool) $response);
}
}

?>
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

namespace Rector\PHPUnit\Tests\CodeQuality\Rector\FuncCall\AssertFuncCallToPHPUnitAssertRector\Fixture;

final class AssertCompareContext
{
public function some($response)
{
assert($response == 2.5);
assert($response === 'yes');
}
}

?>
-----
<?php

namespace Rector\PHPUnit\Tests\CodeQuality\Rector\FuncCall\AssertFuncCallToPHPUnitAssertRector\Fixture;

final class AssertCompareContext
{
public function some($response)
{
\PHPUnit\Framework\Assert::assertEquals(2.5, $response);
\PHPUnit\Framework\Assert::assertSame('yes', $response);
}
}

?>
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

namespace Rector\PHPUnit\Tests\CodeQuality\Rector\FuncCall\AssertFuncCallToPHPUnitAssertRector\Fixture;

final class AssertNullCompareContext
{
public function some($response)
{
assert($response !== null);
}
}

?>
-----
<?php

namespace Rector\PHPUnit\Tests\CodeQuality\Rector\FuncCall\AssertFuncCallToPHPUnitAssertRector\Fixture;

final class AssertNullCompareContext
{
public function some($response)
{
\PHPUnit\Framework\Assert::assertNotNull($response);
}
}

?>
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

namespace Rector\PHPUnit\Tests\CodeQuality\Rector\FuncCall\AssertFuncCallToPHPUnitAssertRector\Fixture;

final class MethodExistsContext
{
public function some($response)
{
assert(method_exists($response, 'some_method'));
}
}

?>
-----
<?php

namespace Rector\PHPUnit\Tests\CodeQuality\Rector\FuncCall\AssertFuncCallToPHPUnitAssertRector\Fixture;

final class MethodExistsContext
{
public function some($response)
{
\PHPUnit\Framework\Assert::assertTrue(method_exists($response, 'some_method'));
}
}

?>
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

namespace Rector\PHPUnit\Tests\CodeQuality\Rector\FuncCall\AssertFuncCallToPHPUnitAssertRector\Fixture;

final class SimpleContext
{
public function some($response)
{
assert($response instanceof \DateTime);

assert($response instanceof \DateTimeInterface, 'extra message');
}
}

?>
-----
<?php

namespace Rector\PHPUnit\Tests\CodeQuality\Rector\FuncCall\AssertFuncCallToPHPUnitAssertRector\Fixture;

final class SimpleContext
{
public function some($response)
{
\PHPUnit\Framework\Assert::assertInstanceOf(\DateTime::class, $response);

\PHPUnit\Framework\Assert::assertInstanceOf(\DateTimeInterface::class, $response, 'extra message');
}
}

?>
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

declare(strict_types=1);

use Rector\Config\RectorConfig;
use Rector\PHPUnit\CodeQuality\Rector\FuncCall\AssertFuncCallToPHPUnitAssertRector;

return RectorConfig::configure()
->withRules([AssertFuncCallToPHPUnitAssertRector::class]);
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
<?php

declare(strict_types=1);

namespace Rector\PHPUnit\CodeQuality\Rector\FuncCall;

use PhpParser\Node;
use PhpParser\Node\Arg;
use PhpParser\Node\Expr;
use PhpParser\Node\Expr\BinaryOp\Equal;
use PhpParser\Node\Expr\BinaryOp\Identical;
use PhpParser\Node\Expr\BinaryOp\NotIdentical;
use PhpParser\Node\Expr\Cast\Bool_;
use PhpParser\Node\Expr\ClassConstFetch;
use PhpParser\Node\Expr\FuncCall;
use PhpParser\Node\Expr\Instanceof_;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Expr\StaticCall;
use PhpParser\Node\Expr\Variable;
use PhpParser\Node\Name\FullyQualified;
use PHPStan\Reflection\ClassReflection;
use Rector\PhpParser\Node\Value\ValueResolver;
use Rector\PHPStan\ScopeFetcher;
use Rector\PHPUnit\Enum\AssertMethod;
use Rector\PHPUnit\Enum\PHPUnitClassName;
use Rector\Rector\AbstractRector;
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;

/**
* @see \Rector\PHPUnit\Tests\CodeQuality\Rector\FuncCall\AssertFuncCallToPHPUnitAssertRector\AssertFuncCallToPHPUnitAssertRectorTest
*/
final class AssertFuncCallToPHPUnitAssertRector extends AbstractRector
{
public function __construct(
private readonly ValueResolver $valueResolver
) {
}

public function getRuleDefinition(): RuleDefinition
{
return new RuleDefinition('Turns assert() calls to their explicit PHPUnit assert alternative', [
new CodeSample('assert($value === 100, "message");', '$this->assertSame(100, $value, "message");'),
]);
}

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

/**
* @param FuncCall $node
*/
public function refactor(Node $node): StaticCall|MethodCall|null
{
if ($node->isFirstClassCallable()) {
return null;
}

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

if (! $this->isTestFilePath($node) && ! $this->isBehatContext($node)) {
return null;
}

$comparedExpr = $node->getArgs()[0]
->value;

if ($comparedExpr instanceof Equal) {
$methodName = AssertMethod::ASSERT_EQUALS;
$exprs = [$comparedExpr->right, $comparedExpr->left];

} elseif ($comparedExpr instanceof Identical) {
$methodName = AssertMethod::ASSERT_SAME;
$exprs = [$comparedExpr->right, $comparedExpr->left];

} elseif ($comparedExpr instanceof NotIdentical) {
if ($this->valueResolver->isNull($comparedExpr->right)) {
$methodName = 'assertNotNull';
$exprs = [$comparedExpr->left];
} else {
return null;
}

} elseif ($comparedExpr instanceof Bool_) {
$methodName = 'assertTrue';
$exprs = [$comparedExpr];
} elseif ($comparedExpr instanceof FuncCall) {
if ($this->isName($comparedExpr, 'method_exists')) {
$methodName = 'assertTrue';
$exprs = [$comparedExpr];
} else {
return null;
}
} elseif ($comparedExpr instanceof Instanceof_) {
// outside TestCase
$methodName = 'assertInstanceOf';
$exprs = [];

if ($comparedExpr->class instanceof FullyQualified) {
$classConstFetch = new ClassConstFetch($comparedExpr->class, 'class');
$exprs[] = $classConstFetch;
} else {
return null;
}

$exprs[] = $comparedExpr->expr;
} else {
return null;
}

// is there a comment message
if (isset($node->getArgs()[1])) {
$exprs[] = $node->getArgs()[1]->value;
}

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

private function isBehatContext(FuncCall $funcCall): bool
{
$scope = ScopeFetcher::fetch($funcCall);
if (! $scope->getClassReflection() instanceof ClassReflection) {
return false;
}

$className = $scope->getClassReflection()
->getName();

// special case with static call
return str_ends_with($className, 'Context');
}

private function isTestFilePath(FuncCall $funcCall): bool
{
$scope = ScopeFetcher::fetch($funcCall);
if (! $scope->getClassReflection() instanceof ClassReflection) {
return false;
}

$className = $scope->getClassReflection()
->getName();
if (str_ends_with($className, 'Test')) {
return true;
}
return str_ends_with($className, 'TestCase');
}

/**
* @param Expr[] $exprs
*/
private function createCall(FuncCall $funcCall, string $methodName, array $exprs): MethodCall|StaticCall
{
$args = [];
foreach ($exprs as $expr) {
$args[] = new Arg($expr);
}

if ($this->isBehatContext($funcCall)) {
$assertFullyQualified = new FullyQualified(PHPUnitClassName::ASSERT);
return new StaticCall($assertFullyQualified, $methodName, $args);
}

return new MethodCall(new Variable('this'), $methodName, $args);
}
}
Loading

0 comments on commit 57a5555

Please sign in to comment.