Various doc parser fixes (#6125)

This commit is contained in:
Tomas Votruba 2021-04-13 20:51:41 +02:00 committed by GitHub
parent 22d35689bf
commit 5bde10c181
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 319 additions and 105 deletions

View File

@ -0,0 +1,3 @@
/**
* @foor a,
*/

View File

@ -38,7 +38,7 @@ final class ArrayParserTest extends AbstractKernelTestCase
{
$betterTokenIterator = $this->tokenIteratorFactory->create($docContent);
$array = $this->arrayParser->parserArray($betterTokenIterator);
$array = $this->arrayParser->parseCurlyArray($betterTokenIterator);
$this->assertSame($expectedArray, $array);
}

View File

@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace Rector\Tests\BetterPhpDocParser\PhpDocParser\TagValueNodeReprint\Fixture;
use Doctrine\ORM\Mapping as ORM;
use Rector\Tests\BetterPhpDocParser\PhpDocParser\TagValueNodeReprint\Source\Embeddable;
final class AnEntityWithAnEmbedded
{
/**
* @ORM\Embedded(class="Embeddable"),
*/
private $embedded;
}
?>
-----
PhpParser\Node\Stmt\Property
-----
Doctrine\ORM\Mapping\Embedded

View File

@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace Rector\Tests\BetterPhpDocParser\PhpDocParser\TagValueNodeReprint\Fixture;
use Annotation\NotFoundErrorResponse;
use Annotation\OpenApi;
final class AnEntityWithAnEmbedded
{
/**
* @NotFoundErrorResponse( "collection" ) ,
*
* @OpenApi("
* summary: Delete collection
* ")
*/
private $embedded;
}
?>
-----
PhpParser\Node\Stmt\Property
-----
Annotation\NotFoundErrorResponse

View File

@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace Rector\Tests\BetterPhpDocParser\PhpDocParser\TagValueNodeReprint\Fixture;
use Annotation\NotFoundErrorResponse;
use Annotation\OpenApi;
final class AnEntityWithAnEmbeddedWithoutSpaces
{
/**
* @NotFoundErrorResponse("collection"),
*
* @OpenApi("
* summary: Delete collection
* ")
*/
private $embedded;
}
?>
-----
PhpParser\Node\Stmt\Property
-----
Annotation\NotFoundErrorResponse

View File

@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace Rector\Tests\BetterPhpDocParser\PhpDocParser\TagValueNodeReprint\Fixture;
use Doctrine\ORM\Mapping as ORM;
use Swagger\Annotations as SWG;
/**
* Long running task to track progress.
*
* @ORM\Entity(repositoryClass="Application\Api\Admin\Repository\AdminTaskRepository")
* @ORM\Table(
* name="admin_task",
* options={"collate": "utf8_bin", "charset": "utf8"},
* )
*
* @SWG\Definition(required={"id", "created", "modified", "status", "progress", "completed"})
*/
final class Mixture
{
}
?>
-----
PhpParser\Node\Stmt\Class_
-----
Swagger\Annotations\Definition

View File

@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace Rector\Tests\BetterPhpDocParser\PhpDocParser\TagValueNodeReprint\Fixture;
/**
* @Api\ApiResource(
* normalizationContext={"groups"={"default"}}
* )
*/
final class QuotedKeysAndValues
{
}
?>
-----
PhpParser\Node\Stmt\Class_
-----
Api\ApiResource

View File

@ -9,6 +9,7 @@ use PhpParser\Node;
use PHPStan\PhpDocParser\Ast\PhpDoc\GenericTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagNode;
use PHPStan\PhpDocParser\Lexer\Lexer;
use Rector\BetterPhpDocParser\Attributes\AttributeMirrorer;
use Rector\BetterPhpDocParser\PhpDoc\DoctrineAnnotationTagValueNode;
use Rector\BetterPhpDocParser\PhpDoc\SpacelessPhpDocTagNode;
@ -21,12 +22,6 @@ use Rector\Core\Exception\ShouldNotHappenException;
final class DoctrineAnnotationDecorator
{
/**
* @see https://regex101.com/r/jHF5D9/1
* @var string
*/
private const OPEN_ANNOTATION_SUFFIX_REGEX = '#(\{|\,)$#';
/**
* @var CurrentNodeProvider
*/
@ -76,6 +71,91 @@ final class DoctrineAnnotationDecorator
// merge split doctrine nested tags
$this->mergeNestedDoctrineAnnotations($phpDocNode);
$this->transformGenericTagValueNodesToDoctrineAnnotationTagValueNodes($phpDocNode, $currentPhpNode);
}
/**
* Join token iterator with all the following nodes if nested
*/
private function mergeNestedDoctrineAnnotations(PhpDocNode $phpDocNode): void
{
$removedKeys = [];
foreach ($phpDocNode->children as $key => $phpDocChildNode) {
if (in_array($key, $removedKeys, true)) {
continue;
}
if (! $phpDocChildNode instanceof PhpDocTagNode) {
continue;
}
if (! $phpDocChildNode->value instanceof GenericTagValueNode) {
continue;
}
$genericTagValueNode = $phpDocChildNode->value;
while (isset($phpDocNode->children[$key])) {
++$key;
// no more next nodes
if (! isset($phpDocNode->children[$key])) {
break;
}
$nextPhpDocChildNode = $phpDocNode->children[$key];
if (! $nextPhpDocChildNode instanceof PhpDocTagNode) {
continue;
}
if (! $nextPhpDocChildNode->value instanceof GenericTagValueNode) {
continue;
}
if ($this->isClosedContent($genericTagValueNode->value)) {
break;
}
$composedContent = $genericTagValueNode->value . PHP_EOL . $nextPhpDocChildNode->name . $nextPhpDocChildNode->value;
$genericTagValueNode->value = $composedContent;
/** @var StartAndEnd $currentStartAndEnd */
$currentStartAndEnd = $phpDocChildNode->getAttribute(PhpDocAttributeKey::START_AND_END);
/** @var StartAndEnd $nextStartAndEnd */
$nextStartAndEnd = $nextPhpDocChildNode->getAttribute(PhpDocAttributeKey::START_AND_END);
$startAndEnd = new StartAndEnd($currentStartAndEnd->getStart(), $nextStartAndEnd->getEnd());
$phpDocChildNode->setAttribute(PhpDocAttributeKey::START_AND_END, $startAndEnd);
$currentChildValueNode = $phpDocNode->children[$key];
if (! $currentChildValueNode instanceof PhpDocTagNode) {
continue;
}
$currentGenericTagValueNode = $currentChildValueNode->value;
if (! $currentGenericTagValueNode instanceof GenericTagValueNode) {
continue;
}
$removedKeys[] = $key;
}
}
foreach (array_keys($phpDocNode->children) as $key) {
if (! in_array($key, $removedKeys, true)) {
continue;
}
unset($phpDocNode->children[$key]);
}
}
private function transformGenericTagValueNodesToDoctrineAnnotationTagValueNodes(
PhpDocNode $phpDocNode,
Node $currentPhpNode
): void {
foreach ($phpDocNode->children as $key => $phpDocChildNode) {
if (! $phpDocChildNode instanceof PhpDocTagNode) {
continue;
@ -111,6 +191,7 @@ final class DoctrineAnnotationDecorator
$values,
SilentKeyMap::CLASS_NAMES_TO_SILENT_KEYS[$fullyQualifiedAnnotationClass] ?? null
);
$doctrineAnnotationTagValueNode->setAttribute(PhpDocAttributeKey::START_AND_END, $formerStartEnd);
$spacelessPhpDocTagNode = new SpacelessPhpDocTagNode(
@ -124,76 +205,36 @@ final class DoctrineAnnotationDecorator
}
/**
* Join token iterator with all the following nodes if nested
* This is closed block, e.g. {( ... )},
* false on: {( ... )
*/
private function mergeNestedDoctrineAnnotations(PhpDocNode $phpDocNode): void
private function isClosedContent(string $composedContent): bool
{
$removedKeys = [];
$composedTokenIterator = $this->tokenIteratorFactory->create($composedContent);
$tokenCount = $composedTokenIterator->count();
foreach ($phpDocNode->children as $key => $phpDocChildNode) {
if (in_array($key, $removedKeys, true)) {
continue;
$openBracketCount = 0;
$closeBracketCount = 0;
do {
if ($composedTokenIterator->isCurrentTokenTypes([
Lexer::TOKEN_OPEN_CURLY_BRACKET,
Lexer::TOKEN_OPEN_PARENTHESES,
]) || Strings::contains($composedTokenIterator->currentTokenValue(), '(')) {
++$openBracketCount;
}
if (! $phpDocChildNode instanceof PhpDocTagNode) {
continue;
if ($composedTokenIterator->isCurrentTokenTypes([
Lexer::TOKEN_CLOSE_CURLY_BRACKET,
Lexer::TOKEN_CLOSE_PARENTHESES,
// sometimes it gets mixed int ")
]) || Strings::contains($composedTokenIterator->currentTokenValue(), ')')) {
++$closeBracketCount;
}
if (! $phpDocChildNode->value instanceof GenericTagValueNode) {
continue;
}
$composedTokenIterator->next();
} while ($composedTokenIterator->currentPosition() < ($tokenCount - 1));
$genericTagValueNode = $phpDocChildNode->value;
/** @var GenericTagValueNode $currentGenericTagValueNode */
$currentGenericTagValueNode = $genericTagValueNode;
while (Strings::match($currentGenericTagValueNode->value, self::OPEN_ANNOTATION_SUFFIX_REGEX)) {
$nextPhpDocChildNode = $phpDocNode->children[$key + 1];
if (! $nextPhpDocChildNode instanceof PhpDocTagNode) {
continue;
}
if (! $nextPhpDocChildNode->value instanceof GenericTagValueNode) {
continue;
}
$genericTagValueNode->value .= PHP_EOL . $nextPhpDocChildNode->name . $nextPhpDocChildNode->value;
/** @var StartAndEnd $currentStartAndEnd */
$currentStartAndEnd = $phpDocChildNode->getAttribute(PhpDocAttributeKey::START_AND_END);
/** @var StartAndEnd $nextStartAndEnd */
$nextStartAndEnd = $nextPhpDocChildNode->getAttribute(PhpDocAttributeKey::START_AND_END);
$startAndEnd = new StartAndEnd($currentStartAndEnd->getStart(), $nextStartAndEnd->getEnd());
$phpDocChildNode->setAttribute(PhpDocAttributeKey::START_AND_END, $startAndEnd);
++$key;
if (! isset($phpDocNode->children[$key])) {
break;
}
$currentChildValueNode = $phpDocNode->children[$key];
if (! $currentChildValueNode instanceof PhpDocTagNode) {
continue;
}
$currentGenericTagValueNode = $currentChildValueNode->value;
if (! $currentGenericTagValueNode instanceof GenericTagValueNode) {
continue;
}
$removedKeys[] = $key;
}
}
foreach (array_keys($phpDocNode->children) as $key) {
if (! in_array($key, $removedKeys, true)) {
continue;
}
unset($phpDocNode->children[$key]);
}
return $openBracketCount === $closeBracketCount;
}
}

View File

@ -116,7 +116,7 @@ final class StaticDoctrineAnnotationParser
private function parseValue(BetterTokenIterator $tokenIterator)
{
if ($tokenIterator->isCurrentTokenType(Lexer::TOKEN_OPEN_CURLY_BRACKET)) {
$items = $this->arrayParser->parserArray($tokenIterator);
$items = $this->arrayParser->parseCurlyArray($tokenIterator);
return new CurlyListNode($items);
}

View File

@ -26,10 +26,15 @@ final class ArrayParser
* Mimics https://github.com/doctrine/annotations/blob/c66f06b7c83e9a2a7523351a9d5a4b55f885e574/lib/Doctrine/Common/Annotations/DocParser.php#L1305-L1352
* @return mixed[]
*/
public function parserArray(BetterTokenIterator $tokenIterator): array
public function parseCurlyArray(BetterTokenIterator $tokenIterator): array
{
$values = [];
// nothing
if ($tokenIterator->isCurrentTokenType(Lexer::TOKEN_CLOSE_CURLY_BRACKET)) {
return [];
}
$tokenIterator->consumeTokenType(Lexer::TOKEN_OPEN_CURLY_BRACKET);
// If the array is empty, stop parsing and return.

View File

@ -33,6 +33,11 @@ final class PlainValueParser
*/
private $currentNodeProvider;
/**
* @var ArrayParser
*/
private $arrayParser;
public function __construct(
ClassAnnotationMatcher $classAnnotationMatcher,
CurrentNodeProvider $currentNodeProvider
@ -44,9 +49,12 @@ final class PlainValueParser
/**
* @required
*/
public function autowirePlainValueParser(StaticDoctrineAnnotationParser $staticDoctrineAnnotationParser): void
{
public function autowirePlainValueParser(
StaticDoctrineAnnotationParser $staticDoctrineAnnotationParser,
ArrayParser $arrayParser
): void {
$this->staticDoctrineAnnotationParser = $staticDoctrineAnnotationParser;
$this->arrayParser = $arrayParser;
}
/**
@ -62,6 +70,11 @@ final class PlainValueParser
}
// consume the token
$isOpenCurlyArray = $tokenIterator->isCurrentTokenType(Lexer::TOKEN_OPEN_CURLY_BRACKET);
if ($isOpenCurlyArray) {
return $this->arrayParser->parseCurlyArray($tokenIterator);
}
$tokenIterator->next();
// normalize value
@ -86,27 +99,34 @@ final class PlainValueParser
// nested entity!
if ($tokenIterator->isCurrentTokenType(Lexer::TOKEN_OPEN_PARENTHESES)) {
// @todo
$annotationShortName = $currentTokenValue;
$values = $this->staticDoctrineAnnotationParser->resolveAnnotationMethodCall($tokenIterator);
$currentNode = $this->currentNodeProvider->getNode();
if (! $currentNode instanceof Node) {
throw new ShouldNotHappenException();
}
$fullyQualifiedAnnotationClass = $this->classAnnotationMatcher->resolveTagFullyQualifiedName(
$annotationShortName,
$currentNode
);
// keep the last ")"
$tokenIterator->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
$tokenIterator->consumeTokenType(Lexer::TOKEN_CLOSE_PARENTHESES);
return new DoctrineAnnotationTagValueNode($fullyQualifiedAnnotationClass, $annotationShortName, $values);
return $this->parseNestedDoctrineAnnotationTagValueNode($currentTokenValue, $tokenIterator);
}
return $currentTokenValue;
}
private function parseNestedDoctrineAnnotationTagValueNode(
string $currentTokenValue,
BetterTokenIterator $tokenIterator
): DoctrineAnnotationTagValueNode {
// @todo
$annotationShortName = $currentTokenValue;
$values = $this->staticDoctrineAnnotationParser->resolveAnnotationMethodCall($tokenIterator);
$currentNode = $this->currentNodeProvider->getNode();
if (! $currentNode instanceof Node) {
throw new ShouldNotHappenException();
}
$fullyQualifiedAnnotationClass = $this->classAnnotationMatcher->resolveTagFullyQualifiedName(
$annotationShortName,
$currentNode
);
// keep the last ")"
$tokenIterator->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
$tokenIterator->consumeTokenType(Lexer::TOKEN_CLOSE_PARENTHESES);
return new DoctrineAnnotationTagValueNode($fullyQualifiedAnnotationClass, $annotationShortName, $values);
}
}

View File

@ -54,6 +54,20 @@ final class BetterTokenIterator extends TokenIterator
return false;
}
/**
* @param int[] $tokenTypes
*/
public function isCurrentTokenTypes(array $tokenTypes): bool
{
foreach ($tokenTypes as $tokenType) {
if ($this->isCurrentTokenType($tokenType)) {
return true;
}
}
return false;
}
public function isTokenTypeOnPosition(int $tokenType, int $position): bool
{
$tokens = $this->getTokens();
@ -112,17 +126,15 @@ final class BetterTokenIterator extends TokenIterator
public function nextTokenType(): ?int
{
$this->pushSavePoint();
$tokens = $this->getTokens();
$index = $this->privatesAccessor->getPrivateProperty($this, self::INDEX);
// does next token exist?
$nextIndex = $index + 1;
$nextIndex = $this->currentPosition() + 1;
if (! isset($tokens[$nextIndex])) {
return null;
}
$this->pushSavePoint();
$this->next();
$nextTokenType = $this->currentTokenType();
$this->rollback();

View File

@ -39,6 +39,10 @@ final class CurlyListNode extends AbstractValuesAwareNode
return 'true';
}
if (is_array($value)) {
return implode(', ', $value);
}
return (string) $value;
}
}

View File

@ -29,6 +29,16 @@ use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
*/
final class DispatchStringToObjectRector extends AbstractRector
{
/**
* @var string
*/
private const STMTS = 'stmts';
/**
* @var string
*/
private const NAME = 'name';
public function getRuleDefinition(): RuleDefinition
{
return new RuleDefinition(
@ -119,7 +129,7 @@ CODE_SAMPLE
return new New_(new Class_(null, [
'implements' => $implements,
'stmts' => $this->createAnonymousEventClassBody(),
self::STMTS => $this->createAnonymousEventClassBody(),
]), [new Arg($expr)]);
}
@ -129,16 +139,16 @@ CODE_SAMPLE
private function createAnonymousEventClassBody(): array
{
return [
new Property(Class_::MODIFIER_PRIVATE, [new PropertyProperty('name')]),
new Property(Class_::MODIFIER_PRIVATE, [new PropertyProperty(self::NAME)]),
new ClassMethod('__construct', [
'flags' => Class_::MODIFIER_PUBLIC,
'params' => $this->createConstructParams(),
'stmts' => [new Expression($this->createConstructAssign())],
self::STMTS => [new Expression($this->createConstructAssign())],
]),
new ClassMethod('eventName', [
'flags' => Class_::MODIFIER_PUBLIC,
'returnType' => 'string',
'stmts' => [new Return_(new Variable('this->name'))],
self::STMTS => [new Return_(new Variable('this->name'))],
]),
];
}
@ -148,13 +158,11 @@ CODE_SAMPLE
*/
private function createConstructParams(): array
{
return [
new Param(new Variable('name'), null, 'string')
];
return [new Param(new Variable(self::NAME), null, 'string')];
}
private function createConstructAssign(): Assign
{
return new Assign(new Variable('this->name'), new Variable('name'));
return new Assign(new Variable('this->name'), new Variable(self::NAME));
}
}