Commit 11ae6ad0be9 for php.net
commit 11ae6ad0be9e6a3f9d02ec5be9ef28cd0567d438
Author: Tim Düsterhus <tim@bastelstu.be>
Date: Tue Jan 13 16:07:50 2026 +0100
zend_language_parser: Backup / restore doc comment when parsing attributes (#20896)
Attributes may themselves contain elements which can have a doc comment on
their own (namely Closures). A doc comment before the attribute list is
generally understood as belonging to the symbol having the attributes.
Fixes php/php-src#20895.
diff --git a/NEWS b/NEWS
index 754ab95409e..6af8ce6b2cb 100644
--- a/NEWS
+++ b/NEWS
@@ -13,6 +13,8 @@ PHP NEWS
. Fix OSS-Fuzz #472563272 (Borked block_pass JMP[N]Z optimization). (ilutov)
. Fixed bug GH-20914 (Internal enums can be cloned and compared). (Arnaud)
. Fix OSS-Fuzz #474613951 (Leaked parent property default value). (ilutov)
+ . Fixed bug GH-20895 (ReflectionProperty does not return the PHPDoc of a
+ property if it contains an attribute with a Closure). (timwolla)
- MbString:
. Fixed bug GH-20833 (mb_str_pad() divide by zero if padding string is
diff --git a/Zend/zend_language_parser.y b/Zend/zend_language_parser.y
index e4d61006fe1..897abbbe977 100644
--- a/Zend/zend_language_parser.y
+++ b/Zend/zend_language_parser.y
@@ -379,7 +379,7 @@ attribute_group:
;
attribute:
- T_ATTRIBUTE attribute_group possible_comma ']' { $$ = $2; }
+ T_ATTRIBUTE backup_doc_comment attribute_group possible_comma ']' { $$ = $3; CG(doc_comment) = $2; }
;
attributes:
diff --git a/ext/reflection/tests/gh20895.phpt b/ext/reflection/tests/gh20895.phpt
new file mode 100644
index 00000000000..f281078de44
--- /dev/null
+++ b/ext/reflection/tests/gh20895.phpt
@@ -0,0 +1,108 @@
+--TEST--
+GH-20895: ReflectionProperty does not return the PHPDoc of a property if it contains an attribute with a Closure
+--FILE--
+<?php
+
+/** Foo */
+#[Attr(
+ /** Closure 1 */
+ static function() { },
+ /** Closure 2 */
+ static function() { },
+)]
+class Foo {
+ /** Foo::$bar */
+ #[Attr(
+ /** Closure 3 */
+ static function() { },
+ /** Closure 4 */
+ static function() { },
+ )]
+ #[Attr(
+ /** Closure 5 */
+ static function() { },
+ )]
+ public $bar;
+
+ /** Foo::bar() */
+ #[Attr(
+ /** Closure 6 */
+ static function() { },
+ )]
+ public function bar() { }
+}
+
+/** foo() */
+#[Attr(
+ /** Closure 7 */
+ static function() { },
+)]
+function foo() { }
+
+#[Attr(
+ /** Closure 8 */
+ static function() { },
+)]
+/** bar() */
+function bar() { }
+
+/** baz() */
+#[Attr(
+ static function() { },
+)]
+function baz() { }
+
+var_dump((new ReflectionClass(Foo::class))->getDocComment());
+foreach ((new ReflectionClass(Foo::class))->getAttributes() as $attribute) {
+ foreach ($attribute->getArguments() as $argument) {
+ var_dump((new ReflectionFunction($argument))->getDocComment());
+ }
+}
+var_dump((new ReflectionProperty(Foo::class, 'bar'))->getDocComment());
+foreach ((new ReflectionProperty(Foo::class, 'bar'))->getAttributes() as $attribute) {
+ foreach ($attribute->getArguments() as $argument) {
+ var_dump((new ReflectionFunction($argument))->getDocComment());
+ }
+}
+var_dump((new ReflectionMethod(Foo::class, 'bar'))->getDocComment());
+foreach ((new ReflectionMethod(Foo::class, 'bar'))->getAttributes() as $attribute) {
+ foreach ($attribute->getArguments() as $argument) {
+ var_dump((new ReflectionFunction($argument))->getDocComment());
+ }
+}
+var_dump((new ReflectionFunction('foo'))->getDocComment());
+foreach ((new ReflectionFunction('foo'))->getAttributes() as $attribute) {
+ foreach ($attribute->getArguments() as $argument) {
+ var_dump((new ReflectionFunction($argument))->getDocComment());
+ }
+}
+var_dump((new ReflectionFunction('bar'))->getDocComment());
+foreach ((new ReflectionFunction('bar'))->getAttributes() as $attribute) {
+ foreach ($attribute->getArguments() as $argument) {
+ var_dump((new ReflectionFunction($argument))->getDocComment());
+ }
+}
+var_dump((new ReflectionFunction('baz'))->getDocComment());
+foreach ((new ReflectionFunction('baz'))->getAttributes() as $attribute) {
+ foreach ($attribute->getArguments() as $argument) {
+ var_dump((new ReflectionFunction($argument))->getDocComment());
+ }
+}
+
+?>
+--EXPECT--
+string(10) "/** Foo */"
+string(16) "/** Closure 1 */"
+string(16) "/** Closure 2 */"
+string(16) "/** Foo::$bar */"
+string(16) "/** Closure 3 */"
+string(16) "/** Closure 4 */"
+string(16) "/** Closure 5 */"
+string(17) "/** Foo::bar() */"
+string(16) "/** Closure 6 */"
+string(12) "/** foo() */"
+string(16) "/** Closure 7 */"
+string(12) "/** bar() */"
+string(16) "/** Closure 8 */"
+string(12) "/** baz() */"
+bool(false)