Add annotation to attribute core rename in AnnotationToAttributeRector (#2384)

* fix fixture name

* [PHP 8.0] Add test case for partial import annotation to attribute rename
This commit is contained in:
Tomas Votruba 2022-05-28 02:19:51 +02:00 committed by GitHub
parent 31e60de5c9
commit 012e9ad553
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 229 additions and 68 deletions

View File

@ -6,7 +6,7 @@ namespace Rector\Tests\PhpAttribute\Printer;
use PhpParser\Node\Arg;
use PhpParser\Node\AttributeGroup;
use Rector\PhpAttribute\Printer\PhpAttributeGroupFactory;
use Rector\PhpAttribute\NodeFactory\PhpAttributeGroupFactory;
use Rector\Testing\PHPUnit\AbstractTestCase;
use Rector\Tests\Transform\Rector\FuncCall\ArgumentFuncCallToMethodCallRector\Fixture\Route;

View File

@ -23,7 +23,7 @@ use Rector\BetterPhpDocParser\PhpDocManipulator\PhpDocTypeChanger;
use Rector\Core\PhpParser\Node\BetterNodeFinder;
use Rector\NodeNameResolver\NodeNameResolver;
use Rector\Php80\NodeAnalyzer\PhpAttributeAnalyzer;
use Rector\PhpAttribute\Printer\PhpAttributeGroupFactory;
use Rector\PhpAttribute\NodeFactory\PhpAttributeGroupFactory;
use Rector\PHPStanStaticTypeMapper\Utils\TypeUnwrapper;
use Rector\StaticTypeMapper\StaticTypeMapper;
use ReturnTypeWillChange;

View File

@ -4,24 +4,96 @@ declare(strict_types=1);
namespace Rector\PhpAttribute\NodeFactory;
use Nette\Utils\Strings;
use PhpParser\Node\Name;
use PhpParser\Node\Name\FullyQualified;
use PhpParser\Node\Stmt\Use_;
use Rector\BetterPhpDocParser\PhpDoc\DoctrineAnnotationTagValueNode;
use Rector\Php80\ValueObject\AnnotationToAttribute;
use Rector\PhpAttribute\ValueObject\UseAliasMetadata;
final class AttributeNameFactory
{
/**
* @param Use_[] $uses
*/
public function create(
AnnotationToAttribute $annotationToAttribute,
DoctrineAnnotationTagValueNode $doctrineAnnotationTagValueNode
DoctrineAnnotationTagValueNode $doctrineAnnotationTagValueNode,
array $uses
): FullyQualified|Name {
// attribute and class name are the same, so we re-use the short form to keep code compatible with previous one
// A. attribute and class name are the same, so we re-use the short form to keep code compatible with previous one
if ($annotationToAttribute->getAttributeClass() === $annotationToAttribute->getTag()) {
$attributeName = $doctrineAnnotationTagValueNode->identifierTypeNode->name;
$attributeName = ltrim($attributeName, '@');
return new Name($attributeName);
}
// B. different name
$useAliasMetadata = $this->matchUseAliasMetadata(
$uses,
$doctrineAnnotationTagValueNode,
$annotationToAttribute
);
if ($useAliasMetadata instanceof UseAliasMetadata) {
$useUse = $useAliasMetadata->getUseUse();
// is same as name?
$useImportName = $useAliasMetadata->getUseImportName();
if ($useUse->name->toString() !== $useImportName) {
// no? rename
$useUse->name = new Name($useImportName);
}
return new Name($useAliasMetadata->getShortAttributeName());
}
// 3. the class is not aliased and is compeltelly new... return the FQN version
return new FullyQualified($annotationToAttribute->getAttributeClass());
}
/**
* @param Use_[] $uses
*/
private function matchUseAliasMetadata(
array $uses,
DoctrineAnnotationTagValueNode $doctrineAnnotationTagValueNode,
AnnotationToAttribute $annotationToAttribute
): ?UseAliasMetadata {
$shortAnnotationName = trim($doctrineAnnotationTagValueNode->identifierTypeNode->name, '@');
foreach ($uses as $use) {
foreach ($use->uses as $useUse) {
if ($useUse->alias === null) {
continue;
}
$alias = $useUse->alias->toString();
if (! str_starts_with($shortAnnotationName, $alias)) {
continue;
}
$importName = $useUse->name->toString();
// previous keyword
$lastImportKeyword = Strings::after($importName, '\\', -1);
if ($lastImportKeyword === null) {
continue;
}
// resolve new short name
$newShortname = Strings::after($annotationToAttribute->getAttributeClass(), $lastImportKeyword);
$beforeImportName = Strings::before(
$annotationToAttribute->getAttributeClass(),
$lastImportKeyword
) . $lastImportKeyword;
return new UseAliasMetadata($alias . $newShortname, $beforeImportName, $useUse);
}
}
return null;
}
}

View File

@ -2,7 +2,7 @@
declare(strict_types=1);
namespace Rector\PhpAttribute\Printer;
namespace Rector\PhpAttribute\NodeFactory;
use PhpParser\Node\Arg;
use PhpParser\Node\Attribute;

View File

@ -2,7 +2,7 @@
declare(strict_types=1);
namespace Rector\PhpAttribute\Printer;
namespace Rector\PhpAttribute\NodeFactory;
use PhpParser\Node\Arg;
use PhpParser\Node\Attribute;
@ -11,6 +11,7 @@ use PhpParser\Node\Expr;
use PhpParser\Node\Expr\ArrayItem;
use PhpParser\Node\Name\FullyQualified;
use PhpParser\Node\Scalar\String_;
use PhpParser\Node\Stmt\Use_;
use Rector\BetterPhpDocParser\PhpDoc\DoctrineAnnotationTagValueNode;
use Rector\Core\Php\PhpVersionProvider;
use Rector\Core\ValueObject\PhpVersionFeature;
@ -18,8 +19,6 @@ use Rector\Php80\ValueObject\AnnotationToAttribute;
use Rector\PhpAttribute\AnnotationToAttributeMapper;
use Rector\PhpAttribute\AttributeArrayNameInliner;
use Rector\PhpAttribute\NodeAnalyzer\ExprParameterReflectionTypeCorrector;
use Rector\PhpAttribute\NodeFactory\AttributeNameFactory;
use Rector\PhpAttribute\NodeFactory\NamedArgsFactory;
/**
* @see \Rector\Tests\PhpAttribute\Printer\PhpAttributeGroupFactoryTest
@ -70,20 +69,24 @@ final class PhpAttributeGroupFactory
return new AttributeGroup([$attribute]);
}
/**
* @param Use_[] $uses
*/
public function create(
DoctrineAnnotationTagValueNode $doctrineAnnotationTagValueNode,
AnnotationToAttribute $annotationToAttribute,
array $uses
): AttributeGroup {
$values = $doctrineAnnotationTagValueNode->getValuesWithExplicitSilentAndWithoutQuotes();
$args = $this->createArgsFromItems($values, $annotationToAttribute->getAttributeClass());
// @todo this can be a different class then the unwrapped crated one
//$argumentNames = $this->namedArgumentsResolver->resolveFromClass($annotationToAttribute->getAttributeClass());
$args = $this->attributeArrayNameInliner->inlineArrayToArgs($args);
$attributeName = $this->attributeNameFactory->create($annotationToAttribute, $doctrineAnnotationTagValueNode);
$attributeName = $this->attributeNameFactory->create(
$annotationToAttribute,
$doctrineAnnotationTagValueNode,
$uses
);
$attribute = new Attribute($attributeName, $args);
return new AttributeGroup([$attribute]);

View File

@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace Rector\PhpAttribute\ValueObject;
use PhpParser\Node\Stmt\UseUse;
final class UseAliasMetadata
{
public function __construct(
private readonly string $shortAttributeName,
private readonly string $useImportName,
private readonly UseUse $useUse
) {
}
public function getShortAttributeName(): string
{
return $this->shortAttributeName;
}
public function getUseImportName(): string
{
return $this->useImportName;
}
public function getUseUse(): UseUse
{
return $this->useUse;
}
}

View File

@ -0,0 +1,29 @@
<?php
namespace Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\Fixture;
use Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\Source\Annotation\OpenApi as OA;
final class AliasUsedWithUsePartialRename
{
/**
* @OA\PastAnnotation
*/
public $value;
}
?>
-----
<?php
namespace Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\Fixture;
use Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\Source\Attribute\OpenApi as OA;
final class AliasUsedWithUsePartialRename
{
#[OA\FutureAttribute]
public $value;
}
?>

View File

@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\Source\Annotation\OpenApi;
final class PastAnnotation
{
}

View File

@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\Source\Attribute\OpenApi;
final class FutureAttribute
{
}

View File

@ -6,6 +6,8 @@ use Rector\Config\RectorConfig;
use Rector\Core\ValueObject\PhpVersionFeature;
use Rector\Php80\Rector\Class_\AnnotationToAttributeRector;
use Rector\Php80\ValueObject\AnnotationToAttribute;
use Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\Source\Annotation\OpenApi\PastAnnotation;
use Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\Source\Attribute\OpenApi\FutureAttribute;
use Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\Source\GenericAnnotation;
use Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\Source\GenericSingleImplicitAnnotation;
@ -14,6 +16,8 @@ return static function (RectorConfig $rectorConfig): void {
$rectorConfig
->ruleWithConfiguration(AnnotationToAttributeRector::class, [
new AnnotationToAttribute(PastAnnotation::class, FutureAttribute::class),
// use always this annotation to test inner part of annotation - arguments, arrays, calls...
new AnnotationToAttribute(GenericAnnotation::class),
new AnnotationToAttribute(GenericSingleImplicitAnnotation::class),

View File

@ -1,6 +1,6 @@
<?php
namespace Rector\Tests\Renaming\Rector\Name\RenameClassRector;
namespace Rector\Tests\Renaming\Rector\Name\RenameClassRector\Fixture;
class MyCustomValidatorTest extends \Rector\Tests\Renaming\Rector\Name\RenameClassRector\Source\OldClass
{
@ -11,7 +11,7 @@ class MyCustomValidatorTest extends \Rector\Tests\Renaming\Rector\Name\RenameCla
-----
<?php
namespace Rector\Tests\Renaming\Rector\Name\RenameClassRector;
namespace Rector\Tests\Renaming\Rector\Name\RenameClassRector\Fixture;
class MyCustomValidatorTest extends \Rector\Tests\Renaming\Rector\Name\RenameClassRector\Source\NewClass
{

View File

@ -17,7 +17,7 @@ use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagNode;
use Rector\Core\Contract\Rector\ConfigurableRectorInterface;
use Rector\Core\Rector\AbstractRector;
use Rector\DowngradePhp80\ValueObject\DowngradeAttributeToAnnotation;
use Rector\PhpAttribute\Printer\DoctrineAnnotationFactory;
use Rector\PhpAttribute\NodeFactory\DoctrineAnnotationFactory;
use Symplify\RuleDocGenerator\ValueObject\CodeSample\ConfiguredCodeSample;
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
use Webmozart\Assert\Assert;

View File

@ -37,4 +37,20 @@ final class UseImportsResolver
fn (Stmt $stmt): bool => $stmt instanceof Use_ || $stmt instanceof GroupUse
);
}
/**
* @return Use_[]
*/
public function resolveBareUsesForNode(Node $node): array
{
$namespace = $this->betterNodeFinder->findParentByTypes(
$node,
[Namespace_::class, FileWithoutNamespace::class]
);
if (! $namespace instanceof Node) {
return [];
}
return array_filter($namespace->stmts, fn (Stmt $stmt): bool => $stmt instanceof Use_);
}
}

View File

@ -5,8 +5,9 @@ declare(strict_types=1);
namespace Rector\Php80\NodeFactory;
use PhpParser\Node\AttributeGroup;
use PhpParser\Node\Stmt\Use_;
use Rector\Php80\ValueObject\DoctrineTagAndAnnotationToAttribute;
use Rector\PhpAttribute\Printer\PhpAttributeGroupFactory;
use Rector\PhpAttribute\NodeFactory\PhpAttributeGroupFactory;
final class AttrGroupsFactory
{
@ -17,9 +18,10 @@ final class AttrGroupsFactory
/**
* @param DoctrineTagAndAnnotationToAttribute[] $doctrineTagAndAnnotationToAttributes
* @param Use_[] $uses
* @return AttributeGroup[]
*/
public function create(array $doctrineTagAndAnnotationToAttributes): array
public function create(array $doctrineTagAndAnnotationToAttributes, array $uses): array
{
$attributeGroups = [];
@ -30,6 +32,7 @@ final class AttrGroupsFactory
$attributeGroups[] = $this->phpAttributeGroupFactory->create(
$doctrineAnnotationTagValueNode,
$doctrineTagAndAnnotationToAttribute->getAnnotationToAttribute(),
$uses
);
}

View File

@ -4,30 +4,13 @@ declare(strict_types=1);
namespace Rector\Php80\NodeManipulator;
use PhpParser\Node\Arg;
use PhpParser\Node\Attribute;
use PhpParser\Node\AttributeGroup;
use PhpParser\Node\Expr;
use PhpParser\Node\Identifier;
use Rector\Core\NodeAnalyzer\ArgsAnalyzer;
use PhpParser\Node\Scalar\String_;
final class AttributeGroupNamedArgumentManipulator
{
/**
* @var array<string, array<string, string|class-string<Expr>>>
*/
private const SPECIAL_CLASS_TYPES = [
'JMS\Serializer\Annotation\AccessType' => [
'common_aliased' => 'JMS\AccessType',
'value' => 'PhpParser\Node\Scalar\String_',
'name' => 'type',
],
];
public function __construct(private readonly ArgsAnalyzer $argsAnalyzer)
{
}
/**
* @param AttributeGroup[] $attributeGroups
* @return AttributeGroup[]
@ -46,33 +29,29 @@ final class AttributeGroupNamedArgumentManipulator
return $attributeGroups;
}
/**
* Special case for JMS Access type, where string is replaced by specific value
*/
private function processReplaceAttr(Attribute $attribute, string $attrName): void
{
foreach (self::SPECIAL_CLASS_TYPES as $classType => $specialClasssType) {
if ($attrName !== $classType && $attrName !== $specialClasssType['common_aliased']) {
continue;
}
$args = $attribute->args;
if (count($args) !== 1) {
continue;
}
if (! $this->argsAnalyzer->isArgInstanceInArgsPosition($args, 0)) {
continue;
}
/** @var Arg $currentArg */
$currentArg = $args[0];
if ($currentArg->name !== null) {
continue;
}
if (! $currentArg->value instanceof $specialClasssType['value']) {
continue;
}
$currentArg->name = new Identifier($specialClasssType['name']);
if (! in_array($attrName, ['JMS\Serializer\Annotation\AccessType', 'JMS\AccessType'], true)) {
return;
}
$args = $attribute->args;
if (count($args) !== 1) {
return;
}
$currentArg = $args[0];
if ($currentArg->name !== null) {
return;
}
if (! $currentArg->value instanceof String_) {
return;
}
$currentArg->name = new Identifier('type');
}
}

View File

@ -23,12 +23,13 @@ use Rector\Core\Contract\Rector\ConfigurableRectorInterface;
use Rector\Core\Php\PhpVersionProvider;
use Rector\Core\Rector\AbstractRector;
use Rector\Core\ValueObject\PhpVersionFeature;
use Rector\Naming\Naming\UseImportsResolver;
use Rector\Php80\NodeFactory\AttrGroupsFactory;
use Rector\Php80\NodeManipulator\AttributeGroupNamedArgumentManipulator;
use Rector\Php80\PhpDoc\PhpDocNodeFinder;
use Rector\Php80\ValueObject\AnnotationToAttribute;
use Rector\Php80\ValueObject\DoctrineTagAndAnnotationToAttribute;
use Rector\PhpAttribute\Printer\PhpAttributeGroupFactory;
use Rector\PhpAttribute\NodeFactory\PhpAttributeGroupFactory;
use Rector\PhpAttribute\RemovableAnnotationAnalyzer;
use Rector\PhpAttribute\UnwrapableAnnotationAnalyzer;
use Rector\VersionBonding\Contract\MinPhpVersionInterface;
@ -58,6 +59,7 @@ final class AnnotationToAttributeRector extends AbstractRector implements Config
private readonly RemovableAnnotationAnalyzer $removableAnnotationAnalyzer,
private readonly AttributeGroupNamedArgumentManipulator $attributeGroupNamedArgumentManipulator,
private readonly PhpVersionProvider $phpVersionProvider,
private readonly UseImportsResolver $useImportsResolver,
) {
}
@ -122,11 +124,13 @@ CODE_SAMPLE
return null;
}
$uses = $this->useImportsResolver->resolveBareUsesForNode($node);
// 1. generic tags
$genericAttributeGroups = $this->processGenericTags($phpDocInfo);
// 2. Doctrine annotation classes
$annotationAttributeGroups = $this->processDoctrineAnnotationClasses($phpDocInfo);
$annotationAttributeGroups = $this->processDoctrineAnnotationClasses($phpDocInfo, $uses);
$attributeGroups = array_merge($genericAttributeGroups, $annotationAttributeGroups);
if ($attributeGroups === []) {
@ -202,9 +206,10 @@ CODE_SAMPLE
}
/**
* @param Node\Stmt\Use_[] $uses
* @return AttributeGroup[]
*/
private function processDoctrineAnnotationClasses(PhpDocInfo $phpDocInfo): array
private function processDoctrineAnnotationClasses(PhpDocInfo $phpDocInfo, array $uses): array
{
if ($phpDocInfo->getPhpDocNode()->children === []) {
return [];
@ -267,7 +272,7 @@ CODE_SAMPLE
$this->phpDocTagRemover->removeTagValueFromNode($phpDocInfo, $doctrineTagValueNode);
}
return $this->attrGroupsFactory->create($doctrineTagAndAnnotationToAttributes);
return $this->attrGroupsFactory->create($doctrineTagAndAnnotationToAttributes, $uses);
}
private function matchAnnotationToAttribute(

View File

@ -19,7 +19,7 @@ use Rector\Core\ValueObject\PhpVersionFeature;
use Rector\Php80\NodeAnalyzer\AnnotationTargetResolver;
use Rector\Php80\NodeAnalyzer\PhpAttributeAnalyzer;
use Rector\Php80\NodeFactory\AttributeFlagFactory;
use Rector\PhpAttribute\Printer\PhpAttributeGroupFactory;
use Rector\PhpAttribute\NodeFactory\PhpAttributeGroupFactory;
use Rector\PostRector\Collector\PropertyToAddCollector;
use Rector\PostRector\ValueObject\PropertyMetadata;
use Rector\VersionBonding\Contract\MinPhpVersionInterface;

View File

@ -15,7 +15,7 @@ use Rector\Core\Contract\Rector\AllowEmptyConfigurableRectorInterface;
use Rector\Core\Rector\AbstractRector;
use Rector\Core\ValueObject\PhpVersionFeature;
use Rector\Php80\NodeAnalyzer\PhpAttributeAnalyzer;
use Rector\PhpAttribute\Printer\PhpAttributeGroupFactory;
use Rector\PhpAttribute\NodeFactory\PhpAttributeGroupFactory;
use Rector\VersionBonding\Contract\MinPhpVersionInterface;
use Symplify\RuleDocGenerator\ValueObject\CodeSample\ConfiguredCodeSample;
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;

View File

@ -14,7 +14,7 @@ use Rector\Core\ValueObject\MethodName;
use Rector\Core\ValueObject\PhpVersionFeature;
use Rector\FamilyTree\Reflection\FamilyRelationsAnalyzer;
use Rector\Php80\NodeAnalyzer\PhpAttributeAnalyzer;
use Rector\PhpAttribute\Printer\PhpAttributeGroupFactory;
use Rector\PhpAttribute\NodeFactory\PhpAttributeGroupFactory;
use Rector\VersionBonding\Contract\MinPhpVersionInterface;
use Symplify\RuleDocGenerator\ValueObject\CodeSample\ConfiguredCodeSample;
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;