From fd279881d80aa9a0526ffd0311927617093c66c6 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Mon, 16 Mar 2026 17:13:40 -0700 Subject: [PATCH 1/3] fix(dev): workaround for phpdoc parsing error with escaped docblock closing tags --- dev/src/Command/DocFxCommand.php | 46 ++++++++++++++++++++++++++-- dev/src/DocFx/Node/DocblockTrait.php | 6 ++++ dev/tests/Unit/DocFx/NodeTest.php | 15 +++++++++ 3 files changed, 65 insertions(+), 2 deletions(-) diff --git a/dev/src/Command/DocFxCommand.php b/dev/src/Command/DocFxCommand.php index 8b188fd70f85..090db8b40f8b 100644 --- a/dev/src/Command/DocFxCommand.php +++ b/dev/src/Command/DocFxCommand.php @@ -30,6 +30,9 @@ use Google\Cloud\Dev\DocFx\Page\PageTree; use Google\Cloud\Dev\DocFx\Page\OverviewPage; use Google\Cloud\Dev\DocFx\XrefValidationTrait; +use RecursiveDirectoryIterator; +use RecursiveIteratorIterator; +use Symfony\Component\Process\Exception\ProcessFailedException; /** * @internal @@ -147,8 +150,21 @@ protected function execute(InputInterface $input, OutputInterface $output) $output->write('Running phpdoc to generate structure.xml... '); // Run "phpdoc" $process = self::getPhpDocCommand($component->getPath(), $outDir); - $process->mustRun(); - $output->writeln('Done.'); + try { + $process->mustRun(); + $output->writeln('Done.'); + } catch (ProcessFailedException $ex) { + if (false !== strpos($process->getErrorOutput(), 'The arguments array must contain 3 items, 0 given')) { + $output->writeln('Process errored out, applying PHPDoc Tag Escape fix and trying again...'); + $this->applyPhpDocTagEscapeFix($component->getPath()); + $process->mustRun(); + $output->writeln('IT WORKED! Reverting Fix... '); + $this->applyPhpDocTagEscapeFix($component->getPath(), revert: true); + $output->writeln('Done.'); + } else { + throw $ex; + } + } $xml = $outDir . '/structure.xml'; } if (!file_exists($xml)) { @@ -345,4 +361,30 @@ private function uploadToStagingBucket(string $outDir, string $stagingBucket): v ]); $process->mustRun(); } + + /** + * Applies a fix to solve an issue where {@*} is being parsed as a tag by + * phpDocumentor, which later causes an error when vprintf sees unescaped + * percent signs. + * + * Replaces "{@*}" with "*/" in the files were the errors occur. + * + * @see https://github.com/phpDocumentor/ReflectionDocBlock/pull/450 + * @TODO: Remove this method once the fix is merged and released. + */ + private function applyPhpDocTagEscapeFix(string $componentPath, $revert = false) + { + $from = $revert ? '*/' : '{@*}'; + $to = $revert ? '{@*}' : '*/'; + $dirIter = new RecursiveDirectoryIterator($componentPath . '/src', RecursiveDirectoryIterator::SKIP_DOTS); + foreach (new RecursiveIteratorIterator($dirIter) as $file) { + if ($file->isFile()) { + $content = file_get_contents($file->getPathname()); + // The error only occurs when both "{@*}" and "%" are present + if (str_contains($content, $from) && str_contains($content, '%')) { + file_put_contents($file->getPathname(), str_replace($from, $to, $content)); + } + } + } + } } diff --git a/dev/src/DocFx/Node/DocblockTrait.php b/dev/src/DocFx/Node/DocblockTrait.php index 93fceab54872..d1c5fc6e8204 100644 --- a/dev/src/DocFx/Node/DocblockTrait.php +++ b/dev/src/DocFx/Node/DocblockTrait.php @@ -45,6 +45,7 @@ public function getContent(): string $content = $this->replaceProtoRef($content); $content = $this->stripSnippetTag($content); $content = $this->addPhpLanguageHintToFencedCodeBlock($content); + $content = $this->unescapeDocblockClosingTags($content); return $content; } @@ -85,4 +86,9 @@ private function stripProtobufGeneratedField(string $content): string $regex = '/Generated from protobuf field .*<\/code>\Z/m'; return rtrim(preg_replace($regex, '', $content)); } + + private function unescapeDocBlockClosingTags(string $content): string + { + return str_replace('{@*}', '*/', $content); + } } diff --git a/dev/tests/Unit/DocFx/NodeTest.php b/dev/tests/Unit/DocFx/NodeTest.php index 4e60c5367692..28eef2b2b88b 100644 --- a/dev/tests/Unit/DocFx/NodeTest.php +++ b/dev/tests/Unit/DocFx/NodeTest.php @@ -18,6 +18,7 @@ namespace Google\Cloud\Dev\Tests\Unit\DocFx; use Google\Cloud\Dev\DocFx\Node\ClassNode; +use Google\Cloud\Dev\DocFx\Node\DocblockTrait; use Google\Cloud\Dev\DocFx\Node\MethodNode; use Google\Cloud\Dev\DocFx\Node\XrefTrait; use Google\Cloud\Dev\DocFx\Node\FencedCodeBlockTrait; @@ -561,4 +562,18 @@ public function provideBrokenXrefs() [sprintf('{@see \%s::OUTPUT_NORMAL}', OutputInterface::class)], // valid constant ]; } + + public function testEscapeDocblockClosingTags() + { + $classXml = 'TestClass%s'; + + $docblock = new class (new SimpleXMLElement(sprintf($classXml, 'the path must match `foo/{@*}bar/{@*}baz`'))) { + use DocblockTrait; + + public function __construct(private SimpleXMLElement $xmlNode) + {} + }; + + $this->assertEquals('the path must match `foo/*/bar/*/baz`', $docblock->getContent()); + } } From 77626998ad0c759d78d8b1d9390e14bb34b844c5 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Mon, 16 Mar 2026 17:27:32 -0700 Subject: [PATCH 2/3] get around snippet parsing error --- dev/src/Command/DocFxCommand.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/dev/src/Command/DocFxCommand.php b/dev/src/Command/DocFxCommand.php index 090db8b40f8b..cda7d00e73ab 100644 --- a/dev/src/Command/DocFxCommand.php +++ b/dev/src/Command/DocFxCommand.php @@ -365,9 +365,8 @@ private function uploadToStagingBucket(string $outDir, string $stagingBucket): v /** * Applies a fix to solve an issue where {@*} is being parsed as a tag by * phpDocumentor, which later causes an error when vprintf sees unescaped - * percent signs. - * - * Replaces "{@*}" with "*/" in the files were the errors occur. + * percent signs. Replaces {@*} with "*/" in the files were the + * errors occur. * * @see https://github.com/phpDocumentor/ReflectionDocBlock/pull/450 * @TODO: Remove this method once the fix is merged and released. From 34925b8a46657ef9a10696de04462a611e2ef238 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Mon, 16 Mar 2026 17:29:10 -0700 Subject: [PATCH 3/3] pointless cleanup --- dev/src/Command/DocFxCommand.php | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/dev/src/Command/DocFxCommand.php b/dev/src/Command/DocFxCommand.php index cda7d00e73ab..8f245c284acb 100644 --- a/dev/src/Command/DocFxCommand.php +++ b/dev/src/Command/DocFxCommand.php @@ -152,19 +152,17 @@ protected function execute(InputInterface $input, OutputInterface $output) $process = self::getPhpDocCommand($component->getPath(), $outDir); try { $process->mustRun(); - $output->writeln('Done.'); } catch (ProcessFailedException $ex) { - if (false !== strpos($process->getErrorOutput(), 'The arguments array must contain 3 items, 0 given')) { - $output->writeln('Process errored out, applying PHPDoc Tag Escape fix and trying again...'); - $this->applyPhpDocTagEscapeFix($component->getPath()); - $process->mustRun(); - $output->writeln('IT WORKED! Reverting Fix... '); - $this->applyPhpDocTagEscapeFix($component->getPath(), revert: true); - $output->writeln('Done.'); - } else { + if (false === strpos($process->getErrorOutput(), 'The arguments array must contain 3 items, 0 given')) { throw $ex; } + $output->writeln('Process errored out, applying PHPDoc Tag Escape fix and trying again...'); + $this->applyPhpDocTagEscapeFix($component->getPath()); + $process->mustRun(); + $output->write('IT WORKED! Reverting Fix... '); + $this->applyPhpDocTagEscapeFix($component->getPath(), revert: true); } + $output->writeln('Done.'); $xml = $outDir . '/structure.xml'; } if (!file_exists($xml)) {