Merge pull request #1193 from rectorphp/phpunit-assert-subset

[PHPUnit] Add ReplaceAssertArraySubsetRector
This commit is contained in:
Tomáš Votruba 2019-03-13 11:22:26 +01:00 committed by GitHub
commit fbc79b4f87
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 460 additions and 4 deletions

View File

@ -22,3 +22,4 @@ services:
tearDown: 'void'
tearDownAfterClass: 'void'
onNotSuccessfulTest: 'void'
Rector\PHPUnit\Rector\MethodCall\ReplaceAssertArraySubsetRector: ~

View File

@ -0,0 +1,193 @@
<?php declare(strict_types=1);
namespace Rector\PHPUnit\Rector\MethodCall;
use PhpParser\BuilderHelpers;
use PhpParser\Node;
use PhpParser\Node\Arg;
use PhpParser\Node\Expr;
use PhpParser\Node\Expr\Array_;
use PhpParser\Node\Expr\ArrayDimFetch;
use PhpParser\Node\Expr\BinaryOp\Identical;
use PhpParser\Node\Expr\FuncCall;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Expr\StaticCall;
use PhpParser\Node\Name;
use Rector\Rector\AbstractPHPUnitRector;
use Rector\RectorDefinition\CodeSample;
use Rector\RectorDefinition\RectorDefinition;
/**
* @see https://github.com/sebastianbergmann/phpunit/issues/3494
* @see https://github.com/sebastianbergmann/phpunit/issues/3495
*/
final class ReplaceAssertArraySubsetRector extends AbstractPHPUnitRector
{
/**
* @var Expr[]
*/
private $expectedKeys = [];
/**
* @var Expr[]
*/
private $expectedValuesByKeys = [];
public function getDefinition(): RectorDefinition
{
return new RectorDefinition('Replace deprecated "assertArraySubset()" method with alternative methods', [
new CodeSample(
<<<'CODE_SAMPLE'
class SomeTest extends \PHPUnit\Framework\TestCase
{
public function test()
{
$checkedArray = [];
$this->assertArraySubset([
'cache_directory' => 'new_value',
], $checkedArray);
}
}
CODE_SAMPLE
,
<<<'CODE_SAMPLE'
class SomeTest extends \PHPUnit\Framework\TestCase
{
public function test()
{
$checkedArray = [];
$this->assertArrayHasKey('cache_directory', $checkedArray);
$this->assertSame('new_value', $checkedArray['cache_directory']);
}
}
CODE_SAMPLE
),
]);
}
/**
* @return string[]
*/
public function getNodeTypes(): array
{
return [MethodCall::class, StaticCall::class];
}
/**
* @param MethodCall|StaticCall $node
*/
public function refactor(Node $node): ?Node
{
if (! $this->isAssertMethod($node, 'assertArraySubset')) {
return null;
}
$this->reset();
$expectedArray = $this->matchArray($node->args[0]->value);
if ($expectedArray === null) {
return null;
}
$this->collectExpectedKeysAndValues($expectedArray);
if ($this->expectedKeys === []) {
// no keys → intersect!
$arrayIntersect = new FuncCall(new Name('array_intersect'));
$arrayIntersect->args[] = new Arg($expectedArray);
$arrayIntersect->args[] = $node->args[1];
$identical = new Identical($arrayIntersect, $expectedArray);
$assertTrue = $this->createCallWithName($node, 'assertTrue');
$assertTrue->args[] = new Arg($identical);
$this->addNodeAfterNode($assertTrue, $node);
} else {
$this->addKeyAsserts($node);
$this->addValueAsserts($node);
}
$this->removeNode($node);
return null;
}
/**
* @param MethodCall|StaticCall $node
*/
private function addKeyAsserts(Node $node): void
{
foreach ($this->expectedKeys as $expectedKey) {
$assertArrayHasKey = $this->createCallWithName($node, 'assertArrayHasKey');
$assertArrayHasKey->args[0] = new Arg($expectedKey);
$assertArrayHasKey->args[1] = $node->args[1];
$this->addNodeAfterNode($assertArrayHasKey, $node);
}
}
/**
* @param MethodCall|StaticCall $node
*/
private function addValueAsserts(Node $node): void
{
foreach ($this->expectedValuesByKeys as $key => $expectedValue) {
$assertSame = $this->createCallWithName($node, 'assertSame');
$assertSame->args[0] = new Arg($expectedValue);
$arrayDimFetch = new ArrayDimFetch($node->args[1]->value, BuilderHelpers::normalizeValue($key));
$assertSame->args[1] = new Arg($arrayDimFetch);
$this->addNodeAfterNode($assertSame, $node);
}
}
/**
* @param StaticCall|MethodCall $node
* @return StaticCall|MethodCall
*/
private function createCallWithName(Node $node, string $name): Node
{
return $node instanceof MethodCall ? new MethodCall($node->var, $name) : new StaticCall($node->class, $name);
}
private function collectExpectedKeysAndValues(Array_ $expectedArray): void
{
foreach ($expectedArray->items as $arrayItem) {
if ($arrayItem->key === null) {
continue;
}
$this->expectedKeys[] = $arrayItem->key;
$key = $this->getValue($arrayItem->key);
$this->expectedValuesByKeys[$key] = $arrayItem->value;
}
}
private function reset(): void
{
$this->expectedKeys = [];
$this->expectedValuesByKeys = [];
}
private function matchArray(Expr $expr): ?Array_
{
if ($expr instanceof Array_) {
return $expr;
}
$value = $this->getValue($expr);
// nothing we can do
if ($value === null || ! is_array($value)) {
return null;
}
// use specific array instead
return BuilderHelpers::normalizeValue($value);
}
}

