[PHPUnit] Add array call to data provider

This commit is contained in:
Tomas Votruba 2019-09-03 10:39:34 +02:00
parent 4e0ba285ca
commit 84f72f7c4a
18 changed files with 924 additions and 94 deletions

View File

@ -1,3 +1,4 @@
services:
Rector\PHPUnit\Rector\MethodCall\RemoveExpectAnyFromMockRector: ~
Rector\PHPUnit\Rector\Class_\AddSeeTestAnnotationRector: ~
Rector\PHPUnit\Rector\Class_\ArrayArgumentInTestToDataProviderRector: ~

View File

@ -28,6 +28,8 @@ services:
- 'PhpParser\NodeVisitor\NameResolver'
- 'PhpParser\Node\*'
- '*Data'
- '*Recipe'
- '*ValueObject'
- 'PhpParser\Comment'
- 'PhpParser\Lexer'
- 'PhpParser\Comment\Doc'
@ -38,7 +40,6 @@ services:
- 'Rector\DependencyInjection\Loader\*'
- 'Symplify\PackageBuilder\*'
- 'Symfony\Component\Console\Input\*Input'
- '*ValueObject'
- 'PHPStan\Analyser\NameScope'
Symplify\CodingStandard\Fixer\Naming\PropertyNameMatchingTypeFixer:
@ -110,10 +111,12 @@ parameters:
Symplify\CodingStandard\Sniffs\CleanCode\CognitiveComplexitySniff:
# tough logic
- 'packages/PHPUnit/src/Rector/Class_/ArrayArgumentInTestToDataProviderRector.php'
- 'packages/DoctrinePhpDocParser/src/Ast/PhpDoc/*/*TagValueNode.php'
- 'packages/NodeTypeResolver/src/PhpDoc/NodeAnalyzer/FqnNamePhpDocNodeDecorator.php'
- 'packages/NodeTypeResolver/src/PHPStan/Type/StaticTypeAnalyzer.php'
- 'src/NodeContainer/ParsedNodesByType.php'
- 'packages/NodeTypeResolver/src/StaticTypeMapper.php'
- 'packages/PHPStan/src/Rector/Node/RemoveNonExistingVarAnnotationRector.php'
- 'packages/Architecture/src/Rector/Class_/ConstructorInjectionToActionInjectionRector.php'
- 'src/PhpParser/Node/Commander/NodeRemovingCommander.php'

View File

@ -10,7 +10,6 @@ use PhpParser\Node\FunctionLike;
use PhpParser\Node\Stmt\ClassLike;
use PhpParser\Node\Stmt\ClassMethod;
use PhpParser\Node\Stmt\Function_;
use PHPStan\PhpDocParser\Ast\PhpDoc\InvalidTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\ParamTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocChildNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocNode;
@ -149,18 +148,6 @@ final class DocBlockManipulator
return $phpDocInfo->hasTag($name);
}
public function removeParamTagByName(Node $node, string $name): void
{
if ($node->getDocComment() === null) {
return;
}
$phpDocInfo = $this->createPhpDocInfoFromNode($node);
$this->removeParamTagByParameter($phpDocInfo, $name);
$this->updateNodeWithPhpDocInfo($node, $phpDocInfo);
}
public function addTag(Node $node, PhpDocChildNode $phpDocChildNode): void
{
$phpDocChildNode = $this->attributeAwareNodeFactory->createFromNode($phpDocChildNode);
@ -363,31 +350,6 @@ final class DocBlockManipulator
}
}
public function removeParamTagByParameter(PhpDocInfo $phpDocInfo, string $parameterName): void
{
$phpDocNode = $phpDocInfo->getPhpDocNode();
/** @var PhpDocTagNode[] $phpDocTagNodes */
$phpDocTagNodes = $phpDocNode->getTagsByName('@param');
foreach ($phpDocTagNodes as $phpDocTagNode) {
/** @var ParamTagValueNode|InvalidTagValueNode $paramTagValueNode */
$paramTagValueNode = $phpDocTagNode->value;
$parameterName = '$' . ltrim($parameterName, '$');
// process invalid tag values
if ($paramTagValueNode instanceof InvalidTagValueNode) {
if ($paramTagValueNode->value === $parameterName) {
$this->removeTagFromPhpDocNode($phpDocNode, $phpDocTagNode);
}
// process normal tag
} elseif ($paramTagValueNode->parameterName === $parameterName) {
$this->removeTagFromPhpDocNode($phpDocNode, $phpDocTagNode);
}
}
}
/**
* @param PhpDocTagNode|PhpDocTagValueNode $phpDocTagOrPhpDocTagValueNode
*/

