Skip to content

Commit

Permalink
[PHP 8.0] Include keys in annotation to attribute transformation (#1766)
Browse files Browse the repository at this point in the history
Co-authored-by: Morgan NEUTE <morgan.neute@niji.fr>
Co-authored-by: GitHub Action <action@github.com>
  • Loading branch information
3 people committed Feb 5, 2022
1 parent 0a63fe4 commit f374b9f
Show file tree
Hide file tree
Showing 42 changed files with 423 additions and 128 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@

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;
Expand All @@ -20,10 +24,23 @@ protected function setUp(): void

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);
}
}
15 changes: 10 additions & 5 deletions packages/PhpAttribute/AnnotationToAttributeMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@

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;

Expand All @@ -21,10 +23,7 @@ public function __construct(
) {
}

/**
* @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)) {
Expand All @@ -36,6 +35,12 @@ public function map(mixed $value): array|Expr|string
return $value;
}

return DocTagNodeState::REMOVE_ARRAY;
// remove node, as handled elsewhere
if ($value instanceof DoctrineAnnotationTagValueNode) {
return DocTagNodeState::REMOVE_ARRAY;
}

// fallback
return BuilderHelpers::normalizeValue($value);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,13 @@
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[]>
Expand All @@ -34,23 +37,41 @@ public function isCandidate(mixed $value): bool
/**
* @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;
}

Assert::isInstanceOf($valueExpr, Expr::class);

// remove value
if ($this->isRemoveArrayPlaceholder($singleValue)) {
continue;
}

unset($values[$key]);
$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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,14 @@
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>
Expand All @@ -34,11 +38,28 @@ public function isCandidate(mixed $value): bool
/**
* @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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,17 @@

namespace Rector\PhpAttribute\AnnotationToAttributeMapper;

use PhpParser\Node\Expr\Array_;
use PhpParser\Node\Expr\New_;
use PhpParser\Node\Name;
use Rector\BetterPhpDocParser\PhpDoc\DoctrineAnnotationTagValueNode;
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;

Expand All @@ -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,
) {
}

Expand Down Expand Up @@ -67,11 +68,12 @@ public function map($value): New_
$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 = [];
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,12 @@

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;

/**
Expand All @@ -20,8 +25,33 @@ public function isCandidate(mixed $value): bool
/**
* @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,
]);
}
}
79 changes: 79 additions & 0 deletions packages/PhpAttribute/AttributeArrayNameInliner.php
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,5 @@ public function isCandidate(mixed $value): bool;
/**
* @param T $value
*/
public function map($value): array|Expr;
public function map($value): Expr;
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
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;
Expand All @@ -29,8 +30,12 @@ public function __construct(
* @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;
}
Expand Down

0 comments on commit f374b9f

Please sign in to comment.