[PHP 8.0] Include keys in annotation to attribute transformation (#1766)

Co-authored-by: Morgan NEUTE <morgan.neute@niji.fr>
Co-authored-by: GitHub Action <action@github.com>
This commit is contained in:
Tomas Votruba 2022-02-05 11:33:29 +01:00 committed by GitHub
parent 0a63fe4d52
commit f374b9faa8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
42 changed files with 430 additions and 135 deletions

View File

@ -4,6 +4,10 @@ declare(strict_types=1);
namespace Rector\Tests\PhpAttribute\AnnotationToAttributeMapper;
use PhpParser\Node\Expr\Array_;
use PhpParser\Node\Expr\ArrayItem;
use PhpParser\Node\Expr\ConstFetch;
use PhpParser\Node\Scalar\LNumber;
use PhpParser\Node\Scalar\String_;
use Rector\PhpAttribute\AnnotationToAttributeMapper;
use Rector\Testing\PHPUnit\AbstractTestCase;
@ -20,10 +24,23 @@ final class AnnotationToAttributeMapperTest extends AbstractTestCase
public function test(): void
{
$mappedExpr = $this->annotationToAttributeMapper->map(false);
$this->assertInstanceOf(ConstFetch::class, $mappedExpr);
$mappedExpr = $this->annotationToAttributeMapper->map('false');
$this->assertInstanceOf(ConstFetch::class, $mappedExpr);
$mappedExpr = $this->annotationToAttributeMapper->map('100');
$this->assertInstanceOf(LNumber::class, $mappedExpr);
$mappedExpr = $this->annotationToAttributeMapper->map('hey');
$this->assertInstanceOf(String_::class, $mappedExpr);
$mappedExprs = $this->annotationToAttributeMapper->map(['hey']);
$this->assertInstanceOf(String_::class, $mappedExprs[0]);
$expr = $this->annotationToAttributeMapper->map(['hey']);
$this->assertInstanceOf(Array_::class, $expr);
$arrayItem = $expr->items[0];
$this->assertInstanceOf(ArrayItem::class, $arrayItem);
$this->assertInstanceOf(String_::class, $arrayItem->value);
}
}

View File

@ -4,7 +4,9 @@ declare(strict_types=1);
namespace Rector\PhpAttribute;
use PhpParser\BuilderHelpers;
use PhpParser\Node\Expr;
use Rector\BetterPhpDocParser\PhpDoc\DoctrineAnnotationTagValueNode;
use Rector\PhpAttribute\Contract\AnnotationToAttributeMapperInterface;
use Rector\PhpAttribute\Enum\DocTagNodeState;
@ -21,10 +23,7 @@ final class AnnotationToAttributeMapper
) {
}
/**
* @return Expr|Expr[]|string
*/
public function map(mixed $value): array|Expr|string
public function map(mixed $value): Expr|string
{
foreach ($this->annotationToAttributeMappers as $annotationToAttributeMapper) {
if ($annotationToAttributeMapper->isCandidate($value)) {
@ -36,6 +35,12 @@ final class AnnotationToAttributeMapper
return $value;
}
return DocTagNodeState::REMOVE_ARRAY;
// remove node, as handled elsewhere
if ($value instanceof DoctrineAnnotationTagValueNode) {
return DocTagNodeState::REMOVE_ARRAY;
}
// fallback
return BuilderHelpers::normalizeValue($value);
}
}

View File

