Skip to content

Commit

Permalink
move script into DocFX command for simplicity
Browse files Browse the repository at this point in the history
  • Loading branch information
bshaffer committed Oct 4, 2023
1 parent ce14d8a commit b150303
Show file tree
Hide file tree
Showing 2 changed files with 114 additions and 87 deletions.
108 changes: 104 additions & 4 deletions dev/src/Command/DocFxCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@
use Symfony\Component\Process\Process;
use Symfony\Component\Yaml\Yaml;
use RuntimeException;
use Google\Cloud\Core\Logger\AppEngineFlexFormatter;
use Google\Cloud\Core\Logger\AppEngineFlexFormatterV2;
use Google\Cloud\Dev\DocFx\Node\ClassNode;
use Google\Cloud\Dev\DocFx\Page\PageTree;
use Google\Cloud\Dev\DocFx\Page\OverviewPage;
use Google\Cloud\Dev\Component;
Expand All @@ -37,6 +40,8 @@ class DocFxCommand extends Command
private array $composerJson;
private array $repoMetadataJson;

private const XREF_REGEX = '/<xref uid="([^ ]*)"/';

protected function configure()
{
$this->setName('docfx')
Expand Down Expand Up @@ -95,6 +100,7 @@ protected function execute(InputInterface $input, OutputInterface $output)
$valid = true;
$tocItems = [];
$packageDescription = $component->getDescription();
$isBeta = 'stable' !== $component->getReleaseLevel();
foreach ($component->getNamespaces() as $namespace => $dir) {
$pageTree = new PageTree(
$xml,
Expand All @@ -105,7 +111,7 @@ protected function execute(InputInterface $input, OutputInterface $output)

foreach ($pageTree->getPages() as $page) {
// validate the docs page. this will fail the job if it's false
$page->getClassNode()->validate($output) && $valid;
$valid = $this->validate($page->getClassNode(), $output) && $valid;
$docFxArray = ['items' => $page->getItems()];

// Dump the YAML for the class node
Expand All @@ -125,11 +131,10 @@ protected function execute(InputInterface $input, OutputInterface $output)
return 1;
}

$releaseLevel = $component->getReleaseLevel();
if (file_exists($overviewFile = sprintf('%s/README.md', $component->getPath()))) {
$overview = new OverviewPage(
file_get_contents($overviewFile),
$releaseLevel !== 'stable'
$isBeta
);
$outFile = sprintf('%s/%s', $outDir, $overview->getFilename());
file_put_contents($outFile, $overview->getContents());
Expand All @@ -141,7 +146,7 @@ protected function execute(InputInterface $input, OutputInterface $output)
$componentToc = array_filter([
'uid' => $component->getReferenceDocumentationUid(),
'name' => $component->getPackageName(),
'status' => $releaseLevel !== 'stable' ? 'beta' : '',
'status' => $isBeta ? 'beta' : '',
'items' => $tocItems,
]);
$tocYaml = Yaml::dump([$componentToc], $inline, $indent, $flags);
Expand Down Expand Up @@ -207,4 +212,99 @@ public static function getPhpDocCommand(string $componentPath, string $outDir):

return $process;
}

private function validate(ClassNode $class, OutputInterface $output): bool
{
$valid = true;
$hadWarning = false;
$isGenerated = $class->isProtobufMessageClass() || $class->isProtobufEnumClass() || $class->isServiceClass();
$nodes = array_merge([$class], $class->getMethods(), $class->getConstants());
foreach ($nodes as $node) {
foreach ($this->getInvalidXrefs($node->getContent()) as $invalidRef) {
$output->write(sprintf("\n<error>Invalid xref in %s: %s</>", $node->getFullname(), $invalidRef));
$valid = false;
$hadWarning = true;
}
foreach ($this->getBrokenXrefs($node->getContent()) as $brokenRef) {
$output->write(sprintf("\n<comment>Broken xref in %s: %s</>", $node->getFullname(), $brokenRef));
$valid = $valid && (false || $isGenerated);
$hadWarning = true;
}
}
if ($hadWarning) {
$output->writeln('');
}
return $valid;
}

/**
* Verifies that protobuf references and @see tags are properly formatted.
*/
private function getInvalidXrefs(string $description): array
{
$invalidRefs = [];
preg_replace_callback(
self::XREF_REGEX,
function ($matches) use (&$invalidRefs) {
// Valid external reference
if (0 === strpos($matches[1], 'http')) {
return;
}
// Invalid reference format
if ('\\' !== $matches[1][0] || substr_count($matches[1], '\Google\\') > 1) {
$invalidRefs[] = $matches[1];
}
},
$description
);

return $invalidRefs;
}

/**
* Verifies that protobuf references and @see tags reference classes that exist.
*/
private function getBrokenXrefs(string $description): array
{
$brokenRefs = [];
preg_replace_callback(
self::XREF_REGEX,
function ($matches) use (&$brokenRefs) {
if (0 === strpos($matches[1], 'http')) {
return; // Valid external reference
}
if (in_array(
$matches[1],
['\\' . AppEngineFlexFormatter::class, '\\' . AppEngineFlexFormatterV2::class])
) {
return; // We cannot run "class_exists" on these because they will throw a fatal error.
}
if (class_exists($matches[1]) || interface_exists($matches[1] || trait_exists($matches[1]))) {
return; // Valid class reference
}
if (false !== strpos($matches[1], '::')) {
if (false !== strpos($matches[1], '()')) {
list($class, $method) = explode('::', str_replace('()', '', $matches[1]));
if (method_exists($class, $method)) {
return; // Valid method reference
}
if ('Async' === substr($method, -5)) {
return; // Skip magic Async methods
}
} elseif (defined($matches[1])) {
return; // Valid constant reference
}
}
// empty hrefs show up as "\\"
if ($matches[1] === '\\\\') {
$brokenRefs[] = '<options=bold>empty</>';
} else {
$brokenRefs[] = $matches[1];
}
},
$description
);

return $brokenRefs;
}
}
93 changes: 10 additions & 83 deletions dev/src/DocFx/Node/ClassNode.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,7 @@

namespace Google\Cloud\Dev\DocFx\Node;

use Google\Cloud\Core\Logger\AppEngineFlexFormatter;
use Google\Cloud\Core\Logger\AppEngineFlexFormatterV2;
use SimpleXMLElement;
use Symfony\Component\Console\Output\OutputInterface;

/**
* @internal
Expand Down Expand Up @@ -174,16 +171,16 @@ public function getMethods(): array
foreach ($this->xmlNode->method as $methodNode) {
$method = new MethodNode($methodNode, $this->protoPackages);
if ($method->isPublic() && !$method->isInherited() && !$method->isExcludedMethod()) {
// This is to fix an issue in phpdocumentor where magic methods do not have
// "inhereted_from" set as expected.
// TODO: Remove this once the above issue is fixed.
// @see https://github.com/phpDocumentor/phpDocumentor/pull/3520
if (false !== strpos($method->getFullname(), 'Async()')) {
list($class, $_) = explode('::', $method->getFullname());
if ($class !== $this->getFullName()) {
continue;
}
}
// // This is to fix an issue in phpdocumentor where magic methods do not have
// // "inhereted_from" set as expected.
// // TODO: Remove this once the above issue is fixed.
// // @see https://github.com/phpDocumentor/phpDocumentor/pull/3520
// if (false !== strpos($method->getFullname(), 'Async()')) {
// list($class, $_) = explode('::', $method->getFullname());
// if ($class !== $this->getFullName()) {
// continue;
// }
// }
$methods[] = $method;
}
}
Expand Down Expand Up @@ -263,74 +260,4 @@ public function setTocName(string $tocName)
{
$this->tocName = $tocName;
}

public function validate(OutputInterface $output): bool
{
$valid = $this->validateXrefs($this->getContent(), $this->getFullname(), $output);
foreach ($this->getMethods() as $method) {
$valid = $this->validateXrefs($method->getContent(), $method->getFullname(), $output) && $valid;
}
foreach ($this->getConstants() as $constant) {
$valid = $this->validateXrefs($constant->getContent(), $constant->getFullname(), $output) && $valid;
}
return $valid;
}

/**
* Verifies that all {@see} tags are valid.
*/
private function validateXrefs(string $description, string $fullname, OutputInterface $output): bool
{
$valid = true;
preg_replace_callback(
'/<xref uid="([^ ]*)"/',
function ($matches) use ($output, &$valid, $fullname) {
if (0 === strpos($matches[1], 'http')) {
return; // Valid external reference
}
if ('\\' !== $matches[1][0] || substr_count($matches[1], '\Google\\') > 1) {
$output->writeln(sprintf(
'Xref not rendered propery in %s: %s',
$fullname,
$matches[1])
);
$valid = false;
return; // Invalid reference format
}
if (in_array(
$matches[1],
['\\' . AppEngineFlexFormatter::class, '\\' .AppEngineFlexFormatterV2::class])
) {
return; // We cannot run "class_exists" on these because they will throw a fatal error.
}
if (class_exists($matches[1]) || interface_exists($matches[1] || trait_exists($matches[1]))) {
return; // Valid class reference
}
if (false !== strpos($matches[1], '::')) {
if (false !== strpos($matches[1], '()')) {
list($class, $method) = explode('::', str_replace('()', '', $matches[1]));
if (method_exists($class, $method)) {
return; // Valid method reference
}
if ('Async' === substr($method, -5)) {
return; // Skip magic Async methods
}
} elseif (defined($matches[1])) {
return; // Valid constant reference
}
}

$output->writeln(sprintf('Invalid xref in %s: %s', $fullname, $matches[1]));

if ($this->isProtobufMessageClass() || $this->isProtobufEnumClass()) {
// Don't report missing references for protobuf messages (we cant control these)
return;
}
$valid = false;
},
$description
);

return $valid;
}
}

0 comments on commit b150303

Please sign in to comment.