From 53ec86181718468a4b622bda19a65a8ef77fb212 Mon Sep 17 00:00:00 2001 From: Raphael Fakhri <153192858+RaphaelFakhri@users.noreply.github.com> Date: Wed, 4 Nov 2026 06:49:04 +0000 Subject: [PATCH] Fix union and intersection types in IDE formats The IDE formats kept only the last member of a union or intersection type, so array_key_first() was documented as returning null instead of int|string|null. Build the full type from the nested elements, including DNF types, and escape types in the IDE XML output. --- phpdotnet/phd/Package/IDE/Base.php | 48 +++++++++++++++++++--- phpdotnet/phd/Package/IDE/XML.php | 4 +- tests/package/ide/data/type_rendering.xml | 30 ++++++++++++++ tests/package/ide/data/version.xml | 5 +++ tests/package/ide/type_rendering_001.phpt | 49 +++++++++++++++++++++++ 5 files changed, 128 insertions(+), 8 deletions(-) create mode 100644 tests/package/ide/data/type_rendering.xml create mode 100644 tests/package/ide/data/version.xml create mode 100644 tests/package/ide/type_rendering_001.phpt diff --git a/phpdotnet/phd/Package/IDE/Base.php b/phpdotnet/phd/Package/IDE/Base.php index b05b1f660..9e6521110 100644 --- a/phpdotnet/phd/Package/IDE/Base.php +++ b/phpdotnet/phd/Package/IDE/Base.php @@ -20,6 +20,7 @@ abstract class Package_IDE_Base extends Format { 'tbody' => 'format_changelog_tbody', 'tip' => 'format_notes', 'warning' => 'format_notes', + 'type' => 'format_type', ); protected $textmap = array( 'function' => 'format_seealso_entry_text', @@ -54,6 +55,8 @@ abstract class Package_IDE_Base extends Format { ); protected $isFunctionRefSet; + /** Stack of the elements being read, used to build union and intersection types */ + protected $typeStack = []; protected $isChangelogRow = false; protected $role = false; protected $versions; @@ -253,6 +256,7 @@ public function format_refentry($open, $name, $attrs, $props) { if ($open) { $this->function = $this->dfunction; $this->cchunk = $this->dchunk; + $this->typeStack = []; $this->function['manualid'] = $attrs[Reader::XMLNS_XML]['id']; return; @@ -292,16 +296,48 @@ public function format_refsect1($open, $name, $attrs, $props) { } } + public function format_type($open, $name, $attrs, $props) { + if (!$this->isFunctionRefSet || $this->role != 'description') { + return; + } + if ($open) { + if (!$props['empty']) { + $this->typeStack[] = [ + 'class' => $attrs[Reader::XMLNS_DOCBOOK]['class'] ?? '', + 'parts' => [], + ]; + } + return; + } + $frame = array_pop($this->typeStack); + if ($frame === null) { + return; + } + $type = match ($frame['class']) { + 'union' => implode('|', $frame['parts']), + 'intersection' => implode('&', $frame['parts']), + default => implode('', $frame['parts']), + }; + if ($this->typeStack !== []) { + if ($frame['class'] === 'intersection') { + $type = '(' . $type . ')'; + } + $this->typeStack[array_key_last($this->typeStack)]['parts'][] = $type; + return; + } + if (isset($this->cchunk['methodparam']) && !$this->cchunk['methodparam']) { + $this->function['return']['type'] = $type; + return; + } + $this->cchunk['param']['type'] = $type; + } + public function format_type_text($value, $tag) { if (!$this->isFunctionRefSet) { return; } - if ($this->role == 'description') { - if (isset($this->cchunk['methodparam']) && !$this->cchunk['methodparam']) { - $this->function['return']['type'] = $value; - return; - } - $this->cchunk['param']['type'] = $value; + if ($this->role == 'description' && $this->typeStack !== []) { + $this->typeStack[array_key_last($this->typeStack)]['parts'][] = trim($value); } } diff --git a/phpdotnet/phd/Package/IDE/XML.php b/phpdotnet/phd/Package/IDE/XML.php index a3dab38d1..2b197c716 100644 --- a/phpdotnet/phd/Package/IDE/XML.php +++ b/phpdotnet/phd/Package/IDE/XML.php @@ -25,7 +25,7 @@ public function parseFunction() { foreach ((array)$this->function['params'] as $param) { $str .= ' ' . PHP_EOL; $str .= ' ' . $param['name'] . '' . PHP_EOL; - $str .= ' ' . $param['type'] . '' . PHP_EOL; + $str .= ' ' . $this->e($param['type'] ?? '') . '' . PHP_EOL; $str .= ' ' . $param['optional'] . '' . PHP_EOL; if (isset($param['initializer'])) { $str .= ' ' . $param['initializer'] . '' . PHP_EOL; @@ -37,7 +37,7 @@ public function parseFunction() { //Return $str .= '' . PHP_EOL; - $str .= ' ' . $this->function['return']['type'] . '' . PHP_EOL; + $str .= ' ' . $this->e($this->function['return']['type'] ?? '') . '' . PHP_EOL; $str .= ' ' . $this->cdata_str($this->function['return']['description']) . '' . PHP_EOL; $str .= '' . PHP_EOL; diff --git a/tests/package/ide/data/type_rendering.xml b/tests/package/ide/data/type_rendering.xml new file mode 100644 index 000000000..f86db615b --- /dev/null +++ b/tests/package/ide/data/type_rendering.xml @@ -0,0 +1,30 @@ + + + + + array_key_first + Gets the first key of an array + + + + intstringnullarray_key_first + arrayarray + + + + + + + type_rendering + Function with union, intersection and DNF parameter types + + + + voidtype_rendering + arraystringunion + CountableTraversableintersection + CountableTraversablenulldnfnull + + + + diff --git a/tests/package/ide/data/version.xml b/tests/package/ide/data/version.xml new file mode 100644 index 000000000..9a5883aa1 --- /dev/null +++ b/tests/package/ide/data/version.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/tests/package/ide/type_rendering_001.phpt b/tests/package/ide/type_rendering_001.phpt new file mode 100644 index 000000000..477798d08 --- /dev/null +++ b/tests/package/ide/type_rendering_001.phpt @@ -0,0 +1,49 @@ +--TEST-- +IDE type rendering 001 - Union, intersection and DNF types +--CONFLICTS-- +all +--FILE-- +xmlFile = __DIR__ . "/data/type_rendering.xml"; +$config->phpwebVersionFilename = __DIR__ . "/data/version.xml"; +$config->outputDir = dirname(__DIR__, 2) . "/output/"; + +$render = new TestRender(new Reader($outputHandler), $config); +$render->attach(new Package_IDE_JSON($config, $outputHandler)); +$render->attach(new Package_IDE_XML($config, $outputHandler)); +$render->run(); + +foreach (["array_key_first", "type_rendering"] as $function) { + $json = json_decode(file_get_contents($config->outputDir . "ide-json/" . $function . ".json"), true); + echo $function, " return: ", $json["return"]["type"], "\n"; + foreach ($json["params"] as $param) { + echo $function, " param ", $param["name"], ": ", $param["type"], "\n"; + } +} + +$xml = simplexml_load_file($config->outputDir . "ide-xml/type_rendering.xml"); +foreach ($xml->params->param as $param) { + echo "XML param ", $param->name, ": ", $param->type, "\n"; +} +?> +--CLEAN-- + +--EXPECT-- +array_key_first return: int|string|null +array_key_first param array: array +type_rendering return: void +type_rendering param union: array|string +type_rendering param intersection: Countable&Traversable +type_rendering param dnf: (Countable&Traversable)|null +XML param union: array|string +XML param intersection: Countable&Traversable +XML param dnf: (Countable&Traversable)|null