Migrate at to with consecutive and will return on consecutive calls (#5822)

This commit is contained in:
Dominik Peters 2021-03-18 00:11:23 +01:00 committed by GitHub
parent d14630e1d2
commit dc94477d54
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 1529 additions and 8 deletions

View File

@ -72,7 +72,7 @@
- [PHPOffice](#phpoffice) (14)
- [PHPUnit](#phpunit) (38)
- [PHPUnit](#phpunit) (39)
- [PSR4](#psr4) (2)
@ -9181,6 +9181,21 @@ Turns getMock*() methods to `createMock()`
<br>
### MigrateAtToConsecutiveExpectationRector
Migrates deprecated `$this->expects($this->at(n))` expectations to `withConsecutive` and `willReturnOnConsecutiveCalls`
- class: [`Rector\PHPUnit\Rector\ClassMethod\MigrateAtToConsecutiveExpectationsRector`](../rules/PHPUnit/Rector/ClassMethod/MigrateAtToConsecutiveExpectationsRector.php)
```diff
$mock = $this->createMock(Foo::class);
-$mock->expects($this->at(0))->with('0')->method('someMethod')->willReturn('1');
-$mock->expects($this->at(1))->with('1')->method('someMethod')->willReturn('2');
+$mock->method('someMethod')->withConsecutive(['0'], ['1'])->willReturnOnConsecutiveCalls('1', '2');
```
<br>
### RemoveDataProviderTestPrefixRector
Data provider methods cannot start with "test" prefix
@ -15068,7 +15083,7 @@ return static function (ContainerConfigurator $containerConfigurator): void {
->call('configure', [[
MergeInterfacesRector::OLD_TO_NEW_INTERFACES => [
'SomeOldInterface' => 'SomeInterface',
], ]]);
};
```
@ -15139,7 +15154,7 @@ return static function (ContainerConfigurator $containerConfigurator): void {
->call('configure', [[
MethodCallToPropertyFetchRector::METHOD_CALL_TO_PROPERTY_FETCHES => [
'someMethod' => 'someProperty',
], ]]);
};
```
@ -15178,7 +15193,7 @@ return static function (ContainerConfigurator $containerConfigurator): void {
->call('configure', [[
MethodCallToReturnRector::METHOD_CALL_WRAPS => [
'SomeClass' => ['deny'],
], ]]);
};
```
@ -15470,7 +15485,7 @@ return static function (ContainerConfigurator $containerConfigurator): void {
->call('configure', [[
ParentClassToTraitsRector::PARENT_CLASS_TO_TRAITS => [
'Nette\Object' => ['Nette\SmartObject'],
], ]]);
};
```
@ -15942,7 +15957,7 @@ return static function (ContainerConfigurator $containerConfigurator): void {
->call('configure', [[
ToStringToMethodCallRector::METHOD_NAMES_BY_TYPE => [
'SomeObject' => 'getPath',
], ]]);
};
```
@ -16671,8 +16686,8 @@ return static function (ContainerConfigurator $containerConfigurator): void {
ChangePropertyVisibilityRector::PROPERTY_TO_VISIBILITY_BY_CLASS => [
'FrameworkClass' => [
'someProperty' => 2,
], ], ]]);
};
```

View File

@ -0,0 +1,74 @@
<?php
namespace Rector\Tests\PHPUnit\Rector\ClassMethod\MigrateAtToConsecutiveExpectationsRector\Fixture;
class Foo
{
public function someMethod()
{
}
}
final class DifferentReturnValues extends \PHPUnit\Framework\TestCase
{
public function test(): void
{
$mock = $this->createMock(Foo::class);
$mock
->expects($this->at(0))
->method('someMethod')
->willReturn('1');
$reference = '2';
$mock
->expects($this->at(1))
->method('someMethod')
->willReturnReference($reference);
$mock
->expects($this->at(2))
->method('someMethod')
->willReturnMap(['foo' => 'bar']);
$mock
->expects($this->at(3))
->method('someMethod')
->willReturnArgument(1);
$mock
->expects($this->at(4))
->method('someMethod')
->willReturnCallback(static function () {
return null;
});
$mock
->expects($this->at(5))
->method('someMethod')
->willReturnSelf();
$mock
->expects($this->at(6))
->method('someMethod')
->willThrowException(new \Exception());
}
}
?>
-----
<?php
namespace Rector\Tests\PHPUnit\Rector\ClassMethod\MigrateAtToConsecutiveExpectationsRector\Fixture;
class Foo
{
public function someMethod()
{
}
}
final class DifferentReturnValues extends \PHPUnit\Framework\TestCase
{
public function test(): void
{
$mock = $this->createMock(Foo::class);
$reference = '2';
$mock->method('someMethod')->willReturnOnConsecutiveCalls('1', new \PHPUnit\Framework\MockObject\Stub\ReturnReference($reference), $this->returnValueMap(['foo' => 'bar']), $this->returnArgument(1), $this->returnCallback(static function () {
return null;
}), $this->returnSelf(), $this->throwException(new \Exception()));
}
}
?>

View File

@ -0,0 +1,22 @@
<?php
namespace Rector\Tests\PHPUnit\Rector\ClassMethod\MigrateAtToConsecutiveExpectationsRector\Fixture;
class Foo
{
public function someMethod()
{
}
}
final class ExpectsNonAtIsSkipped extends \PHPUnit\Framework\TestCase
{
public function test(): void
{
$mock = $this->createMock(Foo::class);
$mock
->expects($this->exactly(1))
->method('someMethod');
}
}
?>

View File

@ -0,0 +1,65 @@
<?php
namespace Rector\Tests\PHPUnit\Rector\ClassMethod\MigrateAtToConsecutiveExpectationsRector\Fixture;
class Foo
{
public function someMethod()
{
}
}
final class HandleMultipleVariables extends \PHPUnit\Framework\TestCase
{
public function test(): void
{
$mock = $this->createMock(Foo::class);
$mock
->expects($this->at(0))
->with('0')
->method('someMethod')
->willReturn('1');
$mock
->expects($this->at(1))
->with('1')
->method('someMethod')
->willReturn('2');
$mock2 = $this->createMock(Foo::class);
$mock2
->expects($this->at(0))
->with('0')
->method('someMethod')
->willReturn('1');
$mock2
->expects($this->at(1))
->with('1')
->method('someMethod')
->willReturn('2');
}
}
?>
-----
<?php
namespace Rector\Tests\PHPUnit\Rector\ClassMethod\MigrateAtToConsecutiveExpectationsRector\Fixture;
class Foo
{
public function someMethod()
{
}
}
final class HandleMultipleVariables extends \PHPUnit\Framework\TestCase
{
public function test(): void
{
$mock = $this->createMock(Foo::class);
$mock->method('someMethod')->withConsecutive(['0'], ['1'])->willReturnOnConsecutiveCalls('1', '2');
$mock2 = $this->createMock(Foo::class);
$mock2->method('someMethod')->withConsecutive(['0'], ['1'])->willReturnOnConsecutiveCalls('1', '2');
}
}
?>

View File

@ -0,0 +1,48 @@
<?php
namespace Rector\Tests\PHPUnit\Rector\ClassMethod\MigrateAtToConsecutiveExpectationsRector\Fixture;
class Foo
{
public function someMethod()
{
}
}
final class MissingWithIsReplacedByEmptyArrayIfNoReturnExpectation extends \PHPUnit\Framework\TestCase
{
public function test(): void
{
$mock = $this->createMock(Foo::class);
$mock
->expects($this->at(0))
->with('0')
->method('someMethod');
$mock
->expects($this->at(2))
->with('2')
->method('someMethod');
}
}
?>
-----
<?php
namespace Rector\Tests\PHPUnit\Rector\ClassMethod\MigrateAtToConsecutiveExpectationsRector\Fixture;
class Foo
{
public function someMethod()
{
}
}
final class MissingWithIsReplacedByEmptyArrayIfNoReturnExpectation extends \PHPUnit\Framework\TestCase
{
public function test(): void
{
$mock = $this->createMock(Foo::class);
$mock->method('someMethod')->withConsecutive(['0'], [], ['2']);
}
}
?>

View File

@ -0,0 +1,48 @@
<?php
namespace Rector\Tests\PHPUnit\Rector\ClassMethod\MigrateAtToConsecutiveExpectationsRector\Fixture;
class Foo
{
public function someMethod()
{
}
}
final class MissingWithIsReplacedByEmptyArrayIfNoReturnExpectation2 extends \PHPUnit\Framework\TestCase
{
public function test(): void
{
$mock = $this->createMock(Foo::class);
$mock
->expects($this->at(1))
->with('1')
->method('someMethod');
$mock
->expects($this->at(2))
->with('2')
->method('someMethod');
}
}
?>
-----
<?php
namespace Rector\Tests\PHPUnit\Rector\ClassMethod\MigrateAtToConsecutiveExpectationsRector\Fixture;
class Foo
{
public function someMethod()
{
}
}
final class MissingWithIsReplacedByEmptyArrayIfNoReturnExpectation2 extends \PHPUnit\Framework\TestCase
{
public function test(): void
{
$mock = $this->createMock(Foo::class);
$mock->method('someMethod')->withConsecutive([], ['1'], ['2']);
}
}
?>

View File

@ -0,0 +1,50 @@
<?php
namespace Rector\Tests\PHPUnit\Rector\ClassMethod\MigrateAtToConsecutiveExpectationsRector\Fixture;
class Foo
{
public function someMethod()
{
}
}
final class Mixed extends \PHPUnit\Framework\TestCase
{
public function test(): void
{
$mock = $this->createMock(Foo::class);
$mock
->expects($this->at(0))
->with(0)
->method('someMethod')
->willReturn('0');
$mock
->expects($this->at(1))
->with(1)
->method('someMethod')
->willReturn('1');
}
}
?>
-----
<?php
namespace Rector\Tests\PHPUnit\Rector\ClassMethod\MigrateAtToConsecutiveExpectationsRector\Fixture;
class Foo
{
public function someMethod()
{
}
}
final class Mixed extends \PHPUnit\Framework\TestCase
{
public function test(): void
{
$mock = $this->createMock(Foo::class);
$mock->method('someMethod')->withConsecutive([0], [1])->willReturnOnConsecutiveCalls('0', '1');
}
}
?>

View File

@ -0,0 +1,56 @@
<?php
namespace Rector\Tests\PHPUnit\Rector\ClassMethod\MigrateAtToConsecutiveExpectationsRector\Fixture;
class Foo
{
public function someMethod()
{
}
}
final class Mixed2 extends \PHPUnit\Framework\TestCase
{
public function test(): void
{
$mock = $this->createMock(Foo::class);
$mock
->expects($this->exactly(2))
->method('someMethod');
$mock
->expects($this->at(0))
->with(0)
->method('someMethod')
->willReturn('0');
$mock
->expects($this->at(1))
->with(1)
->method('someMethod')
->willReturn('1');
}
}
?>
-----
<?php
namespace Rector\Tests\PHPUnit\Rector\ClassMethod\MigrateAtToConsecutiveExpectationsRector\Fixture;
class Foo
{
public function someMethod()
{
}
}
final class Mixed2 extends \PHPUnit\Framework\TestCase
{
public function test(): void
{
$mock = $this->createMock(Foo::class);
$mock
->expects($this->exactly(2))
->method('someMethod');
$mock->method('someMethod')->withConsecutive([0], [1])->willReturnOnConsecutiveCalls('0', '1');
}
}
?>

View File

@ -0,0 +1,39 @@
<?php
namespace Rector\Tests\PHPUnit\Rector\ClassMethod\MigrateAtToConsecutiveExpectationsRector\Fixture;
class Foo
{
public function someMethod()
{
}
public function someOtherMethod()
{
}
}
final class SkipDifferentMethodExpectations extends \PHPUnit\Framework\TestCase
{
public function test(): void
{
$mock = $this->createMock(Foo::class);
$mock
->expects($this->at(0))
->method('someMethod')
->willReturn('1');
$mock
->expects($this->at(1))
->with('1')
->method('someOtherMethod');
$mock
->expects($this->at(2))
->with('2')
->method('someMethod')
->willReturn('3');
}
}
?>

View File

@ -0,0 +1,37 @@
<?php
namespace Rector\Tests\PHPUnit\Rector\ClassMethod\MigrateAtToConsecutiveExpectationsRector\Fixture;
class Foo
{
public function someMethod()
{
}
public function someOtherMethod()
{
}
}
final class SkipMissingReturnExpectation extends \PHPUnit\Framework\TestCase
{
public function test(): void
{
$mock = $this->createMock(Foo::class);
$mock
->expects($this->at(0))
->method('someMethod')
->willReturn('1');
$mock
->expects($this->at(1))
->method('someMethod');
$mock
->expects($this->at(2))
->method('someMethod')
->willReturn('3');
}
}
?>

View File

@ -0,0 +1,30 @@
<?php
namespace Rector\Tests\PHPUnit\Rector\ClassMethod\MigrateAtToConsecutiveExpectationsRector\Fixture;
class Foo
{
public function someMethod()
{
}
}
final class SkipMissingWithIfReturnExpectationsExist extends \PHPUnit\Framework\TestCase
{
public function test(): void
{
$mock = $this->createMock(Foo::class);
$mock
->expects($this->at(0))
->with('0')
->method('someMethod')
->willReturn('1');
$mock
->expects($this->at(2))
->with('2')
->method('someMethod')
->willReturn('3');
}
}
?>

View File

@ -0,0 +1,48 @@
<?php
namespace Rector\Tests\PHPUnit\Rector\ClassMethod\MigrateAtToConsecutiveExpectationsRector\Fixture;
class Foo
{
public function someMethod()
{
}
}
final class WillCallbacksAreKept extends \PHPUnit\Framework\TestCase
{
public function test(): void
{
$mock = $this->createMock(Foo::class);
$mock
->expects($this->at(0))
->method('someMethod')
->will($this->throwException(new \Exception()));
$mock
->expects($this->at(1))
->method('someMethod')
->will($this->returnValue('1'));
}
}
?>
-----
<?php
namespace Rector\Tests\PHPUnit\Rector\ClassMethod\MigrateAtToConsecutiveExpectationsRector\Fixture;
class Foo
{
public function someMethod()
{
}
}
final class WillCallbacksAreKept extends \PHPUnit\Framework\TestCase
{
public function test(): void
{
$mock = $this->createMock(Foo::class);
$mock->method('someMethod')->willReturnOnConsecutiveCalls($this->throwException(new \Exception()), $this->returnValue('1'));
}
}
?>

View File

@ -0,0 +1,56 @@
<?php
namespace Rector\Tests\PHPUnit\Rector\ClassMethod\MigrateAtToConsecutiveExpectationsRector\Fixture;
class Foo
{
public function someMethod()
{
}
}
final class WillReturnOnly extends \PHPUnit\Framework\TestCase
{
public function test(): void
{
$mock = $this->createMock(Foo::class);
$mock
->expects($this->at(0))
->method('someMethod')
->willReturn('0');
$mock
->expects($this->at(1))
->method('someMethod')
->willReturn('1');
$mock
->expects($this->at(2))
->method('someMethod')
->willReturn('2');
$mock
->expects($this->at(3))
->method('someMethod')
->willReturn(null);
}
}
?>
-----
<?php
namespace Rector\Tests\PHPUnit\Rector\ClassMethod\MigrateAtToConsecutiveExpectationsRector\Fixture;
class Foo
{
public function someMethod()
{
}
}
final class WillReturnOnly extends \PHPUnit\Framework\TestCase
{
public function test(): void
{
$mock = $this->createMock(Foo::class);
$mock->method('someMethod')->willReturnOnConsecutiveCalls('0', '1', '2', null);
}
}
?>

View File

@ -0,0 +1,44 @@
<?php
namespace Rector\Tests\PHPUnit\Rector\ClassMethod\MigrateAtToConsecutiveExpectationsRector\Fixture;
class Foo
{
public function someMethod()
{
}
}
final class WithMultipleArguments extends \PHPUnit\Framework\TestCase
{
public function test(): void
{
$mock = $this->createMock(Foo::class);
$mock
->expects($this->at(0))
->with('0', '1')
->method('someMethod');
}
}
?>
-----
<?php
namespace Rector\Tests\PHPUnit\Rector\ClassMethod\MigrateAtToConsecutiveExpectationsRector\Fixture;
class Foo
{
public function someMethod()
{
}
}
final class WithMultipleArguments extends \PHPUnit\Framework\TestCase
{
public function test(): void
{
$mock = $this->createMock(Foo::class);
$mock->method('someMethod')->withConsecutive(['0', '1']);
}
}
?>

View File

@ -0,0 +1,56 @@
<?php
namespace Rector\Tests\PHPUnit\Rector\ClassMethod\MigrateAtToConsecutiveExpectationsRector\Fixture;
class Foo
{
public function someMethod()
{
}
}
final class WithOnly extends \PHPUnit\Framework\TestCase
{
public function test(): void
{
$mock = $this->createMock(Foo::class);
$mock
->expects($this->at(0))
->with('0')
->method('someMethod');
$mock
->expects($this->at(1))
->with('1')
->method('someMethod');
$mock
->expects($this->at(2))
->with('2')
->method('someMethod');
$mock
->expects($this->at(3))
->with(null)
->method('someMethod');
}
}
?>
-----
<?php
namespace Rector\Tests\PHPUnit\Rector\ClassMethod\MigrateAtToConsecutiveExpectationsRector\Fixture;
class Foo
{
public function someMethod()
{
}
}
final class WithOnly extends \PHPUnit\Framework\TestCase
{
public function test(): void
{
$mock = $this->createMock(Foo::class);
$mock->method('someMethod')->withConsecutive(['0'], ['1'], ['2'], [null]);
}
}
?>

View File

@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace Rector\Tests\PHPUnit\Rector\ClassMethod\MigrateAtToConsecutiveExpectationsRector;
use Iterator;
use Rector\PHPUnit\Rector\ClassMethod\MigrateAtToConsecutiveExpectationsRector;
use Rector\Testing\PHPUnit\AbstractRectorTestCase;
use Symplify\SmartFileSystem\SmartFileInfo;
final class MigrateAtToConsecutiveExpectationsRectorTest extends AbstractRectorTestCase
{
/**
* @dataProvider provideData()
*/
public function test(SmartFileInfo $fileInfo): void
{
$this->doTestFileInfo($fileInfo);
}
public function provideData(): Iterator
{
return $this->yieldFilesFromDirectory(__DIR__ . '/Fixture');
}
protected function getRectorClass(): string
{
return MigrateAtToConsecutiveExpectationsRector::class;
}
}

View File

@ -0,0 +1,158 @@
<?php
declare(strict_types=1);
namespace Rector\PHPUnit\NodeAnalyzer;
use PhpParser\Node\Arg;
use PhpParser\Node\Expr;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Expr\Variable;
use PhpParser\Node\Scalar\LNumber;
use PhpParser\Node\Stmt\Expression;
use Rector\PHPUnit\NodeFactory\ConsecutiveAssertionFactory;
use Rector\PHPUnit\ValueObject\ExpectationMock;
use Rector\PHPUnit\ValueObject\ExpectationMockCollection;
final class ExpectationAnalyzer
{
private const PROCESSABLE_WILL_STATEMENTS = [
'will',
'willReturn',
'willReturnReference',
'willReturnMap',
'willReturnArgument',
'willReturnCallback',
'willReturnSelf',
'willThrowException',
];
/**
* @var TestsNodeAnalyzer
*/
private $testsNodeAnalyzer;
/**
* @var ConsecutiveAssertionFactory
*/
private $consecutiveAssertionFactory;
public function __construct(TestsNodeAnalyzer $testsNodeAnalyzer, ConsecutiveAssertionFactory $consecutiveAssertionFactory)
{
$this->testsNodeAnalyzer = $testsNodeAnalyzer;
$this->consecutiveAssertionFactory = $consecutiveAssertionFactory;
}
/**
* @param Expression[] $stmts
*/
public function getExpectationsFromExpressions(array $stmts): ExpectationMockCollection
{
$expectationMockCollection = new ExpectationMockCollection();
foreach ($stmts as $stmt) {
/** @var MethodCall $expr */
$expr = $stmt->expr;
$method = $this->getMethod($expr);
if (!$this->testsNodeAnalyzer->isPHPUnitMethodName($method, 'method')) {
continue;
}
/** @var MethodCall $expects */
$expects = $this->getExpects($method->var, $method);
if (!$this->isValidExpectsCall($expects)) {
continue;
}
$expectsArg = $expects->args[0];
/** @var MethodCall $expectsValue */
$expectsValue = $expectsArg->value;
if (!$this->isValidAtCall($expectsValue)) {
continue;
}
$atArg = $expectsValue->args[0];
$atValue = $atArg->value;
if ($atValue instanceof LNumber && $expects->var instanceof Variable) {
$expectationMockCollection->add(
new ExpectationMock(
$expects->var,
$method->args,
$atValue->value,
$this->getWill($expr),
$this->getWithArgs($method->var),
$stmt
)
);
}
}
return $expectationMockCollection;
}
private function getMethod(MethodCall $expr): MethodCall
{
if ($this->testsNodeAnalyzer->isPHPUnitMethodNames($expr, self::PROCESSABLE_WILL_STATEMENTS) && $expr->var instanceof MethodCall) {
return $expr->var;
}
return $expr;
}
private function getWill(MethodCall $expr): ?Expr
{
if (!$this->testsNodeAnalyzer->isPHPUnitMethodNames($expr, self::PROCESSABLE_WILL_STATEMENTS)) {
return null;
}
return $this->consecutiveAssertionFactory->createWillReturn($expr);
}
private function getExpects(Expr $maybeWith, MethodCall $method): Expr
{
if ($this->testsNodeAnalyzer->isPHPUnitMethodName($maybeWith, 'with') && $maybeWith instanceof MethodCall) {
return $maybeWith->var;
}
return $method->var;
}
/**
* @return array<int, Expr|null>
*/
private function getWithArgs(Expr $maybeWith): array
{
if ($this->testsNodeAnalyzer->isPHPUnitMethodName($maybeWith, 'with') && $maybeWith instanceof MethodCall) {
return array_map(static function (Arg $arg) {
return $arg->value;
}, $maybeWith->args);
}
return [null];
}
public function isValidExpectsCall(MethodCall $expr): bool
{
if (!$this->testsNodeAnalyzer->isPHPUnitMethodName($expr, 'expects')) {
return false;
}
if (count($expr->args) !== 1) {
return false;
}
return true;
}
public function isValidAtCall(MethodCall $expr): bool
{
if (!$this->testsNodeAnalyzer->isPHPUnitMethodName($expr, 'at')) {
return false;
}
if (count($expr->args) !== 1) {
return false;
}
return true;
}
}

View File

@ -0,0 +1,197 @@
<?php
declare(strict_types=1);
namespace Rector\PHPUnit\NodeFactory;
use PhpParser\Node\Arg;
use PhpParser\Node\Expr;
use PhpParser\Node\Expr\ArrayItem;
use PhpParser\Node\Expr\ConstFetch;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Expr\Variable;
use PhpParser\Node\Identifier;
use PhpParser\Node\Name;
use PhpParser\Node\Name\FullyQualified;
use Rector\PHPUnit\ValueObject\ExpectationMock;
use Rector\PHPUnit\ValueObject\ExpectationMockCollection;
final class ConsecutiveAssertionFactory
{
private const REPLACE_WILL_MAP = [
'willReturnMap' => 'returnValueMap',
'willReturnArgument' => 'returnArgument',
'willReturnCallback' => 'returnCallback',
'willThrowException' => 'throwException',
];
public function createAssertionFromExpectationMockCollection(ExpectationMockCollection $expectationMockCollection): MethodCall
{
$expectationMocks = $expectationMockCollection->getExpectationMocks();
$var = $expectationMocks[0]->getExpectationVariable();
$methodArguments = $expectationMocks[0]->getMethodArguments();
$expectationMocks = $this->sortExpectationMocksByIndex($expectationMocks);
if (!$expectationMockCollection->hasReturnValues()) {
return $this->createWithConsecutive(
$this->createMethod(
$var,
$methodArguments
),
$this->createWithArgs($expectationMocks)
);
}
if ($expectationMockCollection->hasWithValues()) {
return $this->createWillReturnOnConsecutiveCalls(
$this->createWithConsecutive(
$this->createMethod(
$var,
$methodArguments
),
$this->createWithArgs($expectationMocks)
),
$this->createReturnArgs($expectationMocks)
);
}
return $this->createWillReturnOnConsecutiveCalls(
$this->createMethod(
$var,
$methodArguments
),
$this->createReturnArgs($expectationMocks)
);
}
/**
* @param ExpectationMock[] $expectationMocks
* @return Arg[]
*/
private function createReturnArgs(array $expectationMocks): array
{
return array_map(static function (ExpectationMock $expectationMock) {
return new Arg($expectationMock->getReturn() ?: new ConstFetch(new Name('null')));
}, $expectationMocks);
}
/**
* @param ExpectationMock[] $expectationMocks
* @return Arg[]
*/
private function createWithArgs(array $expectationMocks): array
{
return array_map(static function (ExpectationMock $expectationMock) {
$arrayItems = array_map(static function (?Expr $expr) {
return new ArrayItem($expr ?: new ConstFetch(new Name('null')));
}, $expectationMock->getWithArguments());
return new Arg(
new Expr\Array_(
$arrayItems
)
);
}, $expectationMocks);
}
/**
* @param Arg[] $args
*/
public function createWillReturnOnConsecutiveCalls(Expr $expr, array $args): MethodCall
{
return $this->createMethodCall($expr, 'willReturnOnConsecutiveCalls', $args);
}
/**
* @param Arg[] $args
*/
public function createMethod(Expr $expr, array $args): MethodCall
{
return $this->createMethodCall($expr, 'method', $args);
}
/**
* @param Arg[] $args
*/
public function createWithConsecutive(Expr $expr, array $args): MethodCall
{
return $this->createMethodCall($expr, 'withConsecutive', $args);
}
public function createWillReturn(MethodCall $methodCall): Expr
{
if (!$methodCall->name instanceof Identifier) {
return $methodCall;
}
$methodCallName = $methodCall->name->name;
if ($methodCallName === 'will') {
return $methodCall->args[0]->value;
}
if ($methodCallName === 'willReturnSelf') {
return $this->createWillReturnSelf();
}
if ($methodCallName === 'willReturnReference') {
return $this->createWillReturnReference($methodCall);
}
if (array_key_exists($methodCallName, self::REPLACE_WILL_MAP)) {
return $this->createMappedWillReturn($methodCallName, $methodCall);
}
return $methodCall->args[0]->value;
}
private function createWillReturnSelf(): MethodCall
{
return $this->createMethodCall(
new Variable('this'),
'returnSelf',
[]
);
}
private function createWillReturnReference(MethodCall $methodCall): Expr\New_
{
return new Expr\New_(
new FullyQualified('PHPUnit\Framework\MockObject\Stub\ReturnReference'),
[new Arg($methodCall->args[0]->value)]
);
}
private function createMappedWillReturn(string $methodCallName, MethodCall $methodCall): MethodCall
{
return $this->createMethodCall(
new Variable('this'),
self::REPLACE_WILL_MAP[$methodCallName],
[new Arg($methodCall->args[0]->value)]
);
}
/**
* @param Arg[] $args
*/
private function createMethodCall(Expr $expr, string $name, array $args): MethodCall
{
return new MethodCall(
$expr,
new Identifier($name),
$args
);
}
/**
* @param ExpectationMock[] $expectationMocks
* @return ExpectationMock[]
*/
private function sortExpectationMocksByIndex(array $expectationMocks): array
{
usort($expectationMocks, static function (ExpectationMock $expectationMockA, ExpectationMock $expectationMockB) {
return $expectationMockA->getIndex() > $expectationMockB->getIndex() ? 1 : -1;
});
return $expectationMocks;
}
}

View File

@ -0,0 +1,221 @@
<?php
declare(strict_types=1);
namespace Rector\PHPUnit\Rector\ClassMethod;
use PhpParser\Node;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Stmt;
use PhpParser\Node\Stmt\ClassMethod;
use PhpParser\Node\Stmt\Expression;
use Rector\Core\Rector\AbstractRector;
use Rector\PHPUnit\NodeAnalyzer\ExpectationAnalyzer;
use Rector\PHPUnit\NodeAnalyzer\TestsNodeAnalyzer;
use Rector\PHPUnit\NodeFactory\ConsecutiveAssertionFactory;
use Rector\PHPUnit\ValueObject\ExpectationMock;
use Rector\PHPUnit\ValueObject\ExpectationMockCollection;
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
/**
* @see \Rector\Tests\PHPUnit\Rector\ClassMethod\MigrateAtToConsecutiveExpectationsRector\MigrateAtToConsecutiveExpectationsRectorTest
*/
final class MigrateAtToConsecutiveExpectationsRector extends AbstractRector
{
/**
* @var ConsecutiveAssertionFactory
*/
private $consecutiveAssertionFactory;
/**
* @var TestsNodeAnalyzer
*/
private $testsNodeAnalyzer;
/**
* @var ExpectationAnalyzer
*/
private $expectationAnalyzer;
public function __construct(
ConsecutiveAssertionFactory $consecutiveAssertionFactory,
TestsNodeAnalyzer $testsNodeAnalyzer,
ExpectationAnalyzer $expectationAnalyzer
)
{
$this->consecutiveAssertionFactory = $consecutiveAssertionFactory;
$this->testsNodeAnalyzer = $testsNodeAnalyzer;
$this->expectationAnalyzer = $expectationAnalyzer;
}
public function getRuleDefinition(): RuleDefinition
{
return new RuleDefinition(
'Migrates deprecated $this->at to $this->withConsecutive and $this->willReturnOnConsecutiveCalls',
[
new CodeSample(
<<<'CODE_SAMPLE'
$mock = $this->createMock(Foo::class);
$mock->expects($this->at(0))->with('0')->method('someMethod')->willReturn('1');
$mock->expects($this->at(1))->with('1')->method('someMethod')->willReturn('2');
CODE_SAMPLE,
<<<'CODE_SAMPLE'
$mock = $this->createMock(Foo::class);
$mock->method('someMethod')->withConsecutive(['0'], ['1'])->willReturnOnConsecutiveCalls('1', '2');
CODE_SAMPLE
),
]
);
}
/**
* @return array<class-string<Node>>
*/
public function getNodeTypes(): array
{
return [ClassMethod::class];
}
/**
* @param ClassMethod $node
*/
public function refactor(Node $node): ?Node
{
$stmts = $node->stmts;
if ($stmts === null) {
return null;
}
$expressions = array_filter($stmts, function (Stmt $expr) {
return $expr instanceof Expression && $expr->expr instanceof MethodCall;
});
$expectationMockCollection = $this->expectationAnalyzer->getExpectationsFromExpressions($expressions);
if (!$expectationMockCollection->hasExpectationMocks()) {
return null;
}
$expectationCollections = $this->groupExpectationCollectionsByVariableName($expectationMockCollection);
foreach ($expectationCollections as $expectationCollection) {
$this->replaceExpectationNodes($expectationCollection);
}
return $node;
}
private function buildNewExpectation(ExpectationMockCollection $expectationMockCollection): MethodCall
{
$expectationMockCollection = $this->fillMissingAtIndexes($expectationMockCollection);
return $this->consecutiveAssertionFactory->createAssertionFromExpectationMockCollection($expectationMockCollection);
}
private function fillMissingAtIndexes(ExpectationMockCollection $expectationMockCollection): ExpectationMockCollection
{
$var = $expectationMockCollection->getExpectationMocks()[0]->getExpectationVariable();
// 0,1,2,3,4
// min = 0 ; max = 4 ; count = 5
// OK
// 1,2,3,4
// min = 1 ; max = 4 ; count = 4
// ADD 0
// OR
// 3
// min = 3; max = 3 ; count = 1
// 0,1,2
if ($expectationMockCollection->getLowestAtIndex() !== 0) {
for ($i = 0; $i < $expectationMockCollection->getLowestAtIndex(); ++$i) {
$expectationMockCollection->add(
new ExpectationMock($var, [], $i, null, [], null)
);
}
}
// 0,1,2,4
// min = 0 ; max = 4 ; count = 4
// ADD 3
if ($expectationMockCollection->isMissingAtIndexBetweenHighestAndLowest()) {
$existingIndexes = array_column($expectationMockCollection->getExpectationMocks(), 'index');
for ($i = 1; $i < $expectationMockCollection->getHighestAtIndex(); ++$i) {
if (!in_array($i, $existingIndexes, true)) {
$expectationMockCollection->add(
new ExpectationMock($var, [], $i, null, [], null)
);
}
}
}
return $expectationMockCollection;
}
private function replaceExpectationNodes(ExpectationMockCollection $expectationMockCollection): void
{
if ($this->shouldSkipReplacement($expectationMockCollection)) {
return;
}
$endLines = array_map(static function (ExpectationMock $expectationMock) {
$originalExpression = $expectationMock->originalExpression();
return $originalExpression === null ? 0 : $originalExpression->getEndLine();
}, $expectationMockCollection->getExpectationMocks());
$max = max($endLines);
foreach ($expectationMockCollection->getExpectationMocks() as $expectationMock) {
$originalExpression = $expectationMock->originalExpression();
if ($originalExpression === null) {
continue;
}
if ($max > $originalExpression->getEndLine()) {
$this->removeNode($originalExpression);
} else {
$originalExpression->expr = $this->buildNewExpectation($expectationMockCollection);
}
}
}
private function shouldSkipReplacement(ExpectationMockCollection $expectationMockCollection): bool
{
if (!$expectationMockCollection->hasReturnValues()) {
return false;
}
if (!$expectationMockCollection->isExpectedMethodAlwaysTheSame()) {
return true;
}
if ($expectationMockCollection->hasMissingAtIndexes()) {
return true;
}
if ($expectationMockCollection->hasMissingReturnValues()) {
return true;
}
return false;
}
/**
* @return ExpectationMockCollection[]
*/
private function groupExpectationCollectionsByVariableName(ExpectationMockCollection $expectationMockCollection): array
{
$groupedByVariable = [];
foreach ($expectationMockCollection->getExpectationMocks() as $expectationMock) {
$variable = $expectationMock->getExpectationVariable();
if (!is_string($variable->name)) {
continue;
}
if (!isset($groupedByVariable[$variable->name])) {
$groupedByVariable[$variable->name] = new ExpectationMockCollection();
}
$groupedByVariable[$variable->name]->add($expectationMock);
}
return array_values($groupedByVariable);
}
}

View File

@ -0,0 +1,94 @@
<?php
declare(strict_types=1);
namespace Rector\PHPUnit\ValueObject;
use PhpParser\Node\Arg;
use PhpParser\Node\Expr;
use PhpParser\Node\Expr\Variable;
use PhpParser\Node\Stmt\Expression;
final class ExpectationMock
{
/**
* @var Variable
*/
private $expectationVariable;
/**
* @var Arg[]
*/
private $methodArguments;
/**
* @var int
*/
private $index;
/**
* @var ?Expr
*/
private $return;
/**
* @var array<int, null|Expr>
*/
private $withArguments;
/**
* @var Expression|null
*/
private $originalExpression;
/**
* @param Arg[] $methodArguments
* @param array<int, null|Expr> $withArguments
*/
public function __construct(Variable $expectationVariable, array $methodArguments, int $index, ?Expr $return, array $withArguments, ?Expression $originalExpression)
{
$this->expectationVariable = $expectationVariable;
$this->methodArguments = $methodArguments;
$this->index = $index;
$this->return = $return;
$this->withArguments = $withArguments;
$this->originalExpression = $originalExpression;
}
public function getExpectationVariable(): Variable
{
return $this->expectationVariable;
}
/**
* @return Arg[]
*/
public function getMethodArguments(): array
{
return $this->methodArguments;
}
public function getIndex(): int
{
return $this->index;
}
public function getReturn(): ?Expr
{
return $this->return;
}
/**
* @return array<int, null|Expr>
*/
public function getWithArguments(): array
{
return $this->withArguments;
}
public function originalExpression(): ?Expression
{
return $this->originalExpression;
}
}

View File

@ -0,0 +1,132 @@
<?php
declare(strict_types=1);
namespace Rector\PHPUnit\ValueObject;
use PhpParser\Node\Scalar\String_;
final class ExpectationMockCollection
{
/**
* @var ExpectationMock[]
*/
private $expectationMocks = [];
/**
* @return ExpectationMock[]
*/
public function getExpectationMocks(): array
{
return $this->expectationMocks;
}
public function hasExpectationMocks(): bool
{
return count($this->expectationMocks) > 0;
}
public function add(ExpectationMock $expectationMock): void
{
$this->expectationMocks[] = $expectationMock;
}
public function getHighestAtIndex(): int
{
if (!$this->hasExpectationMocks()) {
return 0;
}
$indexes = array_map(static function (ExpectationMock $expectationMock) {
return $expectationMock->getIndex();
}, $this->getExpectationMocks());
return max($indexes) ?: 0;
}
public function getLowestAtIndex(): int
{
if (!$this->hasExpectationMocks()) {
return 0;
}
$indexes = array_map(static function (ExpectationMock $expectationMock) {
return $expectationMock->getIndex();
}, $this->getExpectationMocks());
return min($indexes) ?: 0;
}
public function isMissingAtIndexBetweenHighestAndLowest(): bool
{
$highestAtIndex = $this->getHighestAtIndex();
$lowestAtIndex = $this->getLowestAtIndex();
return ($highestAtIndex - $lowestAtIndex + 1) !== count($this->getExpectationMocks());
}
public function hasMissingAtIndexes(): bool
{
if ($this->getLowestAtIndex() !== 0) {
return true;
}
if ($this->isMissingAtIndexBetweenHighestAndLowest()) {
return true;
}
return false;
}
public function hasWithValues(): bool
{
foreach ($this->getExpectationMocks() as $expectationMock) {
if (
count($expectationMock->getWithArguments()) > 1
|| (
count($expectationMock->getWithArguments()) === 1
&& $expectationMock->getWithArguments()[0] !== null
)) {
return true;
}
}
return false;
}
public function hasReturnValues(): bool
{
foreach ($this->getExpectationMocks() as $expectationMock) {
if ($expectationMock->getReturn() !== null) {
return true;
}
}
return false;
}
public function hasMissingReturnValues(): bool
{
foreach ($this->getExpectationMocks() as $expectationMock) {
if ($expectationMock->getReturn() === null) {
return true;
}
}
return false;
}
public function isExpectedMethodAlwaysTheSame(): bool
{
$previousMethod = '';
foreach ($this->getExpectationMocks() as $expectationMock) {
$methodArgument = $expectationMock->getMethodArguments()[0];
if (null !== $methodArgument && $methodArgument->value instanceof String_) {
if ($previousMethod === '') {
$previousMethod = $methodArgument->value->value;
}
if ($previousMethod !== $methodArgument->value->value) {
return false;
}
}
}
return true;
}
}