Skip to content

Commit

Permalink
Improve string support in Doctrine Annotations (#3645)
Browse files Browse the repository at this point in the history
  • Loading branch information
yguedidi committed Jun 2, 2023
1 parent 7cde413 commit 92ff716
Show file tree
Hide file tree
Showing 22 changed files with 247 additions and 66 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use PhpParser\Node\Scalar\String_;
use PHPUnit\Framework\Attributes\DataProvider;
use Rector\BetterPhpDocParser\PhpDoc\ArrayItemNode;
use Rector\BetterPhpDocParser\PhpDoc\StringNode;
use Rector\BetterPhpDocParser\PhpDocInfo\TokenIteratorFactory;
use Rector\BetterPhpDocParser\PhpDocParser\StaticDoctrineAnnotationParser\ArrayParser;
use Rector\Testing\PHPUnit\AbstractTestCase;
Expand Down Expand Up @@ -40,15 +41,15 @@ public function test(string $docContent, array $expectedArrayItemNodes): void

public static function provideData(): Iterator
{
yield ['{key: "value"}', [new ArrayItemNode('value', 'key', String_::KIND_DOUBLE_QUOTED)]];
yield ['{key: "value"}', [new ArrayItemNode(new StringNode('value'), 'key')]];

yield ['{"key": "value"}', [
new ArrayItemNode('value', 'key', String_::KIND_DOUBLE_QUOTED, String_::KIND_DOUBLE_QUOTED),
new ArrayItemNode(new StringNode('value'), new StringNode('key')),
]];

yield ['{"value", "value2"}', [
new ArrayItemNode('value', null, String_::KIND_DOUBLE_QUOTED),
new ArrayItemNode('value2', null, String_::KIND_DOUBLE_QUOTED),
new ArrayItemNode(new StringNode('value')),
new ArrayItemNode(new StringNode('value2')),
]];
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use PhpParser\Node\Scalar\String_;
use PHPUnit\Framework\Attributes\DataProvider;
use Rector\BetterPhpDocParser\PhpDoc\ArrayItemNode;
use Rector\BetterPhpDocParser\PhpDoc\StringNode;
use Rector\BetterPhpDocParser\PhpDocInfo\TokenIteratorFactory;
use Rector\BetterPhpDocParser\PhpDocParser\StaticDoctrineAnnotationParser;
use Rector\BetterPhpDocParser\ValueObject\PhpDoc\DoctrineAnnotation\CurlyListNode;
Expand Down Expand Up @@ -44,8 +45,8 @@ public function test(string $docContent, CurlyListNode | array $expectedValue):
public static function provideData(): Iterator
{
$curlyListNode = new CurlyListNode([
new ArrayItemNode('chalet', null, String_::KIND_DOUBLE_QUOTED),
new ArrayItemNode('apartment', null, String_::KIND_DOUBLE_QUOTED),
new ArrayItemNode(new StringNode('chalet')),
new ArrayItemNode(new StringNode('apartment')),
]);
yield ['{"chalet", "apartment"}', $curlyListNode];

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use PhpParser\Node\Stmt\ClassMethod;
use Rector\BetterPhpDocParser\PhpDoc\ArrayItemNode;
use Rector\BetterPhpDocParser\PhpDoc\DoctrineAnnotationTagValueNode;
use Rector\BetterPhpDocParser\PhpDoc\StringNode;
use Rector\BetterPhpDocParser\PhpDocInfo\PhpDocInfo;
use Rector\BetterPhpDocParser\PhpDocInfo\PhpDocInfoFactory;
use Rector\BetterPhpDocParser\Printer\PhpDocInfoPrinter;
Expand Down Expand Up @@ -56,8 +57,8 @@ public function test(): void

// this will extended tokens of first node
$methodsCurlyListNode = new CurlyListNode([
new ArrayItemNode('GET', null, String_::KIND_DOUBLE_QUOTED),
new ArrayItemNode('HEAD', null, String_::KIND_DOUBLE_QUOTED),
new ArrayItemNode(new StringNode('GET')),
new ArrayItemNode(new StringNode('HEAD')),
]);
$doctrineAnnotationTagValueNode->values[] = new ArrayItemNode($methodsCurlyListNode, 'methods');

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use PhpParser\Node\Arg;
use PhpParser\Node\AttributeGroup;
use Rector\BetterPhpDocParser\PhpDoc\ArrayItemNode;
use Rector\BetterPhpDocParser\PhpDoc\StringNode;
use Rector\PhpAttribute\NodeFactory\PhpAttributeGroupFactory;
use Rector\Testing\PHPUnit\AbstractTestCase;

Expand Down Expand Up @@ -36,8 +37,8 @@ public function testCreateFromClassWithItems(): void
public function testCreateArgsFromItems(): void
{
$args = $this->phpAttributeGroupFactory->createArgsFromItems([
new ArrayItemNode('/path', 'path'),
new ArrayItemNode('action', 'name'),
new ArrayItemNode(new StringNode('/path'), 'path'),
new ArrayItemNode(new StringNode('action'), 'name'),
], 'SomeClass');

$this->assertCount(2, $args);
Expand Down
16 changes: 3 additions & 13 deletions packages/BetterPhpDocParser/PhpDoc/ArrayItemNode.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,31 +13,21 @@ final class ArrayItemNode implements PhpDocTagValueNode, Stringable
{
use NodeAttributes;

/**
* @param String_::KIND_*|null $kindValueQuoted
*/
public function __construct(
public mixed $value,
public mixed $key,
public int|null $kindValueQuoted = null,
public int|null $kindKeyQuoted = null,
public mixed $key = null,
) {
}

public function __toString(): string
{
$value = '';

if ($this->kindKeyQuoted === String_::KIND_DOUBLE_QUOTED) {
$value .= '"' . $this->key . '" = ';
} elseif ($this->key !== null) {
if ($this->key !== null) {
$value .= $this->key . '=';
}

// @todo depends on the context! possibly the top array is quting this stinrg already
if ($this->kindValueQuoted === String_::KIND_DOUBLE_QUOTED) {
$value .= '"' . $this->value . '"';
} elseif (is_array($this->value)) {
if (is_array($this->value)) {
foreach ($this->value as $singleValue) {
$value .= $singleValue;
}
Expand Down
35 changes: 35 additions & 0 deletions packages/BetterPhpDocParser/PhpDoc/StringNode.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php

declare(strict_types=1);

namespace Rector\BetterPhpDocParser\PhpDoc;

use PhpParser\Node\Scalar\String_;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagValueNode;
use Rector\NodeTypeResolver\Node\AttributeKey;
use Stringable;

final class StringNode implements PhpDocTagValueNode, Stringable
{
use NodeAttributes;

public function __construct(
public string $value,
) {
$this->value = str_replace('""', '"', $this->value);

if (str_contains($this->value, "'") && ! str_contains($this->value, "\n")) {
$kind = String_::KIND_DOUBLE_QUOTED;
} else {
$kind = String_::KIND_SINGLE_QUOTED;
}

$this->setAttribute(AttributeKey::KIND, $kind);
}

public function __toString(): string
{
return '"' . str_replace('"', '""', $this->value) . '"';
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use PhpParser\Node;
use Rector\BetterPhpDocParser\PhpDoc\ArrayItemNode;
use Rector\BetterPhpDocParser\PhpDoc\DoctrineAnnotationTagValueNode;
use Rector\BetterPhpDocParser\PhpDoc\StringNode;
use Rector\BetterPhpDocParser\PhpDocInfo\PhpDocInfo;
use Rector\BetterPhpDocParser\PhpDocParser\ClassAnnotationMatcher;
use Rector\BetterPhpDocParser\ValueObject\PhpDoc\DoctrineAnnotation\CurlyListNode;
Expand Down Expand Up @@ -59,13 +60,18 @@ private function processAssertChoiceTagValueNode(array $oldToNewClasses, PhpDocI

$callableCallbackArrayItems = $callbackClass->getValues();
$classNameArrayItemNode = $callableCallbackArrayItems[0];
$classNameStringNode = $classNameArrayItemNode->value;

if (! $classNameStringNode instanceof StringNode) {
return;
}

foreach ($oldToNewClasses as $oldClass => $newClass) {
if ($classNameArrayItemNode->value !== $oldClass) {
if ($classNameStringNode->value !== $oldClass) {
continue;
}

$classNameArrayItemNode->value = $newClass;
$classNameStringNode->value = $newClass;

// trigger reprint
$classNameArrayItemNode->setAttribute(PhpDocAttributeKey::ORIG_NODE, null);
Expand Down Expand Up @@ -107,14 +113,16 @@ private function processSerializerTypeTagValueNode(array $oldToNewClasses, PhpDo
$classNameArrayItemNode = $doctrineAnnotationTagValueNode->getSilentValue();

foreach ($oldToNewClasses as $oldClass => $newClass) {
if ($classNameArrayItemNode instanceof ArrayItemNode) {
if ($classNameArrayItemNode->value === $oldClass) {
$classNameArrayItemNode->value = $newClass;
if ($classNameArrayItemNode instanceof ArrayItemNode && $classNameArrayItemNode->value instanceof StringNode) {
$classNameStringNode = $classNameArrayItemNode->value;

if ($classNameStringNode->value === $oldClass) {
$classNameStringNode->value = $newClass;
continue;
}

$classNameArrayItemNode->value = Strings::replace(
$classNameArrayItemNode->value,
$classNameStringNode->value = Strings::replace(
$classNameStringNode->value,
'#\b' . preg_quote($oldClass, '#') . '\b#',
$newClass
);
Expand All @@ -127,8 +135,13 @@ private function processSerializerTypeTagValueNode(array $oldToNewClasses, PhpDo
continue;
}

if ($currentTypeArrayItemNode->value === $oldClass) {
$currentTypeArrayItemNode->value = $newClass;
$currentTypeStringNode = $currentTypeArrayItemNode->value;
if (! $currentTypeStringNode instanceof StringNode) {
continue;
}

if ($currentTypeStringNode->value === $oldClass) {
$currentTypeStringNode->value = $newClass;
}
}
}
Expand All @@ -150,7 +163,12 @@ private function processDoctrineToMany(
return;
}

$targetEntityClass = $targetEntityArrayItemNode->value;
$targetEntityStringNode = $targetEntityArrayItemNode->value;
if (! $targetEntityStringNode instanceof StringNode) {
return;
}

$targetEntityClass = $targetEntityStringNode->value;

// resolve to FQN
$tagFullyQualifiedName = $this->classAnnotationMatcher->resolveTagFullyQualifiedName($targetEntityClass, $node);
Expand All @@ -160,7 +178,7 @@ private function processDoctrineToMany(
continue;
}

$targetEntityArrayItemNode->value = $newClass;
$targetEntityStringNode->value = $newClass;
$targetEntityArrayItemNode->setAttribute(PhpDocAttributeKey::ORIG_NODE, null);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use PHPStan\PhpDocParser\Lexer\Lexer;
use Rector\BetterPhpDocParser\PhpDoc\ArrayItemNode;
use Rector\BetterPhpDocParser\PhpDoc\DoctrineAnnotationTagValueNode;
use Rector\BetterPhpDocParser\PhpDoc\StringNode;
use Rector\BetterPhpDocParser\PhpDocParser\StaticDoctrineAnnotationParser\ArrayParser;
use Rector\BetterPhpDocParser\PhpDocParser\StaticDoctrineAnnotationParser\PlainValueParser;
use Rector\BetterPhpDocParser\ValueObject\Parser\BetterTokenIterator;
Expand Down Expand Up @@ -49,11 +50,11 @@ public function resolveAnnotationMethodCall(BetterTokenIterator $tokenIterator):
/**
* @api tests
* @see https://github.com/doctrine/annotations/blob/c66f06b7c83e9a2a7523351a9d5a4b55f885e574/lib/Doctrine/Common/Annotations/DocParser.php#L1215-L1224
* @return CurlyListNode|string|array<mixed>|ConstExprNode|DoctrineAnnotationTagValueNode
* @return CurlyListNode|string|array<mixed>|ConstExprNode|DoctrineAnnotationTagValueNode|StringNode
*/
public function resolveAnnotationValue(
BetterTokenIterator $tokenIterator
): CurlyListNode | string | array | ConstExprNode | DoctrineAnnotationTagValueNode {
): CurlyListNode | string | array | ConstExprNode | DoctrineAnnotationTagValueNode | StringNode {
// skips dummy tokens like newlines
$tokenIterator->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);

Expand Down Expand Up @@ -118,11 +119,11 @@ private function resolveAnnotationValues(BetterTokenIterator $tokenIterator): ar
}

/**
* @return CurlyListNode|string|array<mixed>|ConstExprNode|DoctrineAnnotationTagValueNode
* @return CurlyListNode|string|array<mixed>|ConstExprNode|DoctrineAnnotationTagValueNode|StringNode
*/
private function parseValue(
BetterTokenIterator $tokenIterator
): CurlyListNode | string | array | ConstExprNode | DoctrineAnnotationTagValueNode {
): CurlyListNode | string | array | ConstExprNode | DoctrineAnnotationTagValueNode | StringNode {
if ($tokenIterator->isCurrentTokenType(Lexer::TOKEN_OPEN_CURLY_BRACKET)) {
$items = $this->arrayParser->parseCurlyArray($tokenIterator);
return new CurlyListNode($items);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprIntegerNode;
use PHPStan\PhpDocParser\Lexer\Lexer;
use Rector\BetterPhpDocParser\PhpDoc\ArrayItemNode;
use Rector\BetterPhpDocParser\PhpDoc\StringNode;
use Rector\BetterPhpDocParser\ValueObject\Parser\BetterTokenIterator;

/**
Expand Down Expand Up @@ -176,25 +177,29 @@ private function resolveQuoteKind(mixed $val): ?int
return null;
}

private function createArrayItemFromKeyAndValue(mixed $key, mixed $value): ArrayItemNode
private function createArrayItemFromKeyAndValue(mixed $rawKey, mixed $rawValue): ArrayItemNode
{
$valueQuoteKind = $this->resolveQuoteKind($value);
if (is_string($value) && $valueQuoteKind === String_::KIND_DOUBLE_QUOTED) {
$valueQuoteKind = $this->resolveQuoteKind($rawValue);
if (is_string($rawValue) && $valueQuoteKind === String_::KIND_DOUBLE_QUOTED) {
// give raw value
$value = trim($value, '"');
$value = new StringNode(substr($rawValue, 1, strlen($rawValue) - 2));
} else {
$value = $rawValue;
}

$keyQuoteKind = $this->resolveQuoteKind($key);
if (is_string($key) && $keyQuoteKind === String_::KIND_DOUBLE_QUOTED) {
$keyQuoteKind = $this->resolveQuoteKind($rawKey);
if (is_string($rawKey) && $keyQuoteKind === String_::KIND_DOUBLE_QUOTED) {
// give raw value
$key = trim($key, '"');
$key = new StringNode(substr($rawKey, 1, strlen($rawKey) - 2));
} else {
$key = $rawKey;
}

if ($key !== null) {
return new ArrayItemNode($value, $key, $valueQuoteKind, $keyQuoteKind);
return new ArrayItemNode($value, $key);
}

return new ArrayItemNode($value, null, $valueQuoteKind, $keyQuoteKind);
return new ArrayItemNode($value);
}

private function isQuotedWith(mixed $value, string $quotes): bool
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
use PHPStan\PhpDocParser\Lexer\Lexer;
use Rector\BetterPhpDocParser\PhpDoc\DoctrineAnnotationTagValueNode;
use Rector\BetterPhpDocParser\PhpDoc\StringNode;
use Rector\BetterPhpDocParser\PhpDocParser\ClassAnnotationMatcher;
use Rector\BetterPhpDocParser\PhpDocParser\StaticDoctrineAnnotationParser;
use Rector\BetterPhpDocParser\ValueObject\Parser\BetterTokenIterator;
Expand Down Expand Up @@ -42,11 +43,11 @@ public function autowire(
}

/**
* @return string|mixed[]|ConstExprNode|DoctrineAnnotationTagValueNode
* @return string|mixed[]|ConstExprNode|DoctrineAnnotationTagValueNode|StringNode
*/
public function parseValue(
BetterTokenIterator $tokenIterator
): string | array | ConstExprNode | DoctrineAnnotationTagValueNode {
): string | array | ConstExprNode | DoctrineAnnotationTagValueNode | StringNode {
$currentTokenValue = $tokenIterator->currentTokenValue();

// temporary hackaround multi-line doctrine annotations
Expand Down Expand Up @@ -91,7 +92,7 @@ public function parseValue(

$end = $tokenIterator->currentPosition();
if ($start + 1 < $end) {
return $tokenIterator->printFromTo($start, $end);
return new StringNode($tokenIterator->printFromTo($start, $end));
}

return $currentTokenValue;
Expand Down
Loading

0 comments on commit 92ff716

Please sign in to comment.