diff --git a/composer.json b/composer.json index 865465cf183..e6251074a4d 100644 --- a/composer.json +++ b/composer.json @@ -12,7 +12,9 @@ "require": { "php": "^7.1", "composer/xdebug-handler": "^1.3", + "doctrine/annotations": "^1.7", "doctrine/inflector": "^1.3", + "doctrine/orm": "^2.6", "jean85/pretty-package-versions": "^1.2", "nette/robot-loader": "^3.1", "nette/utils": "^2.5|^3.0", @@ -52,6 +54,7 @@ "Rector\\ConsoleDiffer\\": "packages/ConsoleDiffer/src", "Rector\\DeadCode\\": "packages/DeadCode/src", "Rector\\Doctrine\\": "packages/Doctrine/src", + "Rector\\DoctrinePhpDocParser\\": "packages/DoctrinePhpDocParser/src", "Rector\\DomainDrivenDesign\\": "packages/DomainDrivenDesign/src", "Rector\\ElasticSearchDSL\\": "packages/ElasticSearchDSL/src", "Rector\\FileSystemRector\\": "packages/FileSystemRector/src", @@ -97,6 +100,7 @@ "Rector\\CodingStyle\\Tests\\": "packages/CodingStyle/tests", "Rector\\DeadCode\\Tests\\": "packages/DeadCode/tests", "Rector\\Doctrine\\Tests\\": "packages/Doctrine/tests", + "Rector\\DoctrinePhpDocParser\\Tests\\": "packages/DoctrinePhpDocParser/tests", "Rector\\DomainDrivenDesign\\Tests\\": "packages/DomainDrivenDesign/tests", "Rector\\ElasticSearchDSL\\Tests\\": "packages/ElasticSearchDSL/tests", "Rector\\Guzzle\\Tests\\": "packages/Guzzle/tests", diff --git a/ecs.yaml b/ecs.yaml index ecf746c6c09..379fcc76f30 100644 --- a/ecs.yaml +++ b/ecs.yaml @@ -96,6 +96,8 @@ parameters: - 'src/Util/*.php' - 'packages/BetterPhpDocParser/src/Annotation/AnnotationNaming.php' - 'src/Testing/PHPUnit/PHPUnitEnvironment.php' + # honesty first + - 'src/*StaticHelper.php' Symplify\CodingStandard\Fixer\Naming\PropertyNameMatchingTypeFixer: - 'packages/NodeTypeResolver/src/PHPStan/Scope/NodeScopeResolver.php' diff --git a/packages/Architecture/src/Rector/Class_/RemoveRepositoryFromEntityAnnotationRector.php b/packages/Architecture/src/Rector/Class_/RemoveRepositoryFromEntityAnnotationRector.php index 67903a0ab1d..cedd54e5138 100644 --- a/packages/Architecture/src/Rector/Class_/RemoveRepositoryFromEntityAnnotationRector.php +++ b/packages/Architecture/src/Rector/Class_/RemoveRepositoryFromEntityAnnotationRector.php @@ -2,10 +2,8 @@ namespace Rector\Architecture\Rector\Class_; -use Nette\Utils\Strings; use PhpParser\Node; use PhpParser\Node\Stmt\Class_; -use PHPStan\PhpDocParser\Ast\PhpDoc\GenericTagValueNode; use Rector\Architecture\Tests\Rector\Class_\RemoveRepositoryFromEntityAnnotationRector\RemoveRepositoryFromEntityAnnotationRectorTest; use Rector\NodeTypeResolver\PhpDoc\NodeAnalyzer\DocBlockManipulator; use Rector\Rector\AbstractRector; @@ -17,11 +15,6 @@ */ final class RemoveRepositoryFromEntityAnnotationRector extends AbstractRector { - /** - * @var string - */ - private const DOCTRINE_ORM_MAPPING_ENTITY = 'Doctrine\ORM\Mapping\Entity'; - /** * @var DocBlockManipulator */ @@ -79,21 +72,13 @@ public function refactor(Node $node): ?Node } $phpDocInfo = $this->docBlockManipulator->createPhpDocInfoFromNode($node); - if (! $phpDocInfo->hasTag(self::DOCTRINE_ORM_MAPPING_ENTITY)) { - return null; - } - - $entityTags = $phpDocInfo->getTagsByName(self::DOCTRINE_ORM_MAPPING_ENTITY); - if ($entityTags === []) { - return null; - } - $entityTag = $entityTags[0]; - if (! $entityTag->value instanceof GenericTagValueNode) { + $doctrineEntityTag = $phpDocInfo->getDoctrineEntityTag(); + if ($doctrineEntityTag === null) { return null; } - $entityTag->value->value = Strings::replace($entityTag->value->value, '#\(repositoryClass="(.*?)"\)#'); + $doctrineEntityTag->removeRepositoryClass(); // save the entity tag $this->docBlockManipulator->updateNodeWithPhpDocInfo($node, $phpDocInfo); diff --git a/packages/BetterPhpDocParser/src/Attributes/Ast/AttributeAwareNodeFactory.php b/packages/BetterPhpDocParser/src/Attributes/Ast/AttributeAwareNodeFactory.php index d7ccdd23b9c..195f36b7f63 100644 --- a/packages/BetterPhpDocParser/src/Attributes/Ast/AttributeAwareNodeFactory.php +++ b/packages/BetterPhpDocParser/src/Attributes/Ast/AttributeAwareNodeFactory.php @@ -48,8 +48,8 @@ use Rector\BetterPhpDocParser\Attributes\Ast\PhpDoc\Type\AttributeAwareThisTypeNode; use Rector\BetterPhpDocParser\Attributes\Ast\PhpDoc\Type\AttributeAwareUnionTypeNode; use Rector\BetterPhpDocParser\Attributes\Contract\Ast\AttributeAwareNodeInterface; -use Rector\BetterPhpDocParser\Exception\NotImplementedYetException; -use Rector\BetterPhpDocParser\Exception\ShouldNotHappenException; +use Rector\Exception\NotImplementedYetException; +use Rector\Exception\ShouldNotHappenException; final class AttributeAwareNodeFactory { @@ -125,7 +125,8 @@ private function createFromPhpDocValueNode(PhpDocTagValueNode $phpDocTagValueNod $typeNode, $phpDocTagValueNode->isVariadic, $phpDocTagValueNode->parameterName, - $phpDocTagValueNode->description + $phpDocTagValueNode->description, + false // @todo maybe solve better ); } diff --git a/packages/BetterPhpDocParser/src/Exception/ShouldNotHappenException.php b/packages/BetterPhpDocParser/src/Exception/ShouldNotHappenException.php deleted file mode 100644 index 34b7066609c..00000000000 --- a/packages/BetterPhpDocParser/src/Exception/ShouldNotHappenException.php +++ /dev/null @@ -1,9 +0,0 @@ -getResolvedTypesAttribute($varTagValue); } + public function getDoctrineEntityTag(): ?EntityTagValueNode + { + foreach ($this->phpDocNode->children as $phpDocChildNode) { + if ($phpDocChildNode instanceof PhpDocTagNode) { + if ($phpDocChildNode->value instanceof EntityTagValueNode) { + return $phpDocChildNode->value; + } + } + } + + return null; + } + + public function getDoctrineColumnTagValueNode(): ?ColumnTagValueNode + { + foreach ($this->phpDocNode->children as $phpDocChildNode) { + if ($phpDocChildNode instanceof PhpDocTagNode) { + if ($phpDocChildNode->value instanceof ColumnTagValueNode) { + return $phpDocChildNode->value; + } + } + } + + return null; + } + /** * @return string[] */ diff --git a/packages/BetterPhpDocParser/src/PhpDocInfo/PhpDocInfoFactory.php b/packages/BetterPhpDocParser/src/PhpDocInfo/PhpDocInfoFactory.php index 1a36b3fb169..c0e76ce1309 100644 --- a/packages/BetterPhpDocParser/src/PhpDocInfo/PhpDocInfoFactory.php +++ b/packages/BetterPhpDocParser/src/PhpDocInfo/PhpDocInfoFactory.php @@ -10,6 +10,8 @@ use Rector\BetterPhpDocParser\Attributes\Attribute\Attribute; use Rector\BetterPhpDocParser\Attributes\Contract\Ast\AttributeAwareNodeInterface; use Rector\BetterPhpDocParser\Contract\PhpDocNodeDecoratorInterface; +use Rector\BetterPhpDocParser\PhpDocParser\OrmTagParser; +use Rector\Configuration\CurrentNodeProvider; final class PhpDocInfoFactory { @@ -28,21 +30,31 @@ final class PhpDocInfoFactory */ private $lexer; + /** + * @var CurrentNodeProvider + */ + private $currentNodeProvider; + /** * @param PhpDocNodeDecoratorInterface[] $phpDocNodeDecoratorInterfacenodeDecorators */ public function __construct( PhpDocParser $phpDocParser, Lexer $lexer, - array $phpDocNodeDecoratorInterfacenodeDecorators + array $phpDocNodeDecoratorInterfacenodeDecorators, + CurrentNodeProvider $currentNodeProvider ) { $this->phpDocParser = $phpDocParser; $this->lexer = $lexer; $this->phpDocNodeDecoratorInterfaces = $phpDocNodeDecoratorInterfacenodeDecorators; + $this->currentNodeProvider = $currentNodeProvider; } public function createFromNode(Node $node): PhpDocInfo { + /** needed for @see OrmTagParser */ + $this->currentNodeProvider->setNode($node); + $content = $node->getDocComment()->getText(); $tokens = $this->lexer->tokenize($content); diff --git a/packages/BetterPhpDocParser/src/PhpDocParser/BetterPhpDocParser.php b/packages/BetterPhpDocParser/src/PhpDocParser/BetterPhpDocParser.php index eb412f1a13d..87953c63ba4 100644 --- a/packages/BetterPhpDocParser/src/PhpDocParser/BetterPhpDocParser.php +++ b/packages/BetterPhpDocParser/src/PhpDocParser/BetterPhpDocParser.php @@ -7,6 +7,7 @@ use PHPStan\PhpDocParser\Ast\PhpDoc\InvalidTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\ParamTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocNode; +use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagNode; use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagValueNode; use PHPStan\PhpDocParser\Lexer\Lexer; use PHPStan\PhpDocParser\Parser\ConstExprParser; @@ -20,6 +21,7 @@ use Rector\BetterPhpDocParser\Attributes\Attribute\Attribute; use Rector\BetterPhpDocParser\Data\StartEndInfo; use Rector\BetterPhpDocParser\Printer\MultilineSpaceFormatPreserver; +use Rector\DoctrinePhpDocParser\PhpDocParser\OrmTagParser; use Symplify\PackageBuilder\Reflection\PrivatesAccessor; use Symplify\PackageBuilder\Reflection\PrivatesCaller; @@ -50,11 +52,17 @@ final class BetterPhpDocParser extends PhpDocParser */ private $multilineSpaceFormatPreserver; + /** + * @var OrmTagParser + */ + private $ormTagParser; + public function __construct( TypeParser $typeParser, ConstExprParser $constExprParser, AttributeAwareNodeFactory $attributeAwareNodeFactory, - MultilineSpaceFormatPreserver $multilineSpaceFormatPreserver + MultilineSpaceFormatPreserver $multilineSpaceFormatPreserver, + OrmTagParser $ormTagParser ) { parent::__construct($typeParser, $constExprParser); @@ -62,6 +70,7 @@ public function __construct( $this->privatesAccessor = new PrivatesAccessor(); $this->attributeAwareNodeFactory = $attributeAwareNodeFactory; $this->multilineSpaceFormatPreserver = $multilineSpaceFormatPreserver; + $this->ormTagParser = $ormTagParser; } /** @@ -101,8 +110,28 @@ public function parse(TokenIterator $tokenIterator): PhpDocNode return $this->attributeAwareNodeFactory->createFromNode($phpDocNode); } + public function parseTag(TokenIterator $tokenIterator): PhpDocTagNode + { + $tag = $tokenIterator->currentTokenValue(); + $tokenIterator->next(); + + if ($tag === '@ORM') { + $tag .= $tokenIterator->currentTokenValue(); + $tokenIterator->next(); + } + + $value = $this->parseTagValue($tokenIterator, $tag); + + return new PhpDocTagNode($tag, $value); + } + public function parseTagValue(TokenIterator $tokenIterator, string $tag): PhpDocTagValueNode { + // @todo do via extension :) + if (in_array($tag, ['@ORM\Entity', '@ORM\Column'], true)) { + return $this->ormTagParser->parse($tokenIterator, $tag); + } + // needed for reference support in params, see https://github.com/rectorphp/rector/issues/1734 if ($tag === '@param') { try { diff --git a/packages/BetterPhpDocParser/src/Printer/OriginalSpacingRestorer.php b/packages/BetterPhpDocParser/src/Printer/OriginalSpacingRestorer.php index 0cd28a99d78..be061a8ab02 100644 --- a/packages/BetterPhpDocParser/src/Printer/OriginalSpacingRestorer.php +++ b/packages/BetterPhpDocParser/src/Printer/OriginalSpacingRestorer.php @@ -4,8 +4,10 @@ use Nette\Utils\Arrays; use Nette\Utils\Strings; +use PHPStan\PhpDocParser\Ast\Node; use PHPStan\PhpDocParser\Lexer\Lexer; use Rector\BetterPhpDocParser\Data\StartEndInfo; +use Rector\DoctrinePhpDocParser\Contract\Ast\PhpDoc\DoctrineTagNodeInterface; final class OriginalSpacingRestorer { @@ -13,11 +15,12 @@ final class OriginalSpacingRestorer * @param mixed[] $tokens */ public function restoreInOutputWithTokensStartAndEndPosition( + Node $node, string $nodeOutput, array $tokens, StartEndInfo $startEndInfo ): string { - $oldWhitespaces = $this->detectOldWhitespaces($tokens, $startEndInfo); + $oldWhitespaces = $this->detectOldWhitespaces($node, $tokens, $startEndInfo); // no original whitespaces, return if ($oldWhitespaces === []) { @@ -26,7 +29,6 @@ public function restoreInOutputWithTokensStartAndEndPosition( $newNodeOutput = ''; $i = 0; - // replace system whitespace by old ones foreach (Strings::split($nodeOutput, '#\s+#') as $nodeOutputPart) { $newNodeOutput .= ($oldWhitespaces[$i] ?? '') . $nodeOutputPart; @@ -41,11 +43,16 @@ public function restoreInOutputWithTokensStartAndEndPosition( * @param mixed[] $tokens * @return string[] */ - private function detectOldWhitespaces(array $tokens, StartEndInfo $startEndInfo): array + private function detectOldWhitespaces(Node $node, array $tokens, StartEndInfo $startEndInfo): array { $oldWhitespaces = []; - for ($i = $startEndInfo->getStart(); $i < $startEndInfo->getEnd(); ++$i) { + $start = $startEndInfo->getStart(); + if ($node instanceof DoctrineTagNodeInterface) { + --$start; + } + + for ($i = $start; $i < $startEndInfo->getEnd(); ++$i) { if ($tokens[$i][1] === Lexer::TOKEN_HORIZONTAL_WS) { $oldWhitespaces[] = $tokens[$i][0]; } diff --git a/packages/BetterPhpDocParser/src/Printer/PhpDocInfoPrinter.php b/packages/BetterPhpDocParser/src/Printer/PhpDocInfoPrinter.php index b88bcaeb269..3202cad329a 100644 --- a/packages/BetterPhpDocParser/src/Printer/PhpDocInfoPrinter.php +++ b/packages/BetterPhpDocParser/src/Printer/PhpDocInfoPrinter.php @@ -78,6 +78,7 @@ public function printFormatPreserving(PhpDocInfo $phpDocInfo, bool $shouldSkipEm { $this->attributeAwarePhpDocNode = $phpDocInfo->getPhpDocNode(); $this->tokens = $phpDocInfo->getTokens(); + $this->tokenCount = count($phpDocInfo->getTokens()); $this->phpDocInfo = $phpDocInfo; @@ -154,16 +155,17 @@ private function printNode( $this->currentTokenPosition = $startEndInfo->getEnd(); } - if ($attributeAwareNode instanceof PhpDocTagNode && $startEndInfo) { - return $this->printPhpDocTagNode($attributeAwareNode, $startEndInfo, $output); - } - if ($attributeAwareNode instanceof PhpDocTagNode) { + if ($startEndInfo) { + return $this->printPhpDocTagNode($attributeAwareNode, $startEndInfo, $output); + } + return $output . PHP_EOL . ' * ' . $attributeAwareNode; } if (! $attributeAwareNode instanceof PhpDocTextNode && ! $attributeAwareNode instanceof GenericTagValueNode && $startEndInfo) { return $this->originalSpacingRestorer->restoreInOutputWithTokensStartAndEndPosition( + $attributeAwareNode, (string) $attributeAwareNode, $this->tokens, $startEndInfo diff --git a/packages/DeadCode/src/Doctrine/DoctrineEntityManipulator.php b/packages/DeadCode/src/Doctrine/DoctrineEntityManipulator.php index ad4cff60f10..494410a39b5 100644 --- a/packages/DeadCode/src/Doctrine/DoctrineEntityManipulator.php +++ b/packages/DeadCode/src/Doctrine/DoctrineEntityManipulator.php @@ -2,6 +2,13 @@ namespace Rector\DeadCode\Doctrine; +use Doctrine\ORM\Mapping\Entity; +use Doctrine\ORM\Mapping\InheritanceType; +use Doctrine\ORM\Mapping\JoinColumn; +use Doctrine\ORM\Mapping\ManyToMany; +use Doctrine\ORM\Mapping\ManyToOne; +use Doctrine\ORM\Mapping\OneToMany; +use Doctrine\ORM\Mapping\OneToOne; use Nette\Utils\Strings; use PhpParser\Comment\Doc; use PhpParser\Node; @@ -29,16 +36,16 @@ final class DoctrineEntityManipulator * @var string[] */ private const RELATION_ANNOTATIONS = [ - 'Doctrine\ORM\Mapping\OneToMany', + OneToMany::class, self::MANY_TO_ONE_ANNOTATION, - 'Doctrine\ORM\Mapping\OneToOne', - 'Doctrine\ORM\Mapping\ManyToMany', + OneToOne::class, + ManyToMany::class, ]; /** * @var string */ - private const MANY_TO_ONE_ANNOTATION = 'Doctrine\ORM\Mapping\ManyToOne'; + private const MANY_TO_ONE_ANNOTATION = ManyToOne::class; /** * @var string @@ -48,7 +55,7 @@ final class DoctrineEntityManipulator /** * @var string */ - private const JOIN_COLUMN_ANNOTATION = 'Doctrine\ORM\Mapping\JoinColumn'; + private const JOIN_COLUMN_ANNOTATION = JoinColumn::class; /** * @var DocBlockManipulator @@ -140,11 +147,11 @@ public function isStandaloneDoctrineEntityClass(Class_ $class): bool } // is parent entity - if ($this->docBlockManipulator->hasTag($class, 'Doctrine\ORM\Mapping\InheritanceType')) { + if ($this->docBlockManipulator->hasTag($class, InheritanceType::class)) { return false; } - return $this->docBlockManipulator->hasTag($class, 'Doctrine\ORM\Mapping\Entity'); + return $this->docBlockManipulator->hasTag($class, Entity::class); } public function removeMappedByOrInversedByFromProperty(Property $property): void diff --git a/packages/DeadCode/tests/Rector/Class_/RemoveUnusedDoctrineEntityMethodAndPropertyRector/RemoveUnusedDoctrineEntityMethodAndPropertyRectorTest.php b/packages/DeadCode/tests/Rector/Class_/RemoveUnusedDoctrineEntityMethodAndPropertyRector/RemoveUnusedDoctrineEntityMethodAndPropertyRectorTest.php index 7404399718b..2599fe0e044 100644 --- a/packages/DeadCode/tests/Rector/Class_/RemoveUnusedDoctrineEntityMethodAndPropertyRector/RemoveUnusedDoctrineEntityMethodAndPropertyRectorTest.php +++ b/packages/DeadCode/tests/Rector/Class_/RemoveUnusedDoctrineEntityMethodAndPropertyRector/RemoveUnusedDoctrineEntityMethodAndPropertyRectorTest.php @@ -13,7 +13,7 @@ public function test(): void __DIR__ . '/Fixture/fixture.php.inc', __DIR__ . '/Fixture/remove_inversed_by.php.inc', __DIR__ . '/Fixture/remove_inversed_by_non_fqn.php.inc', - // skip + // skip __DIR__ . '/Fixture/skip_double_entity_call.php.inc', __DIR__ . '/Fixture/skip_id_and_system.php.inc', __DIR__ . '/Fixture/skip_trait_called_method.php.inc', diff --git a/packages/DoctrinePhpDocParser/config/config.yaml b/packages/DoctrinePhpDocParser/config/config.yaml new file mode 100644 index 00000000000..bc88adb56d2 --- /dev/null +++ b/packages/DoctrinePhpDocParser/config/config.yaml @@ -0,0 +1,8 @@ +services: + _defaults: + autowire: true + public: true + + Rector\DoctrinePhpDocParser\: + resource: '../src' + exclude: '../src/{Ast/PhpDoc/*,*StaticHelper.php}' diff --git a/packages/DoctrinePhpDocParser/src/AnnotationReader/AnnotationReaderFactory.php b/packages/DoctrinePhpDocParser/src/AnnotationReader/AnnotationReaderFactory.php new file mode 100644 index 00000000000..be428e70cae --- /dev/null +++ b/packages/DoctrinePhpDocParser/src/AnnotationReader/AnnotationReaderFactory.php @@ -0,0 +1,16 @@ +annotationReader = $annotationReader; + $this->nameResolver = $nameResolver; + } + + public function readDoctrineClassAnnotation(Class_ $class, string $annotationClassName): Annotation + { + $classReflection = $this->createClassReflectionFromNode($class); + + /** @var Annotation|null $classAnnotation */ + $classAnnotation = $this->annotationReader->getClassAnnotation($classReflection, $annotationClassName); + if ($classAnnotation === null) { + throw new ShouldNotHappenException(__METHOD__ . '() on line ' . __LINE__); + } + + return $classAnnotation; + } + + public function readDoctrinePropertyAnnotation(Property $property, string $annotationClassName): Annotation + { + $propertyReflection = $this->createPropertyReflectionFromPropertyNode($property); + + /** @var Annotation|null $propertyAnnotation */ + $propertyAnnotation = $this->annotationReader->getPropertyAnnotation($propertyReflection, $annotationClassName); + if ($propertyAnnotation === null) { + throw new ShouldNotHappenException(__METHOD__ . '() on line ' . __LINE__); + } + + return $propertyAnnotation; + } + + private function createPropertyReflectionFromPropertyNode(Property $property): ReflectionProperty + { + /** @var string $propertyName */ + $propertyName = $this->nameResolver->getName($property); + + /** @var string $className */ + $className = $property->getAttribute(AttributeKey::CLASS_NAME); + + return new ReflectionProperty($className, $propertyName); + } + + private function createClassReflectionFromNode(Class_ $class): ReflectionClass + { + /** @var string $className */ + $className = $this->nameResolver->getName($class); + + return new ReflectionClass($className); + } +} diff --git a/packages/DoctrinePhpDocParser/src/Array_/ArrayItemStaticHelper.php b/packages/DoctrinePhpDocParser/src/Array_/ArrayItemStaticHelper.php new file mode 100644 index 00000000000..e3696ea9811 --- /dev/null +++ b/packages/DoctrinePhpDocParser/src/Array_/ArrayItemStaticHelper.php @@ -0,0 +1,48 @@ + $secondItemPosition; + }); + + return $contentItems; + } +} diff --git a/packages/DoctrinePhpDocParser/src/Ast/PhpDoc/ColumnTagValueNode.php b/packages/DoctrinePhpDocParser/src/Ast/PhpDoc/ColumnTagValueNode.php new file mode 100644 index 00000000000..4330ae2fbe3 --- /dev/null +++ b/packages/DoctrinePhpDocParser/src/Ast/PhpDoc/ColumnTagValueNode.php @@ -0,0 +1,138 @@ +name = $name; + $this->type = $type; + $this->length = $length; + $this->precision = $precision; + $this->scale = $scale; + $this->unique = $unique; + $this->nullable = $nullable; + $this->options = $options; + $this->columnDefinition = $columnDefinition; + $this->orderedVisibleItems = $orderedVisibleItems; + } + + public function __toString(): string + { + $contentItems = []; + + // required + $contentItems['type'] = sprintf('type="%s"', $this->type); + $contentItems['name'] = sprintf('name="%s"', $this->name); + $contentItems['length'] = sprintf('length=%s', $this->length); + $contentItems['precision'] = sprintf('precision=%s', $this->precision); + $contentItems['scale'] = sprintf('scale=%s', $this->scale); + $contentItems['unique'] = sprintf('unique=%s', $this->unique ? 'true' : 'false'); + $contentItems['nullable'] = sprintf('nullable=%s', $this->nullable ? 'true' : 'false'); + + if ($this->options !== []) { + $optionsContent = Json::encode($this->options); + $optionsContent = Strings::replace($optionsContent, '#,#', ', '); + $contentItems['options'] = 'options=' . $optionsContent; + } + + $contentItems['columnDefinition'] = sprintf('columnDefinition="%s"', $this->columnDefinition); + + $contentItems = ArrayItemStaticHelper::filterAndSortVisibleItems($contentItems, $this->orderedVisibleItems); + if ($contentItems === []) { + return ''; + } + + return '(' . implode(', ', $contentItems) . ')'; + } + + /** + * @return mixed + */ + public function getType() + { + return $this->type; + } + + public function isNullable(): bool + { + return $this->nullable; + } +} diff --git a/packages/DoctrinePhpDocParser/src/Ast/PhpDoc/EntityTagValueNode.php b/packages/DoctrinePhpDocParser/src/Ast/PhpDoc/EntityTagValueNode.php new file mode 100644 index 00000000000..a95f0f38064 --- /dev/null +++ b/packages/DoctrinePhpDocParser/src/Ast/PhpDoc/EntityTagValueNode.php @@ -0,0 +1,66 @@ +repositoryClass = $repositoryClass; + $this->readOnly = $readOnly; + $this->orderedVisibleItems = $orderedVisibleItems; + } + + public function __toString(): string + { + $contentItems = []; + + $contentItems['repositoryClass'] = sprintf('repositoryClass="%s"', $this->repositoryClass); + + // default value + $contentItems['readOnly'] = sprintf('readOnly=%s', $this->readOnly ? 'true' : 'false'); + + $contentItems = ArrayItemStaticHelper::filterAndSortVisibleItems($contentItems, $this->orderedVisibleItems); + if ($contentItems === []) { + return ''; + } + + return '(' . implode(', ', $contentItems) . ')'; + } + + public function removeRepositoryClass(): void + { + $itemPosition = array_search('repositoryClass', $this->orderedVisibleItems, true); + if ($itemPosition !== null) { + unset($this->orderedVisibleItems[$itemPosition]); + } + + $this->repositoryClass = null; + } +} diff --git a/packages/DoctrinePhpDocParser/src/Contract/Ast/PhpDoc/DoctrineTagNodeInterface.php b/packages/DoctrinePhpDocParser/src/Contract/Ast/PhpDoc/DoctrineTagNodeInterface.php new file mode 100644 index 00000000000..6abc32906d4 --- /dev/null +++ b/packages/DoctrinePhpDocParser/src/Contract/Ast/PhpDoc/DoctrineTagNodeInterface.php @@ -0,0 +1,7 @@ +currentNodeProvider = $currentNodeProvider; + $this->nodeAnnotationReader = $nodeAnnotationReader; + } + + public function parse(TokenIterator $tokenIterator, string $tag): PhpDocTagValueNode + { + /** @var Class_|Property $node */ + $node = $this->currentNodeProvider->getNode(); + + // skip all tokens for this annotation, so next annotation can work with tokens after this one + $annotationContent = $tokenIterator->joinUntil( + Lexer::TOKEN_END, + Lexer::TOKEN_PHPDOC_EOL, + Lexer::TOKEN_CLOSE_PHPDOC + ); + + // Entity tags + if ($node instanceof Class_) { + if ($tag === '@ORM\Entity') { + return $this->createEntityTagValueNode($node, $annotationContent); + } + } + + // Property tags + if ($node instanceof Property) { + if ($tag === '@ORM\Column') { + return $this->createColumnTagValueNode($node, $annotationContent); + } + } + + // @todo + throw new NotImplementedException(__METHOD__); + } + + private function createEntityTagValueNode(Class_ $node, string $content): EntityTagValueNode + { + /** @var Entity $entity */ + $entity = $this->nodeAnnotationReader->readDoctrineClassAnnotation($node, Entity::class); + + $itemsOrder = $this->resolveAnnotationItemsOrder($content); + + return new EntityTagValueNode($entity->repositoryClass, $entity->readOnly, $itemsOrder); + } + + private function createColumnTagValueNode(Property $property, string $content): ColumnTagValueNode + { + /** @var Column $column */ + $column = $this->nodeAnnotationReader->readDoctrinePropertyAnnotation($property, Column::class); + + $itemsOrder = $this->resolveAnnotationItemsOrder($content); + + return new ColumnTagValueNode( + $column->name, + $column->type, + $column->length, + $column->precision, + $column->scale, + $column->unique, + $column->nullable, + $column->options, + $column->columnDefinition, + $itemsOrder + ); + } + + /** + * @return string[] + */ + private function resolveAnnotationItemsOrder(string $content): array + { + $itemsOrder = []; + $matches = Strings::matchAll($content, '#(?\w+)=#m'); + foreach ($matches as $match) { + $itemsOrder[] = $match['item']; + } + + return $itemsOrder; + } +} diff --git a/packages/DoctrinePhpDocParser/tests/PhpDocParser/OrmTagParser/AbstractOrmTagParserTest.php b/packages/DoctrinePhpDocParser/tests/PhpDocParser/OrmTagParser/AbstractOrmTagParserTest.php new file mode 100644 index 00000000000..a1c4a703120 --- /dev/null +++ b/packages/DoctrinePhpDocParser/tests/PhpDocParser/OrmTagParser/AbstractOrmTagParserTest.php @@ -0,0 +1,60 @@ +bootKernel(RectorKernel::class); + + $this->phpDocInfoFactory = self::$container->get(PhpDocInfoFactory::class); + $this->fileInfoParser = self::$container->get(FileInfoParser::class); + + $this->betterNodeFinder = self::$container->get(BetterNodeFinder::class); + $this->phpDocInfoPrinter = self::$container->get(PhpDocInfoPrinter::class); + } + + protected function parseFileAndGetFirstNodeOfType(string $filePath, string $type): Node + { + $nodes = $this->fileInfoParser->parseFileInfoToNodesAndDecorate(new SmartFileInfo($filePath)); + + return $this->betterNodeFinder->findFirstInstanceOf($nodes, $type); + } + + protected function createPhpDocInfoFromNodeAndPrintBackToString(Node $node): string + { + $phpDocInfo = $this->phpDocInfoFactory->createFromNode($node); + + return $this->phpDocInfoPrinter->printFormatPreserving($phpDocInfo); + } +} diff --git a/packages/DoctrinePhpDocParser/tests/PhpDocParser/OrmTagParser/Class_/Fixture/SomeEntity.php b/packages/DoctrinePhpDocParser/tests/PhpDocParser/OrmTagParser/Class_/Fixture/SomeEntity.php new file mode 100644 index 00000000000..a625bc689d4 --- /dev/null +++ b/packages/DoctrinePhpDocParser/tests/PhpDocParser/OrmTagParser/Class_/Fixture/SomeEntity.php @@ -0,0 +1,15 @@ +parseFileAndGetFirstNodeOfType($filePath, Class_::class); + $printedPhpDocInfo = $this->createPhpDocInfoFromNodeAndPrintBackToString($class); + + $this->assertStringEqualsFile($expectedPrintedPhpDoc, $printedPhpDocInfo); + } + + public function provideData(): iterable + { + yield [__DIR__ . '/Fixture/SomeEntity.php', __DIR__ . '/Fixture/expected_some_entity.txt']; + } +} diff --git a/packages/DoctrinePhpDocParser/tests/PhpDocParser/OrmTagParser/Class_/Source/ExistingRepositoryClass.php b/packages/DoctrinePhpDocParser/tests/PhpDocParser/OrmTagParser/Class_/Source/ExistingRepositoryClass.php new file mode 100644 index 00000000000..0c0e6e4cb5b --- /dev/null +++ b/packages/DoctrinePhpDocParser/tests/PhpDocParser/OrmTagParser/Class_/Source/ExistingRepositoryClass.php @@ -0,0 +1,8 @@ +parseFileAndGetFirstNodeOfType($filePath, Property::class); + $printedPhpDocInfo = $this->createPhpDocInfoFromNodeAndPrintBackToString($property); + + $this->assertStringEqualsFile($expectedPrintedPhpDoc, $printedPhpDocInfo); + } + + public function provideData(): iterable + { + yield [__DIR__ . '/Fixture/SomeProperty.php', __DIR__ . '/Fixture/expected_some_property.txt']; + yield [__DIR__ . '/Fixture/PropertyWithName.php', __DIR__ . '/Fixture/expected_property_with_name.txt']; + yield [__DIR__ . '/Fixture/FromOfficialDocs.php', __DIR__ . '/Fixture/expected_from_official_docs.txt']; + } +} diff --git a/packages/FileSystemRector/src/FileSystemFileProcessor.php b/packages/FileSystemRector/src/FileSystemFileProcessor.php index 84f115ef66c..3eb30ce21dd 100644 --- a/packages/FileSystemRector/src/FileSystemFileProcessor.php +++ b/packages/FileSystemRector/src/FileSystemFileProcessor.php @@ -20,14 +20,6 @@ public function __construct(array $fileSystemRectors = []) $this->fileSystemRectors = $fileSystemRectors; } - /** - * @return FileSystemRectorInterface[] - */ - public function getFileSystemRectors(): array - { - return $this->fileSystemRectors; - } - public function processFileInfo(SmartFileInfo $smartFileInfo): void { foreach ($this->fileSystemRectors as $fileSystemRector) { diff --git a/packages/FileSystemRector/src/Parser/FileInfoParser.php b/packages/FileSystemRector/src/Parser/FileInfoParser.php new file mode 100644 index 00000000000..be9f3fccf10 --- /dev/null +++ b/packages/FileSystemRector/src/Parser/FileInfoParser.php @@ -0,0 +1,37 @@ +parser = $parser; + $this->nodeScopeAndMetadataDecorator = $nodeScopeAndMetadataDecorator; + } + + /** + * @return Node[] + */ + public function parseFileInfoToNodesAndDecorate(SmartFileInfo $fileInfo): array + { + $oldStmts = $this->parser->parseFile($fileInfo->getRealPath()); + + return $this->nodeScopeAndMetadataDecorator->decorateNodesFromFile($oldStmts, $fileInfo->getRealPath()); + } +} diff --git a/packages/FileSystemRector/src/Rector/AbstractFileSystemRector.php b/packages/FileSystemRector/src/Rector/AbstractFileSystemRector.php index 8d16907db92..acadd189c16 100644 --- a/packages/FileSystemRector/src/Rector/AbstractFileSystemRector.php +++ b/packages/FileSystemRector/src/Rector/AbstractFileSystemRector.php @@ -92,6 +92,7 @@ protected function parseFileInfoToNodes(SmartFileInfo $smartFileInfo): array $oldStmts = $this->parser->parseFile($smartFileInfo->getRealPath()); $this->oldStmts = $oldStmts; // needed for format preserving + $this->oldStmts = $oldStmts; return $this->nodeScopeAndMetadataDecorator->decorateNodesFromFile( $oldStmts, $smartFileInfo->getRealPath() diff --git a/packages/Symfony/src/Bridge/DefaultAnalyzedSymfonyApplicationContainer.php b/packages/Symfony/src/Bridge/DefaultAnalyzedSymfonyApplicationContainer.php index 504f3bf7406..68b2fd64eef 100644 --- a/packages/Symfony/src/Bridge/DefaultAnalyzedSymfonyApplicationContainer.php +++ b/packages/Symfony/src/Bridge/DefaultAnalyzedSymfonyApplicationContainer.php @@ -2,6 +2,7 @@ namespace Rector\Symfony\Bridge; +use Doctrine\ORM\EntityManagerInterface; use Rector\Bridge\Contract\AnalyzedApplicationContainerInterface; use Rector\Configuration\Option; use Rector\Exception\ShouldNotHappenException; @@ -39,8 +40,8 @@ final class DefaultAnalyzedSymfonyApplicationContainer implements AnalyzedApplic */ private $commonNamesToTypes = [ 'doctrine' => 'Symfony\Bridge\Doctrine\RegistryInterface', - 'doctrine.orm.entity_manager' => 'Doctrine\ORM\EntityManagerInterface', - 'doctrine.orm.default_entity_manager' => 'Doctrine\ORM\EntityManagerInterface', + 'doctrine.orm.entity_manager' => EntityManagerInterface::class, + 'doctrine.orm.default_entity_manager' => EntityManagerInterface::class, ]; public function __construct( @@ -75,6 +76,10 @@ public function getTypeForName(string $name): ?string ), $throwable->getCode(), $throwable); } + if ($service === null) { + return null; + } + $serviceClass = get_class($service); if ($container->has($serviceClass)) { return $serviceClass; diff --git a/packages/TypeDeclaration/src/TypeInferer/PropertyTypeInferer/DoctrineColumnPropertyTypeInferer.php b/packages/TypeDeclaration/src/TypeInferer/PropertyTypeInferer/DoctrineColumnPropertyTypeInferer.php index e26fff5774e..b410bb8a79c 100644 --- a/packages/TypeDeclaration/src/TypeInferer/PropertyTypeInferer/DoctrineColumnPropertyTypeInferer.php +++ b/packages/TypeDeclaration/src/TypeInferer/PropertyTypeInferer/DoctrineColumnPropertyTypeInferer.php @@ -3,19 +3,12 @@ namespace Rector\TypeDeclaration\TypeInferer\PropertyTypeInferer; use DateTimeInterface; -use Nette\Utils\Strings; use PhpParser\Node\Stmt\Property; -use PHPStan\PhpDocParser\Ast\PhpDoc\GenericTagValueNode; use Rector\NodeTypeResolver\PhpDoc\NodeAnalyzer\DocBlockManipulator; use Rector\TypeDeclaration\Contract\TypeInferer\PropertyTypeInfererInterface; final class DoctrineColumnPropertyTypeInferer implements PropertyTypeInfererInterface { - /** - * @var string - */ - private const COLUMN_ANNOTATION = 'Doctrine\ORM\Mapping\Column'; - /** * @var string[] * @see \Doctrine\DBAL\Platforms\MySqlPlatform::initializeDoctrineTypeMappings() @@ -70,23 +63,17 @@ public function __construct(DocBlockManipulator $docBlockManipulator) */ public function inferProperty(Property $property): array { - if (! $this->docBlockManipulator->hasTag($property, self::COLUMN_ANNOTATION)) { - return []; - } - - $columnTag = $this->docBlockManipulator->getTagByName($property, self::COLUMN_ANNOTATION); - if (! $columnTag->value instanceof GenericTagValueNode) { + if ($property->getDocComment() === null) { return []; } - $match = Strings::match($columnTag->value->value, '#type=\"(?.*?)\"#'); - if (! isset($match['type'])) { + $phpDocInfo = $this->docBlockManipulator->createPhpDocInfoFromNode($property); + $doctrineColumnTagValueNode = $phpDocInfo->getDoctrineColumnTagValueNode(); + if ($doctrineColumnTagValueNode === null) { return []; } - $doctrineType = $match['type']; - $scalarType = $this->doctrineTypeToScalarType[$doctrineType] ?? null; - + $scalarType = $this->doctrineTypeToScalarType[$doctrineColumnTagValueNode->getType()] ?? null; if ($scalarType === null) { return []; } @@ -94,7 +81,7 @@ public function inferProperty(Property $property): array $types = [$scalarType]; // is nullable? - if ($this->isNullable($columnTag->value->value)) { + if ($doctrineColumnTagValueNode->isNullable()) { $types[] = 'null'; } @@ -105,9 +92,4 @@ public function getPriority(): int { return 1000; } - - private function isNullable(string $value): bool - { - return (bool) Strings::match($value, '#nullable=true#'); - } } diff --git a/packages/TypeDeclaration/src/TypeInferer/PropertyTypeInferer/DoctrineRelationPropertyTypeInferer.php b/packages/TypeDeclaration/src/TypeInferer/PropertyTypeInferer/DoctrineRelationPropertyTypeInferer.php index 0034f8865d0..e321d689f5c 100644 --- a/packages/TypeDeclaration/src/TypeInferer/PropertyTypeInferer/DoctrineRelationPropertyTypeInferer.php +++ b/packages/TypeDeclaration/src/TypeInferer/PropertyTypeInferer/DoctrineRelationPropertyTypeInferer.php @@ -2,6 +2,11 @@ namespace Rector\TypeDeclaration\TypeInferer\PropertyTypeInferer; +use Doctrine\Common\Collections\Collection; +use Doctrine\ORM\Mapping\ManyToMany; +use Doctrine\ORM\Mapping\ManyToOne; +use Doctrine\ORM\Mapping\OneToMany; +use Doctrine\ORM\Mapping\OneToOne; use PhpParser\Node\Stmt\Property; use Rector\DeadCode\Doctrine\DoctrineEntityManipulator; use Rector\NodeTypeResolver\PhpDoc\NodeAnalyzer\DocBlockManipulator; @@ -12,18 +17,18 @@ final class DoctrineRelationPropertyTypeInferer implements PropertyTypeInfererIn /** * @var string[] */ - private const TO_MANY_ANNOTATIONS = ['Doctrine\ORM\Mapping\OneToMany', 'Doctrine\ORM\Mapping\ManyToMany']; + private const TO_MANY_ANNOTATIONS = [OneToMany::class, ManyToMany::class]; /** * Nullable by default, @see https://www.doctrine-project.org/projects/doctrine-orm/en/2.6/reference/annotations-reference.html#joincolumn - "JoinColumn" and nullable=true * @var string[] */ - private const TO_ONE_ANNOTATIONS = ['Doctrine\ORM\Mapping\ManyToOne', 'Doctrine\ORM\Mapping\OneToOne']; + private const TO_ONE_ANNOTATIONS = [ManyToOne::class, OneToOne::class]; /** * @var string */ - private const COLLECTION_TYPE = 'Doctrine\Common\Collections\Collection'; + private const COLLECTION_TYPE = Collection::class; /** * @var DocBlockManipulator diff --git a/phpstan.neon b/phpstan.neon index c656a9b4c6b..b8052902956 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -184,6 +184,9 @@ parameters: - '#Parameter \#1 \$node of method Rector\\NodeTypeResolver\\NodeTypeResolver\:\:resolveSingleTypeToStrings\(\) expects PhpParser\\Node, PhpParser\\Node\\Expr\|null given#' - '#Parameter \#1 \$name of class ReflectionFunction constructor expects Closure\|string, callable\(\)\: mixed given#' + - '#Method Rector\\DoctrinePhpDocParser\\PhpDocParser\\OrmTagParser\:\:readDoctrineAnnotation\(\) should return Doctrine\\ORM\\Mapping\\Annotation\|null but returns object\|null#' + - '#Method Rector\\DoctrinePhpDocParser\\Tests\\PhpDocParser\\OrmTagParser\\AbstractOrmTagParserTest\:\:parseFileAndGetFirstNodeOfType\(\) should return PhpParser\\Node but returns PhpParser\\Node\|null#' + - '#Method Rector\\Symfony\\Bridge\\DefaultAnalyzedSymfonyApplicationContainer\:\:getService\(\) should return object but returns object\|null#' # PHP 7.4 1_000 support - '#Property PhpParser\\Node\\Scalar\\DNumber\:\:\$value \(float\) does not accept string#' diff --git a/src/Configuration/CurrentNodeProvider.php b/src/Configuration/CurrentNodeProvider.php new file mode 100644 index 00000000000..99ac58b71e7 --- /dev/null +++ b/src/Configuration/CurrentNodeProvider.php @@ -0,0 +1,23 @@ +node = $node; + } + + public function getNode(): Node + { + return $this->node; + } +} diff --git a/src/Console/Command/AbstractCommand.php b/src/Console/Command/AbstractCommand.php index 185d80a499f..999bce7a2c6 100644 --- a/src/Console/Command/AbstractCommand.php +++ b/src/Console/Command/AbstractCommand.php @@ -52,6 +52,10 @@ public function run(InputInterface $input, OutputInterface $output): int protected function initialize(InputInterface $input, OutputInterface $output): void { if ($input->getOption('debug')) { + if ($this->getApplication() === null) { + return; + } + $this->getApplication()->setCatchExceptions(false); } } diff --git a/packages/BetterPhpDocParser/src/Exception/NotImplementedYetException.php b/src/Exception/NotImplementedYetException.php similarity index 70% rename from packages/BetterPhpDocParser/src/Exception/NotImplementedYetException.php rename to src/Exception/NotImplementedYetException.php index 7a52c451708..16b486d820a 100644 --- a/packages/BetterPhpDocParser/src/Exception/NotImplementedYetException.php +++ b/src/Exception/NotImplementedYetException.php @@ -1,6 +1,6 @@ doctrineEntityAndRepositoryMapper = $doctrineEntityAndRepositoryMapper; $this->entityRepositoryClass = $entityRepositoryClass; @@ -84,8 +86,8 @@ public function __construct(\Doctrine\ORM\EntityManager $entityManager) CODE_SAMPLE , [ - '$entityRepositoryClass' => 'Doctrine\ORM\EntityRepository', - '$entityManagerClass' => 'Doctrine\ORM\EntityManager', + '$entityRepositoryClass' => EntityRepository::class, + '$entityManagerClass' => EntityManager::class, ] ), ]); diff --git a/src/Rector/Architecture/RepositoryAsService/ReplaceParentRepositoryCallsByRepositoryPropertyRector.php b/src/Rector/Architecture/RepositoryAsService/ReplaceParentRepositoryCallsByRepositoryPropertyRector.php index 747735b1db3..c2b4c679487 100644 --- a/src/Rector/Architecture/RepositoryAsService/ReplaceParentRepositoryCallsByRepositoryPropertyRector.php +++ b/src/Rector/Architecture/RepositoryAsService/ReplaceParentRepositoryCallsByRepositoryPropertyRector.php @@ -2,6 +2,7 @@ namespace Rector\Rector\Architecture\RepositoryAsService; +use Doctrine\ORM\EntityRepository; use PhpParser\Node; use PhpParser\Node\Expr\MethodCall; use PhpParser\Node\Identifier; @@ -32,7 +33,7 @@ final class ReplaceParentRepositoryCallsByRepositoryPropertyRector extends Abstr 'matching', ]; - public function __construct(string $entityRepositoryClass = 'Doctrine\ORM\EntityRepository') + public function __construct(string $entityRepositoryClass = EntityRepository::class) { $this->entityRepositoryClass = $entityRepositoryClass; }