View File

@ -0,0 +1,137 @@
<?php declare(strict_types=1);
namespace Rector\NodeTypeResolver;
use PhpParser\Node;
use PhpParser\Node\Identifier;
use PhpParser\Node\Name;
use PhpParser\Node\Name\FullyQualified;
use PhpParser\Node\NullableType;
use PHPStan\PhpDocParser\Ast\Type\ArrayTypeNode;
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
use PHPStan\Type\ArrayType;
use PHPStan\Type\BooleanType;
use PHPStan\Type\FloatType;
use PHPStan\Type\IntegerType;
use PHPStan\Type\MixedType;
use PHPStan\Type\ObjectType;
use PHPStan\Type\StringType;
use PHPStan\Type\Type;
use PHPStan\Type\UnionType;
use Rector\BetterPhpDocParser\Attributes\Ast\PhpDoc\Type\AttributeAwareUnionTypeNode;
use Rector\Exception\NotImplementedException;
use Rector\Exception\ShouldNotHappenException;
use Rector\Php\PhpVersionProvider;
/**
* Inspired by @see StaticTypeToStringResolver
*/
final class StaticTypeMapper
{
/**
* @var string
*/
private const PHP_VERSION_SCALAR_TYPES = '7.0';
/**
* @var PhpVersionProvider
*/
private $phpVersionProvider;
public function __construct(PhpVersionProvider $phpVersionProvider)
{
$this->phpVersionProvider = $phpVersionProvider;
}
public function mapPHPStanTypeToPHPStanPhpDocTypeNode(Type $currentPHPStanType): ?TypeNode
{
if ($currentPHPStanType instanceof UnionType) {
$unionTypesNodes = [];
foreach ($currentPHPStanType->getTypes() as $unionedType) {
$unionTypesNodes[] = $this->mapPHPStanTypeToPHPStanPhpDocTypeNode($unionedType);
}
return new AttributeAwareUnionTypeNode($unionTypesNodes);
}
if ($currentPHPStanType instanceof ArrayType) {
$itemTypeNode = $this->mapPHPStanTypeToPHPStanPhpDocTypeNode($currentPHPStanType->getItemType());
if ($itemTypeNode === null) {
throw new ShouldNotHappenException();
}
return new ArrayTypeNode($itemTypeNode);
}
if ($currentPHPStanType instanceof IntegerType) {
return new IdentifierTypeNode('int');
}
if ($currentPHPStanType instanceof StringType) {
return new IdentifierTypeNode('string');
}
if ($currentPHPStanType instanceof FloatType) {
return new IdentifierTypeNode('float');
}
throw new NotImplementedException(__METHOD__ . ' for ' . get_class($currentPHPStanType));
}
/**
* @return Identifier|Name|NullableType|null
*/
public function mapPHPStanTypeToPhpParserNode(Type $currentPHPStanType): ?Node
{
if ($currentPHPStanType instanceof IntegerType) {
if ($this->phpVersionProvider->isAtLeast(self::PHP_VERSION_SCALAR_TYPES)) {
return new Identifier('int');
}
return null;
}
if ($currentPHPStanType instanceof StringType) {
if ($this->phpVersionProvider->isAtLeast(self::PHP_VERSION_SCALAR_TYPES)) {
return new Identifier('string');
}
return null;
}
if ($currentPHPStanType instanceof BooleanType) {
if ($this->phpVersionProvider->isAtLeast(self::PHP_VERSION_SCALAR_TYPES)) {
return new Identifier('bool');
}
return null;
}
if ($currentPHPStanType instanceof FloatType) {
if ($this->phpVersionProvider->isAtLeast(self::PHP_VERSION_SCALAR_TYPES)) {
return new Identifier('float');
}
return null;
}
if ($currentPHPStanType instanceof ArrayType) {
return new Identifier('array');
}
if ($currentPHPStanType instanceof ObjectType) {
return new FullyQualified($currentPHPStanType->getClassName());
}
if ($currentPHPStanType instanceof UnionType) {
return null;
}
if ($currentPHPStanType instanceof MixedType) {
return null;
}
throw new NotImplementedException(__METHOD__ . ' for ' . get_class($currentPHPStanType));
}
}

