Skip to content

Commit

Permalink
Update documentation generation (#591)
Browse files Browse the repository at this point in the history
* Update doc gen

* Update code parser to include inherit docs, fix internal links

* Update generation with heirarchy

* Update traits, extends, link to implementation

* Update external classes
michaelbausor authored and dwsupplee committed Jul 17, 2017
1 parent 5a437c8 commit 71de21d
Showing 4 changed files with 382 additions and 52 deletions.
7 changes: 3 additions & 4 deletions dev/src/DocGenerator/DocGenerator.php
Original file line number Diff line number Diff line change
@@ -19,8 +19,6 @@

use Google\Cloud\Dev\DocGenerator\Parser\CodeParser;
use Google\Cloud\Dev\DocGenerator\Parser\MarkdownParser;
use phpDocumentor\Reflection\DocBlock\Description;
use phpDocumentor\Reflection\DocBlock\Tag\SeeTag;
use phpDocumentor\Reflection\FileReflector;

/**
@@ -67,6 +65,7 @@ public function __construct(
*/
public function generate($basePath, $pretty)
{
$fileReflectorRegister = new ReflectorRegister();
foreach ($this->files as $file) {

if ($basePath) {
@@ -79,15 +78,15 @@ public function generate($basePath, $pretty)
$isPhp = strrpos($file, '.php') == strlen($file) - strlen('.php');

if ($isPhp) {
$fileReflector = new FileReflector($file);
$parser = new CodeParser(
$file,
$currentFile,
$fileReflector,
$fileReflectorRegister,
dirname($this->executionPath),
$this->componentId,
$this->manifestPath,
$this->release,
$basePath,
$this->isComponent
);
} else {
251 changes: 203 additions & 48 deletions dev/src/DocGenerator/Parser/CodeParser.php
Original file line number Diff line number Diff line change
@@ -18,12 +18,14 @@
namespace Google\Cloud\Dev\DocGenerator\Parser;

use Google\Cloud\Dev\DocBlockStripSpaces;
use Google\Cloud\Dev\DocGenerator\ReflectorRegister;
use Google\Cloud\Dev\GetComponentsTrait;
use phpDocumentor\Reflection\DocBlock;
use phpDocumentor\Reflection\DocBlock\Context;
use phpDocumentor\Reflection\ClassReflector;
use phpDocumentor\Reflection\DocBlock\Description;
use phpDocumentor\Reflection\DocBlock\Tag\SeeTag;
use phpDocumentor\Reflection\FileReflector;
use phpDocumentor\Reflection\InterfaceReflector;
use phpDocumentor\Reflection\TraitReflector;

class CodeParser implements ParserInterface
{
@@ -36,68 +38,156 @@ class CodeParser implements ParserInterface

private $path;
private $fileName;
private $reflector;
private $register;
private $markdown;
private $projectRoot;
private $externalTypes;
private $componentId;
private $manifestPath;
private $release;
private $isComponent;
private $basePath;

public function __construct(
$path,
$fileName,
FileReflector $reflector,
ReflectorRegister $register,
$projectRoot,
$componentId,
$manifestPath,
$release,
$basePath,
$isComponent = true
) {
$this->path = $path;
$this->fileName = $fileName;
$this->reflector = $reflector;
$this->register = $register;
$this->markdown = \Parsedown::instance();
$this->projectRoot = $projectRoot;
$this->externalTypes = json_decode(file_get_contents($this->projectRoot . '/docs/external-classes.json'), true);
$this->componentId = $componentId;
$this->manifestPath = $manifestPath;
$this->release = $release;
$this->basePath = $basePath;
$this->isComponent = $isComponent;
}

public function parse()
{
$this->reflector->process();
$reflector = $this->getReflector($this->reflector);
list($fileReflector, $reflector) = $this->register->getReflectorsFromFileName($this->path);

return $reflector
? $this->buildDocument($reflector)
? $this->buildDocument($fileReflector, $reflector)
: null;
}

private function getReflector($fileReflector)
private function buildInfo($fileReflector, $reflector)
{
if (isset($fileReflector->getClasses()[0])) {
return $fileReflector->getClasses()[0];
$classInfo = [
'methods' => [],
'traits' => [],
'parents' => [],
'interfaces' => [],
'interfaceMethods' => [],
];

$this->buildClassInfoRecursive($fileReflector, $reflector, $classInfo);

return $classInfo;
}

private function buildClassInfoRecursive($fileReflector, $reflector, &$classInfo)
{
if (is_null($fileReflector) || is_null($reflector)) {
return;
}

$methods = $this->buildMethodInfo($fileReflector, $reflector);

$isInternal = substr_compare($reflector->getName(), '\Google\Cloud', 0, 13) === 0;
if ($isInternal) {
$classInfo['methods'] += $methods;
}

foreach ($reflector->getTraits() as $trait) {
list($traitFileReflector, $traitReflector) = $this->register->getReflectors($trait);
$this->buildClassInfoRecursive($traitFileReflector, $traitReflector, $classInfo);
}

foreach ($reflector->getInterfaces() as $interface) {
list($interfaceFileReflector, $interfaceReflector) = $this->register->getReflectors($interface);
$this->buildInterfaceInfoRecursive($interfaceFileReflector, $interfaceReflector, $classInfo);
}

$parent = $reflector->getParentClass();
if (!empty($parent)) {
list($parentFileReflector, $parentReflector) = $this->register->getReflectors($parent);
$this->buildClassInfoRecursive($parentFileReflector, $parentReflector, $classInfo);
// Add $parent to array after calling getMethodsRecursive so that parents are correctly
// ordered
$classInfo['parents'][] = $parent;
}
}

private function buildInterfaceInfo($fileReflector, $reflector)
{
$classInfo = [
'interfaces' => [],
'interfaceMethods' => [],
];

$this->buildInterfaceInfoRecursive($fileReflector, $reflector, $classInfo);

return $classInfo;
}

private function buildInterfaceInfoRecursive($fileReflector, $reflector, &$classInfo)
{
if (is_null($fileReflector) || is_null($reflector)) {
return false;
}

if (isset($fileReflector->getInterfaces()[0])) {
return $fileReflector->getInterfaces()[0];
$isInternal = substr_compare($reflector->getName(), '\Google\Cloud', 0, 13) === 0;
if ($isInternal) {
$classInfo['interfaceMethods'] += $this->buildMethodInfo($fileReflector, $reflector);
}

if (isset($fileReflector->getTraits()[0])) {
return $fileReflector->getTraits()[0];
// Add parent interfaces to array before calling getMethodsRecursive to use PHP array
// ordering, so that parent interfaces are before more deeply nested interfaces
$classInfo['interfaces'] += $reflector->getParentInterfaces();
foreach ($reflector->getParentInterfaces() as $parentInterface) {
list($parentFileReflector, $parentReflector) = $this->register->getReflectors($parentInterface);
$this->buildInterfaceInfoRecursive($parentFileReflector, $parentReflector, $classInfo);
}
}

private function buildMethodInfo($fileReflector, $reflector)
{
$methods = [];
foreach ($reflector->getMethods() as $name => $method) {
if ($method->getVisibility() !== 'public') {
continue;
}
$methods[$name] = [
'methodReflector' => $method,
'source' => $this->getPath($fileReflector),
'container' => $reflector->getName(),
];
}
return $methods;
}

return null;
private function getPath($fileReflector)
{
$fileSplit = explode($this->basePath, trim($fileReflector->getFileName(), '/'));
return 'src/' . trim($fileSplit[1], '/');
}

private function buildDocument($reflector)
private function buildDocument($fileReflector, $reflector)
{
$name = $reflector->getShortName();
$id = substr($reflector->getName(), 14);
$fullName = $reflector->getName();
$id = substr($fullName, 14);
$id = str_replace('\\', '/', $id);
// @todo see if there is a better way to determine the type
$parts = explode('_', get_class($reflector->getNode()));
@@ -111,33 +201,63 @@ private function buildDocument($reflector)
return ($tag->getName() === 'method');
});

$magic = $this->buildMagicMethods($magicMethods, $name);
$magic = $this->buildMagicMethods($magicMethods, $fullName);
}

$methods = $reflector->getMethods();

if (is_null($docBlock)) {
throw new \Exception(sprintf('%s has no description', $reflector->getName()));
throw new \Exception(sprintf('%s has no description', $fullName));
}

$split = $this->splitDescription($docBlock->getText());

if ($this->isInterface($reflector)) {
$classInfo = $this->buildInterfaceInfo($fileReflector, $reflector);
$description = $this->buildInterfaceDescription($classInfo, $docBlock, $split['description']);
$methods = $this->buildMethods($classInfo['interfaceMethods'], $fullName);
} else {
$classInfo = $this->buildInfo($fileReflector, $reflector);
$description = $this->buildClassDescription($classInfo, $docBlock, $split['description']);
$methods = $this->buildMethods($classInfo['methods'], $fullName);
}

return [
'id' => strtolower($id),
'type' => strtolower($type),
'title' => $reflector->getNamespace() . '\\' . $name,
'name' => $name,
'description' => $this->buildDescription($docBlock, $split['description']),
'description' => $description,
'examples' => $this->buildExamples($split['examples']),
'resources' => $this->buildResources($docBlock->getTagsByName('see')),
'methods' => array_merge(
$this->buildMethods($methods, $name),
$magic
)
'methods' => array_merge($methods, $magic)
];
}

private function isInterface($reflector)
{
return ($reflector instanceof InterfaceReflector) &&
!($reflector instanceof ClassReflector);
}

private function buildDescription($docBlock, $content = null)
{
return $this->markdown->parse($this->buildDescriptionContent($docBlock, $content));
}

private function buildClassDescription($classInfo, $docBlock, $content = null)
{
$content = $this->buildDescriptionContent($docBlock, $content);
$content .= $this->buildInheritDoc($classInfo);
return $this->markdown->parse($content);
}

private function buildInterfaceDescription($classInfo, $docBlock, $content = null)
{
$content = $this->buildDescriptionContent($docBlock, $content);
$content .= $this->buildInterfaceInheritDoc($classInfo);
return $this->markdown->parse($content);
}

private function buildDescriptionContent($docBlock, $content = null)
{
if ($content === null) {
$content = $docBlock->getText();
@@ -149,28 +269,63 @@ private function buildDescription($docBlock, $content = null)
if (count($parsedContents) > 1) {
// convert inline {@see} tag to custom type link
foreach ($parsedContents as &$part) {
if ($part instanceof Seetag) {
$reference = $part->getReference();

if ($this->hasInternalType($reference)) {
$part = $this->buildLink($reference);
} elseif ($this->hasExternalType(trim(str_replace('@see', '', $part)))) {
$part = $this->buildExternalType(trim(str_replace('@see', '', $part)));
}
if ($part instanceof SeeTag) {
$part = $this->buildReference($part->getReference(), $part);
}
}

$content = implode('', $parsedContents);
}

$content = str_ireplace('[optional]', '', $content);
return $this->markdown->parse($content);
return $content;
}

private function buildReference($reference, $default = null)
{
if ($this->hasInternalType($reference)) {
return $this->buildLink($reference);
} elseif ($this->hasExternalType($reference)) {
return $this->buildExternalType($reference);
} else {
return isset($default) ? $default : $reference;
}
}

private function buildInheritDoc($classInfo)
{
$content = '';
if (count($classInfo['parents']) > 0) {
$content .= $this->implodeInheritDocLinks(" > ", $classInfo['parents'], "Extends");
}
if (count($classInfo['interfaces']) > 0) {
$content .= $this->implodeInheritDocLinks(", ", $classInfo['interfaces'], "Implements");
}

return $content;
}

private function buildInterfaceInheritDoc($classInfo)
{
$content = '';
if (count($classInfo['interfaces']) > 0) {
$content .= $this->implodeInheritDocLinks(", ", $classInfo['interfaces'], "Extends");
}

return $content;
}

private function implodeInheritDocLinks($glue, $pieces, $prefix)
{
return "\n\n$prefix " . implode($glue, array_map([$this, 'buildReference'], $pieces));
}

private function buildMethods($methods, $className)
{
$methodArray = [];
foreach ($methods as $name => $method) {
foreach ($methods as $name => $methodInfo) {
$method = $methodInfo['methodReflector'];

if ($method->getVisibility() !== 'public') {
continue;
}
@@ -188,7 +343,7 @@ private function buildMethods($methods, $className)
}
}

$methodArray[] = $this->buildMethod($method);
$methodArray[] = $this->buildMethod($method, $methodInfo, $className);
}

return $methodArray;
@@ -209,25 +364,29 @@ private function buildMagicMethods($magicMethods, $className)
return $methodArray;
}

private function buildMethod($method)
private function buildMethod($method, $methodInfo, $className)
{
$docBlock = $method->getDocBlock();
$fullDescription = $docBlock->getText();
$resources = $docBlock->getTagsByName('see');
$params = $docBlock->getTagsByName('param');
$exceptions = $docBlock->getTagsByName('throws');
$returns = $docBlock->getTagsByName('return');
$docText = '';
$examples = null;

$split = $this->splitDescription($fullDescription);

$description = $this->buildDescription($docBlock, $split['description']);
if ($methodInfo['container'] !== $className) {
$description .= "\n\nImplemented in " . $this->buildReference($methodInfo['container']);
}

return [
'id' => $method->getName(),
'type' => $method->getName() === '__construct' ? 'constructor' : 'instance',
'name' => $method->getName(),
'source' => $this->getSource() . '#L' . $method->getLineNumber(),
'description' => $this->buildDescription($docBlock, $split['description']),
'source' => $methodInfo['source'] . '#L' . $method->getLineNumber(),
'description' => $description,
'examples' => $this->buildExamples($split['examples']),
'resources' => $this->buildResources($resources),
'params' => $this->buildParams($params),
@@ -244,7 +403,6 @@ private function buildMagicMethod($magicMethod)
$params = $docBlock->getTagsByName('param');
$exceptions = $docBlock->getTagsByName('throws');
$returns = $docBlock->getTagsByName('return');
$docText = '';
$examples = null;

$parts = explode('Example:', $fullDescription);
@@ -482,10 +640,8 @@ private function handleTypes($types, array $aliases = [])
}

$type = sprintf(htmlentities('%s<%s>'), $matches[1], $matches[2]);
} elseif ($this->hasInternalType($type)) {
$type = $this->buildLink($type);
} elseif ($this->hasExternalType($type)) {
$type = $this->buildExternalType($type);
} else {
$type = $this->buildReference($type);
}

$res[] = $type;
@@ -508,7 +664,6 @@ private function resolveTypeAlias($type, array $aliases)
private function hasInternalType($type)
{
$type = trim($type, '\\');

if (substr_compare($type, 'Google\\Cloud', 0, 12) === 0) {
$matches = [];
preg_match(self::CLASS_TYPE_REGEX, $type, $matches);
119 changes: 119 additions & 0 deletions dev/src/DocGenerator/ReflectorRegister.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
<?php
/**
* Copyright 2017 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

namespace Google\Cloud\Dev\DocGenerator;

use Google\Cloud\Dev\DocGenerator\Parser\CodeParser;
use Google\Cloud\Dev\DocGenerator\Parser\MarkdownParser;
use phpDocumentor\Reflection\ClassReflector;
use phpDocumentor\Reflection\FileReflector;
use phpDocumentor\Reflection\InterfaceReflector;
use phpDocumentor\Reflection\TraitReflector;

class ReflectorRegister
{
private $fileReflectors = [];
private $nameFileMap = [];

/**
* @param string $name The name of a class, trait or interface
* @return array [$fileReflector, $reflector]
*/
public function getReflectors($name)
{
$fileName = $this->getFileForName($name);
return $this->getReflectorsFromFileName($fileName);
}

/**
* @param string $fileName The file name containing a single class, trait or interface
* @return array [$fileReflector, $reflector]
*/
public function getReflectorsFromFileName($fileName)
{
$fileReflector = $this->getFileReflector($fileName);
$reflector = $this->getReflectorFromFileReflector($fileReflector);
return [$fileReflector, $reflector];
}

/**
* @param $fileName
* @return FileReflector|null
*/
private function getFileReflector($fileName)
{
if (empty($fileName)) {
return null;
}

if (!isset($this->fileReflectors[$fileName])) {
$this->fileReflectors[$fileName] = new FileReflector($fileName);
$this->fileReflectors[$fileName]->process();
}
return $this->fileReflectors[$fileName];
}

/**
* @param FileReflector $fileReflector
* @return InterfaceReflector|ClassReflector|TraitReflector|null
*/
private function getReflectorFromFileReflector($fileReflector)
{
if (is_null($fileReflector)) {
return null;
}

if (isset($fileReflector->getClasses()[0])) {
return $fileReflector->getClasses()[0];
}

if (isset($fileReflector->getInterfaces()[0])) {
return $fileReflector->getInterfaces()[0];
}

if (isset($fileReflector->getTraits()[0])) {
return $fileReflector->getTraits()[0];
}

return null;
}

/**
* @param $name
* @return string|null
*/
private function getFileForName($name)
{
if (empty($name)) {
return null;
}
if (!array_key_exists($name, $this->nameFileMap)) {
if (class_exists($name) || interface_exists($name) || trait_exists($name)) {
$refClass = new \ReflectionClass((string)$name);
$fileName = $refClass->getFileName();
if (empty($fileName)) {
echo "Could not find file for $name\n";
}
} else {
$fileName = null;
echo "Could not find class, trait or interface for $name\n";
}
$this->nameFileMap[$name] = $fileName;
}
return $this->nameFileMap[$name];
}
}
57 changes: 57 additions & 0 deletions docs/external-classes.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,61 @@
[{
"name": "array",
"uri": "http://php.net/manual/en/language.types.array.php"
}, {
"name": "bool",
"uri": "http://php.net/manual/en/language.types.boolean.php"
}, {
"name": "Boolean",
"uri": "http://php.net/manual/en/language.types.boolean.php"
}, {
"name": "callable",
"uri": "http://php.net/manual/en/language.types.callable.php"
}, {
"name": "float",
"uri": "http://php.net/manual/en/language.types.float.php"
}, {
"name": "int",
"uri": "http://php.net/manual/en/language.types.integer.php"
}, {
"name": "mixed",
"uri": "http://php.net/manual/en/language.pseudo-types.php#language.types.mixed"
}, {
"name": "null",
"uri": "http://php.net/manual/en/language.types.null.php"
}, {
"name": "resource",
"uri": "http://php.net/manual/en/language.types.resource.php"
}, {
"name": "string",
"uri": "http://php.net/manual/en/language.types.string.php"
}, {
"name": "void",
"uri": "http://php.net/manual/en/language.pseudo-types.php#language.types.void"
}, {
"name": "DateTimeInterface",
"uri": "http://php.net/manual/en/class.datetimeinterface.php"
}, {
"name": "Exception",
"uri": "http://php.net/manual/en/class.exception.php"
}, {
"name": "Generator",
"uri": "http://php.net/manual/en/class.generator.php"
}, {
"name": "Iterator",
"uri": "http://php.net/manual/en/class.iterator.php"
}, {
"name": "JsonSerializable",
"uri": "http://php.net/manual/en/class.jsonserializable.php"
}, {
"name": "Psr\\Cache\\",
"uri": "https://github.com/php-fig/cache/blob/master/src/%s.php"
}, {
"name": "Psr\\Log\\",
"uri": "https://github.com/php-fig/log/blob/master/Psr/Log/%s.php"
}, {
"name": "Psr\\Http\\Message\\",
"uri": "https://github.com/php-fig/http-message/blob/master/src/%s.php"
}, {
"name": "Google\\Auth\\",
"uri": "https://github.com/google/google-auth-library-php/blob/master/src/%s.php"
}, {

0 comments on commit 71de21d

Please sign in to comment.