Skip to content

Commit

Permalink
[PHP 8.1] Add nested attributes support - part #1 (#1266)
Browse files Browse the repository at this point in the history
  • Loading branch information
TomasVotruba committed Nov 20, 2021
1 parent 60c06a7 commit f83d744
Show file tree
Hide file tree
Showing 26 changed files with 337 additions and 169 deletions.
Expand Up @@ -16,7 +16,6 @@ final class PhpAttributeGroupFactoryTest extends AbstractTestCase
protected function setUp(): void
{
$this->boot();

$this->phpAttributeGroupFactory = $this->getService(PhpAttributeGroupFactory::class);
}

Expand Down
Expand Up @@ -69,7 +69,7 @@ public function hasClassName(string $className): bool
return true;
}

// the name is not fully qualified in the original name, look for resolvd class attirubte
// the name is not fully qualified in the original name, look for resolved class attribute
$resolvedClass = $this->identifierTypeNode->getAttribute(PhpDocAttributeKey::RESOLVED_CLASS);
return $resolvedClass === $className;
}
Expand Down
Expand Up @@ -84,6 +84,7 @@ public function parseTagValue(TokenIterator $tokenIterator, string $tag): PhpDoc
{
$startPosition = $tokenIterator->currentPosition();
$tagValueNode = parent::parseTagValue($tokenIterator, $tag);

$endPosition = $tokenIterator->currentPosition();

$startAndEnd = new StartAndEnd($startPosition, $endPosition);
Expand Down
Expand Up @@ -7,6 +7,7 @@
use Nette\Utils\Strings;
use PhpParser\Node;
use PHPStan\PhpDocParser\Ast\PhpDoc\GenericTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocChildNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTextNode;
Expand Down Expand Up @@ -36,6 +37,12 @@ final class DoctrineAnnotationDecorator
*/
private const LONG_ANNOTATION_REGEX = '#@\\\\(?<class_name>.*?)(?<annotation_content>\(.*?\))#';

/**
* @see https://regex101.com/r/xWaLOz/1
* @var string
*/
private const NESTED_ANNOTATION_END_REGEX = '#(\s+)?\}\)(\s+)?#';

public function __construct(
private CurrentNodeProvider $currentNodeProvider,
private ClassAnnotationMatcher $classAnnotationMatcher,
Expand All @@ -54,7 +61,6 @@ public function decorate(PhpDocNode $phpDocNode): void

// merge split doctrine nested tags
$this->mergeNestedDoctrineAnnotations($phpDocNode);

$this->transformGenericTagValueNodesToDoctrineAnnotationTagValueNodes($phpDocNode, $currentPhpNode);
}

Expand Down Expand Up @@ -89,6 +95,24 @@ private function mergeNestedDoctrineAnnotations(PhpDocNode $phpDocNode): void
}

$nextPhpDocChildNode = $phpDocNode->children[$key];

if ($nextPhpDocChildNode instanceof PhpDocTextNode && Strings::match(
$nextPhpDocChildNode->text,
self::NESTED_ANNOTATION_END_REGEX
)) {
// @todo how to detect previously opened brackets?
// probably local property with holding count of opened brackets
$composedContent = $genericTagValueNode->value . PHP_EOL . $nextPhpDocChildNode->text;
$genericTagValueNode->value = $composedContent;

$startAndEnd = $this->combineStartAndEnd($phpDocChildNode, $nextPhpDocChildNode);
$phpDocChildNode->setAttribute(PhpDocAttributeKey::START_AND_END, $startAndEnd);

$removedKeys[] = $key;
$removedKeys[] = $key + 1;
continue;
}

if (! $nextPhpDocChildNode instanceof PhpDocTagNode) {
continue;
}
Expand All @@ -101,16 +125,12 @@ private function mergeNestedDoctrineAnnotations(PhpDocNode $phpDocNode): void
break;
}

$composedContent = $genericTagValueNode->value . PHP_EOL . $nextPhpDocChildNode->name . $nextPhpDocChildNode->value;
$genericTagValueNode->value = $composedContent;
$composedContent = $genericTagValueNode->value . PHP_EOL . $nextPhpDocChildNode->name . $nextPhpDocChildNode->value->value;

/** @var StartAndEnd $currentStartAndEnd */
$currentStartAndEnd = $phpDocChildNode->getAttribute(PhpDocAttributeKey::START_AND_END);

/** @var StartAndEnd $nextStartAndEnd */
$nextStartAndEnd = $nextPhpDocChildNode->getAttribute(PhpDocAttributeKey::START_AND_END);
// cleanup the next from closing
$genericTagValueNode->value = $composedContent;

