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
6 changes: 6 additions & 0 deletions config/sets/phpunit-code-quality.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@
use Rector\PHPUnit\CodeQuality\Rector\MethodCall\UseSpecificWithMethodRector;
use Rector\PHPUnit\CodeQuality\Rector\MethodCall\WithCallbackIdenticalToStandaloneAssertsRector;
use Rector\PHPUnit\CodeQuality\Rector\StmtsAwareInterface\DeclareStrictTypesTestsRector;
use Rector\PHPUnit\PHPUnit120\Rector\CallLike\CreateStubOverCreateMockArgRector;
use Rector\PHPUnit\PHPUnit120\Rector\ClassMethod\ExpressionCreateMockToCreateStubRector;
use Rector\PHPUnit\PHPUnit60\Rector\MethodCall\GetMockBuilderGetMockToCreateMockRector;
use Rector\PHPUnit\PHPUnit90\Rector\MethodCall\ReplaceAtMethodWithDesiredMatcherRector;
use Rector\Privatization\Rector\Class_\FinalizeTestCaseClassRector;
Expand Down Expand Up @@ -125,6 +127,10 @@
SimplerWithIsInstanceOfRector::class,
DirectInstanceOverMockArgRector::class,

// stub over mock
CreateStubOverCreateMockArgRector::class,
ExpressionCreateMockToCreateStubRector::class,

// @test first, enable later
// \Rector\PHPUnit\CodeQuality\Rector\Expression\ConfiguredMockEntityToSetterObjectRector::class,

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php

namespace Rector\PHPUnit\Tests\PHPUnit120\Rector\CallLike\CreateStubOverCreateMockArgRector\Fixture;

use PHPUnit\Framework\TestCase;

final class HandleAssignedVariable extends TestCase
{
public function testThat()
{
$itemMock = $this->createMock(\stdClass::class);

$items = [
$itemMock
];
}
}

?>
-----
<?php

namespace Rector\PHPUnit\Tests\PHPUnit120\Rector\CallLike\CreateStubOverCreateMockArgRector\Fixture;

use PHPUnit\Framework\TestCase;

final class HandleAssignedVariable extends TestCase
{
public function testThat()
{
$itemMock = $this->createStub(\stdClass::class);

$items = [
$itemMock
];
}
}

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

namespace Rector\PHPUnit\Tests\PHPUnit120\Rector\CallLike\CreateStubOverCreateMockArgRector\Fixture;

use PHPUnit\Framework\TestCase;

final class HandleAssignedVariableMultipleTimes extends TestCase
{
public function testThat()
{
$itemMock = $this->createMock(\stdClass::class);

$items = [
$itemMock
];

$anotherValue = new \stdClass($itemMock);
}
}

?>
-----
<?php

namespace Rector\PHPUnit\Tests\PHPUnit120\Rector\CallLike\CreateStubOverCreateMockArgRector\Fixture;

use PHPUnit\Framework\TestCase;

final class HandleAssignedVariableMultipleTimes extends TestCase
{
public function testThat()
{
$itemMock = $this->createStub(\stdClass::class);

$items = [
$itemMock
];

$anotherValue = new \stdClass($itemMock);
}
}

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

declare(strict_types=1);

namespace Rector\PHPUnit\Tests\PHPUnit120\Rector\ClassMethod\ExpressionCreateMockToCreateStubRector\Fixture;

use PHPUnit\Framework\TestCase;
use Rector\PHPUnit\Tests\PHPUnit120\Rector\ClassMethod\ExpressionCreateMockToCreateStubRector\Source\ClassWithDependency;

final class HandleUsedOutsideArg extends TestCase
{
public function test()
{
$mock = $this->createMock(\stdClass::class);

if ($mock instanceof \stdClass) {
// do something
}

$someObject = new ClassWithDependency($mock);
$this->assertSame($mock, $someObject->getDependency());
}
}

?>
-----
<?php

declare(strict_types=1);

namespace Rector\PHPUnit\Tests\PHPUnit120\Rector\ClassMethod\ExpressionCreateMockToCreateStubRector\Fixture;

use PHPUnit\Framework\TestCase;
use Rector\PHPUnit\Tests\PHPUnit120\Rector\ClassMethod\ExpressionCreateMockToCreateStubRector\Source\ClassWithDependency;

final class HandleUsedOutsideArg extends TestCase
{
public function test()
{
$mock = $this->createStub(\stdClass::class);

if ($mock instanceof \stdClass) {
// do something
}

$someObject = new ClassWithDependency($mock);
$this->assertSame($mock, $someObject->getDependency());
}
}

?>

This file was deleted.

2 changes: 1 addition & 1 deletion rules/CodeQuality/NodeAnalyser/AssertMethodAnalyzer.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public function detectTestCaseCall(MethodCall|StaticCall $call): bool
? $call->var
: $call->class;

