diff --git a/packages/better-php-doc-parser/src/AnnotationReader/AnnotationReaderFactory.php b/packages/better-php-doc-parser/src/AnnotationReader/AnnotationReaderFactory.php index 6d743c54e08c..3b630c00a0ab 100644 --- a/packages/better-php-doc-parser/src/AnnotationReader/AnnotationReaderFactory.php +++ b/packages/better-php-doc-parser/src/AnnotationReader/AnnotationReaderFactory.php @@ -46,15 +46,7 @@ public function create(): Reader AnnotationRegistry::registerLoader('class_exists'); // generated - if (class_exists(ConstantPreservingAnnotationReader::class) && class_exists( - ConstantPreservingDocParser::class - )) { - $docParser = new ConstantPreservingDocParser(); - $annotationReader = new ConstantPreservingAnnotationReader($docParser); - } else { - // fallback for testing incompatibilities - $annotationReader = new AnnotationReader(new DocParser()); - } + $annotationReader = $this->createAnnotationReader(); // without this the reader will try to resolve them and fails with an exception // don't forget to add it to "stubs/Doctrine/Empty" directory, because the class needs to exists @@ -67,4 +59,21 @@ public function create(): Reader return $annotationReader; } + + /** + * @return AnnotationReader|ConstantPreservingAnnotationReader + */ + private function createAnnotationReader(): Reader + { + // these 2 classes are generated by "bin/rector sync-annotation-parser" command + if (class_exists(ConstantPreservingAnnotationReader::class) && class_exists( + ConstantPreservingDocParser::class + )) { + $docParser = new ConstantPreservingDocParser(); + return new ConstantPreservingAnnotationReader($docParser); + } + + // fallback for testing incompatibilities + return new AnnotationReader(new DocParser()); + } } diff --git a/packages/better-php-doc-parser/src/AnnotationReader/NodeAnnotationReader.php b/packages/better-php-doc-parser/src/AnnotationReader/NodeAnnotationReader.php index 0ab9e3febc97..776b7f811b47 100644 --- a/packages/better-php-doc-parser/src/AnnotationReader/NodeAnnotationReader.php +++ b/packages/better-php-doc-parser/src/AnnotationReader/NodeAnnotationReader.php @@ -9,6 +9,7 @@ use PhpParser\Node\Stmt\Class_; use PhpParser\Node\Stmt\ClassMethod; use PhpParser\Node\Stmt\Property; +use Rector\DoctrineAnnotationGenerated\PhpDocNode\ConstantReferenceIdentifierRestorer; use Rector\NodeNameResolver\NodeNameResolver; use Rector\NodeTypeResolver\ClassExistenceStaticHelper; use Rector\NodeTypeResolver\Node\AttributeKey; @@ -34,10 +35,19 @@ final class NodeAnnotationReader */ private $nodeNameResolver; - public function __construct(Reader $reader, NodeNameResolver $nodeNameResolver) - { + /** + * @var ConstantReferenceIdentifierRestorer + */ + private $constantReferenceIdentifierRestorer; + + public function __construct( + Reader $reader, + NodeNameResolver $nodeNameResolver, + ConstantReferenceIdentifierRestorer $constantReferenceIdentifierRestorer + ) { $this->reader = $reader; $this->nodeNameResolver = $nodeNameResolver; + $this->constantReferenceIdentifierRestorer = $constantReferenceIdentifierRestorer; } /** @@ -69,11 +79,12 @@ public function readMethodAnnotation(ClassMethod $classMethod, string $annotatio } $this->alreadyProvidedAnnotations[] = $objectHash; + $this->constantReferenceIdentifierRestorer->restoreObject($methodAnnotation); return $methodAnnotation; } } catch (AnnotationException $annotationException) { - // unable to laod + // unable to load return null; } @@ -87,7 +98,14 @@ public function readClassAnnotation(Class_ $class, string $annotationClassName) { $classReflection = $this->createClassReflectionFromNode($class); - return $this->reader->getClassAnnotation($classReflection, $annotationClassName); + $annotation = $this->reader->getClassAnnotation($classReflection, $annotationClassName); + if ($annotation === null) { + return null; + } + + $this->constantReferenceIdentifierRestorer->restoreObject($annotation); + + return $annotation; } /** @@ -105,6 +123,7 @@ public function readPropertyAnnotation(Property $property, string $annotationCla /** @var object[] $propertyAnnotations */ $propertyAnnotations = $this->reader->getPropertyAnnotations($propertyReflection); + foreach ($propertyAnnotations as $propertyAnnotation) { if (! is_a($propertyAnnotation, $annotationClassName, true)) { continue; @@ -116,6 +135,7 @@ public function readPropertyAnnotation(Property $property, string $annotationCla } $this->alreadyProvidedAnnotations[] = $objectHash; + $this->constantReferenceIdentifierRestorer->restoreObject($propertyAnnotation); return $propertyAnnotation; } diff --git a/packages/better-php-doc-parser/src/PhpDocNode/Doctrine/Property_/GeneratedValueTagValueNode.php b/packages/better-php-doc-parser/src/PhpDocNode/Doctrine/Property_/GeneratedValueTagValueNode.php index db4eb922b952..bb1479f5bcd8 100644 --- a/packages/better-php-doc-parser/src/PhpDocNode/Doctrine/Property_/GeneratedValueTagValueNode.php +++ b/packages/better-php-doc-parser/src/PhpDocNode/Doctrine/Property_/GeneratedValueTagValueNode.php @@ -38,8 +38,9 @@ public function __toString(): string public static function createFromAnnotationAndAnnotationContent( GeneratedValue $generatedValue, string $annotationContent - ) { + ): self { $items = get_object_vars($generatedValue); + return new self($items, $annotationContent); } diff --git a/packages/better-php-doc-parser/tests/PhpDocParser/AbstractPhpDocInfoTest.php b/packages/better-php-doc-parser/tests/PhpDocParser/AbstractPhpDocInfoTest.php index c504bda0197e..faf81d1b29a4 100644 --- a/packages/better-php-doc-parser/tests/PhpDocParser/AbstractPhpDocInfoTest.php +++ b/packages/better-php-doc-parser/tests/PhpDocParser/AbstractPhpDocInfoTest.php @@ -65,7 +65,7 @@ protected function doTestPrintedPhpDocInfo(string $filePath, string $tagValueNod $errorMessage = $this->createErrorMessage($filePath); $this->assertSame($originalDocCommentText, $printedPhpDocInfo, $errorMessage); - $this->doTestContainsTagValueNodeType($nodeWithPhpDocInfo, $tagValueNodeType); + $this->doTestContainsTagValueNodeType($nodeWithPhpDocInfo, $tagValueNodeType, $filePath); } protected function yieldFilesFromDirectory(string $directory, string $suffix = '*.php'): Iterator @@ -107,10 +107,13 @@ private function createErrorMessage(string $filePath): string return 'Caused by: ' . $fileInfo->getRelativeFilePathFromCwd() . PHP_EOL; } - private function doTestContainsTagValueNodeType(Node $node, string $tagValueNodeType): void + private function doTestContainsTagValueNodeType(Node $node, string $tagValueNodeType, string $filePath): void { /** @var PhpDocInfo $phpDocInfo */ $phpDocInfo = $node->getAttribute(AttributeKey::PHP_DOC_INFO); - $this->assertTrue($phpDocInfo->hasByType($tagValueNodeType)); + + $message = (new SmartFileInfo($filePath))->getRelativeFilePathFromCwd(); + + $this->assertTrue($phpDocInfo->hasByType($tagValueNodeType), $message); } } diff --git a/packages/better-php-doc-parser/tests/PhpDocParser/Helper/TagValueToPhpParserNodeMap.php b/packages/better-php-doc-parser/tests/PhpDocParser/Helper/TagValueToPhpParserNodeMap.php index 41fb831f4981..fa06c1819302 100644 --- a/packages/better-php-doc-parser/tests/PhpDocParser/Helper/TagValueToPhpParserNodeMap.php +++ b/packages/better-php-doc-parser/tests/PhpDocParser/Helper/TagValueToPhpParserNodeMap.php @@ -7,6 +7,7 @@ use PhpParser\Node\Stmt\Class_; use PhpParser\Node\Stmt\ClassMethod; use PhpParser\Node\Stmt\Property; +use PHPStan\PhpDocParser\Ast\PhpDoc\GenericTagValueNode; use Rector\BetterPhpDocParser\PhpDocNode\Doctrine\Class_\EntityTagValueNode; use Rector\BetterPhpDocParser\PhpDocNode\Doctrine\Class_\TableTagValueNode; use Rector\BetterPhpDocParser\PhpDocNode\Doctrine\Property_\ColumnTagValueNode; @@ -40,5 +41,8 @@ final class TagValueToPhpParserNodeMap TableTagValueNode::class => Class_::class, CustomIdGeneratorTagValueNode::class => Property::class, GeneratedValueTagValueNode::class => Property::class, + + // special case for constants + GenericTagValueNode::class => Property::class, ]; } diff --git a/packages/better-php-doc-parser/tests/PhpDocParser/TagValueNodeReprint/Fixture/AssertChoice/AssertChoiceNonQuoteValues.php b/packages/better-php-doc-parser/tests/PhpDocParser/TagValueNodeReprint/Fixture/AssertChoice/AssertChoiceNonQuoteValues.php index 50476db2ca5e..90ef2c480248 100644 --- a/packages/better-php-doc-parser/tests/PhpDocParser/TagValueNodeReprint/Fixture/AssertChoice/AssertChoiceNonQuoteValues.php +++ b/packages/better-php-doc-parser/tests/PhpDocParser/TagValueNodeReprint/Fixture/AssertChoice/AssertChoiceNonQuoteValues.php @@ -9,7 +9,7 @@ class AssertChoiceNonQuoteValues { /** - * @Assert\Choice({chalet, apartment}) + * @Assert\Choice({"chalet", "apartment"}) */ public $type; } diff --git a/packages/better-php-doc-parser/tests/PhpDocParser/TagValueNodeReprint/Fixture/AssertChoice/AssertQuoteChoice.php b/packages/better-php-doc-parser/tests/PhpDocParser/TagValueNodeReprint/Fixture/AssertChoice/AssertQuoteChoice.php index 90e654a97cdd..f631c3dc8147 100644 --- a/packages/better-php-doc-parser/tests/PhpDocParser/TagValueNodeReprint/Fixture/AssertChoice/AssertQuoteChoice.php +++ b/packages/better-php-doc-parser/tests/PhpDocParser/TagValueNodeReprint/Fixture/AssertChoice/AssertQuoteChoice.php @@ -11,7 +11,7 @@ class AssertQuoteChoice const CHOICE_ONE = 'choice_one'; const CHOICE_TWO = 'choice_two'; /** - * @Assert\Choice({SomeEntity::CHOICE_ONE, SomeEntity::CHOICE_TWO}) + * @Assert\Choice({AssertQuoteChoice::CHOICE_ONE, AssertQuoteChoice::CHOICE_TWO}) */ private $someChoice = self::CHOICE_ONE; } diff --git a/packages/better-php-doc-parser/tests/PhpDocParser/TagValueNodeReprint/Fixture/AssertType/AssertStringType.php b/packages/better-php-doc-parser/tests/PhpDocParser/TagValueNodeReprint/Fixture/AssertType/AssertStringType.php index cd016f87f800..70746d1b5640 100644 --- a/packages/better-php-doc-parser/tests/PhpDocParser/TagValueNodeReprint/Fixture/AssertType/AssertStringType.php +++ b/packages/better-php-doc-parser/tests/PhpDocParser/TagValueNodeReprint/Fixture/AssertType/AssertStringType.php @@ -9,7 +9,7 @@ final class AssertStringType { /** - * @Assert\Type(string) + * @Assert\Type("string") */ public $anotherProperty; } diff --git a/packages/better-php-doc-parser/tests/PhpDocParser/TagValueNodeReprint/Fixture/ConstantReference/Book.php b/packages/better-php-doc-parser/tests/PhpDocParser/TagValueNodeReprint/Fixture/ConstantReference/Book.php new file mode 100644 index 000000000000..4138386ddb28 --- /dev/null +++ b/packages/better-php-doc-parser/tests/PhpDocParser/TagValueNodeReprint/Fixture/ConstantReference/Book.php @@ -0,0 +1,17 @@ + __DIR__ . '/Fixture/DoctrineTable', CustomIdGeneratorTagValueNode::class => __DIR__ . '/Fixture/DoctrineCustomIdGenerator', GeneratedValueTagValueNode::class => __DIR__ . '/Fixture/DoctrineGeneratedValue', + + // special case + GenericTagValueNode::class => __DIR__ . '/Fixture/ConstantReference', ]; } } diff --git a/packages/doctrine-annotation-generated/config/config.yaml b/packages/doctrine-annotation-generated/config/config.yaml new file mode 100644 index 000000000000..b54efe7321e9 --- /dev/null +++ b/packages/doctrine-annotation-generated/config/config.yaml @@ -0,0 +1,8 @@ +services: + _defaults: + public: true + autowire: true + autoconfigure: true + + Rector\DoctrineAnnotationGenerated\: + resource: '../src' diff --git a/packages/doctrine-annotation-generated/src/ConstantPreservingDocParser.php b/packages/doctrine-annotation-generated/src/ConstantPreservingDocParser.php index 6d23954709cd..3faa11add020 100644 --- a/packages/doctrine-annotation-generated/src/ConstantPreservingDocParser.php +++ b/packages/doctrine-annotation-generated/src/ConstantPreservingDocParser.php @@ -679,16 +679,60 @@ private function Values() private function Constant() { $identifier = $this->Identifier(); - if ($identifier === 'true') { - return true; - } - if ($identifier === 'false') { - return false; - } - if ($identifier === 'null') { - return null; + $originalIdentifier = $identifier; + if (!defined($identifier) && false !== strpos($identifier, '::') && '\\' !== $identifier[0]) { + list($className, $const) = explode('::', $identifier); + $pos = strpos($className, '\\'); + $alias = false === $pos ? $className : substr($className, 0, $pos); + $found = false; + $loweredAlias = strtolower($alias); + switch (true) { + case !empty($this->namespaces): + foreach ($this->namespaces as $ns) { + if (class_exists($ns . '\\' . $className) || interface_exists($ns . '\\' . $className)) { + $className = $ns . '\\' . $className; + $found = true; + break; + } + } + break; + case isset($this->imports[$loweredAlias]): + $found = true; + $className = false !== $pos ? $this->imports[$loweredAlias] . substr($className, $pos) : $this->imports[$loweredAlias]; + break; + default: + if (isset($this->imports['__NAMESPACE__'])) { + $ns = $this->imports['__NAMESPACE__']; + if (class_exists($ns . '\\' . $className) || interface_exists($ns . '\\' . $className)) { + $className = $ns . '\\' . $className; + $found = true; + } + } + break; + } + if ($found) { + $identifier = $className . '::' . $const; + } } - return $identifier; + /** + * Checks if identifier ends with ::class and remove the leading backslash if it exists. + */ + if ($this->identifierEndsWithClassConstant($identifier) && !$this->identifierStartsWithBackslash($identifier)) { + $resolvedValue = substr($identifier, 0, $this->getClassConstantPositionInIdentifier($identifier)); + \Rector\DoctrineAnnotationGenerated\DataCollector\ResolvedConstantStaticCollector::collect($originalIdentifier, $resolvedValue); + return $resolvedValue; + } + if ($this->identifierEndsWithClassConstant($identifier) && $this->identifierStartsWithBackslash($identifier)) { + $resolvedValue = substr($identifier, 1, $this->getClassConstantPositionInIdentifier($identifier) - 1); + \Rector\DoctrineAnnotationGenerated\DataCollector\ResolvedConstantStaticCollector::collect($originalIdentifier, $resolvedValue); + return $resolvedValue; + } + if (!defined($identifier)) { + throw \Doctrine\Common\Annotations\AnnotationException::semanticalErrorConstants($identifier, $this->context); + } + $resolvedValue = constant($identifier); + \Rector\DoctrineAnnotationGenerated\DataCollector\ResolvedConstantStaticCollector::collect($originalIdentifier, $resolvedValue); + return $resolvedValue; } private function identifierStartsWithBackslash(string $identifier): bool { diff --git a/packages/doctrine-annotation-generated/src/DataCollector/ResolvedConstantStaticCollector.php b/packages/doctrine-annotation-generated/src/DataCollector/ResolvedConstantStaticCollector.php new file mode 100644 index 000000000000..2f7f594dc22a --- /dev/null +++ b/packages/doctrine-annotation-generated/src/DataCollector/ResolvedConstantStaticCollector.php @@ -0,0 +1,37 @@ + $value) { + $originalIdentifier = $this->matchIdentifierBasedOnResolverValue($identifierToResolvedValues, $value); + if ($originalIdentifier !== null) { + // restore value + $choice->{$propertyName} = $originalIdentifier; + continue; + } + + // nested resolved value + if (! is_array($value)) { + continue; + } + + foreach ($value as $key => $nestedValue) { + $originalIdentifier = $this->matchIdentifierBasedOnResolverValue( + $identifierToResolvedValues, + $nestedValue + ); + + if ($originalIdentifier === null) { + continue; + } + + // restore value + $choice->{$propertyName}[$key] = $originalIdentifier; + } + } + + ResolvedConstantStaticCollector::clear(); + } + + /** + * @return mixed|null + */ + private function matchIdentifierBasedOnResolverValue(array $identifierToResolvedValues, $value) + { + foreach ($identifierToResolvedValues as $identifier => $resolvedValue) { + if ($value !== $resolvedValue) { + continue; + } + + return $identifier; + } + + return null; + } +} diff --git a/utils/doctrine-annotation-parser-syncer/src/Rector/ClassMethod/LogIdentifierAndResolverValueInConstantClassMethodRector.php b/utils/doctrine-annotation-parser-syncer/src/Rector/ClassMethod/LogIdentifierAndResolverValueInConstantClassMethodRector.php new file mode 100644 index 000000000000..b749565e2155 --- /dev/null +++ b/utils/doctrine-annotation-parser-syncer/src/Rector/ClassMethod/LogIdentifierAndResolverValueInConstantClassMethodRector.php @@ -0,0 +1,99 @@ +isInClassNamed($node, 'Doctrine\Common\Annotations\DocParser')) { + return null; + } + + if (! $this->isName($node->name, 'Constant')) { + return null; + } + + // 1. store original value right in the start + $firstStmt = $node->stmts[0]; + unset($node->stmts[0]); + $assignExpression = $this->createAssignOriginalIdentifierExpression(); + $node->stmts = array_merge([$firstStmt], [$assignExpression], $node->stmts); + + // 2. record value in each return + $this->traverseNodesWithCallable((array) $node->stmts, function (Node $node) { + if (! $node instanceof Return_) { + return null; + } + + // assign resolved value to temporary variable + $resolvedValueVariable = new Variable('resolvedValue'); + $assign = new Assign($resolvedValueVariable, $node->expr); + $assignExpression = new Expression($assign); + $this->addNodeBeforeNode($assignExpression, $node); + + // log the value in static call + $originalIdentifier = new Variable('originalIdentifier'); + $staticCallExpression = $this->createStaticCallExpression($originalIdentifier, $resolvedValueVariable); + $this->addNodeBeforeNode($staticCallExpression, $node); + + $node->expr = $resolvedValueVariable; + return $node; + }); + + return $node; + } + + public function getDefinition(): RectorDefinition + { + return new RectorDefinition('Log original and changed constant value'); + } + + private function createAssignOriginalIdentifierExpression(): Expression + { + $originalIdentifier = new Variable('originalIdentifier'); + $identifier = new Variable('identifier'); + + $assign = new Assign($originalIdentifier, $identifier); + + return new Expression($assign); + } + + private function createStaticCallExpression(Variable $identifierVariable, Variable $resolvedVariable): Expression + { + $args = [new Arg($identifierVariable), new Arg($resolvedVariable)]; + $staticCall = new StaticCall(new FullyQualified(ResolvedConstantStaticCollector::class), 'collect', $args); + + return new Expression($staticCall); + } +} diff --git a/utils/doctrine-annotation-parser-syncer/src/Rector/ClassMethod/RemoveValueChangeFromConstantClassMethodRector.php b/utils/doctrine-annotation-parser-syncer/src/Rector/ClassMethod/RemoveValueChangeFromConstantClassMethodRector.php deleted file mode 100644 index 64d2b183dd39..000000000000 --- a/utils/doctrine-annotation-parser-syncer/src/Rector/ClassMethod/RemoveValueChangeFromConstantClassMethodRector.php +++ /dev/null @@ -1,68 +0,0 @@ -isInClassNamed($node, 'Doctrine\Common\Annotations\DocParser')) { - return null; - } - - if (! $this->isName($node->name, 'Constant')) { - return null; - } - - $identifierVariable = new Variable('identifier'); - - $ifTrue = $this->createIfWithStringToBool($identifierVariable, 'true'); - $ifFalse = $this->createIfWithStringToBool($identifierVariable, 'false'); - $ifNull = $this->createIfWithStringToBool($identifierVariable, 'null'); - $returnExpression = new Return_($identifierVariable); - - $node->stmts = [$node->stmts[0], $ifTrue, $ifFalse, $ifNull, $returnExpression]; - - return $node; - } - - public function getDefinition(): RectorDefinition - { - return new RectorDefinition('Remove value change from Constant() class method'); - } - - private function createIfWithStringToBool(Variable $variable, string $value): If_ - { - $identical = new Identical($variable, new String_($value)); - $if = new If_($identical); - $if->stmts[] = new Return_(new ConstFetch(new Name($value))); - - return $if; - } -}