From dbf405db422c363f2de68c9b2d0f867847aabdf1 Mon Sep 17 00:00:00 2001 From: Carlos Granados Date: Tue, 20 Feb 2024 19:12:04 +0100 Subject: [PATCH] Preserve comments from annotations --- composer.json | 22 +++++++++++++-- ...detyperesolver-node-attributekey-php.patch | 11 ++++++++ ...er-printer-betterstandardprinter-php.patch | 18 ++++++++++++ src/AnnotationsToAttributesRector.php | 21 ++++++++++++-- tests/Fixture/IsReadOnlyAttributeTest.php.inc | 8 ++++++ tests/Fixture/MethodAttributeTest.php.inc | 2 ++ tests/Fixture/ParamAttributeTest.php.inc | 14 ++++++++++ tests/Fixture/PropertyAttributeTest.php.inc | 2 ++ .../Fixture/PropertyReadAttributeTest.php.inc | 2 ++ .../PropertyWriteAttributeTest.php.inc | 2 ++ tests/Fixture/ReturnsAttributeTest.php.inc | 14 ++++++++++ tests/Fixture/TemplateAttributeTest.php.inc | 28 +++++++++++++++++++ ...TemplateContravariantAttributeTest.php.inc | 8 ++++-- .../TemplateCovariantAttributeTest.php.inc | 12 +++++--- tests/Fixture/TypeAttributeTest.php.inc | 16 +++++++++++ 15 files changed, 169 insertions(+), 11 deletions(-) create mode 100644 patches/rector-rector-src-nodetyperesolver-node-attributekey-php.patch create mode 100644 patches/rector-rector-src-phpparser-printer-betterstandardprinter-php.patch diff --git a/composer.json b/composer.json index 16a806f..b98e111 100644 --- a/composer.json +++ b/composer.json @@ -2,7 +2,10 @@ "name": "php-static-analysis/rector-rule", "description": "RectorPHP rule to convert PHPDoc annotations for static analysis to PHP attributes", "type": "rector-extension", - "keywords": ["dev", "static analysis"], + "keywords": [ + "dev", + "static analysis" + ], "license": "MIT", "autoload": { "psr-4": { @@ -24,7 +27,8 @@ "prefer-stable": true, "require": { "php": ">=8.0", - "php-static-analysis/attributes": "^0.1.6 || dev-main", + "cweagans/composer-patches": "^1.7", + "php-static-analysis/attributes": "^0.1.7 || dev-main", "rector/rector": "^0.19 || ^1.0" }, "require-dev": { @@ -34,6 +38,7 @@ "phpstan/phpstan": "^1.8", "phpunit/phpunit": "^9.0", "symplify/easy-coding-standard": "^12.1", + "symplify/vendor-patches": "^11.3", "vimeo/psalm": "^5", "webmozart/assert": "^1.11" }, @@ -56,8 +61,19 @@ }, "config": { "allow-plugins": { - "phpstan/extension-installer": true + "phpstan/extension-installer": true, + "cweagans/composer-patches": true }, "sort-packages": true + }, + "extra": { + "patches": { + "rector/rector": [ + "patches/rector-rector-src-phpparser-printer-betterstandardprinter-php.patch", + "patches/rector-rector-src-nodetyperesolver-node-attributekey-php.patch" + ] + }, + "composer-exit-on-patch-failure": true, + "enable-patching": true } } diff --git a/patches/rector-rector-src-nodetyperesolver-node-attributekey-php.patch b/patches/rector-rector-src-nodetyperesolver-node-attributekey-php.patch new file mode 100644 index 0000000..4065af3 --- /dev/null +++ b/patches/rector-rector-src-nodetyperesolver-node-attributekey-php.patch @@ -0,0 +1,11 @@ +--- /dev/null ++++ ../src/NodeTypeResolver/Node/AttributeKey.php +@@ -249,4 +249,8 @@ + * @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/patches/rector-rector-src-phpparser-printer-betterstandardprinter-php.patch b/patches/rector-rector-src-phpparser-printer-betterstandardprinter-php.patch new file mode 100644 index 0000000..9865855 --- /dev/null +++ b/patches/rector-rector-src-phpparser-printer-betterstandardprinter-php.patch @@ -0,0 +1,18 @@ +--- /dev/null ++++ ../src/PhpParser/Printer/BetterStandardPrinter.php +@@ -113,6 +113,15 @@ + $content = parent::p($node, $parentFormatPreserved); + return $node->getAttribute(AttributeKey::WRAPPED_IN_PARENTHESES) === \true ? '(' . $content . ')' : $content; + } ++ protected function pAttributeGroup(Node\AttributeGroup $node) ++ { ++ $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)) { diff --git a/src/AnnotationsToAttributesRector.php b/src/AnnotationsToAttributesRector.php index 1d3b1e7..2c32622 100644 --- a/src/AnnotationsToAttributesRector.php +++ b/src/AnnotationsToAttributesRector.php @@ -4,6 +4,7 @@ namespace PhpStaticAnalysis\RectorRule; +use PhpParser\Comment; use PhpParser\Node; use PhpParser\Node\Attribute; use PhpParser\Node\AttributeGroup; @@ -29,6 +30,7 @@ use Rector\BetterPhpDocParser\PhpDocManipulator\PhpDocTagRemover; use Rector\Comments\NodeDocBlock\DocBlockUpdater; use Rector\Contract\Rector\ConfigurableRectorInterface; +use Rector\NodeTypeResolver\Node\AttributeKey; use Rector\Php80\NodeManipulator\AttributeGroupNamedArgumentManipulator; use Rector\Php80\ValueObject\AnnotationToAttribute; use Rector\Rector\AbstractRector; @@ -225,10 +227,16 @@ private function processAnnotations(PhpDocInfo $phpDocInfo): array } $tagValueNode = $phpDocChildNode->value; + $attributeComment = null; switch (true) { case $tagValueNode instanceof MethodTagValueNode: + $methodSignature = (string)($tagValueNode); + $attributeComment = $tagValueNode->description; + if ($attributeComment) { + $methodSignature = substr($methodSignature, 0, -(strlen($attributeComment) + 1)); + } $args = [ - new Node\Arg(new Scalar\String_((string)($tagValueNode))) + new Node\Arg(new Scalar\String_($methodSignature)) ]; break; case $tagValueNode instanceof ParamTagValueNode: @@ -238,6 +246,7 @@ private function processAnnotations(PhpDocInfo $phpDocInfo): array name: new Node\Identifier(substr($tagValueNode->parameterName, 1)) ) ]; + $attributeComment = $tagValueNode->description; break; case $tagValueNode instanceof PropertyTagValueNode: $args = [ @@ -246,12 +255,14 @@ private function processAnnotations(PhpDocInfo $phpDocInfo): array name: new Node\Identifier(substr($tagValueNode->propertyName, 1)) ) ]; + $attributeComment = $tagValueNode->description; break; case $tagValueNode instanceof ReturnTagValueNode: case $tagValueNode instanceof VarTagValueNode: $args = [ new Node\Arg(new Scalar\String_((string)($tagValueNode->type))) ]; + $attributeComment = $tagValueNode->description; break; case $tagValueNode instanceof TemplateTagValueNode: $args = [ @@ -260,9 +271,11 @@ private function processAnnotations(PhpDocInfo $phpDocInfo): array if ($tagValueNode->bound instanceof IdentifierTypeNode) { $args[] = new Node\Arg(new Scalar\String_($tagValueNode->bound->name)); } + $attributeComment = $tagValueNode->description; break; case $tagValueNode instanceof GenericTagValueNode: $args = []; + $attributeComment = (string)$tagValueNode; break; default: continue 2; @@ -277,7 +290,11 @@ private function processAnnotations(PhpDocInfo $phpDocInfo): array $attributeName = new FullyQualified($annotationToAttribute->getAttributeClass()); $attribute = new Attribute($attributeName, $args); - $attributeGroups[] = new AttributeGroup([$attribute]); + $attributeGroup = new AttributeGroup([$attribute]); + if ($attributeComment && defined('Rector\NodeTypeResolver\Node\AttributeKey::ATTRIBUTE_COMMENT')) { + $attributeGroup->setAttribute(AttributeKey::ATTRIBUTE_COMMENT, $attributeComment); + } + $attributeGroups[] = $attributeGroup; } foreach ($tagValueNodes as $tagValueNode) { diff --git a/tests/Fixture/IsReadOnlyAttributeTest.php.inc b/tests/Fixture/IsReadOnlyAttributeTest.php.inc index 9e7b35c..cc05b9c 100644 --- a/tests/Fixture/IsReadOnlyAttributeTest.php.inc +++ b/tests/Fixture/IsReadOnlyAttributeTest.php.inc @@ -23,6 +23,11 @@ class IsReadOnlyAttributeTest #[Type('string')] public $otherName; + /** + * @readonly please do not write here + */ + public $nameWithComment; + /** * @psalm-readonly */ @@ -57,6 +62,9 @@ class IsReadOnlyAttributeTest #[\PhpStaticAnalysis\Attributes\IsReadOnly] public $otherName; + #[\PhpStaticAnalysis\Attributes\IsReadOnly] // please do not write here + public $nameWithComment; + #[\PhpStaticAnalysis\Attributes\IsReadOnly] public $psalmName; diff --git a/tests/Fixture/MethodAttributeTest.php.inc b/tests/Fixture/MethodAttributeTest.php.inc index b8614f7..f73eefe 100644 --- a/tests/Fixture/MethodAttributeTest.php.inc +++ b/tests/Fixture/MethodAttributeTest.php.inc @@ -7,6 +7,7 @@ use PhpStaticAnalysis\Attributes\Template; /** * @deprecated * @method string getString() + * @method string getName() used to get the name * @psalm-method void setString(string $text) * @phpstan-method static string staticGetter() */ @@ -28,6 +29,7 @@ use PhpStaticAnalysis\Attributes\Template; */ #[Template('T')] #[\PhpStaticAnalysis\Attributes\Method('string getString()')] +#[\PhpStaticAnalysis\Attributes\Method('string getName()')] // used to get the name #[\PhpStaticAnalysis\Attributes\Method('void setString(string $text)')] #[\PhpStaticAnalysis\Attributes\Method('static string staticGetter()')] class MethodAttributeTest diff --git a/tests/Fixture/ParamAttributeTest.php.inc b/tests/Fixture/ParamAttributeTest.php.inc index 3fadcc7..b2ca332 100644 --- a/tests/Fixture/ParamAttributeTest.php.inc +++ b/tests/Fixture/ParamAttributeTest.php.inc @@ -51,6 +51,14 @@ class ParamAttributeTest return $name1 . $name2; } + /** + * @param string $name the name of the user + */ + public function getUserName($name) + { + return $name; + } + /** * @psalm-param string $name */ @@ -123,6 +131,12 @@ class ParamAttributeTest return $name1 . $name2; } + #[\PhpStaticAnalysis\Attributes\Param(name: 'string')] // the name of the user + public function getUserName($name) + { + return $name; + } + #[\PhpStaticAnalysis\Attributes\Param(name: 'string')] public function getPsalmName($name) { diff --git a/tests/Fixture/PropertyAttributeTest.php.inc b/tests/Fixture/PropertyAttributeTest.php.inc index 36ae623..9f27507 100644 --- a/tests/Fixture/PropertyAttributeTest.php.inc +++ b/tests/Fixture/PropertyAttributeTest.php.inc @@ -7,6 +7,7 @@ use PhpStaticAnalysis\Attributes\Template; /** * @deprecated * @property string $name + * @property string $userName the name of the user * @psalm-property int $num * @phpstan-property string[] $index */ @@ -28,6 +29,7 @@ use PhpStaticAnalysis\Attributes\Template; */ #[Template('T')] #[\PhpStaticAnalysis\Attributes\Property(name: 'string')] +#[\PhpStaticAnalysis\Attributes\Property(userName: 'string')] // the name of the user #[\PhpStaticAnalysis\Attributes\Property(num: 'int')] #[\PhpStaticAnalysis\Attributes\Property(index: 'string[]')] class PropertyAttributeTest diff --git a/tests/Fixture/PropertyReadAttributeTest.php.inc b/tests/Fixture/PropertyReadAttributeTest.php.inc index d286667..3dae0a8 100644 --- a/tests/Fixture/PropertyReadAttributeTest.php.inc +++ b/tests/Fixture/PropertyReadAttributeTest.php.inc @@ -7,6 +7,7 @@ use PhpStaticAnalysis\Attributes\Template; /** * @deprecated * @property-read string $name + * @property-read string $userName the name of the user * @psalm-property-read int $num * @phpstan-property-read string[] $index */ @@ -28,6 +29,7 @@ use PhpStaticAnalysis\Attributes\Template; */ #[Template('T')] #[\PhpStaticAnalysis\Attributes\PropertyRead(name: 'string')] +#[\PhpStaticAnalysis\Attributes\PropertyRead(userName: 'string')] // the name of the user #[\PhpStaticAnalysis\Attributes\PropertyRead(num: 'int')] #[\PhpStaticAnalysis\Attributes\PropertyRead(index: 'string[]')] class PropertyReadAttributeTest diff --git a/tests/Fixture/PropertyWriteAttributeTest.php.inc b/tests/Fixture/PropertyWriteAttributeTest.php.inc index 6bbfe05..f994ecd 100644 --- a/tests/Fixture/PropertyWriteAttributeTest.php.inc +++ b/tests/Fixture/PropertyWriteAttributeTest.php.inc @@ -7,6 +7,7 @@ use PhpStaticAnalysis\Attributes\Template; /** * @deprecated * @property-write string $name + * @property-write string $userName the name of the user * @psalm-property-write int $num * @phpstan-property-write string[] $index */ @@ -28,6 +29,7 @@ use PhpStaticAnalysis\Attributes\Template; */ #[Template('T')] #[\PhpStaticAnalysis\Attributes\PropertyWrite(name: 'string')] +#[\PhpStaticAnalysis\Attributes\PropertyWrite(userName: 'string')] // the name of the user #[\PhpStaticAnalysis\Attributes\PropertyWrite(num: 'int')] #[\PhpStaticAnalysis\Attributes\PropertyWrite(index: 'string[]')] class PropertyWriteAttributeTest diff --git a/tests/Fixture/ReturnsAttributeTest.php.inc b/tests/Fixture/ReturnsAttributeTest.php.inc index eb95340..914e705 100644 --- a/tests/Fixture/ReturnsAttributeTest.php.inc +++ b/tests/Fixture/ReturnsAttributeTest.php.inc @@ -32,6 +32,14 @@ class ReturnsAttributeTest return "Hello " . $name; } + /** + * @return string the name of the user + */ + public function getUserName() + { + return 'John'; + } + /** * @psalm-return string */ @@ -89,6 +97,12 @@ class ReturnsAttributeTest return "Hello " . $name; } + #[\PhpStaticAnalysis\Attributes\Returns('string')] // the name of the user + public function getUserName() + { + return 'John'; + } + #[\PhpStaticAnalysis\Attributes\Returns('string')] public function getPsalmName() { diff --git a/tests/Fixture/TemplateAttributeTest.php.inc b/tests/Fixture/TemplateAttributeTest.php.inc index 6a5e189..9cdee73 100644 --- a/tests/Fixture/TemplateAttributeTest.php.inc +++ b/tests/Fixture/TemplateAttributeTest.php.inc @@ -43,6 +43,22 @@ class TemplateAttributeTest return "Hello " . $name; } + /** + * @template T the type of object + */ + public function getUserName() + { + return 'John'; + } + + /** + * @template T of object the type of object + */ + public function getAnotherUserName() + { + return 'John'; + } + /** * @psalm-template T */ @@ -107,6 +123,18 @@ class TemplateAttributeTest return "Hello " . $name; } + #[\PhpStaticAnalysis\Attributes\Template('T')] // the type of object + public function getUserName() + { + return 'John'; + } + + #[\PhpStaticAnalysis\Attributes\Template('T', 'object')] // the type of object + public function getAnotherUserName() + { + return 'John'; + } + #[\PhpStaticAnalysis\Attributes\Template('T')] public function getPsalmName() { diff --git a/tests/Fixture/TemplateContravariantAttributeTest.php.inc b/tests/Fixture/TemplateContravariantAttributeTest.php.inc index 2dbeb93..05ff3a0 100644 --- a/tests/Fixture/TemplateContravariantAttributeTest.php.inc +++ b/tests/Fixture/TemplateContravariantAttributeTest.php.inc @@ -9,7 +9,9 @@ use PhpStaticAnalysis\Attributes\Property; * @deprecated * @template-contravariant T1 * @template-contravariant T2 of Exception - * @phpstan-template-contravariant T3 + * @template-contravariant T3 this is contravariant + * @template-contravariant T4 of Exception this is also contravariant + * @phpstan-template-contravariant T5 */ #[Property(name:'string')] class TemplateContravariantAttributeTest @@ -31,7 +33,9 @@ use PhpStaticAnalysis\Attributes\Property; #[Property(name:'string')] #[\PhpStaticAnalysis\Attributes\TemplateContravariant('T1')] #[\PhpStaticAnalysis\Attributes\TemplateContravariant('T2', 'Exception')] -#[\PhpStaticAnalysis\Attributes\TemplateContravariant('T3')] +#[\PhpStaticAnalysis\Attributes\TemplateContravariant('T3')] // this is contravariant +#[\PhpStaticAnalysis\Attributes\TemplateContravariant('T4', 'Exception')] // this is also contravariant +#[\PhpStaticAnalysis\Attributes\TemplateContravariant('T5')] class TemplateContravariantAttributeTest { } diff --git a/tests/Fixture/TemplateCovariantAttributeTest.php.inc b/tests/Fixture/TemplateCovariantAttributeTest.php.inc index 922e352..33ff4b6 100644 --- a/tests/Fixture/TemplateCovariantAttributeTest.php.inc +++ b/tests/Fixture/TemplateCovariantAttributeTest.php.inc @@ -9,8 +9,10 @@ use PhpStaticAnalysis\Attributes\Property; * @deprecated * @template-covariant T1 * @template-covariant T2 of Exception - * @psalm-template-covariant T3 - * @phpstan-template-covariant T4 + * @template-covariant T3 this is covariant + * @template-covariant T4 of Exception this is also covariant + * @psalm-template-covariant T5 + * @phpstan-template-covariant T6 */ #[Property(name:'string')] class TemplateCovariantAttributeTest @@ -32,8 +34,10 @@ use PhpStaticAnalysis\Attributes\Property; #[Property(name:'string')] #[\PhpStaticAnalysis\Attributes\TemplateCovariant('T1')] #[\PhpStaticAnalysis\Attributes\TemplateCovariant('T2', 'Exception')] -#[\PhpStaticAnalysis\Attributes\TemplateCovariant('T3')] -#[\PhpStaticAnalysis\Attributes\TemplateCovariant('T4')] +#[\PhpStaticAnalysis\Attributes\TemplateCovariant('T3')] // this is covariant +#[\PhpStaticAnalysis\Attributes\TemplateCovariant('T4', 'Exception')] // this is also covariant +#[\PhpStaticAnalysis\Attributes\TemplateCovariant('T5')] +#[\PhpStaticAnalysis\Attributes\TemplateCovariant('T6')] class TemplateCovariantAttributeTest { } diff --git a/tests/Fixture/TypeAttributeTest.php.inc b/tests/Fixture/TypeAttributeTest.php.inc index d7ebe97..4ae0e2b 100644 --- a/tests/Fixture/TypeAttributeTest.php.inc +++ b/tests/Fixture/TypeAttributeTest.php.inc @@ -23,6 +23,16 @@ class TypeAttributeTest #[IsReadOnly] public $otherName; + /** + * @var string this is the name of the user + */ + public $userName; + + /** + * @var string $otherUserName this is also the name of the user + */ + public $otherUserName; + /** * @psalm-var string */ @@ -57,6 +67,12 @@ class TypeAttributeTest #[\PhpStaticAnalysis\Attributes\Type('string')] public $otherName; + #[\PhpStaticAnalysis\Attributes\Type('string')] // this is the name of the user + public $userName; + + #[\PhpStaticAnalysis\Attributes\Type('string')] // this is also the name of the user + public $otherUserName; + #[\PhpStaticAnalysis\Attributes\Type('string')] public $psalmName;