diff --git a/dev/src/Command/DocFxCommand.php b/dev/src/Command/DocFxCommand.php index 8b188fd70f85..8f245c284acb 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,7 +150,18 @@ 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(); + try { + $process->mustRun(); + } catch (ProcessFailedException $ex) { + 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'; } @@ -345,4 +359,29 @@ 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()); + } }