diff --git a/packages/BetterPhpDocParser/src/PhpDocInfo/PhpDocInfoFactory.php b/packages/BetterPhpDocParser/src/PhpDocInfo/PhpDocInfoFactory.php index a7d0a83c3890..46d7ee4a00f6 100644 --- a/packages/BetterPhpDocParser/src/PhpDocInfo/PhpDocInfoFactory.php +++ b/packages/BetterPhpDocParser/src/PhpDocInfo/PhpDocInfoFactory.php @@ -11,6 +11,7 @@ use Rector\BetterPhpDocParser\Attributes\Contract\Ast\AttributeAwareNodeInterface; use Rector\Configuration\CurrentNodeProvider; use Rector\DoctrinePhpDocParser\PhpDocParser\OrmTagParser; +use Rector\Exception\ShouldNotHappenException; use Rector\NodeTypeResolver\StaticTypeMapper; final class PhpDocInfoFactory @@ -35,6 +36,11 @@ final class PhpDocInfoFactory */ private $staticTypeMapper; + /** + * @var PhpDocInfo[] + */ + private $phpDocInfoByObjectHash = []; + public function __construct( PhpDocParser $phpDocParser, Lexer $lexer, @@ -49,6 +55,12 @@ public function __construct( public function createFromNode(Node $node): PhpDocInfo { + $hash = $this->createUniqueDocNodeHash($node); + + if (isset($this->phpDocInfoByObjectHash[$hash])) { + return $this->phpDocInfoByObjectHash[$hash]; + } + /** needed for @see OrmTagParser */ $this->currentNodeProvider->setNode($node); @@ -60,7 +72,10 @@ public function createFromNode(Node $node): PhpDocInfo $phpDocNode = $this->setPositionOfLastToken($phpDocNode); - return new PhpDocInfo($phpDocNode, $tokens, $content, $this->staticTypeMapper, $node); + $phpDocInfo = new PhpDocInfo($phpDocNode, $tokens, $content, $this->staticTypeMapper, $node); + $this->phpDocInfoByObjectHash[$hash] = $phpDocInfo; + + return $phpDocInfo; } /** @@ -84,4 +99,16 @@ private function setPositionOfLastToken( return $attributeAwarePhpDocNode; } + + private function createUniqueDocNodeHash(Node $node): string + { + $objectHash = spl_object_hash($node); + + if ($node->getDocComment() === null) { + throw new ShouldNotHappenException(sprintf('"%s" is missing a DocComment node', get_class($node))); + } + $docCommentHash = spl_object_hash($node->getDocComment()); + + return $objectHash . $docCommentHash; + } } diff --git a/packages/BetterPhpDocParser/tests/PhpDocInfo/PhpDocInfoPrinter/DoctrineTest.php b/packages/BetterPhpDocParser/tests/PhpDocInfo/PhpDocInfoPrinter/DoctrineTest.php new file mode 100644 index 000000000000..538ae8f87c04 --- /dev/null +++ b/packages/BetterPhpDocParser/tests/PhpDocInfo/PhpDocInfoPrinter/DoctrineTest.php @@ -0,0 +1,41 @@ +createPhpDocInfoFromDocCommentAndNode($docComment, $node); + + $expectedDoc = FileSystem::read($expectedDocFilePath); + + $this->assertSame( + $expectedDoc, + $this->phpDocInfoPrinter->printFormatPreserving($phpDocInfo), + 'Caused in ' . $docFilePath + ); + } + + /** + * @return string[]|Class_[] + */ + public function provideDataForTestClass(): Iterator + { + yield [ + __DIR__ . '/Source/Doctrine/index_in_table.txt', + new Class_(IndexInTable::class), + __DIR__ . '/Source/Doctrine/expected_index_in_table.txt', + ]; + } +} diff --git a/packages/BetterPhpDocParser/tests/PhpDocInfo/PhpDocInfoPrinter/Source/Doctrine/IndexInTable.php b/packages/BetterPhpDocParser/tests/PhpDocInfo/PhpDocInfoPrinter/Source/Doctrine/IndexInTable.php new file mode 100644 index 000000000000..87382433a66e --- /dev/null +++ b/packages/BetterPhpDocParser/tests/PhpDocInfo/PhpDocInfoPrinter/Source/Doctrine/IndexInTable.php @@ -0,0 +1,18 @@ +getAttribute(AttributeKey::CLASS_NAME); - if ($className === null || ! class_exists($className)) { + if ($className === null || ! ClassExistenceStaticHelper::doesClassLikeExist($className)) { throw new ShouldNotHappenException(sprintf( 'Class "%s" for property "%s" was not found.', (string) $className, diff --git a/packages/DoctrinePhpDocParser/src/Ast/PhpDoc/Property_/UniqueConstraintTagValueNode.php b/packages/DoctrinePhpDocParser/src/Ast/PhpDoc/Class_/AbstractIndexTagValueNode.php similarity index 88% rename from packages/DoctrinePhpDocParser/src/Ast/PhpDoc/Property_/UniqueConstraintTagValueNode.php rename to packages/DoctrinePhpDocParser/src/Ast/PhpDoc/Class_/AbstractIndexTagValueNode.php index 54e9e1ea3645..e92440556aed 100644 --- a/packages/DoctrinePhpDocParser/src/Ast/PhpDoc/Property_/UniqueConstraintTagValueNode.php +++ b/packages/DoctrinePhpDocParser/src/Ast/PhpDoc/Class_/AbstractIndexTagValueNode.php @@ -1,17 +1,12 @@ schema); } - if ($this->indexes) { - $contentItems['indexes'] = $this->printArrayItem($this->indexes, 'indexes'); + if ($this->indexes !== []) { + $indexesAsString = $this->printTagValueNodesSeparatedByComma( + $this->indexes, + IndexTagValueNode::SHORT_NAME + ); + $contentItems['indexes'] = sprintf('indexes={%s}', $indexesAsString); } if ($this->uniqueConstraints !== []) { diff --git a/packages/DoctrinePhpDocParser/src/Ast/PhpDoc/Class_/UniqueConstraintTagValueNode.php b/packages/DoctrinePhpDocParser/src/Ast/PhpDoc/Class_/UniqueConstraintTagValueNode.php new file mode 100644 index 000000000000..357da77cabc1 --- /dev/null +++ b/packages/DoctrinePhpDocParser/src/Ast/PhpDoc/Class_/UniqueConstraintTagValueNode.php @@ -0,0 +1,11 @@ +.*?)\),?#s'; + private const JOIN_COLUMN_PATTERN = '#@ORM\\\\JoinColumn\((?.*?)\),?#s'; /** - * @var string + * @var NameResolver */ - public const UNIQUE_CONSTRAINT_PATTERN = '#@ORM\\\\UniqueConstraint\((?.*?)\),?#s'; + private $nameResolver; /** - * @var NameResolver + * @var TableTagValueNodeFactory */ - private $nameResolver; + private $tableTagValueNodeFactory; - public function __construct(NameResolver $nameResolver) + public function __construct(NameResolver $nameResolver, TableTagValueNodeFactory $tableTagValueNodeFactory) { $this->nameResolver = $nameResolver; + $this->tableTagValueNodeFactory = $tableTagValueNodeFactory; } public function parse(TokenIterator $tokenIterator, string $tag): ?PhpDocTagValueNode @@ -82,7 +80,7 @@ public function parse(TokenIterator $tokenIterator, string $tag): ?PhpDocTagValu } if ($tag === TableTagValueNode::SHORT_NAME) { - return $this->createTableTagValueNode($currentPhpNode, $annotationContent); + return $this->tableTagValueNodeFactory->create($currentPhpNode, $annotationContent); } } @@ -164,34 +162,6 @@ private function createInheritanceTypeTagValueNode(Class_ $class): InheritanceTy return new InheritanceTypeTagValueNode($inheritanceType->value); } - private function createTableTagValueNode(Class_ $class, string $annotationContent): TableTagValueNode - { - /** @var Table $table */ - $table = $this->nodeAnnotationReader->readClassAnnotation($class, Table::class); - - $uniqueConstraintContents = Strings::matchAll($annotationContent, self::UNIQUE_CONSTRAINT_PATTERN); - - $uniqueConstraintsTagValueNodes = []; - if ($table->uniqueConstraints !== null) { - foreach ($table->uniqueConstraints as $key => $uniqueConstraint) { - $currentUniqueConstraintContent = $uniqueConstraintContents[$key]['singleUniqueConstraint']; - $uniqueConstraintsTagValueNodes[] = $this->createUniqueConstantTagValueNodeFromUniqueConstaint( - $uniqueConstraint, - $currentUniqueConstraintContent - ); - } - } - - return new TableTagValueNode( - $table->name, - $table->schema, - $table->indexes, - $uniqueConstraintsTagValueNodes, - $table->options, - $annotationContent - ); - } - private function createColumnTagValueNode(Property $property, string $annotationContent): ColumnTagValueNode { /** @var Column $column */ @@ -336,22 +306,6 @@ private function resolveFqnTargetEntity(string $targetEntity, Node $node): strin return $targetEntity; } - private function createUniqueConstantTagValueNodeFromUniqueConstaint( - UniqueConstraint $uniqueConstraint, - string $annotationContent - ): UniqueConstraintTagValueNode { - // doctrine orm compatibility - $flags = property_exists($uniqueConstraint, 'flags') ? $uniqueConstraint->flags : []; - - return new UniqueConstraintTagValueNode( - $uniqueConstraint->name, - $uniqueConstraint->columns, - $flags, - $uniqueConstraint->options, - $annotationContent - ); - } - private function createJoinColumnTagValueNodeFromJoinColumnAnnotation( JoinColumn $joinColumn, string $annotationContent diff --git a/packages/DoctrinePhpDocParser/src/PhpDocParser/TableTagValueNodeFactory.php b/packages/DoctrinePhpDocParser/src/PhpDocParser/TableTagValueNodeFactory.php new file mode 100644 index 000000000000..dd622b7d15c9 --- /dev/null +++ b/packages/DoctrinePhpDocParser/src/PhpDocParser/TableTagValueNodeFactory.php @@ -0,0 +1,130 @@ +.*?)\),?#s'; + + /** + * @var string + */ + private const INDEX_PATTERN = '#@ORM\\\\Index\((?.*?)\),?#s'; + + /** + * @var NodeAnnotationReader + */ + private $nodeAnnotationReader; + + public function __construct(NodeAnnotationReader $nodeAnnotationReader) + { + $this->nodeAnnotationReader = $nodeAnnotationReader; + } + + public function create(Class_ $class, string $annotationContent): TableTagValueNode + { + /** @var Table $table */ + $table = $this->nodeAnnotationReader->readClassAnnotation($class, Table::class); + + $indexTagValueNodes = $this->createIndexTagValueNodes($table->indexes, $annotationContent); + $uniqueConstraintTagValueNodes = $this->createUniqueConstraintTagValueNodes( + $table->uniqueConstraints, + $annotationContent + ); + + return new TableTagValueNode( + $table->name, + $table->schema, + $indexTagValueNodes, + $uniqueConstraintTagValueNodes, + $table->options, + $annotationContent + ); + } + + /** + * @param mixed[]|null $indexes + * @return IndexTagValueNode[] + */ + private function createIndexTagValueNodes(?array $indexes, string $annotationContent): array + { + if ($indexes === null) { + return []; + } + + $indexContents = Strings::matchAll($annotationContent, self::INDEX_PATTERN); + + $indexTagValueNodes = []; + foreach ($indexes as $key => $index) { + $indexTagValueNodes[] = $this->createIndexOrUniqueConstantTagValueNode( + $index, + $indexContents[$key]['singleIndex'] + ); + } + + return $indexTagValueNodes; + } + + /** + * @return UniqueConstraintTagValueNode[] + */ + private function createUniqueConstraintTagValueNodes(?array $uniqueConstraints, string $annotationContent): array + { + if ($uniqueConstraints === null) { + return []; + } + + $uniqueConstraintContents = Strings::matchAll($annotationContent, self::UNIQUE_CONSTRAINT_PATTERN); + + $uniqueConstraintTagValueNodes = []; + foreach ($uniqueConstraints as $key => $uniqueConstraint) { + $uniqueConstraintTagValueNodes[] = $this->createIndexOrUniqueConstantTagValueNode( + $uniqueConstraint, + $uniqueConstraintContents[$key]['singleUniqueConstraint'] + ); + } + + return $uniqueConstraintTagValueNodes; + } + + /** + * @param Index|UniqueConstraint $annotation + * @param string $annotationContent + * @return IndexTagValueNode|UniqueConstraintTagValueNode + */ + private function createIndexOrUniqueConstantTagValueNode( + Annotation $annotation, + string $annotationContent + ): AbstractIndexTagValueNode { + // doctrine orm compatibility + $flags = property_exists($annotation, 'flags') ? $annotation->flags : []; + + $arguments = [$annotation->name, $annotation->columns, $flags, $annotation->options, $annotationContent]; + + if ($annotation instanceof UniqueConstraint) { + return new UniqueConstraintTagValueNode(...$arguments); + } + + if ($annotation instanceof Index) { + return new IndexTagValueNode(...$arguments); + } + + throw new ShouldNotHappenException(); + } +} diff --git a/packages/Php/src/Rector/Assign/AssignArrayToStringRector.php b/packages/Php/src/Rector/Assign/AssignArrayToStringRector.php index c4ff6f38b682..5bd38481dd25 100644 --- a/packages/Php/src/Rector/Assign/AssignArrayToStringRector.php +++ b/packages/Php/src/Rector/Assign/AssignArrayToStringRector.php @@ -40,13 +40,11 @@ public function getDefinition(): RectorDefinition { return new RectorDefinition( 'String cannot be turned into array by assignment anymore', - [new CodeSample( - <<<'PHP' + [new CodeSample(<<<'PHP' $string = ''; $string[] = 1; PHP - , - <<<'PHP' + , <<<'PHP' $string = []; $string[] = 1; PHP diff --git a/packages/Php/src/Rector/FuncCall/SensitiveDefineRector.php b/packages/Php/src/Rector/FuncCall/SensitiveDefineRector.php index bcfe425d4dd2..6a3b268fe3d7 100644 --- a/packages/Php/src/Rector/FuncCall/SensitiveDefineRector.php +++ b/packages/Php/src/Rector/FuncCall/SensitiveDefineRector.php @@ -18,12 +18,10 @@ public function getDefinition(): RectorDefinition { return new RectorDefinition( 'Changes case insensitive constants to sensitive ones.', - [new CodeSample( - <<<'PHP' + [new CodeSample(<<<'PHP' define('FOO', 42, true); PHP - , - <<<'PHP' + , <<<'PHP' define('FOO', 42); PHP )] diff --git a/packages/Php/src/Rector/Name/ReservedObjectRector.php b/packages/Php/src/Rector/Name/ReservedObjectRector.php index 78ff8165287f..11c55b8bbbbd 100644 --- a/packages/Php/src/Rector/Name/ReservedObjectRector.php +++ b/packages/Php/src/Rector/Name/ReservedObjectRector.php @@ -35,14 +35,12 @@ public function getDefinition(): RectorDefinition { return new RectorDefinition( 'Changes reserved "Object" name to "Object" where can be configured', - [new CodeSample( - <<<'PHP' + [new CodeSample(<<<'PHP' class Object { } PHP - , - <<<'PHP' + , <<<'PHP' class SmartObject { } diff --git a/phpstan.neon b/phpstan.neon index 94e3c1c9d8ae..de55d2353df9 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -50,7 +50,7 @@ parameters: - '#Strict comparison using === between string and null will always evaluate to false#' # subtype - '#(.*?) expects ReflectionFunction\|ReflectionMethod, ReflectionFunctionAbstract given#' - - '#PHPDoc tag @return with type array is incompatible with native type Iterator#' + - '#PHPDoc tag @return with type (.*?) is incompatible with native type Iterator#' # nette container - '#Method Rector\\NodeTypeResolver\\DependencyInjection\\PHPStanServicesFactory::create(.*?)() should return (.*?) but returns object#' @@ -217,3 +217,7 @@ parameters: - '#Call to function is_string\(\) with float will always evaluate to false#' - '#PHPDoc tag @return with type array is incompatible with native type Iterator#' + + - '#Method Rector\\DoctrinePhpDocParser\\PhpDocParser\\TableTagValueNodeFactory\:\:createIndexTagValueNodes\(\) should return array but returns array#' + + - '#Method Rector\\DoctrinePhpDocParser\\PhpDocParser\\TableTagValueNodeFactory\:\:createUniqueConstraintTagValueNodes\(\) should return array but returns array#' diff --git a/src/Rector/Class_/RenameClassRector.php b/src/Rector/Class_/RenameClassRector.php index 984a9d4a9ac8..901cba293636 100644 --- a/src/Rector/Class_/RenameClassRector.php +++ b/src/Rector/Class_/RenameClassRector.php @@ -129,35 +129,21 @@ public function getNodeTypes(): array */ public function refactor(Node $node): ?Node { - // replace on @var/@param/@return/@throws - if ($this->docBlockManipulator->hasNodeTypeTags($node)) { - foreach ($this->oldToNewClasses as $oldClass => $newClass) { - $oldClassType = new ObjectType($oldClass); - $newClassType = new FullyQualifiedObjectType($newClass); - - $this->docBlockManipulator->changeType($node, $oldClassType, $newClassType); - } - - $this->phpDocClassRenamer->changeTypeInAnnotationTypes($node, $this->oldToNewClasses); - } + $this->refactorPhpDoc($node); if ($node instanceof Name) { return $this->refactorName($node); } if ($node instanceof Namespace_) { - $node = $this->refactorNamespaceNode($node); + return $this->refactorNamespaceNode($node); } if ($node instanceof ClassLike) { - $node = $this->refactorClassLikeNode($node); - } - - if ($node === null) { - return null; + return $this->refactorClassLikeNode($node); } - return $node; + return null; } /** @@ -318,4 +304,29 @@ private function refactorName(Node $node): ?Name return new FullyQualified($newName); } + + /** + * Replace types in @var/@param/@return/@throws, + * Doctrine @ORM entity targetClass, Serialize, Assert etc. + */ + private function refactorPhpDoc(Node $node): void + { + $nodePhpDocInfo = $this->getPhpDocInfo($node); + if ($nodePhpDocInfo === null) { + return; + } + + if (! $this->docBlockManipulator->hasNodeTypeTags($node)) { + return; + } + + foreach ($this->oldToNewClasses as $oldClass => $newClass) { + $oldClassType = new ObjectType($oldClass); + $newClassType = new FullyQualifiedObjectType($newClass); + + $this->docBlockManipulator->changeType($node, $oldClassType, $newClassType); + } + + $this->phpDocClassRenamer->changeTypeInAnnotationTypes($node, $this->oldToNewClasses); + } } diff --git a/stubs/Doctrine/ORM/Mapping/Index.php b/stubs/Doctrine/ORM/Mapping/Index.php new file mode 100644 index 000000000000..a3377b0fbd71 --- /dev/null +++ b/stubs/Doctrine/ORM/Mapping/Index.php @@ -0,0 +1,34 @@ + */ + /** @var string[] */ public $columns; - /** @var array */ + /** @var string[] */ public $flags = []; /** @var array */