1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-12-01 19:22:42 +01:00

Generate some amount of PHP class documentation

Summary:
Ref T988. This brings the class/interface atomizer over. A lot of parts of this are still varying degrees of very-rough, but most of the data ends up in approximatley the right place.

ALSO: PROGRESS BARS

Test Plan: See screenshots.

Reviewers: btrahan

Reviewed By: btrahan

CC: aran

Maniphest Tasks: T988

Differential Revision: https://secure.phabricator.com/D6817
This commit is contained in:
epriestley 2013-08-28 09:54:39 -07:00
parent 9acddd7722
commit 41ac06959e
11 changed files with 342 additions and 20 deletions

View file

@ -0,0 +1,5 @@
ALTER TABLE {$NAMESPACE}_diviner.diviner_livesymbol
ADD nodeHash VARCHAR(64) COLLATE utf8_bin;
ALTER TABLE {$NAMESPACE}_diviner.diviner_livesymbol
ADD UNIQUE KEY (nodeHash);

View file

@ -4,6 +4,7 @@ final class DivinerAtom {
const TYPE_FILE = 'file'; const TYPE_FILE = 'file';
const TYPE_ARTICLE = 'article'; const TYPE_ARTICLE = 'article';
const TYPE_METHOD = 'method';
private $type; private $type;
private $name; private $name;
@ -181,6 +182,10 @@ final class DivinerAtom {
return mpull($this->extends, 'toDictionary'); return mpull($this->extends, 'toDictionary');
} }
public function getExtends() {
return $this->extends;
}
public function getHash() { public function getHash() {
if ($this->hash) { if ($this->hash) {
return $this->hash; return $this->hash;
@ -326,6 +331,10 @@ final class DivinerAtom {
$atom->addChildHash($child); $atom->addChildHash($child);
} }
foreach (idx($dictionary, 'extends', array()) as $extends) {
$atom->addExtends(DivinerAtomRef::newFromDictionary($extends));
}
return $atom; return $atom;
} }

View file

@ -31,6 +31,90 @@ final class DivinerPHPAtomizer extends DivinerAtomizer {
$atoms[] = $atom; $atoms[] = $atom;
} }
$class_types = array(
'class' => 'n_CLASS_DECLARATION',
'interface' => 'n_INTERFACE_DECLARATION',
);
foreach ($class_types as $atom_type => $node_type) {
$class_decls = $root->selectDescendantsOfType($node_type);
foreach ($class_decls as $class) {
$name = $class->getChildByIndex(1, 'n_CLASS_NAME');
$atom = id(new DivinerAtom())
->setType($atom_type)
->setName($name->getConcreteString())
->setFile($file_name)
->setLine($class->getLineNumber());
// If this exists, it is n_EXTENDS_LIST.
$extends = $class->getChildByIndex(2);
$extends_class = $extends->selectDescendantsOfType('n_CLASS_NAME');
foreach ($extends_class as $parent_class) {
$atom->addExtends(
DivinerAtomRef::newFromDictionary(
array(
'type' => 'class',
'name' => $parent_class->getConcreteString(),
)));
}
// If this exists, it is n_IMPLEMENTS_LIST.
$implements = $class->getChildByIndex(3);
$iface_names = $implements->selectDescendantsOfType('n_CLASS_NAME');
foreach ($iface_names as $iface_name) {
$atom->addExtends(
DivinerAtomRef::newFromDictionary(
array(
'type' => 'interface',
'name' => $iface_name->getConcreteString(),
)));
}
$this->findAtomDocblock($atom, $class);
$methods = $class->selectDescendantsOfType('n_METHOD_DECLARATION');
foreach ($methods as $method) {
$matom = id(new DivinerAtom())
->setType('method');
$this->findAtomDocblock($matom, $method);
$attribute_list = $method->getChildByIndex(0);
$attributes = $attribute_list->selectDescendantsOfType('n_STRING');
if ($attributes) {
foreach ($attributes as $attribute) {
$attr = strtolower($attribute->getConcreteString());
switch ($attr) {
case 'static':
$matom->setProperty($attr, true);
break;
case 'public':
case 'protected':
case 'private':
$matom->setProperty('access', $attr);
break;
}
}
} else {
$matom->setProperty('access', 'public');
}
$this->parseParams($matom, $method);
$matom->setName($method->getChildByIndex(2)->getConcreteString());
$matom->setLine($method->getLineNumber());
$matom->setFile($file_name);
$this->parseReturnType($matom, $method);
$atom->addChild($matom);
$atoms[] = $matom;
}
$atoms[] = $atom;
}
}
return $atoms; return $atoms;
} }