View File

@ -0,0 +1,33 @@
<?php
namespace Rector\PHPUnit\Tests\Rector\MethodCall\ReplaceAssertArraySubsetRector\Fixture;
class SomeTest extends \PHPUnit\Framework\TestCase
{
public function test()
{
$checkedArray = [];
$this->assertArraySubset([
'cache_directory' => 'new_value',
], $checkedArray);
}
}
?>
-----
<?php
namespace Rector\PHPUnit\Tests\Rector\MethodCall\ReplaceAssertArraySubsetRector\Fixture;
class SomeTest extends \PHPUnit\Framework\TestCase
{
public function test()
{
$checkedArray = [];
$this->assertArrayHasKey('cache_directory', $checkedArray);
$this->assertSame('new_value', $checkedArray['cache_directory']);
}
}
?>

View File

@ -0,0 +1,46 @@
<?php
namespace Rector\PHPUnit\Tests\Rector\MethodCall\ReplaceAssertArraySubsetRector\Fixture;
class Issue2069 extends \PHPUnit\Framework\TestCase
{
public function test()
{
$result = ['Hello' => 'a', 'World!' => 'b'];
$this->assertArraySubset(['World!' => 'b', 'Hello' => 'a'], $result);
}
public function shouldWorkToo()
{
$result = ['a', 'b', 'c', 'd'];
$this->assertArraySubset(['b', 'c'], $result);
$this->assertArraySubset(['a', 'c', 'b'], $result);
}
}
?>
-----
<?php
namespace Rector\PHPUnit\Tests\Rector\MethodCall\ReplaceAssertArraySubsetRector\Fixture;
class Issue2069 extends \PHPUnit\Framework\TestCase
{
public function test()
{
$result = ['Hello' => 'a', 'World!' => 'b'];
$this->assertArrayHasKey('World!', $result);
$this->assertArrayHasKey('Hello', $result);
$this->assertSame('b', $result['World!']);
$this->assertSame('a', $result['Hello']);
}
public function shouldWorkToo()
{
$result = ['a', 'b', 'c', 'd'];
$this->assertTrue(array_intersect(['b', 'c'], $result) === ['b', 'c']);
$this->assertTrue(array_intersect(['a', 'c', 'b'], $result) === ['a', 'c', 'b']);
}
}
?>

