diff --git a/rules-tests/Php80/Rector/Class_/AnnotationToAttributeRector/Fixture/with_comment.php.inc b/rules-tests/Php80/Rector/Class_/AnnotationToAttributeRector/Fixture/with_comment.php.inc new file mode 100644 index 00000000000..2f7e8f19323 --- /dev/null +++ b/rules-tests/Php80/Rector/Class_/AnnotationToAttributeRector/Fixture/with_comment.php.inc @@ -0,0 +1,45 @@ + +----- + diff --git a/rules/CodeQuality/Rector/Class_/CompleteDynamicPropertiesRector.php b/rules/CodeQuality/Rector/Class_/CompleteDynamicPropertiesRector.php index 4e65c38b9b6..d06f7cb939f 100644 --- a/rules/CodeQuality/Rector/Class_/CompleteDynamicPropertiesRector.php +++ b/rules/CodeQuality/Rector/Class_/CompleteDynamicPropertiesRector.php @@ -154,7 +154,9 @@ private function shouldSkipClass(Class_ $class): bool return true; } - return $class->extends instanceof FullyQualified && ! $this->reflectionProvider->hasClass($class->extends->toString()); + return $class->extends instanceof FullyQualified && ! $this->reflectionProvider->hasClass( + $class->extends->toString() + ); } /** diff --git a/rules/Php80/Rector/Class_/AnnotationToAttributeRector.php b/rules/Php80/Rector/Class_/AnnotationToAttributeRector.php index 23d9cf1f357..860d20f0dd4 100644 --- a/rules/Php80/Rector/Class_/AnnotationToAttributeRector.php +++ b/rules/Php80/Rector/Class_/AnnotationToAttributeRector.php @@ -76,7 +76,7 @@ public function getRuleDefinition(): RuleDefinition class SymfonyRoute { /** - * @Route("/path", name="action") + * @Route("/path", name="action") api route */ public function action() { @@ -89,7 +89,7 @@ public function action() class SymfonyRoute { - #[Route(path: '/path', name: 'action')] + #[Route(path: '/path', name: 'action')] // api route public function action() { } diff --git a/rules/Transform/Rector/ConstFetch/ConstFetchToClassConstFetchRector.php b/rules/Transform/Rector/ConstFetch/ConstFetchToClassConstFetchRector.php index f994048df26..7a6f3ab1e2e 100644 --- a/rules/Transform/Rector/ConstFetch/ConstFetchToClassConstFetchRector.php +++ b/rules/Transform/Rector/ConstFetch/ConstFetchToClassConstFetchRector.php @@ -4,9 +4,9 @@ namespace Rector\Transform\Rector\ConstFetch; -use PhpParser\Node\Expr\ConstFetch; -use PhpParser\Node\Expr\ClassConstFetch; use PhpParser\Node; +use PhpParser\Node\Expr\ClassConstFetch; +use PhpParser\Node\Expr\ConstFetch; use Rector\Contract\Rector\ConfigurableRectorInterface; use Rector\Rector\AbstractRector; use Rector\Transform\ValueObject\ConstFetchToClassConstFetch; diff --git a/rules/Transform/Rector/FileWithoutNamespace/RectorConfigBuilderRector.php b/rules/Transform/Rector/FileWithoutNamespace/RectorConfigBuilderRector.php index ae2f2ca2000..a0c2ccdeb93 100644 --- a/rules/Transform/Rector/FileWithoutNamespace/RectorConfigBuilderRector.php +++ b/rules/Transform/Rector/FileWithoutNamespace/RectorConfigBuilderRector.php @@ -188,11 +188,7 @@ public function refactor(Node $node): ?Node if ($name === 'fileExtensions') { Assert::isAOf($value, Array_::class); - $newExpr = $this->nodeFactory->createMethodCall( - $newExpr, - 'withFileExtensions', - [$value] - ); + $newExpr = $this->nodeFactory->createMethodCall($newExpr, 'withFileExtensions', [$value]); $hasChanged = true; continue; diff --git a/rules/Transform/ValueObject/ConstFetchToClassConstFetch.php b/rules/Transform/ValueObject/ConstFetchToClassConstFetch.php index a5379089bd1..dac185a81d5 100644 --- a/rules/Transform/ValueObject/ConstFetchToClassConstFetch.php +++ b/rules/Transform/ValueObject/ConstFetchToClassConstFetch.php @@ -8,8 +8,11 @@ final readonly class ConstFetchToClassConstFetch { - public function __construct(private string $oldConstName, private string $newClassName, private string $newConstName) - { + public function __construct( + private string $oldConstName, + private string $newClassName, + private string $newConstName + ) { RectorAssert::constantName($this->oldConstName); RectorAssert::className($this->newClassName); RectorAssert::constantName($this->newConstName); diff --git a/src/BetterPhpDocParser/PhpDoc/DoctrineAnnotationTagValueNode.php b/src/BetterPhpDocParser/PhpDoc/DoctrineAnnotationTagValueNode.php index 6897661f991..29465c7a1a3 100644 --- a/src/BetterPhpDocParser/PhpDoc/DoctrineAnnotationTagValueNode.php +++ b/src/BetterPhpDocParser/PhpDoc/DoctrineAnnotationTagValueNode.php @@ -7,6 +7,7 @@ use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; use Rector\BetterPhpDocParser\ValueObject\PhpDoc\DoctrineAnnotation\AbstractValuesAwareNode; use Rector\BetterPhpDocParser\ValueObject\PhpDocAttributeKey; +use Rector\NodeTypeResolver\Node\AttributeKey; use Stringable; final class DoctrineAnnotationTagValueNode extends AbstractValuesAwareNode implements Stringable @@ -18,11 +19,16 @@ public function __construct( public IdentifierTypeNode $identifierTypeNode, ?string $originalContent = null, array $values = [], - ?string $silentKey = null + ?string $silentKey = null, + ?string $comment = null ) { $this->hasChanged = true; parent::__construct($values, $originalContent, $silentKey); + + if (! in_array($comment, ['', null], true)) { + $this->setAttribute(AttributeKey::ATTRIBUTE_COMMENT, $comment); + } } public function __toString(): string diff --git a/src/BetterPhpDocParser/PhpDocParser/DoctrineAnnotationDecorator.php b/src/BetterPhpDocParser/PhpDocParser/DoctrineAnnotationDecorator.php index 172bec7177e..609db53a52d 100644 --- a/src/BetterPhpDocParser/PhpDocParser/DoctrineAnnotationDecorator.php +++ b/src/BetterPhpDocParser/PhpDocParser/DoctrineAnnotationDecorator.php @@ -409,6 +409,11 @@ private function createDoctrineSpacelessPhpDocTagNode( $currentPhpNode ); + $comment = $this->staticDoctrineAnnotationParser->getCommentFromRestOfAnnotation( + $nestedTokenIterator, + $annotationContent + ); + $identifierTypeNode = new IdentifierTypeNode($tagName); $identifierTypeNode->setAttribute(PhpDocAttributeKey::RESOLVED_CLASS, $fullyQualifiedAnnotationClass); @@ -416,7 +421,8 @@ private function createDoctrineSpacelessPhpDocTagNode( $identifierTypeNode, $annotationContent, $values, - SilentKeyMap::CLASS_NAMES_TO_SILENT_KEYS[$fullyQualifiedAnnotationClass] ?? null + SilentKeyMap::CLASS_NAMES_TO_SILENT_KEYS[$fullyQualifiedAnnotationClass] ?? null, + $comment ); $doctrineAnnotationTagValueNode->setAttribute(PhpDocAttributeKey::START_AND_END, $startAndEnd); diff --git a/src/BetterPhpDocParser/PhpDocParser/StaticDoctrineAnnotationParser.php b/src/BetterPhpDocParser/PhpDocParser/StaticDoctrineAnnotationParser.php index 79fbfe00660..dfc9cb9c89d 100644 --- a/src/BetterPhpDocParser/PhpDocParser/StaticDoctrineAnnotationParser.php +++ b/src/BetterPhpDocParser/PhpDocParser/StaticDoctrineAnnotationParser.php @@ -4,6 +4,7 @@ namespace Rector\BetterPhpDocParser\PhpDocParser; +use Nette\Utils\Strings; use PhpParser\Node; use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprNode; use PHPStan\PhpDocParser\Lexer\Lexer; @@ -21,6 +22,18 @@ */ final readonly class StaticDoctrineAnnotationParser { + /** + * @var string + * @see https://regex101.com/r/aU2knc/1 + */ + private const NEWLINES_REGEX = "#\r?\n#"; + + /** + * @var string + * @see https://regex101.com/r/Pthg5d/1 + */ + private const END_OF_VALUE_CHARACTERS_REGEX = '/^[)} \r\n"\']+$/i'; + public function __construct( private PlainValueParser $plainValueParser, private ArrayParser $arrayParser @@ -80,6 +93,24 @@ public function resolveAnnotationValue( ]; } + public function getCommentFromRestOfAnnotation( + BetterTokenIterator $tokenIterator, + string $annotationContent + ): string { + // we skip all the remaining tokens from the end of the declaration of values + while ( + preg_match(self::END_OF_VALUE_CHARACTERS_REGEX, $tokenIterator->currentTokenValue()) + ) { + $tokenIterator->next(); + } + + // the remaining of the annotation content is the comment + $comment = substr($annotationContent, $tokenIterator->currentTokenOffset()); + // we only keep the first line as this will be added as a line comment at the end of the attribute + $commentLines = Strings::split($comment, self::NEWLINES_REGEX); + return $commentLines[0]; + } + /** * @see https://github.com/doctrine/annotations/blob/c66f06b7c83e9a2a7523351a9d5a4b55f885e574/lib/Doctrine/Common/Annotations/DocParser.php#L1051-L1079 * diff --git a/src/NodeTypeResolver/DependencyInjection/PHPStanServicesFactory.php b/src/NodeTypeResolver/DependencyInjection/PHPStanServicesFactory.php index 130ad4e86a6..bebc8c5ea4a 100644 --- a/src/NodeTypeResolver/DependencyInjection/PHPStanServicesFactory.php +++ b/src/NodeTypeResolver/DependencyInjection/PHPStanServicesFactory.php @@ -4,7 +4,6 @@ namespace Rector\NodeTypeResolver\DependencyInjection; -use Throwable; use PhpParser\Lexer; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\ScopeFactory; @@ -20,6 +19,7 @@ use Symfony\Component\Console\Input\ArrayInput; use Symfony\Component\Console\Output\ConsoleOutput; use Symfony\Component\Console\Style\SymfonyStyle; +use Throwable; use Webmozart\Assert\Assert; /** @@ -29,8 +29,6 @@ */ final readonly class PHPStanServicesFactory { - private Container $container; - /** * @var string */ @@ -44,6 +42,8 @@ MESSAGE_ERROR; + private Container $container; + public function __construct() { $containerFactory = new ContainerFactory(getcwd()); @@ -57,7 +57,6 @@ public function __construct() ); } catch (Throwable $throwable) { if ($throwable->getMessage() === "File 'phar://phpstan.phar/conf/bleedingEdge.neon' is missing or is not readable.") { - $symfonyStyle = new SymfonyStyle(new ArrayInput([]), new ConsoleOutput()); $symfonyStyle->error(sprintf(self::INVALID_BLEEDING_EDGE_PATH_MESSAGE, $throwable->getMessage())); diff --git a/src/NodeTypeResolver/Node/AttributeKey.php b/src/NodeTypeResolver/Node/AttributeKey.php index 3d6dfb4282d..ef8f9264e44 100644 --- a/src/NodeTypeResolver/Node/AttributeKey.php +++ b/src/NodeTypeResolver/Node/AttributeKey.php @@ -301,4 +301,9 @@ final class AttributeKey * @var string */ public const IS_USED_AS_ARG_BY_REF_VALUE = 'is_used_as_arg_by_ref_value'; + + /** + * @var string + */ + public const ATTRIBUTE_COMMENT = 'attribute_comment'; } diff --git a/src/PhpAttribute/NodeFactory/PhpAttributeGroupFactory.php b/src/PhpAttribute/NodeFactory/PhpAttributeGroupFactory.php index 1fb65345a9f..5498528bf88 100644 --- a/src/PhpAttribute/NodeFactory/PhpAttributeGroupFactory.php +++ b/src/PhpAttribute/NodeFactory/PhpAttributeGroupFactory.php @@ -87,7 +87,12 @@ public function create( $attributeName->setAttribute(AttributeKey::PHP_ATTRIBUTE_NAME, $annotationToAttribute->getAttributeClass()); $attribute = new Attribute($attributeName, $args); - return new AttributeGroup([$attribute]); + $attributeGroup = new AttributeGroup([$attribute]); + $comment = $doctrineAnnotationTagValueNode->getAttribute(AttributeKey::ATTRIBUTE_COMMENT); + if ($comment) { + $attributeGroup->setAttribute(AttributeKey::ATTRIBUTE_COMMENT, $comment); + } + return $attributeGroup; } /** diff --git a/src/PhpParser/Printer/BetterStandardPrinter.php b/src/PhpParser/Printer/BetterStandardPrinter.php index b545a31c9c7..9d23ec1395c 100644 --- a/src/PhpParser/Printer/BetterStandardPrinter.php +++ b/src/PhpParser/Printer/BetterStandardPrinter.php @@ -139,6 +139,16 @@ protected function p(Node $node, $parentFormatPreserved = false): string : $content; } + protected function pAttributeGroup(Node\AttributeGroup $node): string + { + $ret = parent::pAttributeGroup($node); + $comment = $node->getAttribute(AttributeKey::ATTRIBUTE_COMMENT); + if (! in_array($comment, ['', null], true)) { + $ret .= ' // ' . $comment; + } + return $ret; + } + protected function pExpr_ArrowFunction(ArrowFunction $arrowFunction): string { if (! $arrowFunction->hasAttribute(AttributeKey::COMMENT_CLOSURE_RETURN_MIRRORED)) {