@ -5,10 +5,13 @@ declare(strict_types=1);
namespace Rector\PhpAttribute\AnnotationToAttributeMapper;
use PhpParser\Node\Expr;
use PhpParser\Node\Expr\Array_;
use PhpParser\Node\Expr\ArrayItem;
use Rector\PhpAttribute\AnnotationToAttributeMapper;
use Rector\PhpAttribute\Contract\AnnotationToAttributeMapperInterface;
use Rector\PhpAttribute\Enum\DocTagNodeState;
use Symfony\Contracts\Service\Attribute\Required;
use Webmozart\Assert\Assert;
/**
* @implements AnnotationToAttributeMapperInterface<mixed[]>
@ -34,23 +37,41 @@ final class ArrayAnnotationToAttributeMapper implements AnnotationToAttributeMap
/**
* @param mixed[] $value
*/
public function map($value): array|Expr
public function map($value): Expr
{
$values = array_map(fn ($item): Expr|array => $this->annotationToAttributeMapper->map($item), $value);
$arrayItems = [];
foreach ($values as $key => $value) {
// remove the key and value? useful in case of unwrapping nested attributes
if (! $this->isRemoveArrayPlaceholder($value)) {
foreach ($value as $key => $singleValue) {
$valueExpr = $this->annotationToAttributeMapper->map($singleValue);
// remove node
if ($valueExpr === DocTagNodeState::REMOVE_ARRAY) {
continue;
}
unset($values[$key]);
Assert::isInstanceOf($valueExpr, Expr::class);
// remove value
if ($this->isRemoveArrayPlaceholder($singleValue)) {
continue;
}
$keyExpr = null;
if (! is_int($key)) {
$keyExpr = $this->annotationToAttributeMapper->map($key);
Assert::isInstanceOf($keyExpr, Expr::class);
}
$arrayItems[] = new ArrayItem($valueExpr, $keyExpr);
}
return $values;
return new Array_($arrayItems);
}
private function isRemoveArrayPlaceholder(Expr|array $value): bool
/**
* @param mixed $value
*/
private function isRemoveArrayPlaceholder($value): bool
{
if (! is_array($value)) {
return false;

View File

@ -5,10 +5,14 @@ declare(strict_types=1);
namespace Rector\PhpAttribute\AnnotationToAttributeMapper;
use PhpParser\Node\Expr;
use PhpParser\Node\Expr\Array_;
use PhpParser\Node\Expr\ArrayItem;
use Rector\BetterPhpDocParser\ValueObject\PhpDoc\DoctrineAnnotation\CurlyListNode;
use Rector\PhpAttribute\AnnotationToAttributeMapper;
use Rector\PhpAttribute\Contract\AnnotationToAttributeMapperInterface;
use Rector\PhpAttribute\Enum\DocTagNodeState;
use Symfony\Contracts\Service\Attribute\Required;
use Webmozart\Assert\Assert;
/**
* @implements AnnotationToAttributeMapperInterface<CurlyListNode>
@ -34,11 +38,28 @@ final class CurlyListNodeAnnotationToAttributeMapper implements AnnotationToAttr
/**
* @param CurlyListNode $value
*/
public function map($value): array|Expr
public function map($value): Array_
{
return array_map(
fn ($node): mixed => $this->annotationToAttributeMapper->map($node),
$value->getValuesWithExplicitSilentAndWithoutQuotes()
);
$arrayItems = [];
foreach ($value->getValuesWithExplicitSilentAndWithoutQuotes() as $key => $singleValue) {
$valueExpr = $this->annotationToAttributeMapper->map($singleValue);
// remove node
if ($valueExpr === DocTagNodeState::REMOVE_ARRAY) {
continue;
}
Assert::isInstanceOf($valueExpr, Expr::class);
$keyExpr = null;
if (! is_int($key)) {
$keyExpr = $this->annotationToAttributeMapper->map($key);
Assert::isInstanceOf($keyExpr, Expr::class);
}
$arrayItems[] = new ArrayItem($valueExpr, $keyExpr);
}
return new Array_($arrayItems);
}
}

View File

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Rector\PhpAttribute\AnnotationToAttributeMapper;
use PhpParser\Node\Expr\Array_;
use PhpParser\Node\Expr\New_;
use PhpParser\Node\Name;
use Rector\BetterPhpDocParser\PhpDoc\DoctrineAnnotationTagValueNode;
@ -11,9 +12,9 @@ use Rector\Core\Exception\ShouldNotHappenException;
use Rector\Core\Php\PhpVersionProvider;
use Rector\Core\ValueObject\PhpVersionFeature;
use Rector\PhpAttribute\AnnotationToAttributeMapper;
use Rector\PhpAttribute\AttributeArrayNameInliner;
use Rector\PhpAttribute\Contract\AnnotationToAttributeMapperInterface;
use Rector\PhpAttribute\Exception\InvalidNestedAttributeException;
use Rector\PhpAttribute\NodeFactory\NamedArgsFactory;
use Rector\PhpAttribute\UnwrapableAnnotationAnalyzer;
use Symfony\Contracts\Service\Attribute\Required;
@ -26,8 +27,8 @@ final class DoctrineAnnotationAnnotationToAttributeMapper implements AnnotationT
public function __construct(
private readonly PhpVersionProvider $phpVersionProvider,
private readonly NamedArgsFactory $namedArgsFactory,
private readonly UnwrapableAnnotationAnalyzer $unwrapableAnnotationAnalyzer,
private readonly AttributeArrayNameInliner $attributeArrayNameInliner,
) {
}
@ -67,11 +68,12 @@ final class DoctrineAnnotationAnnotationToAttributeMapper implements AnnotationT
$value->getValuesWithExplicitSilentAndWithoutQuotes()
);
if (! is_array($argValues)) {
if ($argValues instanceof Array_) {
// create named args
$args = $this->attributeArrayNameInliner->inlineArrayToArgs($argValues);
} else {
throw new ShouldNotHappenException();
}
$args = $this->namedArgsFactory->createFromValues($argValues);
} else {
$args = [];
}

View File

@ -4,7 +4,12 @@ declare(strict_types=1);
namespace Rector\PhpAttribute\AnnotationToAttributeMapper;
use PhpParser\Node\Expr;
use PhpParser\Node\Expr\ConstFetch;
use PhpParser\Node\Name;
use PhpParser\Node\Scalar\LNumber;
use PhpParser\Node\Scalar\String_;
use Rector\NodeTypeResolver\Node\AttributeKey;
use Rector\PhpAttribute\Contract\AnnotationToAttributeMapperInterface;
/**
@ -20,8 +25,33 @@ final class StringAnnotationToAttributeMapper implements AnnotationToAttributeMa
/**
* @param string $value
*/
public function map($value): String_
public function map($value): Expr
{
return new String_($value);
if (strtolower($value) === 'true') {
return new ConstFetch(new Name('true'));
}
if (strtolower($value) === 'false') {
return new ConstFetch(new Name('false'));
}
if (strtolower($value) === 'null') {
return new ConstFetch(new Name('null'));
}
// number as string to number
if (is_numeric($value) && strlen((string) (int) $value) === strlen($value)) {
return LNumber::fromString($value);
}
if (str_contains($value, "'") && ! str_contains($value, "\n")) {
$kind = String_::KIND_DOUBLE_QUOTED;
} else {
$kind = String_::KIND_SINGLE_QUOTED;
}
return new String_($value, [
AttributeKey::KIND => $kind,
]);
}
}

View File

@ -0,0 +1,79 @@
<?php
declare(strict_types=1);
namespace Rector\PhpAttribute;
use PhpParser\Node\Arg;
use PhpParser\Node\Expr\Array_;
use PhpParser\Node\Expr\ArrayItem;
use PhpParser\Node\Identifier;
use PhpParser\Node\Scalar\String_;
use Webmozart\Assert\Assert;
final class AttributeArrayNameInliner
{
/**
* @param Array_|Arg[] $array
* @return Arg[]
*/
public function inlineArrayToArgs(Array_|array $array): array
{
if (is_array($array)) {
return $this->inlineArray($array);
}
return $this->inlineArrayNode($array);
}
/**
* @return Arg[]
*/
private function inlineArrayNode(Array_ $array): array
{
$args = [];
foreach ($array->items as $arrayItem) {
if (! $arrayItem instanceof ArrayItem) {
continue;
}
$key = $arrayItem->key;
if ($key instanceof String_) {
$args[] = new Arg($arrayItem->value, false, false, [], new Identifier($key->value));
} else {
$args[] = new Arg($arrayItem->value);
}
}
return $args;
}
/**
* @param Arg[] $args
* @return Arg[]
*/
private function inlineArray(array $args): array
{
Assert::allIsAOf($args, Arg::class);
$newArgs = [];
foreach ($args as $arg) {
// matching top root array key
if ($arg->value instanceof ArrayItem) {
$arrayItem = $arg->value;
if ($arrayItem->key instanceof String_) {
$arrayItemString = $arrayItem->key;
$newArgs[] = new Arg($arrayItem->value, false, false, [], new Identifier($arrayItemString->value));
}
}
}
if ($newArgs !== []) {
return $newArgs;
}
return $args;
}
}

View File

@ -16,5 +16,5 @@ interface AnnotationToAttributeMapperInterface
/**
* @param T $value
*/
public function map($value): array|Expr;
public function map($value): Expr;
}