View File

@ -81,6 +81,21 @@ final class StaticTypeToStringResolver
$this->resolversByArgumentType = $callableCollectorPopulator->populate($resolvers);
}
/**
* @param Type[] $staticTypes
* @return string[]
*/
public function resolveTypes(array $staticTypes): array
{
$typesAsStrings = [];
foreach ($staticTypes as $staticType) {
$currentTypesAsStrings = $this->resolveObjectType($staticType);
$typesAsStrings = array_merge($typesAsStrings, $currentTypesAsStrings);
}
return array_unique($typesAsStrings);
}
/**
* @return string[]
*/

View File

@ -56,30 +56,6 @@ final class RemoveTest extends AbstractKernelTestCase
yield [__DIR__ . '/RemoveSource/before.txt', '', '@var'];
}
/**
* @dataProvider provideDataForRemoveParamTagByParameter()
*/
public function testRemoveParamTagByParameter(
string $phpDocBeforeFilePath,
string $phpDocAfterFilePath,
string $parameterName
): void {
$phpDocInfo = $this->createPhpDocInfoFromFile($phpDocBeforeFilePath);
$this->docBlockManipulator->removeParamTagByParameter($phpDocInfo, $parameterName);
$this->assertStringEqualsFile(
$phpDocAfterFilePath,
$this->phpDocInfoPrinter->printFormatPreserving($phpDocInfo)
);
}
public function provideDataForRemoveParamTagByParameter(): Iterator
{
yield [__DIR__ . '/RemoveSource/before3.txt', __DIR__ . '/RemoveSource/after3.txt', 'paramName'];
yield [__DIR__ . '/RemoveSource/before3.txt', __DIR__ . '/RemoveSource/after3.txt', '$paramName'];
}
private function createPhpDocInfoFromFile(string $phpDocBeforeFilePath): PhpDocInfo
{
$phpDocBefore = FileSystem::read($phpDocBeforeFilePath);

View File

@ -30,31 +30,6 @@ final class DocBlockManipulatorTest extends AbstractKernelTestCase
$this->assertFalse($this->docBlockManipulator->hasTag($node, 'var'));
}
public function testRemoveAnnotationFromNode(): void
{
$node = $this->createNodeWithDoc('@param ParamType $paramName');
$this->assertNotSame('', $node->getDocComment()->getText());
$this->docBlockManipulator->removeTagFromNode($node, 'param');
$this->assertNull($node->getDocComment());
$initDoc = <<<'CODE_SAMPLE'
* @param ParamType $paramName
* @param AnotherValue $anotherValue
CODE_SAMPLE;
$node = $this->createNodeWithDoc($initDoc);
$this->docBlockManipulator->removeParamTagByName($node, 'paramName');
$expectedDoc = <<<'CODE_SAMPLE'
/**
* @param AnotherValue $anotherValue
*/
CODE_SAMPLE;
$this->assertSame($expectedDoc, $node->getDocComment()->getText());
}
private function createNodeWithDoc(string $doc): String_
{
$node = new String_('string');

View File

@ -5,4 +5,4 @@ services:
Rector\PHPUnit\:
resource: '../src'
exclude: '../src/{Rector/**/*Rector.php}'
exclude: '../src/{Rector/**/*Rector.php,ValueObject/*}'

View File

@ -0,0 +1,78 @@
<?php declare(strict_types=1);
namespace Rector\PHPUnit\NodeFactory;
use PhpParser\BuilderFactory;
use PhpParser\Node\Expr\Array_;
use PhpParser\Node\Expr\Yield_;
use PhpParser\Node\Identifier;
use PhpParser\Node\Stmt\ClassMethod;
use PhpParser\Node\Stmt\Expression;
use Rector\NodeTypeResolver\PhpDoc\NodeAnalyzer\DocBlockManipulator;
use Rector\NodeTypeResolver\StaticTypeToStringResolver;
use Rector\PHPUnit\ValueObject\DataProviderClassMethodRecipe;
final class DataProviderClassMethodFactory
{
/**
* @var BuilderFactory
*/
private $builderFactory;
/**
* @var StaticTypeToStringResolver
*/
private $staticTypeToStringResolver;
/**
* @var DocBlockManipulator
*/
private $docBlockManipulator;
public function __construct(
BuilderFactory $builderFactory,
StaticTypeToStringResolver $staticTypeToStringResolver,
DocBlockManipulator $docBlockManipulator
) {
$this->builderFactory = $builderFactory;
$this->staticTypeToStringResolver = $staticTypeToStringResolver;
$this->docBlockManipulator = $docBlockManipulator;
}
public function createFromRecipe(DataProviderClassMethodRecipe $dataProviderClassMethodRecipe): ClassMethod
{
$methodBuilder = $this->builderFactory->method($dataProviderClassMethodRecipe->getMethodName());
$methodBuilder->makePublic();
$classMethod = $methodBuilder->getNode();
foreach ($dataProviderClassMethodRecipe->getArgs() as $arg) {
$value = $arg->value;
if ($value instanceof Array_) {
foreach ($value->items as $arrayItem) {
$returnStatement = new Yield_($arrayItem->value);
$classMethod->stmts[] = new Expression($returnStatement);
}
}
}
$this->decorateClassMethodWithReturnTypeAndTag($classMethod, $dataProviderClassMethodRecipe);
return $classMethod;
}
private function decorateClassMethodWithReturnTypeAndTag(
ClassMethod $classMethod,
DataProviderClassMethodRecipe $dataProviderClassMethodRecipe
): void {
$classMethod->returnType = new Identifier('iterable');
$providedType = $dataProviderClassMethodRecipe->getProvidedType();
if ($providedType === null) {
return;
}
$typesAsStrings = $this->staticTypeToStringResolver->resolveTypes([$providedType]);
$this->docBlockManipulator->addReturnTag($classMethod, implode('|', $typesAsStrings));
}
}

View File

@ -0,0 +1,449 @@
<?php declare(strict_types=1);
namespace Rector\PHPUnit\Rector\Class_;
use PhpParser\Node;
use PhpParser\Node\Arg;
use PhpParser\Node\Expr\Array_;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Expr\Variable;
use PhpParser\Node\Identifier;
use PhpParser\Node\Param;
use PhpParser\Node\Stmt\Class_;
use PhpParser\Node\Stmt\ClassMethod;
use PHPStan\PhpDocParser\Ast\PhpDoc\GenericTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\ParamTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagNode;
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
use PHPStan\Type\ArrayType;
use PHPStan\Type\MixedType;
use PHPStan\Type\Type;
use PHPStan\Type\UnionType;
use Rector\Exception\ShouldNotHappenException;
use Rector\NodeTypeResolver\Node\AttributeKey;
use Rector\NodeTypeResolver\PhpDoc\NodeAnalyzer\DocBlockManipulator;
use Rector\NodeTypeResolver\StaticTypeMapper;
use Rector\NodeTypeResolver\StaticTypeToStringResolver;
use Rector\PHPUnit\NodeFactory\DataProviderClassMethodFactory;
use Rector\PHPUnit\ValueObject\DataProviderClassMethodRecipe;
use Rector\PHPUnit\ValueObject\ParamAndArgValueObject;
use Rector\Rector\AbstractPHPUnitRector;
use Rector\RectorDefinition\ConfiguredCodeSample;
use Rector\RectorDefinition\RectorDefinition;
/**
* @see \Rector\PHPUnit\Tests\Rector\Class_\ArrayArgumentInTestToDataProviderRector\ArrayArgumentInTestToDataProviderRectorTest
*/
final class ArrayArgumentInTestToDataProviderRector extends AbstractPHPUnitRector
{
/**
* @var mixed[]
*/
private $configuration = [];
/**
* @var DocBlockManipulator
*/
private $docBlockManipulator;
/**
* @var StaticTypeToStringResolver
*/
private $staticTypeToStringResolver;
/**
* @var StaticTypeMapper
*/
private $staticTypeMapper;
/**
* @var DataProviderClassMethodRecipe[]
*/
private $dataProviderClassMethodRecipes = [];
/**
* @var DataProviderClassMethodFactory
*/
private $dataProviderClassMethodFactory;
/**
* @param mixed[] $configuration
*/
public function __construct(
DocBlockManipulator $docBlockManipulator,
StaticTypeToStringResolver $staticTypeToStringResolver,
StaticTypeMapper $staticTypeMapper,
DataProviderClassMethodFactory $dataProviderClassMethodFactory,
array $configuration = []
) {
$this->docBlockManipulator = $docBlockManipulator;
$this->staticTypeToStringResolver = $staticTypeToStringResolver;
$this->staticTypeMapper = $staticTypeMapper;
$this->dataProviderClassMethodFactory = $dataProviderClassMethodFactory;
$this->configuration = $configuration;
}
public function getDefinition(): RectorDefinition
{
return new RectorDefinition('Move array argument from tests into data provider [configurable]', [
new ConfiguredCodeSample(
<<<'CODE_SAMPLE'
class SomeServiceTest extends \PHPUnit\Framework\TestCase
{
public function test()
{
$this->doTestMultiple([1, 2, 3]);
}
}
CODE_SAMPLE
,
<<<'CODE_SAMPLE'
class SomeServiceTest extends \PHPUnit\Framework\TestCase
{
/**
* @dataProvider provideDataForTest()
*/
public function test(int $value)
{
$this->doTestSingle($value);
}
/**
* @return int[]
*/
public function provideDataForTest(): iterable
{
yield 1;
yield 2;
yield 3;
}
}
CODE_SAMPLE
,
[
'$configuration' => [
[
'class' => 'PHPUnit\Framework\TestCase',
'old_method' => 'doTestMultiple',
'new_method' => 'doTestSingle',
],
],
]
),
]);
}
/**
* @return string[]
*/
public function getNodeTypes(): array
{
return [Class_::class];
}
/**
* @param Class_ $node
*/
public function refactor(Node $node): ?Node
{
if (! $this->isInTestClass($node)) {
return null;
}
$this->dataProviderClassMethodRecipes = [];
$this->traverseNodesWithCallable($node->stmts, function (Node $node) {
if (! $node instanceof MethodCall) {
return null;
}
foreach ($this->configuration as $singleConfiguration) {
if (! $this->isMethodCallMatch($node, $singleConfiguration)) {
continue;
}
if (count($node->args) !== 1) {
throw new ShouldNotHappenException(__METHOD__);
}
// resolve value types
$firstArgumentValue = $node->args[0]->value;
if (! $firstArgumentValue instanceof Array_) {
throw new ShouldNotHappenException();
}
// rename method to new one handling non-array input
$node->name = new Identifier($singleConfiguration['new_method']);
$dataProviderMethodName = $this->createDataProviderMethodName($node);
$this->dataProviderClassMethodRecipes[] = new DataProviderClassMethodRecipe(
$dataProviderMethodName,
$node->args,
$this->resolveUniqueArrayStaticType($firstArgumentValue)
);
$node->args = [];
$paramAndArgs = $this->collectParamAndArgsFromArray($firstArgumentValue);
foreach ($paramAndArgs as $paramAndArg) {
$node->args[] = new Arg($paramAndArg->getVariable());
}
/** @var ClassMethod $methodNode */
$methodNode = $node->getAttribute(AttributeKey::METHOD_NODE);
$this->refactorTestClassMethodParams($methodNode, $paramAndArgs);
// add data provider annotation
$dataProviderTagNode = $this->createDataProviderTagNode($dataProviderMethodName);
$this->docBlockManipulator->addTag($methodNode, $dataProviderTagNode);
return null;
}
return null;
});
if ($this->dataProviderClassMethodRecipes === []) {
return null;
}
$dataProviderClassMethods = $this->createDataProviderClassMethodsFromRecipes();
$node->stmts = array_Merge($node->stmts, $dataProviderClassMethods);
return $node;
}
private function createDataProviderTagNode(string $dataProviderMethodName): PhpDocTagNode
{
return new PhpDocTagNode('@dataProvider', new GenericTagValueNode($dataProviderMethodName . '()'));
}
private function createParamTagNode(string $name, TypeNode $typeNode): PhpDocTagNode
{
return new PhpDocTagNode('@param', new ParamTagValueNode($typeNode, false, '$' . $name, ''));
}
private function resolveUniqueArrayStaticTypes(Array_ $array): ?Type
{
$itemStaticTypes = [];
foreach ($array->items as $arrayItem) {
$arrayItemStaticType = $this->getStaticType($arrayItem->value);
if ($arrayItemStaticType === null) {
continue;
}
$valueObjectHash = implode('_', $this->staticTypeToStringResolver->resolveObjectType($arrayItemStaticType));
$itemStaticTypes[$valueObjectHash] = new ArrayType(new MixedType(), $arrayItemStaticType);
}
$itemStaticTypes = array_values($itemStaticTypes);
if (count($itemStaticTypes) > 1) {
return new UnionType($itemStaticTypes);
}
if (count($itemStaticTypes) === 1) {
return $itemStaticTypes[0];
}
return null;
}
private function createDataProviderMethodName(Node $node): string
{
/** @var string $methodName */
$methodName = $node->getAttribute(AttributeKey::METHOD_NAME);
return 'provideDataFor' . ucfirst($methodName);
}
/**
* @return ClassMethod[]
*/
private function createDataProviderClassMethodsFromRecipes(): array
{
$dataProviderClassMethods = [];
foreach ($this->dataProviderClassMethodRecipes as $dataProviderClassMethodRecipe) {
$dataProviderClassMethods[] = $this->dataProviderClassMethodFactory->createFromRecipe(
$dataProviderClassMethodRecipe
);
}
return $dataProviderClassMethods;
}
/**
* @return ParamAndArgValueObject[]
*/
private function collectParamAndArgsFromArray(Array_ $array): array
{
// multiple arguments
$i = 1;
$paramAndArgs = [];
$isNestedArray = $this->isNestedArray($array);
$itemsStaticType = $this->resolveItemStaticType($array, $isNestedArray);
if ($isNestedArray === false) {
foreach ($array->items as $arrayItem) {
$variable = new Variable('variable' . ($i === 1 ? '' : $i));
$paramAndArgs[] = new ParamAndArgValueObject($variable, $itemsStaticType);
++$i;
if (! $arrayItem->value instanceof Array_) {
break;
}
}
} else {
foreach ($array->items as $arrayItem) {
/** @var Array_ $nestedArray */
$nestedArray = $arrayItem->value;
foreach ($nestedArray->items as $nestedArrayItem) {
$variable = new Variable('variable' . ($i === 1 ? '' : $i));
$itemsStaticType = $this->getStaticType($nestedArrayItem->value);
$paramAndArgs[] = new ParamAndArgValueObject($variable, $itemsStaticType);
++$i;
}
}
}
return $paramAndArgs;
}
/**
* @param ParamAndArgValueObject[] $paramAndArgs
* @return Param[]
*/
private function createParams(array $paramAndArgs): array
{
$params = [];
foreach ($paramAndArgs as $paramAndArg) {
$param = new Param($paramAndArg->getVariable());
$staticType = $paramAndArg->getType();
if ($staticType !== null && ! $staticType instanceof UnionType) {
$phpNodeType = $this->staticTypeMapper->mapPHPStanTypeToPhpParserNode($staticType);
if ($phpNodeType !== null) {
$param->type = $phpNodeType;
}
}
$params[] = $param;
}
return $params;
}
/**
* @param Type[] $itemsStaticTypes
* @return Type[]
*/
private function filterUniqueStaticTypes(array $itemsStaticTypes): array
{
$uniqueStaticTypes = [];
foreach ($itemsStaticTypes as $itemsStaticType) {
$uniqueHash = implode('_', $this->staticTypeToStringResolver->resolveObjectType($itemsStaticType));
$uniqueHash = md5($uniqueHash);
$uniqueStaticTypes[$uniqueHash] = $itemsStaticType;
}
return array_values($uniqueStaticTypes);
}
private function resolveItemStaticType(Array_ $array, bool $isNestedArray): ?Type
{
$itemsStaticTypes = [];
if ($isNestedArray === false) {
foreach ($array->items as $arrayItem) {
$arrayItemStaticType = $this->getStaticType($arrayItem->value);
if ($arrayItemStaticType) {
$itemsStaticTypes[] = $arrayItemStaticType;
}
}
}
$itemsStaticTypes = $this->filterUniqueStaticTypes($itemsStaticTypes);
if ($itemsStaticTypes !== null && count($itemsStaticTypes) > 1) {
return new UnionType($itemsStaticTypes);
}
if (count($itemsStaticTypes) === 1) {
return $itemsStaticTypes[0];
}
return null;
}
private function isNestedArray(Array_ $array): bool
{
foreach ($array->items as $arrayItem) {
if ($arrayItem->value instanceof Array_) {
return true;
}
}
return false;
}
/**
* @param string[] $singleConfiguration
*/
private function isMethodCallMatch(MethodCall $methodCall, array $singleConfiguration): bool
{
if (! $this->isType($methodCall->var, $singleConfiguration['class'])) {
return false;
}
return $this->isName($methodCall->name, $singleConfiguration['old_method']);
}
private function resolveUniqueArrayStaticType(Array_ $array): ?Type
{
$isNestedArray = $this->isNestedArray($array);
$uniqueArrayStaticType = $this->resolveUniqueArrayStaticTypes($array);
if ($isNestedArray && $uniqueArrayStaticType instanceof ArrayType) {
// unwrap one level up
return $uniqueArrayStaticType->getItemType();
}
return $uniqueArrayStaticType;
}
/**
* @param ParamAndArgValueObject[] $paramAndArgs
*/
private function refactorTestClassMethodParams(ClassMethod $classMethod, array $paramAndArgs): void
{
$classMethod->params = $this->createParams($paramAndArgs);
foreach ($paramAndArgs as $paramAndArg) {
$staticType = $paramAndArg->getType();
if (! $staticType instanceof UnionType) {
continue;
}
/** @var string $paramName */
$paramName = $this->getName($paramAndArg->getVariable());
/** @var TypeNode $staticTypeNode */
$staticTypeNode = $this->staticTypeMapper->mapPHPStanTypeToPHPStanPhpDocTypeNode($staticType);
$paramTagNode = $this->createParamTagNode($paramName, $staticTypeNode);
$this->docBlockManipulator->addTag($classMethod, $paramTagNode);
}
}
}

View File

@ -0,0 +1,52 @@
<?php declare(strict_types=1);
namespace Rector\PHPUnit\ValueObject;
use PhpParser\Node\Arg;
use PHPStan\Type\Type;
final class DataProviderClassMethodRecipe
{
/**
* @var string
*/
private $methodName;
/**
* @var Arg[]
*/
private $args = [];
/**
* @var Type|null
*/
private $providedType;
/**
* @param Arg[] $args
*/
public function __construct(string $methodName, array $args, ?Type $providedType)
{
$this->methodName = $methodName;
$this->args = $args;
$this->providedType = $providedType;
}
public function getMethodName(): string
{
return $this->methodName;
}
/**
* @return Arg[]
*/
public function getArgs(): array
{
return $this->args;
}
public function getProvidedType(): ?Type
{
return $this->providedType;
}
}

View File

@ -0,0 +1,35 @@
<?php declare(strict_types=1);
namespace Rector\PHPUnit\ValueObject;
use PhpParser\Node\Expr\Variable;
use PHPStan\Type\Type;
final class ParamAndArgValueObject
{
/**
* @var Variable
*/
private $variable;
/**
* @var Type|null
*/
private $type;
public function __construct(Variable $variable, ?Type $type)
{
$this->variable = $variable;
$this->type = $type;
}
public function getVariable(): Variable
{
return $this->variable;
}
public function getType(): ?Type
{
return $this->type;
}
}

View File

@ -8,5 +8,3 @@ namespace Rector\PHPUnit\Tests\Rector\Class_\AddSeeTestAnnotationRector\Fixture;
class SomeExisting
{
}

View File

@ -0,0 +1,36 @@
<?php declare(strict_types=1);
namespace Rector\PHPUnit\Tests\Rector\Class_\ArrayArgumentInTestToDataProviderRector;
use Rector\PHPUnit\Rector\Class_\ArrayArgumentInTestToDataProviderRector;
use Rector\Testing\PHPUnit\AbstractRectorTestCase;
final class ArrayArgumentInTestToDataProviderRectorTest extends AbstractRectorTestCase
{
public function test(): void
{
$this->doTestFiles([
__DIR__ . '/Fixture/fixture.php.inc',
__DIR__ . '/Fixture/various_types.php.inc',
__DIR__ . '/Fixture/two_arguments.php.inc',
]);
}
/**
* @return mixed[]
*/
protected function getRectorsWithConfiguration(): array
{
return [
ArrayArgumentInTestToDataProviderRector::class => [
'$configuration' => [
[
'class' => 'PHPUnit\Framework\TestCase',
'old_method' => 'doTestMultiple',
'new_method' => 'doTestSingle',
],
],
],
];
}
}

View File

@ -0,0 +1,39 @@
<?php
namespace Rector\PHPUnit\Tests\Rector\Class_\ArrayArgumentInTestToDataProviderRector\Fixture;
class SomeServiceTest extends \PHPUnit\Framework\TestCase
{
public function test()
{
$this->doTestMultiple([1, 2, 3]);
}
}
?>
-----
<?php
namespace Rector\PHPUnit\Tests\Rector\Class_\ArrayArgumentInTestToDataProviderRector\Fixture;
class SomeServiceTest extends \PHPUnit\Framework\TestCase
{
/**
* @dataProvider provideDataForTest()
*/
public function test(int $variable)
{
$this->doTestSingle($variable);
}
/**
* @return int[]
*/
public function provideDataForTest(): iterable
{
yield 1;
yield 2;
yield 3;
}
}
?>

View File

@ -0,0 +1,37 @@
<?php
namespace Rector\PHPUnit\Tests\Rector\Class_\ArrayArgumentInTestToDataProviderRector\Fixture;
class TwoArgumentsTest extends \PHPUnit\Framework\TestCase
{
public function test()
{
$this->doTestMultiple([['before', 'after']]);
}
}
?>
-----
<?php
namespace Rector\PHPUnit\Tests\Rector\Class_\ArrayArgumentInTestToDataProviderRector\Fixture;
class TwoArgumentsTest extends \PHPUnit\Framework\TestCase
{
/**
* @dataProvider provideDataForTest()
*/
public function test(string $variable, string $variable2)
{
$this->doTestSingle($variable, $variable2);
}
/**
* @return string[]
*/
public function provideDataForTest(): iterable
{
yield ['before', 'after'];
}
}
?>

View File

@ -0,0 +1,40 @@
<?php
namespace Rector\PHPUnit\Tests\Rector\Class_\ArrayArgumentInTestToDataProviderRector\Fixture;
class VariousTypesTest extends \PHPUnit\Framework\TestCase
{
public function test()
{
$this->doTestMultiple([1, '2', 3.5]);
}
}
?>
-----
<?php
namespace Rector\PHPUnit\Tests\Rector\Class_\ArrayArgumentInTestToDataProviderRector\Fixture;
class VariousTypesTest extends \PHPUnit\Framework\TestCase
{
/**
* @param int|float|string $variable
* @dataProvider provideDataForTest()
*/
public function test($variable)
{
$this->doTestSingle($variable);
}
/**
* @return float[]|int[]|string[]
*/
public function provideDataForTest(): iterable
{
yield 1;
yield '2';
yield 3.5;
}
}
?>

View File

@ -13,7 +13,4 @@ parameters:
php_version_features: '7.1'
services:
# Rector\Doctrine\Rector\Class_\AddUuidToEntityWhereMissingRector: ~
# Rector\Doctrine\Rector\Class_\AddUuidMirrorForRelationPropertyRector: ~
Rector\CodingStyle\Rector\Namespace_\ImportFullyQualifiedNamesRector: ~
# Rector\PHPUnit\Rector\Class_\AddSeeTestAnnotationRector: ~