View file

@ -46,6 +46,7 @@ final class DivinerAtomController extends DivinerController {
->withContexts(array($this->atomContext)) ->withContexts(array($this->atomContext))
->withIndexes(array($this->atomIndex)) ->withIndexes(array($this->atomIndex))
->needAtoms(true) ->needAtoms(true)
->needExtends(true)
->executeOne(); ->executeOne();
if (!$symbol) { if (!$symbol) {
@ -54,6 +55,19 @@ final class DivinerAtomController extends DivinerController {
$atom = $symbol->getAtom(); $atom = $symbol->getAtom();
$extends = $atom->getExtends();
$child_hashes = $atom->getChildHashes();
if ($child_hashes) {
$children = id(new DivinerAtomQuery())
->setViewer($viewer)
->withIncludeUndocumentable(true)
->withNodeHashes($child_hashes)
->execute();
} else {
$children = array();
}
$crumbs = $this->buildApplicationCrumbs(); $crumbs = $this->buildApplicationCrumbs();
$crumbs->addCrumb( $crumbs->addCrumb(
@ -90,6 +104,9 @@ final class DivinerAtomController extends DivinerController {
pht('Defined'), pht('Defined'),
$atom->getFile().':'.$atom->getLine()); $atom->getFile().':'.$atom->getLine());
$this->buildExtendsAndImplements($properties, $symbol);
$warnings = $atom->getWarnings(); $warnings = $atom->getWarnings();
if ($warnings) { if ($warnings) {
$warnings = id(new AphrontErrorView()) $warnings = id(new AphrontErrorView())
@ -157,6 +174,17 @@ final class DivinerAtomController extends DivinerController {
->setReturn($return)); ->setReturn($return));
} }
if ($children) {
$document->appendChild(
id(new PhabricatorHeaderView())
->setHeader(pht('Methods')));
foreach ($children as $child) {
$document->appendChild(
id(new PhabricatorHeaderView())
->setHeader($child->getName()));
}
}
if ($toc) { if ($toc) {
$side = new PHUIListView(); $side = new PHUIListView();
$side->addMenuItem( $side->addMenuItem(
@ -188,4 +216,69 @@ final class DivinerAtomController extends DivinerController {
return phutil_utf8_ucwords($name); return phutil_utf8_ucwords($name);
} }
private function buildExtendsAndImplements(
PhabricatorPropertyListView $view,
DivinerLiveSymbol $symbol) {
$lineage = $this->getExtendsLineage($symbol);
if ($lineage) {
$lineage = mpull($lineage, 'getName');
$lineage = implode(' > ', $lineage);
$view->addProperty(pht('Extends'), $lineage);
}
$implements = $this->getImplementsLineage($symbol);
if ($implements) {
$items = array();
foreach ($implements as $spec) {
$via = $spec['via'];
$iface = $spec['interface'];
if ($via == $symbol) {
$items[] = $iface->getName();
} else {
$items[] = $iface->getName().' (via '.$via->getName().')';
}
}
$view->addProperty(
pht('Implements'),
phutil_implode_html(phutil_tag('br'), $items));
}
}
private function getExtendsLineage(DivinerLiveSymbol $symbol) {
foreach ($symbol->getExtends() as $extends) {
if ($extends->getType() == 'class') {
$lineage = $this->getExtendsLineage($extends);
$lineage[] = $extends;
return $lineage;
}
}
return array();
}
private function getImplementsLineage(DivinerLiveSymbol $symbol) {
$implements = array();
// Do these first so we get interfaces ordered from most to least specific.
foreach ($symbol->getExtends() as $extends) {
if ($extends->getType() == 'interface') {
$implements[$extends->getName()] = array(
'interface' => $extends,
'via' => $symbol,
);
}
}
// Now do parent interfaces.
foreach ($symbol->getExtends() as $extends) {
if ($extends->getType() == 'class') {
$implements += $this->getImplementsLineage($extends);
}
}
return $implements;
}
} }

View file

@ -63,9 +63,11 @@ final class DivinerLivePublisher extends DivinerPublisher {
} }
protected function loadAllPublishedHashes() { protected function loadAllPublishedHashes() {
$symbols = id(new DivinerLiveSymbol())->loadAllWhere( $symbols = id(new DivinerAtomQuery())
'bookPHID = %s AND graphHash IS NOT NULL', ->setViewer(PhabricatorUser::getOmnipotentUser())
$this->loadBook()->getPHID()); ->withBookPHIDs(array($this->loadBook()->getPHID()))
->withIncludeUndocumentable(true)
->execute();
return mpull($symbols, 'getGraphHash'); return mpull($symbols, 'getGraphHash');
} }
@ -84,7 +86,8 @@ final class DivinerLivePublisher extends DivinerPublisher {
foreach (PhabricatorLiskDAO::chunkSQL($strings, ', ') as $chunk) { foreach (PhabricatorLiskDAO::chunkSQL($strings, ', ') as $chunk) {
queryfx( queryfx(
$conn_w, $conn_w,
'UPDATE %T SET graphHash = NULL WHERE graphHash IN (%Q)', 'UPDATE %T SET graphHash = NULL, nodeHash = NULL
WHERE graphHash IN (%Q)',
$symbol_table->getTableName(), $symbol_table->getTableName(),
$chunk); $chunk);
} }
@ -111,7 +114,8 @@ final class DivinerLivePublisher extends DivinerPublisher {
->setGraphHash($hash) ->setGraphHash($hash)
->setIsDocumentable((int)$is_documentable) ->setIsDocumentable((int)$is_documentable)
->setTitle($ref->getTitle()) ->setTitle($ref->getTitle())
->setGroupName($ref->getGroup()); ->setGroupName($ref->getGroup())
->setNodeHash($atom->getHash());
if ($is_documentable) { if ($is_documentable) {
$renderer = $this->getRenderer(); $renderer = $this->getRenderer();

View file

@ -142,6 +142,7 @@ abstract class DivinerPublisher {
protected function shouldGenerateDocumentForAtom(DivinerAtom $atom) { protected function shouldGenerateDocumentForAtom(DivinerAtom $atom) {
switch ($atom->getType()) { switch ($atom->getType()) {
case DivinerAtom::TYPE_METHOD:
case DivinerAtom::TYPE_FILE: case DivinerAtom::TYPE_FILE:
return false; return false;
case DivinerAtom::TYPE_ARTICLE: case DivinerAtom::TYPE_ARTICLE:

View file

@ -12,8 +12,10 @@ final class DivinerAtomQuery
private $indexes; private $indexes;
private $includeUndocumentable; private $includeUndocumentable;
private $includeGhosts; private $includeGhosts;
private $nodeHashes;
private $needAtoms; private $needAtoms;
private $needExtends;
public function withIDs(array $ids) { public function withIDs(array $ids) {
$this->ids = $ids; $this->ids = $ids;
@ -50,11 +52,17 @@ final class DivinerAtomQuery
return $this; return $this;
} }
public function withNodeHashes(array $hashes) {
$this->nodeHashes = $hashes;
return $this;
}
public function needAtoms($need) { public function needAtoms($need) {
$this->needAtoms = $need; $this->needAtoms = $need;
return $this; return $this;
} }
/** /**
* Include "ghosts", which are symbols which used to exist but do not exist * Include "ghosts", which are symbols which used to exist but do not exist
* currently (for example, a function which existed in an older version of * currently (for example, a function which existed in an older version of
@ -78,6 +86,12 @@ final class DivinerAtomQuery
return $this; return $this;
} }
public function needExtends($need) {
$this->needExtends = $need;
return $this;
}
public function withIncludeUndocumentable($include) { public function withIncludeUndocumentable($include) {
$this->includeUndocumentable = $include; $this->includeUndocumentable = $include;
return $this; return $this;
@ -116,6 +130,8 @@ final class DivinerAtomQuery
$atom->attachBook($book); $atom->attachBook($book);
} }
$need_atoms = $this->needAtoms;
if ($this->needAtoms) { if ($this->needAtoms) {
$atom_data = id(new DivinerLiveAtom())->loadAllWhere( $atom_data = id(new DivinerLiveAtom())->loadAllWhere(
'symbolPHID IN (%Ls)', 'symbolPHID IN (%Ls)',
@ -132,6 +148,80 @@ final class DivinerAtomQuery
} }
} }
// Load all of the symbols this symbol extends, recursively. Commonly,
// this means all the ancestor classes and interfaces it extends and
// implements.
if ($this->needExtends) {
// First, load all the matching symbols by name. This does 99% of the
// work in most cases, assuming things are named at all reasonably.
$names = array();
foreach ($atoms as $atom) {
foreach ($atom->getAtom()->getExtends() as $xref) {
$names[] = $xref->getName();
}
}
if ($names) {
$xatoms = id(new DivinerAtomQuery())
->setViewer($this->getViewer())
->withNames($names)
->needExtends(true)
->needAtoms(true)
->execute();
$xatoms = mgroup($xatoms, 'getName', 'getType', 'getBookPHID');
} else {
$xatoms = array();
}
foreach ($atoms as $atom) {
$alang = $atom->getAtom()->getLanguage();
$extends = array();
foreach ($atom->getAtom()->getExtends() as $xref) {
// If there are no symbols of the matching name and type, we can't
// resolve this.
if (empty($xatoms[$xref->getName()][$xref->getType()])) {
continue;
}
// If we found matches in the same documentation book, prefer them
// over other matches. Otherwise, look at all the the matches.
$matches = $xatoms[$xref->getName()][$xref->getType()];
if (isset($matches[$atom->getBookPHID()])) {
$maybe = $matches[$atom->getBookPHID()];
} else {
$maybe = array_mergev($matches);
}
if (!$maybe) {
continue;
}
// Filter out matches in a different language, since, e.g., PHP
// classes can not implement JS classes.
$same_lang = array();
foreach ($maybe as $xatom) {
if ($xatom->getAtom()->getLanguage() == $alang) {
$same_lang[] = $xatom;
}
}
if (!$same_lang) {
continue;
}
// If we have duplicates remaining, just pick the first one. There's
// nothing more we can do to figure out which is the real one.
$extends[] = head($same_lang);
}
$atom->attachExtends($extends);
}
}
return $atoms; return $atoms;
} }
@ -220,6 +310,13 @@ final class DivinerAtomQuery
'graphHash IS NOT NULL'); 'graphHash IS NOT NULL');
} }
if ($this->nodeHashes) {
$where[] = qsprintf(
$conn_r,
'nodeHash IN (%Ls)',
$this->nodeHashes);
}
$where[] = $this->buildPagingClause($conn_r); $where[] = $this->buildPagingClause($conn_r);
return $this->formatWhereClause($where); return $this->formatWhereClause($where);

View file

@ -11,14 +11,16 @@ final class DivinerLiveSymbol extends DivinerDAO
protected $atomIndex; protected $atomIndex;
protected $graphHash; protected $graphHash;
protected $identityHash; protected $identityHash;
protected $nodeHash;
protected $title; protected $title;
protected $groupName; protected $groupName;
protected $summary; protected $summary;
protected $isDocumentable = 0; protected $isDocumentable = 0;
private $book; private $book = self::ATTACHABLE;
private $atom; private $atom = self::ATTACHABLE;
private $extends = self::ATTACHABLE;
public function getConfiguration() { public function getConfiguration() {
return array( return array(
@ -33,10 +35,7 @@ final class DivinerLiveSymbol extends DivinerDAO
} }
public function getBook() { public function getBook() {
if ($this->book === null) { return $this->assertAttached($this->book);
throw new Exception("Call attachBook() before getBook()!");
}
return $this->book;
} }
public function attachBook(DivinerLiveBook $book) { public function attachBook(DivinerLiveBook $book) {
@ -45,10 +44,7 @@ final class DivinerLiveSymbol extends DivinerDAO
} }
public function getAtom() { public function getAtom() {
if ($this->atom === null) { return $this->assertAttached($this->atom);
throw new Exception("Call attachAtom() before getAtom()!");
}
return $this->atom;
} }
public function attachAtom(DivinerLiveAtom $atom) { public function attachAtom(DivinerLiveAtom $atom) {
@ -109,6 +105,16 @@ final class DivinerLiveSymbol extends DivinerDAO
return $title; return $title;
} }
public function attachExtends(array $extends) {
assert_instances_of($extends, 'DivinerLiveSymbol');
$this->extends = $extends;
return $this;
}
public function getExtends() {
return $this->assertAttached($this->extends);
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */ /* -( PhabricatorPolicyInterface )----------------------------------------- */

View file

@ -86,8 +86,10 @@ final class DivinerAtomizeWorkflow extends DivinerWorkflow {
$atoms = $atomizer->atomize($file, $data); $atoms = $atomizer->atomize($file, $data);
foreach ($atoms as $atom) { foreach ($atoms as $atom) {
if (!$atom->getParentHash()) {
$file_atom->addChild($atom); $file_atom->addChild($atom);
} }
}
$all_atoms[] = $atoms; $all_atoms[] = $atoms;
} }

View file

@ -34,8 +34,7 @@ final class DivinerGenerateWorkflow extends DivinerWorkflow {
protected function log($message) { protected function log($message) {
$console = PhutilConsole::getConsole(); $console = PhutilConsole::getConsole();
$console->getServer()->setEnableLog(true); $console->writeErr($message."\n");
$console->writeLog($message."\n");
} }
public function execute(PhutilArgumentParser $args) { public function execute(PhutilArgumentParser $args) {
@ -154,6 +153,9 @@ final class DivinerGenerateWorkflow extends DivinerWorkflow {
$this->log(pht('Found %d file(s) to atomize.', count($file_atomizers))); $this->log(pht('Found %d file(s) to atomize.', count($file_atomizers)));
$futures = $this->buildAtomizerFutures($file_atomizers); $futures = $this->buildAtomizerFutures($file_atomizers);
$this->log(pht('Atomizing %d file(s).', count($file_atomizers)));
if ($futures) { if ($futures) {
$this->resolveAtomizerFutures($futures, $file_hashes); $this->resolveAtomizerFutures($futures, $file_hashes);
$this->log(pht("Atomization complete.")); $this->log(pht("Atomization complete."));
@ -278,21 +280,31 @@ final class DivinerGenerateWorkflow extends DivinerWorkflow {
$atomizers[$atomizer][] = $file; $atomizers[$atomizer][] = $file;
} }
$root = dirname(phutil_get_library_root('phabricator'));
$config_root = $this->getConfig('root');
$bar = id(new PhutilConsoleProgressBar())
->setTotal(count($file_atomizers));
$futures = array(); $futures = array();
foreach ($atomizers as $class => $files) { foreach ($atomizers as $class => $files) {
foreach (array_chunk($files, 32) as $chunk) { foreach (array_chunk($files, 32) as $chunk) {
$future = new ExecFuture( $future = new ExecFuture(
'%s atomize --ugly --book %s --atomizer %s -- %Ls', '%s atomize --ugly --book %s --atomizer %s -- %Ls',
dirname(phutil_get_library_root('phabricator')).'/bin/diviner', $root.'/bin/diviner',
$this->getBookConfigPath(), $this->getBookConfigPath(),
$class, $class,
$chunk); $chunk);
$future->setCWD($this->getConfig('root')); $future->setCWD($config_root);
$futures[] = $future; $futures[] = $future;
$bar->update(count($chunk));
} }
} }
$bar->done();
return $futures; return $futures;
} }
@ -300,6 +312,8 @@ final class DivinerGenerateWorkflow extends DivinerWorkflow {
assert_instances_of($futures, 'Future'); assert_instances_of($futures, 'Future');
$atom_cache = $this->getAtomCache(); $atom_cache = $this->getAtomCache();
$bar = id(new PhutilConsoleProgressBar())
->setTotal(count($futures));
foreach (Futures($futures)->limit(4) as $key => $future) { foreach (Futures($futures)->limit(4) as $key => $future) {
$atoms = $future->resolveJSON(); $atoms = $future->resolveJSON();
@ -310,7 +324,10 @@ final class DivinerGenerateWorkflow extends DivinerWorkflow {
} }
$atom_cache->addAtom($atom); $atom_cache->addAtom($atom);
} }
$bar->update(1);
} }
$bar->done();
} }

View file

@ -1555,6 +1555,10 @@ final class PhabricatorBuiltinPatchList extends PhabricatorSQLPatchList {
'type' => 'sql', 'type' => 'sql',
'name' => $this->getPatchPath('20130820.releephxactions.sql'), 'name' => $this->getPatchPath('20130820.releephxactions.sql'),
), ),
'20130826.divinernode.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('20130826.divinernode.sql'),
),
); );
} }
} }