View File

@ -0,0 +1,57 @@
<?php
namespace Rector\PHPUnit\Tests\Rector\MethodCall\ReplaceAssertArraySubsetRector\Fixture;
class Issue2237 extends \PHPUnit\Framework\TestCase
{
public function test()
{
$result = [
'a' => 'item a',
'b' => 'item k', // wrong value
'c' => [
// 'd' not present
'g' => 'item g',
],
];
$this->assertArraySubset([
'a' => 'item a',
'b' => 'item b',
'c' => [
'd' => 'item d',
],
], $result);
}
}
?>
-----
<?php
namespace Rector\PHPUnit\Tests\Rector\MethodCall\ReplaceAssertArraySubsetRector\Fixture;
class Issue2237 extends \PHPUnit\Framework\TestCase
{
public function test()
{
$result = [
'a' => 'item a',
'b' => 'item k', // wrong value
'c' => [
// 'd' not present
'g' => 'item g',
],
];
$this->assertArrayHasKey('a', $result);
$this->assertArrayHasKey('b', $result);
$this->assertArrayHasKey('c', $result);
$this->assertSame('item a', $result['a']);
$this->assertSame('item b', $result['b']);
$this->assertSame([
'd' => 'item d',
], $result['c']);
}
}
?>

View File

@ -0,0 +1,37 @@
<?php
namespace Rector\PHPUnit\Tests\Rector\MethodCall\ReplaceAssertArraySubsetRector\Fixture;
class VariableTest extends \PHPUnit\Framework\TestCase
{
public function test()
{
$checkedArray = [];
$expectedSubset = [
'cache_directory' => 'new_value',
];
$this->assertArraySubset($expectedSubset, $checkedArray);
}
}
?>
-----
<?php
namespace Rector\PHPUnit\Tests\Rector\MethodCall\ReplaceAssertArraySubsetRector\Fixture;
class VariableTest extends \PHPUnit\Framework\TestCase
{
public function test()
{
$checkedArray = [];
$expectedSubset = [
'cache_directory' => 'new_value',
];
$this->assertArrayHasKey('cache_directory', $checkedArray);
$this->assertSame('new_value', $checkedArray['cache_directory']);
}
}
?>

View File

@ -0,0 +1,24 @@
<?php declare(strict_types=1);
namespace Rector\PHPUnit\Tests\Rector\MethodCall\ReplaceAssertArraySubsetRector;
use Rector\PHPUnit\Rector\MethodCall\ReplaceAssertArraySubsetRector;
use Rector\Testing\PHPUnit\AbstractRectorTestCase;
final class ReplaceAssertArraySubsetRectorTest extends AbstractRectorTestCase
{
public function test(): void
{
$this->doTestFiles([
__DIR__ . '/Fixture/fixture.php.inc',
__DIR__ . '/Fixture/issue_2069.php.inc',
__DIR__ . '/Fixture/issue_2237.php.inc',
__DIR__ . '/Fixture/variable.php.inc',
]);
}
protected function getRectorClass(): string
{
return ReplaceAssertArraySubsetRector::class;
}
}

View File

@ -140,4 +140,6 @@ parameters:
- '#Access to an undefined property PHPStan\\PhpDocParser\\Ast\\PhpDoc\\PhpDocTagValueNode\:\:\$type#'
- '#Parameter \#1 \$children of class PHPStan\\PhpDocParser\\Ast\\PhpDoc\\PhpDocNode constructor expects array<PHPStan\\PhpDocParser\\Ast\\PhpDoc\\PhpDocChildNode\>, array<int, PHPStan\\PhpDocParser\\Ast\\Node\> given#'
# false positive
- '#If condition is always false#'
- '#If condition is always false#'
- '#Call to an undefined method PHPStan\\Type\\Type\:\:getValue\(\)#'
- '#Method Rector\\PHPUnit\\Rector\\MethodCall\\ReplaceAssertArraySubsetRector\:\:matchArray\(\) should return PhpParser\\Node\\Expr\\Array_\|null but returns PhpParser\\Node\\Expr#'

