[phd] attribute-constants-v2: improve attribute formatting with better argument parsing

From: Date: Fri, 11 Sep 2026 14:26:06 +0000
Subject: [phd] attribute-constants-v2: improve attribute formatting with better argument parsing
Groups: php.doc.cvs 
Request: Send a blank email to doc-cvs+get-23745@lists.php.net to get a copy of this message
Author: Jordi Kroon (jordikroon) Date: 2026-09-11T16:04:41+02:00 Commit: https://github.com/php/phd/commit/4f117227b7dc35719ce3e853e37899e3070fc4fe Raw diff: https://github.com/php/phd/commit/4f117227b7dc35719ce3e853e37899e3070fc4fe.diff improve attribute formatting with better argument parsing Changed paths: A tests/package/generic/attribute_formatting_005.phpt A tests/package/generic/data/attribute_formatting_005.xml M phpdotnet/phd/Package/Generic/XHTML.php M tests/package/generic/attribute_formatting_001.phpt M tests/package/generic/attribute_formatting_002.phpt M tests/package/generic/attribute_formatting_003.phpt Diff: diff --git a/phpdotnet/phd/Package/Generic/XHTML.php b/phpdotnet/phd/Package/Generic/XHTML.php index 6719d01de..c9bea71fb 100644 --- a/phpdotnet/phd/Package/Generic/XHTML.php +++ b/phpdotnet/phd/Package/Generic/XHTML.php @@ -1491,23 +1491,150 @@ public function format_modifier_text($value, $tag) { } private function format_attribute_modifier_text(string $value): string { - // Anything that is not a leading "#[\Attribute(" / "#[\Attribute]" chunk - // e.g. "|" separator between arguments passes through. - if (!preg_match('/^(#\[)(.+?)([](])$/', $value, $match)) { - if (trim($value) === '|') { - return ' | '; + $trimmed = trim($value); + $namePattern = '\\\\?[A-Za-z_][A-Za-z0-9_\\\\]*'; + + // Full attribute with literal arguments: #[\Name(args)] + if (preg_match('/^(#\[)(' . $namePattern . ')\((.+)\)]$/s', $trimmed, $match)) { + [, $prefix, $name, $args] = $match; + return $prefix . $this->link_attribute_name($name) . '(' . $this->render_attribute_args($args) . ')]'; + } + + // Simple attribute: #[\Name] + if (preg_match('/^#\[(' . $namePattern . ')]$/', $trimmed, $match)) { + $name = $match[1]; + $attribute = strtolower(ltrim($name, "\\")); + $href = $this->getFilename('class.' . $attribute); + $token = '#[' . $name . ']'; + if (!$href) { + return $token; } - return $value; + + return '<a href="' . $href . $this->getExt() . '">' . $token . '</a> '; } - [, $prefix, $name, $suffix] = $match; + // Opening of an attribute followed by child elements: #[\Name( + if (preg_match('/^(#\[)(' . $namePattern . ')\($/', $trimmed, $match)) { + [, $prefix, $name] = $match; + return $prefix . $this->link_attribute_name($name) . '('; + } + + // Separator between attribute arguments + if ($trimmed === '|') { + return ' | '; + } + + return $trimmed === '' ? '' : $value; + } + + private function link_attribute_name(string $name): string { $attribute = strtolower(ltrim($name, "\\")); $href = $this->getFilename('class.' . $attribute); if (!$href) { - return $value; + return $name; + } + + return '<a href="' . $href . $this->getExt() . '">' . $name . '</a>'; + } + + private function render_attribute_args(string $args): string { + $args = trim(preg_replace('/\s+/', ' ', $args)); + $parts = $this->split_attribute_args($args); + + if (count($parts) <= 1) { + return $this->link_constants_in_text($parts[0]['value'] ?? ''); + } + + $lines = []; + $prefix = ''; + foreach ($parts as $part) { + $rendered = $this->link_constants_in_text($part['value']); + $line = '&nbsp;&nbsp;&nbsp;&nbsp;' . $prefix . $rendered; + if ($part['separator'] === ',') { + $line .= ','; + $prefix = ''; + } elseif ($part['separator'] === '|') { + $prefix = '| '; + } else { + $prefix = ''; + } + $lines[] = $line; + } + + return '<br>' . implode('<br>', $lines) . '<br>'; + } + + /** + * @return list<array{value: string, separator: string|null}> + */ + private function split_attribute_args(string $args): array { + $parts = []; + $current = ''; + $depth = 0; + $inString = false; + $stringChar = ''; + $length = strlen($args); + + for ($i = 0; $i < $length; $i++) { + $ch = $args[$i]; + + if ($inString) { + $current .= $ch; + if ($ch === '\\' && $i + 1 < $length) { + $current .= $args[++$i]; + continue; + } + if ($ch === $stringChar) { + $inString = false; + } + continue; + } + + if ($ch === '"' || $ch === "'") { + $inString = true; + $stringChar = $ch; + $current .= $ch; + continue; + } + + if ($ch === '(' || $ch === '[') { + $depth++; + } elseif ($ch === ')' || $ch === ']') { + $depth--; + } + + if ($depth === 0 && ($ch === '|' || $ch === ',')) { + $parts[] = ['value' => trim($current), 'separator' => $ch]; + $current = ''; + continue; + } + + $current .= $ch; + } + + if (trim($current) !== '') { + $parts[] = ['value' => trim($current), 'separator' => null]; } - return $prefix . '<a href="' . $href . $this->getExt() . '">' . $name . '</a>' . $suffix; + return $parts; + } + + private function link_constants_in_text(string $text): string { + $escaped = htmlspecialchars($text, ENT_NOQUOTES, 'UTF-8'); + + // Link Class::CONST references that appear as literal text + return preg_replace_callback( + '/\\\\?[A-Za-z_][\w\\\\]*::[A-Za-z_][\w]*/', + function (array $match): string { + $constant = $match[0]; + $link = $this->createLink($this->convertConstantNameToId($constant)); + if ($link === null) { + return $constant; + } + return '<a href="' . $link . '">' . $constant . '</a>'; + }, + $escaped, + ); } public function format_methodsynopsis($open, $name, $attrs, $props) { diff --git a/tests/package/generic/attribute_formatting_001.phpt b/tests/package/generic/attribute_formatting_001.phpt index 5c4c6d2b6..76bbf787f 100644 --- a/tests/package/generic/attribute_formatting_001.phpt +++ b/tests/package/generic/attribute_formatting_001.phpt @@ -42,7 +42,7 @@ Content: <div class="section"> <p class="para">2. Class methodparameter with known attribute</p> - <div class="constructorsynopsis dc-description"><span class="modifier">public</span> <span class="methodname">mysqli::__construct</span>(<span class="methodparam"><span class="attribute">#[<a href="file.knownattribute.is.in.html">\KnownAttribute</a>]</span><span class="type"><span class="type">string</span><span class="type">null</span></span> <code class="parameter">$password</code><span class="initializer"> = <span class="type">null</span></span></span>)</div> + <div class="constructorsynopsis dc-description"><span class="modifier">public</span> <span class="methodname">mysqli::__construct</span>(<span class="methodparam"><span class="attribute"><a href="file.knownattribute.is.in.html">#[\KnownAttribute]</a> </span><span class="type"><span class="type">string</span><span class="type">null</span></span> <code class="parameter">$password</code><span class="initializer"> = <span class="type">null</span></span></span>)</div> </div> @@ -54,7 +54,7 @@ Content: <div class="section"> <p class="para">4. Function parameter with known attribute</p> - <div class="methodsynopsis dc-description"><span class="type">bool</span> <span class="methodname">password_verify</span>(<span class="methodparam"><span class="attribute">#[<a href="file.knownattribute.is.in.html">\KnownAttribute</a>]</span><span class="type">string</span> <code class="parameter">$password</code></span>, <span class="methodparam"><span class="type">string</span> <code class="parameter">$hash</code></span>)</div> + <div class="methodsynopsis dc-description"><span class="type">bool</span> <span class="methodname">password_verify</span>(<span class="methodparam"><span class="attribute"><a href="file.knownattribute.is.in.html">#[\KnownAttribute]</a> </span><span class="type">string</span> <code class="parameter">$password</code></span>, <span class="methodparam"><span class="type">string</span> <code class="parameter">$hash</code></span>)</div> </div> </div> diff --git a/tests/package/generic/attribute_formatting_002.phpt b/tests/package/generic/attribute_formatting_002.phpt index b5e0d17f7..ef6f021f0 100644 --- a/tests/package/generic/attribute_formatting_002.phpt +++ b/tests/package/generic/attribute_formatting_002.phpt @@ -50,7 +50,7 @@ Content: <div class="section"> <p class="para">1. Class with unknown attributes</p> <div class="classsynopsis"><div class="classsynopsisinfo"> - + <span class="attribute">#[\UnknownAttribute]</span><br> <span class="attribute">#[\AnotherUnknownAttribute]</span><br> <span class="modifier">class</span> <strong class="classname">DateTime</strong> @@ -61,9 +61,9 @@ Content: <div class="section"> <p class="para">2. Class with known attributes</p> <div class="classsynopsis"><div class="classsynopsisinfo"> - - <span class="attribute">#[<a href="file.knownattribute.is.in.html">\KnownAttribute</a>]</span><br> - <span class="attribute">#[<a href="file.anotherknownattribute.is.in.html">\AnotherKnownAttribute</a>]</span><br> + + <span class="attribute"><a href="file.knownattribute.is.in.html">#[\KnownAttribute]</a> </span><br> + <span class="attribute"><a href="file.anotherknownattribute.is.in.html">#[\AnotherKnownAttribute]</a> </span><br> <span class="modifier">class</span> <strong class="classname">DateTime</strong> {</div> }</div> @@ -80,8 +80,8 @@ Content: <div class="section"> <p class="para">4. Method with known attributes</p> - <div class="methodsynopsis dc-description"><span class="attribute">#[<a href="file.knownattribute.is.in.html">\KnownAttribute</a>]</span><br> - <span class="attribute">#[<a href="file.anotherknownattribute.is.in.html">\AnotherKnownAttribute</a>]</span><br> + <div class="methodsynopsis dc-description"><span class="attribute"><a href="file.knownattribute.is.in.html">#[\KnownAttribute]</a> </span><br> + <span class="attribute"><a href="file.anotherknownattribute.is.in.html">#[\AnotherKnownAttribute]</a> </span><br> <span class="modifier">public</span> <span class="methodname">ClassName::methodName</span>()</div> </div> @@ -96,8 +96,8 @@ Content: <div class="section"> <p class="para">6. Constructor with known attributes</p> - <div class="constructorsynopsis dc-description"><span class="attribute">#[<a href="file.knownattribute.is.in.html">\KnownAttribute</a>]</span><br> - <span class="attribute">#[<a href="file.anotherknownattribute.is.in.html">\AnotherKnownAttribute</a>]</span><br> + <div class="constructorsynopsis dc-description"><span class="attribute"><a href="file.knownattribute.is.in.html">#[\KnownAttribute]</a> </span><br> + <span class="attribute"><a href="file.anotherknownattribute.is.in.html">#[\AnotherKnownAttribute]</a> </span><br> <span class="modifier">public</span> <span class="methodname">ClassName::__construct</span>()</div> </div> @@ -105,7 +105,7 @@ Content: <div class="section"> <p class="para">7. Class, constructor and methods with unknown attributes</p> <div class="classsynopsis"><div class="classsynopsisinfo"> - + <span class="attribute">#[\UnknownAttribute]</span><br> <span class="attribute">#[\AnotherUnknownAttribute]</span><br> <span class="modifier">class</span> <strong class="classname">DateTime</strong> @@ -129,22 +129,22 @@ Content: <div class="section"> <p class="para">8. Class, constructor and methods with known attributes</p> <div class="classsynopsis"><div class="classsynopsisinfo"> - - <span class="attribute">#[<a href="file.knownattribute.is.in.html">\KnownAttribute</a>]</span><br> - <span class="attribute">#[<a href="file.anotherknownattribute.is.in.html">\AnotherKnownAttribute</a>]</span><br> + + <span class="attribute"><a href="file.knownattribute.is.in.html">#[\KnownAttribute]</a> </span><br> + <span class="attribute"><a href="file.anotherknownattribute.is.in.html">#[\AnotherKnownAttribute]</a> </span><br> <span class="modifier">class</span> <strong class="classname">DateTime</strong> {</div> <div class="constructorsynopsis dc-description"> - <span class="attribute">#[<a href="file.knownattribute.is.in.html">\KnownAttribute</a>]</span><br> - <span class="attribute">#[<a href="file.anotherknownattribute.is.in.html">\AnotherKnownAttribute</a>]</span><br> + <span class="attribute"><a href="file.knownattribute.is.in.html">#[\KnownAttribute]</a> </span><br> + <span class="attribute"><a href="file.anotherknownattribute.is.in.html">#[\AnotherKnownAttribute]</a> </span><br> <span class="modifier">public</span> <span class="methodname">ClassName::__construct</span>()</div> - <div class="methodsynopsis dc-description"><span class="attribute">#[<a href="file.knownattribute.is.in.html">\KnownAttribute</a>]</span><br> - <span class="attribute">#[<a href="file.anotherknownattribute.is.in.html">\AnotherKnownAttribute</a>]</span><br> + <div class="methodsynopsis dc-description"><span class="attribute"><a href="file.knownattribute.is.in.html">#[\KnownAttribute]</a> </span><br> + <span class="attribute"><a href="file.anotherknownattribute.is.in.html">#[\AnotherKnownAttribute]</a> </span><br> <span class="modifier">public</span> <span class="methodname">ClassName::methodName1</span>()</div> - <div class="methodsynopsis dc-description"><span class="attribute">#[<a href="file.knownattribute.is.in.html">\KnownAttribute</a>]</span><br> - <span class="attribute">#[<a href="file.anotherknownattribute.is.in.html">\AnotherKnownAttribute</a>]</span><br> + <div class="methodsynopsis dc-description"><span class="attribute"><a href="file.knownattribute.is.in.html">#[\KnownAttribute]</a> </span><br> + <span class="attribute"><a href="file.anotherknownattribute.is.in.html">#[\AnotherKnownAttribute]</a> </span><br> <span class="modifier">public</span> <span class="methodname">ClassName::methodName2</span>()</div> }</div> @@ -161,8 +161,8 @@ Content: <div class="section"> <p class="para">10. Function with known attributes</p> - <div class="methodsynopsis dc-description"><span class="attribute">#[<a href="file.knownattribute.is.in.html">\KnownAttribute</a>]</span><br> - <span class="attribute">#[<a href="file.anotherknownattribute.is.in.html">\AnotherKnownAttribute</a>]</span><br> + <div class="methodsynopsis dc-description"><span class="attribute"><a href="file.knownattribute.is.in.html">#[\KnownAttribute]</a> </span><br> + <span class="attribute"><a href="file.anotherknownattribute.is.in.html">#[\AnotherKnownAttribute]</a> </span><br> <span class="type">void</span> <span class="methodname">function_name</span>)</div> </div> diff --git a/tests/package/generic/attribute_formatting_003.phpt b/tests/package/generic/attribute_formatting_003.phpt index 0506abaa3..b1066f9cd 100644 --- a/tests/package/generic/attribute_formatting_003.phpt +++ b/tests/package/generic/attribute_formatting_003.phpt @@ -52,7 +52,7 @@ Content: <div class="section"> <p class="para">1. Property/Constant with unknown attributes</p> <div class="classsynopsis"><div class="classsynopsisinfo"> - + <span class="modifier">class</span> <strong class="classname">ClassName</strong> {</div> <div class="classsynopsisinfo classsynopsisinfo_comment">/* Properties/Constants */</div> @@ -70,13 +70,13 @@ Content: <div class="section"> <p class="para">2. Property/Constant with known attributes</p> <div class="classsynopsis"><div class="classsynopsisinfo"> - + <span class="modifier">class</span> <strong class="classname">ClassName</strong> {</div> <div class="classsynopsisinfo classsynopsisinfo_comment">/* Properties/Constants */</div> <div class="fieldsynopsis"> - <span class="attribute">#[<a href="file.knownattribute.is.in.html">\KnownAttribute</a>]</span><br> - <span class="attribute">#[<a href="file.anotherknownattribute.is.in.html">\AnotherKnownAttribute</a>]</span><br> + <span class="attribute"><a href="file.knownattribute.is.in.html">#[\KnownAttribute]</a> </span><br> + <span class="attribute"><a href="file.anotherknownattribute.is.in.html">#[\AnotherKnownAttribute]</a> </span><br> <span class="modifier">public</span> <span class="modifier">readonly</span> <span class="type">string</span> @@ -88,7 +88,7 @@ Content: <div class="section"> <p class="para">3. Constant of a class with attribute</p> <div class="classsynopsis"><div class="classsynopsisinfo"> - + <span class="attribute">#[\UnknownAttribute]</span><br> <span class="modifier">class</span> <strong class="classname">ClassName</strong> {</div> diff --git a/tests/package/generic/attribute_formatting_005.phpt b/tests/package/generic/attribute_formatting_005.phpt new file mode 100644 index 000000000..96e95aa1f --- /dev/null +++ b/tests/package/generic/attribute_formatting_005.phpt @@ -0,0 +1,119 @@ +--TEST-- +Attribute formatting 005 - Attribute with literal parameter arguments +--FILE-- +<?php +namespace phpdotnet\phd; + +require_once __DIR__ . "/../../setup.php"; + +$xmlFile = __DIR__ . "/data/attribute_formatting_005.xml"; + +$config->xmlFile = $xmlFile; + +$format = new TestGenericChunkedXHTML($config, $outputHandler); + +$format->SQLiteIndex( + null, null, + "class.deprecated", + "class.deprecated", + "", "", "", "", "", "", 0, +); +$format->SQLiteIndex( + null, null, + "class.attribute", + "class.attribute", + "", "", "", "", "", "", 0, +); +$format->SQLiteIndex( + null, null, + "attribute.constants.target-function", + "class.attribute", + "", "", "", "", "", "", 0, +); +$format->SQLiteIndex( + null, null, + "attribute.constants.target-method", + "class.attribute", + "", "", "", "", "", "", 0, +); +$format->SQLiteIndex( + null, null, + "attribute.constants.target-class", + "class.attribute", + "", "", "", "", "", "", 0, +); + +$render = new TestRender(new Reader($outputHandler), $config, $format); + +$render->run(); +?> +--EXPECT-- +Filename: attribute-formatting-005.html +Content: +<div id="attribute-formatting-005" class="chapter"> + <div class="section"> + <p class="para">1. Attribute with literal named arguments</p> + <div class="classsynopsis"><div class="classsynopsisinfo"> + + <span class="attribute">#[<a href="class.deprecated.html">\Deprecated</a>(<br>&nbsp;&nbsp;&nbsp;&nbsp;since: '8.5',<br>&nbsp;&nbsp;&nbsp;&nbsp;message: 'Deprecated since PHP 8.4'<br>)]</span><br> + <span class="modifier">final</span> + <span class="modifier">class</span> <strong class="classname">Deprecated</strong> + {</div> + }</div> + </div> + + <div class="section"> + <p class="para">2. Attribute with single literal positional argument</p> + <div class="classsynopsis"><div class="classsynopsisinfo"> + + <span class="attribute">#[<a href="class.deprecated.html">\Deprecated</a>('Deprecated since PHP 8.4')]</span><br> + <span class="modifier">final</span> + <span class="modifier">class</span> <strong class="classname">Deprecated</strong> + {</div> + }</div> + </div> + + <div class="section"> + <p class="para">3. Unknown attribute with literal argument</p> + <div class="classsynopsis"><div class="classsynopsisinfo"> + + <span class="attribute">#[\UnknownAttribute(foo: 'bar')]</span><br> + <span class="modifier">final</span> + <span class="modifier">class</span> <strong class="classname">Deprecated</strong> + {</div> + }</div> + </div> + + <div class="section"> + <p class="para">4. Namespaced attribute with literal argument</p> + <div class="classsynopsis"><div class="classsynopsisinfo"> + + <span class="attribute">#[\Some\Namespaced\Attribute(value: 42)]</span><br> + <span class="modifier">final</span> + <span class="modifier">class</span> <strong class="classname">Deprecated</strong> + {</div> + }</div> + </div> + + <div class="section"> + <p class="para">5. Multi-line attribute with literal class constant arguments</p> + <div class="classsynopsis"><div class="classsynopsisinfo"> + + <span class="attribute">#[<a href="class.attribute.html">\Attribute</a>(<br>&nbsp;&nbsp;&nbsp;&nbsp;<a href="class.attribute.html#attribute.constants.target-function">Attribute::TARGET_FUNCTION</a><br>&nbsp;&nbsp;&nbsp;&nbsp;| <a href="class.attribute.html#attribute.constants.target-method">Attribute::TARGET_METHOD</a><br>&nbsp;&nbsp;&nbsp;&nbsp;| <a href="class.attribute.html#attribute.constants.target-class">Attribute::TARGET_CLASS</a><br>)]</span><br> + <span class="modifier">final</span> + <span class="modifier">class</span> <strong class="classname">Deprecated</strong> + {</div> + }</div> + </div> + + <div class="section"> + <p class="para">6. Attribute with mix of known and unknown class constants</p> + <div class="classsynopsis"><div class="classsynopsisinfo"> + + <span class="attribute">#[<a href="class.attribute.html">\Attribute</a>(<br>&nbsp;&nbsp;&nbsp;&nbsp;<a href="class.attribute.html#attribute.constants.target-class">Attribute::TARGET_CLASS</a><br>&nbsp;&nbsp;&nbsp;&nbsp;| Unknown::CONST<br>)]</span><br> + <span class="modifier">final</span> + <span class="modifier">class</span> <strong class="classname">Deprecated</strong> + {</div> + }</div> + </div> +</div> diff --git a/tests/package/generic/data/attribute_formatting_005.xml b/tests/package/generic/data/attribute_formatting_005.xml new file mode 100644 index 000000000..c296e7fa5 --- /dev/null +++ b/tests/package/generic/data/attribute_formatting_005.xml @@ -0,0 +1,69 @@ +<chapter xml:id="attribute-formatting-005"> + <section> + <para>1. Attribute with literal named arguments</para> + <classsynopsis class="class"> + <ooclass> + <modifier role="attribute">#[\Deprecated(since: '8.5', message: 'Deprecated since PHP 8.4')]</modifier> + <modifier>final</modifier> + <classname>Deprecated</classname> + </ooclass> + </classsynopsis> + </section> + + <section> + <para>2. Attribute with single literal positional argument</para> + <classsynopsis class="class"> + <ooclass> + <modifier role="attribute">#[\Deprecated('Deprecated since PHP 8.4')]</modifier> + <modifier>final</modifier> + <classname>Deprecated</classname> + </ooclass> + </classsynopsis> + </section> + + <section> + <para>3. Unknown attribute with literal argument</para> + <classsynopsis class="class"> + <ooclass> + <modifier role="attribute">#[\UnknownAttribute(foo: 'bar')]</modifier> + <modifier>final</modifier> + <classname>Deprecated</classname> + </ooclass> + </classsynopsis> + </section> + + <section> + <para>4. Namespaced attribute with literal argument</para> + <classsynopsis class="class"> + <ooclass> + <modifier role="attribute">#[\Some\Namespaced\Attribute(value: 42)]</modifier> + <modifier>final</modifier> + <classname>Deprecated</classname> + </ooclass> + </classsynopsis> + </section> + + <section> + <para>5. Multi-line attribute with literal class constant arguments</para> + <classsynopsis class="class"> + <ooclass> + <modifier role="attribute"> + #[\Attribute(Attribute::TARGET_FUNCTION|Attribute::TARGET_METHOD|Attribute::TARGET_CLASS)] + </modifier> + <modifier>final</modifier> + <classname>Deprecated</classname> + </ooclass> + </classsynopsis> + </section> + + <section> + <para>6. Attribute with mix of known and unknown class constants</para> + <classsynopsis class="class"> + <ooclass> + <modifier role="attribute">#[\Attribute(Attribute::TARGET_CLASS | Unknown::CONST)]</modifier> + <modifier>final</modifier> + <classname>Deprecated</classname> + </ooclass> + </classsynopsis> + </section> +</chapter>

« previous php.doc.cvs (#23745) next »