$startAndEnd = new StartAndEnd($currentStartAndEnd->getStart(), $nextStartAndEnd->getEnd());
$startAndEnd = $this->combineStartAndEnd($phpDocChildNode, $nextPhpDocChildNode);
$phpDocChildNode->setAttribute(PhpDocAttributeKey::START_AND_END, $startAndEnd);

$currentChildValueNode = $phpDocNode->children[$key];
Expand Down Expand Up @@ -143,25 +163,11 @@ private function transformGenericTagValueNodesToDoctrineAnnotationTagValueNodes(
foreach ($phpDocNode->children as $key => $phpDocChildNode) {
// the @\FQN use case
if ($phpDocChildNode instanceof PhpDocTextNode) {
$match = Strings::match($phpDocChildNode->text, self::LONG_ANNOTATION_REGEX);
$fullyQualifiedAnnotationClass = $match['class_name'] ?? null;

if ($fullyQualifiedAnnotationClass === null) {
$spacelessPhpDocTagNode = $this->resolveFqnAnnotationSpacelessPhpDocTagNode($phpDocChildNode);
if (! $spacelessPhpDocTagNode instanceof SpacelessPhpDocTagNode) {
continue;
}

$annotationContent = $match['annotation_content'] ?? null;
$tagName = '@\\' . $fullyQualifiedAnnotationClass;

$formerStartEnd = $phpDocChildNode->getAttribute(PhpDocAttributeKey::START_AND_END);

$spacelessPhpDocTagNode = $this->createDoctrineSpacelessPhpDocTagNode(
$annotationContent,
$tagName,
$fullyQualifiedAnnotationClass,
$formerStartEnd
);

$phpDocNode->children[$key] = $spacelessPhpDocTagNode;
continue;
}
Expand Down Expand Up @@ -280,4 +286,39 @@ private function createDoctrineSpacelessPhpDocTagNode(

return new SpacelessPhpDocTagNode($tagName, $doctrineAnnotationTagValueNode);
}

private function combineStartAndEnd(
\PHPStan\PhpDocParser\Ast\Node $startPhpDocChildNode,
PhpDocChildNode $endPhpDocChildNode
): StartAndEnd {
/** @var StartAndEnd $currentStartAndEnd */
$currentStartAndEnd = $startPhpDocChildNode->getAttribute(PhpDocAttributeKey::START_AND_END);

/** @var StartAndEnd $nextStartAndEnd */
$nextStartAndEnd = $endPhpDocChildNode->getAttribute(PhpDocAttributeKey::START_AND_END);

return new StartAndEnd($currentStartAndEnd->getStart(), $nextStartAndEnd->getEnd());
}

private function resolveFqnAnnotationSpacelessPhpDocTagNode(PhpDocTextNode $phpDocTextNode): ?SpacelessPhpDocTagNode
{
$match = Strings::match($phpDocTextNode->text, self::LONG_ANNOTATION_REGEX);
$fullyQualifiedAnnotationClass = $match['class_name'] ?? null;

if ($fullyQualifiedAnnotationClass === null) {
return null;
}

$annotationContent = $match['annotation_content'] ?? null;
$tagName = '@\\' . $fullyQualifiedAnnotationClass;

$formerStartEnd = $phpDocTextNode->getAttribute(PhpDocAttributeKey::START_AND_END);

return $this->createDoctrineSpacelessPhpDocTagNode(
$annotationContent,
$tagName,
$fullyQualifiedAnnotationClass,
$formerStartEnd
);
}
}
Expand Up @@ -148,7 +148,6 @@ public function getValuesWithExplicitSilentAndWithoutQuotes(): array

foreach (array_keys($this->values) as $key) {
$valueWithoutQuotes = $this->getValueWithoutQuotes($key);

if (is_int($key) && $this->silentKey !== null) {
$explicitKeysValues[$this->silentKey] = $valueWithoutQuotes;
} else {
Expand Down
@@ -0,0 +1,11 @@
<?php

declare(strict_types=1);

namespace Rector\PhpAttribute\Exception;

use Exception;

final class InvalidNestedAttributeException extends Exception
{
}
20 changes: 12 additions & 8 deletions packages/PhpAttribute/Printer/PhpAttributeGroupFactory.php
Expand Up @@ -57,10 +57,9 @@ public function createFromClassWithItems(string $attributeClass, array $items):

public function create(
DoctrineAnnotationTagValueNode $doctrineAnnotationTagValueNode,
AnnotationToAttribute $annotationToAttribute
AnnotationToAttribute $annotationToAttribute,
): AttributeGroup {
$fullyQualified = new FullyQualified($annotationToAttribute->getAttributeClass());

$values = $doctrineAnnotationTagValueNode->getValuesWithExplicitSilentAndWithoutQuotes();

$args = $this->createArgsFromItems($values);
Expand All @@ -80,18 +79,14 @@ public function createArgsFromItems(array $items, ?string $silentKey = null): ar
{
$args = [];
if ($silentKey !== null && isset($items[$silentKey])) {
$silentValue = BuilderHelpers::normalizeValue($items[$silentKey]);
$this->normalizeStringDoubleQuote($silentValue);
$silentValue = $this->mapAnnotationValueToAttribute($items[$silentKey]);

$args[] = new Arg($silentValue);
unset($items[$silentKey]);
}

foreach ($items as $key => $value) {
$value = $this->valueNormalizer->normalize($value);
$value = BuilderHelpers::normalizeValue($value);

$this->normalizeStringDoubleQuote($value);
$value = $this->mapAnnotationValueToAttribute($value);

$name = null;
if (is_string($key)) {
Expand Down Expand Up @@ -161,4 +156,13 @@ private function completeNamedArguments(array $args, array $argumentNames): void
$arg->name = new Identifier($argumentName);
}
}

private function mapAnnotationValueToAttribute(mixed $annotationValue): Expr
{
$value = $this->valueNormalizer->normalize($annotationValue);
$value = BuilderHelpers::normalizeValue($value);
$this->normalizeStringDoubleQuote($value);

return $value;
}
}
65 changes: 53 additions & 12 deletions packages/PhpAttribute/Value/ValueNormalizer.php
Expand Up @@ -6,37 +6,43 @@

use PhpParser\Node\Expr;
use PhpParser\Node\Expr\ClassConstFetch;
use PhpParser\Node\Expr\New_;
use PhpParser\Node\Name;
use PhpParser\Node\Name\FullyQualified;
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprFalseNode;
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprIntegerNode;
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprNode;
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprTrueNode;
use PHPStan\PhpDocParser\Ast\Node;
use PHPStan\Type\Constant\ConstantBooleanType;
use PHPStan\Type\Constant\ConstantFloatType;
use Rector\BetterPhpDocParser\PhpDoc\DoctrineAnnotationTagValueNode;
use Rector\BetterPhpDocParser\ValueObject\PhpDoc\DoctrineAnnotation\CurlyListNode;
use Rector\BetterPhpDocParser\ValueObject\PhpDocAttributeKey;
use Rector\Core\Exception\ShouldNotHappenException;
use Rector\Core\Php\PhpVersionProvider;
use Rector\Core\ValueObject\PhpVersionFeature;
use Rector\PhpAttribute\Exception\InvalidNestedAttributeException;

final class ValueNormalizer
{
public function __construct(
private PhpVersionProvider $phpVersionProvider
) {
}

/**
* @param mixed $value
* @return array<mixed>
*/
public function normalize($value): bool | float | int | string | array | Expr
{
if ($value instanceof ConstExprIntegerNode) {
return (int) $value->value;
if ($value instanceof DoctrineAnnotationTagValueNode) {
return $this->normalizeDoctrineAnnotationTagValueNode($value);
}

if ($value instanceof ConstantFloatType || $value instanceof ConstantBooleanType) {
return $value->getValue();
}

if ($value instanceof ConstExprTrueNode) {
return true;
}

if ($value instanceof ConstExprFalseNode) {
return false;
if ($value instanceof ConstExprNode) {
return $this->normalizeConstrExprNode($value);
}

if ($value instanceof CurlyListNode) {
Expand All @@ -62,4 +68,39 @@ public function normalize($value): bool | float | int | string | array | Expr

return $value;
}

private function normalizeDoctrineAnnotationTagValueNode(
DoctrineAnnotationTagValueNode $doctrineAnnotationTagValueNode
): New_ {
// if PHP 8.0- throw exception
if (! $this->phpVersionProvider->isAtLeastPhpVersion(PhpVersionFeature::NEW_INITIALIZERS)) {
throw new InvalidNestedAttributeException();
}

$resolveClass = $doctrineAnnotationTagValueNode->identifierTypeNode->getAttribute(
PhpDocAttributeKey::RESOLVED_CLASS
);
return new New_(new FullyQualified($resolveClass));
}

private function normalizeConstrExprNode(ConstExprNode $constExprNode): int|bool|float
{
if ($constExprNode instanceof ConstExprIntegerNode) {
return (int) $constExprNode->value;
}

if ($constExprNode instanceof ConstantFloatType || $constExprNode instanceof ConstantBooleanType) {
return $constExprNode->getValue();
}

if ($constExprNode instanceof ConstExprTrueNode) {
return true;
}

if ($constExprNode instanceof ConstExprFalseNode) {
return false;
}

throw new ShouldNotHappenException();
}
}

This file was deleted.

0 comments on commit f83d744

Please sign in to comment.