diff --git a/config/set/phpunit/phpunit-code-quality.yaml b/config/set/phpunit/phpunit-code-quality.yaml index 8ff08c6c4b9..b7895767a47 100644 --- a/config/set/phpunit/phpunit-code-quality.yaml +++ b/config/set/phpunit/phpunit-code-quality.yaml @@ -1,3 +1,4 @@ services: Rector\PHPUnit\Rector\MethodCall\RemoveExpectAnyFromMockRector: ~ Rector\PHPUnit\Rector\Class_\AddSeeTestAnnotationRector: ~ + Rector\PHPUnit\Rector\Class_\ArrayArgumentInTestToDataProviderRector: ~ diff --git a/ecs.yaml b/ecs.yaml index 3d8de32a87e..b81ceccc331 100644 --- a/ecs.yaml +++ b/ecs.yaml @@ -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' diff --git a/packages/NodeTypeResolver/src/PhpDoc/NodeAnalyzer/DocBlockManipulator.php b/packages/NodeTypeResolver/src/PhpDoc/NodeAnalyzer/DocBlockManipulator.php index 48b864fb015..1c3d39d62c6 100644 --- a/packages/NodeTypeResolver/src/PhpDoc/NodeAnalyzer/DocBlockManipulator.php +++ b/packages/NodeTypeResolver/src/PhpDoc/NodeAnalyzer/DocBlockManipulator.php @@ -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 */ diff --git a/packages/NodeTypeResolver/src/StaticTypeMapper.php b/packages/NodeTypeResolver/src/StaticTypeMapper.php new file mode 100644 index 00000000000..8d4f5d845ec --- /dev/null +++ b/packages/NodeTypeResolver/src/StaticTypeMapper.php @@ -0,0 +1,137 @@ +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)); + } +} diff --git a/packages/NodeTypeResolver/src/StaticTypeToStringResolver.php b/packages/NodeTypeResolver/src/StaticTypeToStringResolver.php index 222859829e0..8917e82b1f7 100644 --- a/packages/NodeTypeResolver/src/StaticTypeToStringResolver.php +++ b/packages/NodeTypeResolver/src/StaticTypeToStringResolver.php @@ -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[] */ diff --git a/packages/NodeTypeResolver/tests/PhpDoc/NodeAnalyzer/DocBlockManipulator/RemoveTest.php b/packages/NodeTypeResolver/tests/PhpDoc/NodeAnalyzer/DocBlockManipulator/RemoveTest.php index 6490ea1d27b..db8ba331494 100644 --- a/packages/NodeTypeResolver/tests/PhpDoc/NodeAnalyzer/DocBlockManipulator/RemoveTest.php +++ b/packages/NodeTypeResolver/tests/PhpDoc/NodeAnalyzer/DocBlockManipulator/RemoveTest.php @@ -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); diff --git a/packages/NodeTypeResolver/tests/PhpDoc/NodeAnalyzer/DocBlockManipulatorTest.php b/packages/NodeTypeResolver/tests/PhpDoc/NodeAnalyzer/DocBlockManipulatorTest.php index 94e988dfbe3..57b74fb4165 100644 --- a/packages/NodeTypeResolver/tests/PhpDoc/NodeAnalyzer/DocBlockManipulatorTest.php +++ b/packages/NodeTypeResolver/tests/PhpDoc/NodeAnalyzer/DocBlockManipulatorTest.php @@ -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'); diff --git a/packages/PHPUnit/config/config.yaml b/packages/PHPUnit/config/config.yaml index 46214f97bb3..797ac5dd960 100644 --- a/packages/PHPUnit/config/config.yaml +++ b/packages/PHPUnit/config/config.yaml @@ -5,4 +5,4 @@ services: Rector\PHPUnit\: resource: '../src' - exclude: '../src/{Rector/**/*Rector.php}' + exclude: '../src/{Rector/**/*Rector.php,ValueObject/*}' diff --git a/packages/PHPUnit/src/NodeFactory/DataProviderClassMethodFactory.php b/packages/PHPUnit/src/NodeFactory/DataProviderClassMethodFactory.php new file mode 100644 index 00000000000..b06781c6be3 --- /dev/null +++ b/packages/PHPUnit/src/NodeFactory/DataProviderClassMethodFactory.php @@ -0,0 +1,78 @@ +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)); + } +} diff --git a/packages/PHPUnit/src/Rector/Class_/ArrayArgumentInTestToDataProviderRector.php b/packages/PHPUnit/src/Rector/Class_/ArrayArgumentInTestToDataProviderRector.php new file mode 100644 index 00000000000..260f892deeb --- /dev/null +++ b/packages/PHPUnit/src/Rector/Class_/ArrayArgumentInTestToDataProviderRector.php @@ -0,0 +1,449 @@ +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); + } + } +} diff --git a/packages/PHPUnit/src/ValueObject/DataProviderClassMethodRecipe.php b/packages/PHPUnit/src/ValueObject/DataProviderClassMethodRecipe.php new file mode 100644 index 00000000000..b57dbee0419 --- /dev/null +++ b/packages/PHPUnit/src/ValueObject/DataProviderClassMethodRecipe.php @@ -0,0 +1,52 @@ +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; + } +} diff --git a/packages/PHPUnit/src/ValueObject/ParamAndArgValueObject.php b/packages/PHPUnit/src/ValueObject/ParamAndArgValueObject.php new file mode 100644 index 00000000000..f350000b8e2 --- /dev/null +++ b/packages/PHPUnit/src/ValueObject/ParamAndArgValueObject.php @@ -0,0 +1,35 @@ +variable = $variable; + $this->type = $type; + } + + public function getVariable(): Variable + { + return $this->variable; + } + + public function getType(): ?Type + { + return $this->type; + } +} diff --git a/packages/PHPUnit/tests/Rector/Class_/AddSeeTestAnnotationRector/Fixture/skip_existing.php.inc b/packages/PHPUnit/tests/Rector/Class_/AddSeeTestAnnotationRector/Fixture/skip_existing.php.inc index edeb16cab62..ded7f027774 100644 --- a/packages/PHPUnit/tests/Rector/Class_/AddSeeTestAnnotationRector/Fixture/skip_existing.php.inc +++ b/packages/PHPUnit/tests/Rector/Class_/AddSeeTestAnnotationRector/Fixture/skip_existing.php.inc @@ -8,5 +8,3 @@ namespace Rector\PHPUnit\Tests\Rector\Class_\AddSeeTestAnnotationRector\Fixture; class SomeExisting { } - - diff --git a/packages/PHPUnit/tests/Rector/Class_/ArrayArgumentInTestToDataProviderRector/ArrayArgumentInTestToDataProviderRectorTest.php b/packages/PHPUnit/tests/Rector/Class_/ArrayArgumentInTestToDataProviderRector/ArrayArgumentInTestToDataProviderRectorTest.php new file mode 100644 index 00000000000..b04e2e21326 --- /dev/null +++ b/packages/PHPUnit/tests/Rector/Class_/ArrayArgumentInTestToDataProviderRector/ArrayArgumentInTestToDataProviderRectorTest.php @@ -0,0 +1,36 @@ +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', + ], + ], + ], + ]; + } +} diff --git a/packages/PHPUnit/tests/Rector/Class_/ArrayArgumentInTestToDataProviderRector/Fixture/fixture.php.inc b/packages/PHPUnit/tests/Rector/Class_/ArrayArgumentInTestToDataProviderRector/Fixture/fixture.php.inc new file mode 100644 index 00000000000..03caec09ba7 --- /dev/null +++ b/packages/PHPUnit/tests/Rector/Class_/ArrayArgumentInTestToDataProviderRector/Fixture/fixture.php.inc @@ -0,0 +1,39 @@ +doTestMultiple([1, 2, 3]); + } +} + +?> +----- +doTestSingle($variable); + } + /** + * @return int[] + */ + public function provideDataForTest(): iterable + { + yield 1; + yield 2; + yield 3; + } +} + +?> diff --git a/packages/PHPUnit/tests/Rector/Class_/ArrayArgumentInTestToDataProviderRector/Fixture/two_arguments.php.inc b/packages/PHPUnit/tests/Rector/Class_/ArrayArgumentInTestToDataProviderRector/Fixture/two_arguments.php.inc new file mode 100644 index 00000000000..bea65dc174d --- /dev/null +++ b/packages/PHPUnit/tests/Rector/Class_/ArrayArgumentInTestToDataProviderRector/Fixture/two_arguments.php.inc @@ -0,0 +1,37 @@ +doTestMultiple([['before', 'after']]); + } +} + +?> +----- +doTestSingle($variable, $variable2); + } + /** + * @return string[] + */ + public function provideDataForTest(): iterable + { + yield ['before', 'after']; + } +} + +?> diff --git a/packages/PHPUnit/tests/Rector/Class_/ArrayArgumentInTestToDataProviderRector/Fixture/various_types.php.inc b/packages/PHPUnit/tests/Rector/Class_/ArrayArgumentInTestToDataProviderRector/Fixture/various_types.php.inc new file mode 100644 index 00000000000..929524c2550 --- /dev/null +++ b/packages/PHPUnit/tests/Rector/Class_/ArrayArgumentInTestToDataProviderRector/Fixture/various_types.php.inc @@ -0,0 +1,40 @@ +doTestMultiple([1, '2', 3.5]); + } +} + +?> +----- +doTestSingle($variable); + } + /** + * @return float[]|int[]|string[] + */ + public function provideDataForTest(): iterable + { + yield 1; + yield '2'; + yield 3.5; + } +} + +?> diff --git a/rector.yaml b/rector.yaml index 46ff0e2d80c..16e58da7747 100644 --- a/rector.yaml +++ b/rector.yaml @@ -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: ~