View File

@ -5,6 +5,7 @@ declare(strict_types=1);
namespace Rector\PhpAttribute\NodeAnalyzer;
use PhpParser\Node\Expr;
use PhpParser\Node\Expr\Array_;
use PhpParser\Node\Scalar\LNumber;
use PhpParser\Node\Scalar\String_;
use PHPStan\Reflection\ParameterReflection;
@ -29,8 +30,12 @@ final class ExprParameterReflectionTypeCorrector
* @param array<string|int, Expr|mixed> $items
* @return array<string|int, Expr|mixed>
*/
public function correctItemsByAttributeClass(array $items, string $attributeClass): array
public function correctItemsByAttributeClass(array|Array_ $items, string $attributeClass): array
{
if ($items instanceof Array_) {
$items = $items->items;
}
if (! $this->reflectionProvider->hasClass($attributeClass)) {
return $items;
}

View File

@ -1,45 +0,0 @@
<?php
declare(strict_types=1);
namespace Rector\PhpAttribute\NodeAnalyzer;
use PHPStan\Reflection\ParameterReflection;
use PHPStan\Reflection\ParametersAcceptorSelector;
use PHPStan\Reflection\ReflectionProvider;
final class NamedArgumentsResolver
{
public function __construct(
private readonly ReflectionProvider $reflectionProvider
) {
}
/**
* @param class-string $class
* @return array<int, string>
*/
public function resolveFromClass(string $class): array
{
// decorate args with names if attribute class was found
if (! $this->reflectionProvider->hasClass($class)) {
return [];
}
$classReflection = $this->reflectionProvider->getClass($class);
if (! $classReflection->hasConstructor()) {
return [];
}
$argumentNames = [];
$constructorMethodReflection = $classReflection->getConstructor();
$parametersAcceptor = ParametersAcceptorSelector::selectSingle($constructorMethodReflection->getVariants());
foreach ($parametersAcceptor->getParameters() as $key => $parameterReflection) {
/** @var ParameterReflection $parameterReflection */
$argumentNames[$key] = $parameterReflection->getName();
}
return $argumentNames;
}
}

View File

@ -8,29 +8,44 @@ use PhpParser\Node\Arg;
use PhpParser\Node\Attribute;
use PhpParser\Node\AttributeGroup;
use PhpParser\Node\Expr;
use PhpParser\Node\Identifier;
use PhpParser\Node\Expr\ArrayItem;
use PhpParser\Node\Name\FullyQualified;
use PhpParser\Node\Scalar\String_;
use Rector\BetterPhpDocParser\PhpDoc\DoctrineAnnotationTagValueNode;
use Rector\Core\Php\PhpVersionProvider;
use Rector\Core\ValueObject\PhpVersionFeature;
use Rector\Php80\ValueObject\AnnotationToAttribute;
use Rector\PhpAttribute\AnnotationToAttributeMapper;
use Rector\PhpAttribute\AttributeArrayNameInliner;
use Rector\PhpAttribute\NodeAnalyzer\ExprParameterReflectionTypeCorrector;
use Rector\PhpAttribute\NodeAnalyzer\NamedArgumentsResolver;
use Rector\PhpAttribute\NodeFactory\AttributeNameFactory;
use Rector\PhpAttribute\NodeFactory\NamedArgsFactory;
use Webmozart\Assert\Assert;
/**
* @see \Rector\Tests\PhpAttribute\Printer\PhpAttributeGroupFactoryTest
*/
final class PhpAttributeGroupFactory
{
/**
* @var array<string, string[]>>
*/
private array $unwrappedAnnotations = [
'Doctrine\ORM\Mapping\Table' => ['uniqueConstraints'],
'Doctrine\ORM\Mapping\Entity' => ['uniqueConstraints'],
];
public function __construct(
private readonly NamedArgumentsResolver $namedArgumentsResolver,
private readonly AnnotationToAttributeMapper $annotationToAttributeMapper,
private readonly AttributeNameFactory $attributeNameFactory,
private readonly NamedArgsFactory $namedArgsFactory,
private readonly ExprParameterReflectionTypeCorrector $exprParameterReflectionTypeCorrector
private readonly ExprParameterReflectionTypeCorrector $exprParameterReflectionTypeCorrector,
private readonly AttributeArrayNameInliner $attributeArrayNameInliner,
PhpVersionProvider $phpVersionProvider
) {
// nested indexes supported only since PHP 8.1
if (! $phpVersionProvider->isAtLeastPhpVersion(PhpVersionFeature::NEW_INITIALIZERS)) {
$this->unwrappedAnnotations['Doctrine\ORM\Mapping\Table'][] = 'indexes';
}
}
public function createFromSimpleTag(AnnotationToAttribute $annotationToAttribute): AttributeGroup
@ -64,9 +79,11 @@ final class PhpAttributeGroupFactory
$values = $doctrineAnnotationTagValueNode->getValuesWithExplicitSilentAndWithoutQuotes();
$args = $this->createArgsFromItems($values, $annotationToAttribute->getAttributeClass());
$argumentNames = $this->namedArgumentsResolver->resolveFromClass($annotationToAttribute->getAttributeClass());
$this->completeNamedArguments($args, $argumentNames);
// @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);
@ -80,33 +97,45 @@ final class PhpAttributeGroupFactory
*/
public function createArgsFromItems(array $items, string $attributeClass): array
{
/** @var Expr[] $items */
/** @var Expr[]|Expr\Array_ $items */
$items = $this->annotationToAttributeMapper->map($items);
$items = $this->exprParameterReflectionTypeCorrector->correctItemsByAttributeClass($items, $attributeClass);
$items = $this->removeUnwrappedItems($attributeClass, $items);
return $this->namedArgsFactory->createFromValues($items);
}
/**
* @param Arg[] $args
* @param string[] $argumentNames
* @param mixed[] $items
* @return mixed[]
*/
private function completeNamedArguments(array $args, array $argumentNames): void
private function removeUnwrappedItems(string $attributeClass, array $items): array
{
Assert::allIsAOf($args, Arg::class);
foreach ($args as $key => $arg) {
$argumentName = $argumentNames[$key] ?? null;
if ($argumentName === null) {
continue;
}
if ($arg->name !== null) {
continue;
}
$arg->name = new Identifier($argumentName);
// unshift annotations that can be extracted
$unwrappeColumns = $this->unwrappedAnnotations[$attributeClass] ?? [];
if ($unwrappeColumns === []) {
return $items;
}
foreach ($items as $key => $item) {
if (! $item instanceof ArrayItem) {
continue;
}
$arrayItemKey = $item->key;
if (! $arrayItemKey instanceof String_) {
continue;
}
if (! in_array($arrayItemKey->value, $unwrappeColumns, true)) {
continue;
}
unset($items[$key]);
}
return $items;
}
}

View File

@ -573,10 +573,6 @@ parameters:
message: '#Cognitive complexity for "Rector\\Php80\\Rector\\Class_\\AnnotationToAttributeRector\:\:processDoctrineAnnotationClasses\(\)" is \d+, keep it under 10#'
path: rules/Php80/Rector/Class_/AnnotationToAttributeRector.php
- '#Anonymous function should return array\|PhpParser\\Node\\Expr but returns array<PhpParser\\Node\\Expr\>\|PhpParser\\Node\\Expr\|string#'
- '#Method Rector\\PhpAttribute\\AnnotationToAttributeMapper\\ArrayAnnotationToAttributeMapper\:\:isRemoveArrayPlaceholder\(\) has parameter \$value with no value type specified in iterable type array#'
-
message: '#Use value object over return of values#'
path: src/Application/ApplicationFileProcessor.php
@ -603,7 +599,21 @@ parameters:
# mapper re-use
- '#Parameter \#1 \$type of method Rector\\PHPStanStaticTypeMapper\\TypeMapper\\ObjectWithoutClassTypeMapper\:\:mapToPhpParserNode\(\) expects PHPStan\\Type\\ObjectWithoutClassType, PHPStan\\Type\\Accessory\\Has(Property|Method)Type given#'
# due to new https://github.com/symplify/symplify/pull/3912 NoMixedPropertyFetcherRule
- '#Anonymous variables in a property fetch can lead to false dead property\. Make sure the variable type is known#'
# due to new https://github.com/symplify/symplify/pull/3913 NoMixedMethodCallerRule
# false positive
- '#Method Rector\\VersionBonding\\PhpVersionedFilter\:\:filter\(\) should return array<T of Rector\\Core\\Contract\\Rector\\RectorInterface\> but returns array<int, \(Rector\\VersionBonding\\Contract\\MinPhpVersionInterface&T of Rector\\Core\\Contract\\Rector\\RectorInterface\)\|T of Rector\\Core\\Contract\\Rector\\RectorInterface\>#'
# fix later
- '#Parameter \#1 \$value of class PhpParser\\Node\\Expr\\ArrayItem constructor expects PhpParser\\Node\\Expr, array<PhpParser\\Node\\Expr\>\|PhpParser\\Node\\Expr\|string given#'
- '#Parameter \#2 \$key of class PhpParser\\Node\\Expr\\ArrayItem constructor expects PhpParser\\Node\\Expr\|null, array<PhpParser\\Node\\Expr\>\|PhpParser\\Node\\Expr\|string\|null given#'
# broken on CI
- '#Anonymous variables in a method call can lead to false dead methods\. Make sure the variable type is known#'
- '#Anonymous variables in a property fetch can lead to false dead property\. Make sure the variable type is known#'
-
message: '#This property type might be inlined to PHP\. Do you have confidence it is correct\? Put it here#'
path: packages/PhpAttribute/Printer/PhpAttributeGroupFactory.php
-
message: '#Array with keys is not allowed\. Use value object to pass data instead#'
path: packages/PhpAttribute/Printer/PhpAttributeGroupFactory.php

View File

@ -0,0 +1,32 @@
<?php
namespace Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\Fixture;
use Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\Source\GenericAnnotation;
use Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\Source\ConstantReference;
/**
* @GenericAnnotation(
* itemOperations={
* "get" = {
ConstantReference::FIRST_NAME = true
* }
* }
* )
*/
final class DemoClass {
}
?>
-----
<?php
namespace Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\Fixture;
use Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\Source\GenericAnnotation;
use Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\Source\ConstantReference;
#[GenericAnnotation(itemOperations: ['get' => [ConstantReference::FIRST_NAME => true]])]
final class DemoClass
{
}
?>

View File

@ -2,10 +2,10 @@
namespace Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\Fixture;
use Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\Source\GenericAnnotation;
use Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\Source\GenericSingleImplicitAnnotation;
/**
* @GenericAnnotation("/")
* @GenericSingleImplicitAnnotation("/")
*/
final class FromImplicitToName
{
@ -17,9 +17,9 @@ final class FromImplicitToName
namespace Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\Fixture;
use Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\Source\GenericAnnotation;
use Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\Source\GenericSingleImplicitAnnotation;
#[GenericAnnotation(some: '/')]
#[GenericSingleImplicitAnnotation('/')]
final class FromImplicitToName
{
}

View File

@ -24,7 +24,7 @@ use Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\Source\GenericA
final class FunctionCallInside
{
#[GenericAnnotation(some: "is_granted('ROLE_USER')")]
#[GenericAnnotation("is_granted('ROLE_USER')")]
public function action()
{
}

View File

@ -21,7 +21,7 @@ namespace Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\Fixture;
use Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\Source\GenericAnnotation;
#[GenericAnnotation(some: '
#[GenericAnnotation('
summary: Send webcam reward
')]
final class MultilineContent

View File

@ -7,7 +7,7 @@ use Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\Source\Constant
/**
* @GenericAnnotation(
* properties={
* summary={
* ConstantReference::FIRST_NAME = "John",
* }
* )
@ -25,7 +25,7 @@ namespace Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\Fixture;
use Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\Source\GenericAnnotation;
use Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\Source\ConstantReference;
#[GenericAnnotation(properties: [ConstantReference::FIRST_NAME => 'John'])]
#[GenericAnnotation(summary: [ConstantReference::FIRST_NAME => 'John'])]
final class NestedArray
{
}

View File

@ -34,7 +34,7 @@ namespace Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\Fixture;
use Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\Source\GenericAnnotation;
use Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\Source\ConstantReference;
#[GenericAnnotation(some: [ConstantReference::FIRST_NAME => ['foo' => ['bar']], 0 => ConstantReference::LAST_NAME])]
#[GenericAnnotation(some: [ConstantReference::FIRST_NAME => ['foo' => ['bar']], ConstantReference::LAST_NAME])]
#[GenericAnnotation(some: [ConstantReference::LAST_NAME, 'trailingValue'])]
final class ArrayWithConstantAsKey
{

View File

@ -22,7 +22,7 @@ namespace Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\Fixture;
use Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\Source\GenericAnnotation;
#[GenericAnnotation(some: '
#[GenericAnnotation('
summary: key
description: \'something `id => name`.\'
')]

View File

@ -24,7 +24,7 @@ namespace Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\Fixture;
use Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\Source\GenericAnnotation;
#[GenericAnnotation(some: '
#[GenericAnnotation('
key: value
another_key:
another_value/*:

View File

@ -2,7 +2,7 @@
/**
* @Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\Source\GenericAnnotation(
* routePrefix="/demo/"
* some="/demo/"
* )
*/
final class NonNamespacedClassWithAnnotation
@ -12,7 +12,7 @@ final class NonNamespacedClassWithAnnotation
-----
<?php
#[Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\Source\GenericAnnotation(routePrefix: '/demo/')]
#[Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\Source\GenericAnnotation(some: '/demo/')]
final class NonNamespacedClassWithAnnotation
{
}

View File

@ -24,7 +24,7 @@ use Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\Source as Asser
final class EntityColumnAndAssertChoice
{
#[Assert\GenericAnnotation(some: ['php5', 'php7', 'php8'])]
#[Assert\GenericAnnotation(['php5', 'php7', 'php8'])]
#[Assert\GenericAnnotation(choices: ['5.0', '5.1', 'id' => '5.2'])]
#[Assert\GenericAnnotation(choices: [2, 3, 5, 7, 11, 13, 17, 19])]
public $primeNumbers;

View File

@ -9,7 +9,7 @@ use Symfony\Component\Validator\Constraints as Assert;
final class SkipUnwrapSymfonyNestedValidators
{
/**
* @GenericAnnotation("hey")
* @GenericAnnotation(some = "hey")
*
* @var array
* @Assert\All(

View File

@ -8,8 +8,8 @@ final class TrailingCommadAfterArray
{
/**
* @GenericAnnotation(
* "first" = "item",
* "next" = "item",
* "some" = "item",
* "summary" = "item",
* )
*/
public function action()
@ -27,7 +27,7 @@ use Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\Source\GenericA
final class TrailingCommadAfterArray
{
#[GenericAnnotation(first: 'item', next: 'item')]
#[GenericAnnotation(some: 'item', summary: 'item')]
public function action()
{
}

View File

@ -25,7 +25,7 @@ use Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\SourcePhp81 as
final class MultipleNestedAttribute
{
#[Assert\All(nestedAsserts: [new Assert\NotNull(), new Assert\NotNumber()])]
#[Assert\All([new Assert\NotNull(), new Assert\NotNumber()])]
public $value;
}

View File

@ -24,7 +24,7 @@ use Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\SourcePhp81 as
final class NamedArgument
{
#[Assert\All(nestedAsserts: [new Assert\NotNumber(secondValue: 1000)])]
#[Assert\All([new Assert\NotNumber(secondValue: 1000)])]
public $value;
}

View File

@ -24,7 +24,7 @@ use Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\SourcePhp81 as
final class NestedAttribute
{
#[Assert\All(nestedAsserts: [new Assert\NotNull()])]
#[Assert\All([new Assert\NotNull()])]
public $value;
}

View File

@ -24,7 +24,7 @@ use Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\SourcePhp81 as
final class NestedAttributesWithArgument
{
#[Assert\All(nestedAsserts: [new Assert\NotNumber(1000)])]
#[Assert\All([new Assert\NotNumber(1000)])]
public $value;
}

View File

@ -24,7 +24,7 @@ use Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\SourcePhp81 as
final class NestedAttributeWithBrackets
{
#[Assert\All(nestedAsserts: [new Assert\NotNull()])]
#[Assert\All([new Assert\NotNull()])]
public $value;
}

View File

@ -6,7 +6,7 @@ use Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\Source\GenericA
/**
* @GenericAnnotation(filters={
* @GenericAnnotation(sql="
* @GenericAnnotation(some="
* $this.id IN(
* SELECT id
* FROM foo
@ -27,7 +27,7 @@ namespace Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\FixturePh
use Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\Source\GenericAnnotation;
#[GenericAnnotation(filters: [new GenericAnnotation(sql: '
#[GenericAnnotation(filters: [new GenericAnnotation(some: '
$this.id IN(
SELECT id
FROM foo

View File

@ -25,7 +25,7 @@ use Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\SourcePhp81 as
final class VariousArguments
{
#[Assert\All(nestedAsserts: [new Assert\Length(1000), new Assert\NotNumber(hey: 10, hi: 'hello')])]
#[Assert\All([new Assert\Length(1000), new Assert\NotNumber(hey: 10, hi: 'hello')])]
public $value;
}

View File

@ -10,7 +10,7 @@ namespace Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\Source;
*/
final class GenericAnnotation
{
public function __construct($some)
public function __construct($some = null, $itemOperations = null, $collectionOperations = null, $graphql = null, $summary = null, $title = null, $route = null, $choices = [])
{
}
}

View File

@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\Source;
/**
* Single class for all the examples, as class name is often not the tested part
* @annotation
*/
final class GenericSingleImplicitAnnotation
{
public function __construct($singleValue)
{
}
}

View File

@ -6,7 +6,7 @@ namespace Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\SourcePhp
final class NotNumber
{
public function __construct($firstValue = null, $secondValue = null)
public function __construct($firstValue = null, $secondValue = null, $hey = null, $hi = null)
{
}
}

View File

@ -7,6 +7,7 @@ use Rector\Core\ValueObject\PhpVersionFeature;
use Rector\Php80\Rector\Class_\AnnotationToAttributeRector;
use Rector\Php80\ValueObject\AnnotationToAttribute;
use Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\Source\GenericAnnotation;
use Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\Source\GenericSingleImplicitAnnotation;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
return static function (ContainerConfigurator $containerConfigurator): void {
@ -19,6 +20,7 @@ return static function (ContainerConfigurator $containerConfigurator): void {
->configure([
// use always this annotation to test inner part of annotation - arguments, arrays, calls...
new AnnotationToAttribute(GenericAnnotation::class),
new AnnotationToAttribute(GenericSingleImplicitAnnotation::class),
new AnnotationToAttribute('inject', 'Nette\DI\Attributes\Inject'),
new AnnotationToAttribute('Symfony\Component\Routing\Annotation\Route'),

View File

@ -32,6 +32,8 @@ final class ShouldNotHappenException extends Exception
$method = $class !== null ? ($class . '::' . $function) : $function;
/** @var string $method */
/** @var int $line */
return sprintf('Look at "%s()" on line %d', $method, $line);
}
}

View File

@ -4,6 +4,8 @@ declare(strict_types=1);
namespace Rector\Core\NodeManipulator;
use Doctrine\ORM\Mapping\ManyToMany;
use Doctrine\ORM\Mapping\Table;
use PhpParser\Node;
use PhpParser\Node\Arg;
use PhpParser\Node\Expr;
@ -40,7 +42,7 @@ use Symplify\PackageBuilder\Php\TypeChecker;
final class PropertyManipulator
{
/**
* @var string[]
* @var string[]|class-string<Table>[]
*/
private const ALLOWED_NOT_READONLY_ANNOTATION_CLASS_OR_ATTRIBUTES = [
'Doctrine\ORM\Mapping\Entity',
@ -48,7 +50,7 @@ final class PropertyManipulator
];
/**
* @var string[]
* @var string[]|class-string<ManyToMany>[]
*/
private const ALLOWED_READONLY_ANNOTATION_CLASS_OR_ATTRIBUTES = [
'Doctrine\ORM\Mapping\Id',

View File

@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Mapping;
if (class_exists('Doctrine\ORM\Mapping\JoinColumn')) {
return;
}
final class JoinColumn
{
public function __construct($name, $referencedColumnName = null)
{
}
}

View File

@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Mapping;
if (class_exists('Doctrine\ORM\Mapping\ManyToMany')) {
return;
}
final class ManyToMany
{
public function __construct($targetEntity)
{
}
}

View File

@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Mapping;
if (class_exists('Doctrine\ORM\Mapping\Table')) {
return;
}
final class Table
{
public function __construct($name, $uniqueConstraints = [])
{
}
}

View File

@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Mapping;
if (class_exists('Doctrine\ORM\Mapping\UniqueConstraint')) {
return;
}
final class UniqueConstraint
{
public function __construct($name, $columns = [])
{
}
}

View File

@ -10,5 +10,8 @@ if (class_exists('Symfony\Component\Routing\Annotation\Route')) {
class Route
{
public function __construct($path, $name = '')
{
}
}