if (! $this->nodeTypeResolver->isObjectType($objectCaller, new ObjectType('PHPUnit\Framework\TestCase'))) {
if (! $this->nodeTypeResolver->isObjectType($objectCaller, new ObjectType(PHPUnitClassName::TEST_CASE))) {
return false;
}

Expand Down
61 changes: 61 additions & 0 deletions rules/CodeQuality/NodeAnalyser/MockObjectExprDetector.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<?php

declare(strict_types=1);

namespace Rector\PHPUnit\CodeQuality\NodeAnalyser;

use PhpParser\Node\Expr;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Expr\Variable;
use PhpParser\Node\Stmt\ClassMethod;
use Rector\NodeNameResolver\NodeNameResolver;
use Rector\PhpParser\Node\BetterNodeFinder;
use Rector\PHPUnit\CodeQuality\NodeFinder\VariableFinder;

final readonly class MockObjectExprDetector
{
public function __construct(
private BetterNodeFinder $betterNodeFinder,
private NodeNameResolver $nodeNameResolver,
private VariableFinder $variableFinder,
) {
}

public function isUsedForMocking(Expr $expr, ClassMethod $classMethod): bool
{
if (! $expr instanceof Variable) {
return false;
}

$variableName = $this->nodeNameResolver->getName($expr);

// to be safe
if ($variableName === null) {
return true;
}

$relatedVariables = $this->variableFinder->find($classMethod, $variableName);

// only self variable found, nothing to mock
if (count($relatedVariables) === 1) {
return false;
}

// find out, how many are used in call likes as args
/** @var array<Expr\MethodCall> $methodCalls */
$methodCalls = $this->betterNodeFinder->findInstancesOfScoped((array) $classMethod->stmts, [MethodCall::class]);

foreach ($methodCalls as $methodCall) {
if (! $methodCall->var instanceof Variable) {
continue;
}

if ($this->nodeNameResolver->isName($methodCall->var, $variableName)) {
// variable is being called on, most like mocking, lets skip
return true;
}
}

return false;
}
}
103 changes: 88 additions & 15 deletions rules/PHPUnit120/Rector/CallLike/CreateStubOverCreateMockArgRector.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,18 @@

use PhpParser\Node;
use PhpParser\Node\ArrayItem;
use PhpParser\Node\Expr;
use PhpParser\Node\Expr\Assign;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Expr\New_;
use PhpParser\Node\Expr\StaticCall;
use PhpParser\Node\Identifier;
use PhpParser\Node\Stmt\ClassMethod;
use PhpParser\Node\Stmt\Expression;
use Rector\PHPStan\ScopeFetcher;
use Rector\PHPUnit\CodeQuality\NodeAnalyser\MockObjectExprDetector;
use Rector\PHPUnit\Enum\PHPUnitClassName;
use Rector\PHPUnit\NodeAnalyzer\TestsNodeAnalyzer;
use Rector\Rector\AbstractRector;
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
Expand All @@ -23,6 +29,12 @@
*/
final class CreateStubOverCreateMockArgRector extends AbstractRector
{
public function __construct(
private readonly TestsNodeAnalyzer $testsNodeAnalyzer,
private readonly MockObjectExprDetector $mockObjectExprDetector,
) {
}

public function getRuleDefinition(): RuleDefinition
{
return new RuleDefinition(
Expand Down Expand Up @@ -70,13 +82,13 @@ private function someMethod($someClass)
*/
public function getNodeTypes(): array
{
return [StaticCall::class, MethodCall::class, New_::class, ArrayItem::class];
return [StaticCall::class, MethodCall::class, New_::class, ArrayItem::class, ClassMethod::class];
}

/**
* @param MethodCall|StaticCall|New_|ArrayItem $node
* @param MethodCall|StaticCall|New_|ArrayItem|ClassMethod $node
*/
public function refactor(Node $node): MethodCall|StaticCall|New_|ArrayItem|null
public function refactor(Node $node): MethodCall|StaticCall|New_|ArrayItem|ClassMethod|null
{
$scope = ScopeFetcher::fetch($node);
if (! $scope->isInClass()) {
Expand All @@ -88,19 +100,12 @@ public function refactor(Node $node): MethodCall|StaticCall|New_|ArrayItem|null
return null;
}

if ($node instanceof ArrayItem) {
if (! $node->value instanceof MethodCall) {
return null;
}

$methodCall = $node->value;
if (! $this->isName($methodCall->name, 'createMock')) {
return null;
}

$methodCall->name = new Identifier('createStub');
if ($node instanceof ClassMethod) {
return $this->refactorClassMethod($node);
}

return $node;
if ($node instanceof ArrayItem) {
return $this->refactorArrayItem($node);
}

$hasChanges = false;
Expand Down Expand Up @@ -129,4 +134,72 @@ public function refactor(Node $node): MethodCall|StaticCall|New_|ArrayItem|null

return null;
}

private function matchCreateMockMethodCall(Expr $expr): null|MethodCall
{
if (! $expr instanceof MethodCall) {
return null;
}

if (! $this->isName($expr->name, 'createMock')) {
return null;
}

return $expr;
}

private function refactorClassMethod(ClassMethod $classMethod): ?ClassMethod
{
if (! $this->testsNodeAnalyzer->isTestClassMethod($classMethod)) {
return null;
}

$hasChanged = false;
foreach ((array) $classMethod->stmts as $stmt) {
if (! $stmt instanceof Expression) {
continue;
}

if (! $stmt->expr instanceof Assign) {
continue;
}

$assign = $stmt->expr;
$createMockMethodCall = $this->matchCreateMockMethodCall($assign->expr);

if (! $createMockMethodCall instanceof MethodCall) {
continue;
}

// no change, as we use the variable for mocking later
if ($this->mockObjectExprDetector->isUsedForMocking($assign->var, $classMethod)) {
continue;
}

$createMockMethodCall->name = new Identifier('createStub');
$hasChanged = true;
}

if ($hasChanged) {
return $classMethod;
}

return null;
}

private function refactorArrayItem(ArrayItem $arrayItem): ?ArrayItem
{
if (! $arrayItem->value instanceof MethodCall) {
return null;
}

$methodCall = $arrayItem->value;
if (! $this->isName($methodCall->name, 'createMock')) {
return null;
}

$methodCall->name = new Identifier('createStub');

return $arrayItem;
}
}
Loading