View File

@ -7,9 +7,12 @@ use PhpParser\Node\Expr;
use PhpParser\Node\Expr\ClassConstFetch;
use PhpParser\Node\Scalar\MagicConst\Dir;
use PhpParser\Node\Scalar\MagicConst\File;
use PHPStan\Type\Constant\ConstantArrayType;
use PHPStan\Type\ConstantScalarType;
use Rector\Exception\ShouldNotHappenException;
use Rector\NodeTypeResolver\Application\ConstantNodeCollector;
use Rector\NodeTypeResolver\Node\Attribute;
use Rector\NodeTypeResolver\NodeTypeAnalyzer;
use Rector\PhpParser\Node\Resolver\NameResolver;
use Symplify\PackageBuilder\FileSystem\SmartFileInfo;
@ -30,10 +33,19 @@ final class ValueResolver
*/
private $constantNodeCollector;
public function __construct(NameResolver $nameResolver, ConstantNodeCollector $constantNodeCollector)
{
/**
* @var NodeTypeAnalyzer
*/
private $nodeTypeAnalyzer;
public function __construct(
NameResolver $nameResolver,
ConstantNodeCollector $constantNodeCollector,
NodeTypeAnalyzer $nodeTypeAnalyzer
) {
$this->nameResolver = $nameResolver;
$this->constantNodeCollector = $constantNodeCollector;
$this->nodeTypeAnalyzer = $nodeTypeAnalyzer;
}
/**
@ -41,7 +53,22 @@ final class ValueResolver
*/
public function resolve(Expr $expr)
{
return $this->getConstExprEvaluator()->evaluateDirectly($expr);
$value = $this->getConstExprEvaluator()->evaluateDirectly($expr);
if ($value !== null) {
return $value;
}
$nodeStaticType = $this->nodeTypeAnalyzer->getNodeStaticType($expr);
if ($nodeStaticType instanceof ConstantArrayType) {
return $this->extractConstantArrayTypeValue($nodeStaticType);
}
if ($nodeStaticType instanceof ConstantScalarType) {
return $nodeStaticType->getValue();
}
return null;
}
private function getConstExprEvaluator(): ConstExprEvaluator
@ -125,4 +152,25 @@ final class ValueResolver
return $this->constExprEvaluator->evaluateDirectly($classConstNode->consts[0]->value);
}
/**
* @return mixed[]
*/
private function extractConstantArrayTypeValue(ConstantArrayType $constantArrayType): array
{
$keys = [];
foreach ($constantArrayType->getKeyTypes() as $i => $keyType) {
/** @var ConstantScalarType $keyType */
$keys[$i] = $keyType->getValue();
}
$values = [];
foreach ($constantArrayType->getValueTypes() as $i => $valueType) {
/** @var ConstantScalarType $valueType */
$value = $valueType->getValue();
$values[$keys[$i]] = $value;
}
return $values;
}
}

View File

@ -3,10 +3,25 @@
namespace Rector\Rector;
use PhpParser\Node;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Expr\StaticCall;
use Rector\NodeTypeResolver\Node\Attribute;
abstract class AbstractPHPUnitRector extends AbstractRector
{
protected function isAssertMethod(Node $node, string $name): bool
{
if (! $this->isInTestClass($node)) {
return false;
}
if (! $node instanceof MethodCall && ! $node instanceof StaticCall) {
return false;
}
return $this->isName($node, $name);
}
protected function isInTestClass(Node $node): bool
{
$classNode = $node->getAttribute(Attribute::CLASS_NODE);