mirror of
https://github.com/rectorphp/rector.git
synced 2025-03-14 12:29:43 +01:00
Merge pull request #1193 from rectorphp/phpunit-assert-subset
[PHPUnit] Add ReplaceAssertArraySubsetRector
This commit is contained in:
commit
fbc79b4f87
@ -22,3 +22,4 @@ services:
|
||||
tearDown: 'void'
|
||||
tearDownAfterClass: 'void'
|
||||
onNotSuccessfulTest: 'void'
|
||||
Rector\PHPUnit\Rector\MethodCall\ReplaceAssertArraySubsetRector: ~
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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']);
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
@ -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']);
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
@ -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']);
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
@ -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']);
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
@ -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;
|
||||
}
|
||||
}
|
@ -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#'
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
Loading…
x
Reference in New Issue
Block a user