diff --git a/src/applications/diviner/controller/DivinerAtomController.php b/src/applications/diviner/controller/DivinerAtomController.php index d381a53d03..d923d3d641 100644 --- a/src/applications/diviner/controller/DivinerAtomController.php +++ b/src/applications/diviner/controller/DivinerAtomController.php @@ -47,6 +47,7 @@ final class DivinerAtomController extends DivinerController { ->withIndexes(array($this->atomIndex)) ->needAtoms(true) ->needExtends(true) + ->needChildren(true) ->executeOne(); if (!$symbol) { @@ -54,20 +55,6 @@ final class DivinerAtomController extends DivinerController { } $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->addCrumb( @@ -170,14 +157,63 @@ final class DivinerAtomController extends DivinerController { ->setReturn($return)); } - if ($children) { + $methods = $this->composeMethods($symbol); + if ($methods) { + + $tasks = $this->composeTasks($symbol); + + if ($tasks) { + $methods_by_task = igroup($methods, 'task'); + + $document->appendChild( + id(new PhabricatorHeaderView()) + ->setHeader(pht('Tasks'))); + + if (isset($methods_by_task[''])) { + $tasks[''] = array( + 'name' => '', + 'title' => pht('Other Methods'), + 'defined' => $symbol, + ); + } + + foreach ($tasks as $spec) { + $document->appendChild( + id(new PhabricatorHeaderView()) + ->setHeader($spec['title'])); + + $task_methods = idx($methods_by_task, $spec['name'], array()); + if ($task_methods) { + $document->appendChild(hsprintf('')); + } else { + $document->appendChild("No methods for this task."); + } + } + } + $document->appendChild( id(new PhabricatorHeaderView()) ->setHeader(pht('Methods'))); - foreach ($children as $child) { - $document->appendChild( - id(new PhabricatorHeaderView()) - ->setHeader($child->getName())); + foreach ($methods as $spec) { + $method_header = id(new PhabricatorHeaderView()) + ->setHeader(last($spec['atoms'])->getName()); + + $inherited = $spec['inherited']; + if ($inherited) { + $method_header->addTag( + id(new PhabricatorTagView()) + ->setType(PhabricatorTagView::TYPE_STATE) + ->setBackgroundColor(PhabricatorTagView::COLOR_GREY) + ->setName(pht('Inherited'))); + } + + $document->appendChild($method_header); } } @@ -301,4 +337,88 @@ final class DivinerAtomController extends DivinerController { $view->addProperty(pht('Defined'), $defined); } + private function composeMethods(DivinerLiveSymbol $symbol) { + $methods = $this->findMethods($symbol); + if (!$methods) { + return $methods; + } + + foreach ($methods as $name => $method) { + // Check for "@task" on each parent, to find the most recently declared + // "@task". + $task = null; + foreach ($method['atoms'] as $key => $method_symbol) { + $atom = $method_symbol->getAtom(); + if ($atom->getDocblockMetaValue('task')) { + $task = $atom->getDocblockMetaValue('task'); + } + } + $methods[$name]['task'] = $task; + + // Set 'inherited' if this atom has no implementation of the method. + if (last($method['implementations']) !== $symbol) { + $methods[$name]['inherited'] = true; + } else { + $methods[$name]['inherited'] = false; + } + } + + return $methods; + } + + private function findMethods(DivinerLiveSymbol $symbol) { + $child_specs = array(); + foreach ($symbol->getExtends() as $extends) { + if ($extends->getType() == DivinerAtom::TYPE_CLASS) { + $child_specs = $this->findMethods($extends); + } + } + + foreach ($symbol->getChildren() as $child) { + if ($child->getType() == DivinerAtom::TYPE_METHOD) { + $name = $child->getName(); + if (isset($child_specs[$name])) { + $child_specs[$name]['atoms'][] = $child; + $child_specs[$name]['implementations'][] = $symbol; + } else { + $child_specs[$name] = array( + 'atoms' => array($child), + 'defined' => $symbol, + 'implementations' => array($symbol), + ); + } + } + } + + return $child_specs; + } + + private function composeTasks(DivinerLiveSymbol $symbol) { + $extends_task_specs = array(); + foreach ($symbol->getExtends() as $extends) { + $extends_task_specs += $this->composeTasks($extends); + } + + $task_specs = array(); + + $tasks = $symbol->getAtom()->getDocblockMetaValue('task'); + if (strlen($tasks)) { + $tasks = phutil_split_lines($tasks, $retain_endings = false); + + foreach ($tasks as $task) { + list($name, $title) = explode(' ', $task, 2); + $name = trim($name); + $title = trim($title); + + $task_specs[$name] = array( + 'name' => $name, + 'title' => $title, + 'defined' => $symbol, + ); + } + } + + return $task_specs + $extends_task_specs; + } + } diff --git a/src/applications/diviner/publisher/DivinerLivePublisher.php b/src/applications/diviner/publisher/DivinerLivePublisher.php index 164aa6f548..62004da063 100644 --- a/src/applications/diviner/publisher/DivinerLivePublisher.php +++ b/src/applications/diviner/publisher/DivinerLivePublisher.php @@ -126,12 +126,19 @@ final class DivinerLivePublisher extends DivinerPublisher { $symbol->save(); - if ($is_documentable) { - $storage = $this->loadAtomStorageForSymbol($symbol) - ->setAtomData($atom->toDictionary()) - ->setContent(null) - ->save(); - } + // TODO: We probably need a finer-grained sense of what "documentable" + // atoms are. Neither files nor methods are currently considered + // documentable, but for different reasons: files appear nowhere, while + // methods just don't appear at the top level. These are probably + // separate concepts. Since we need atoms in order to build method + // documentation, we insert them here. This also means we insert files, + // which are unnecessary and unused. Make sure this makes sense, but then + // probably introduce separate "isTopLevel" and "isDocumentable" flags? + + $storage = $this->loadAtomStorageForSymbol($symbol) + ->setAtomData($atom->toDictionary()) + ->setContent(null) + ->save(); } } diff --git a/src/applications/diviner/query/DivinerAtomQuery.php b/src/applications/diviner/query/DivinerAtomQuery.php index 06e6fda1e3..662bc834d8 100644 --- a/src/applications/diviner/query/DivinerAtomQuery.php +++ b/src/applications/diviner/query/DivinerAtomQuery.php @@ -16,6 +16,7 @@ final class DivinerAtomQuery private $needAtoms; private $needExtends; + private $needChildren; public function withIDs(array $ids) { $this->ids = $ids; @@ -62,6 +63,11 @@ final class DivinerAtomQuery return $this; } + public function needChildren($need) { + $this->needChildren = $need; + return $this; + } + /** * Include "ghosts", which are symbols which used to exist but do not exist @@ -130,8 +136,6 @@ final class DivinerAtomQuery $atom->attachBook($book); } - $need_atoms = $this->needAtoms; - if ($this->needAtoms) { $atom_data = id(new DivinerLiveAtom())->loadAllWhere( 'symbolPHID IN (%Ls)', @@ -170,6 +174,7 @@ final class DivinerAtomQuery ->withNames($names) ->needExtends(true) ->needAtoms(true) + ->needChildren($this->needChildren) ->execute(); $xatoms = mgroup($xatoms, 'getName', 'getType', 'getBookPHID'); } else { @@ -222,6 +227,25 @@ final class DivinerAtomQuery } } + if ($this->needChildren) { + $child_hashes = $this->getAllChildHashes($atoms, $this->needExtends); + + if ($child_hashes) { + $children = id(new DivinerAtomQuery()) + ->setViewer($this->getViewer()) + ->withIncludeUndocumentable(true) + ->withNodeHashes($child_hashes) + ->needAtoms($this->needAtoms) + ->execute(); + + $children = mpull($children, null, 'getNodeHash'); + } else { + $children = array(); + } + + $this->attachAllChildren($atoms, $children, $this->needExtends); + } + return $atoms; } @@ -322,4 +346,63 @@ final class DivinerAtomQuery return $this->formatWhereClause($where); } + + /** + * Walk a list of atoms and collect all the node hashes of the atoms' + * children. When recursing, also walk up the tree and collect children of + * atoms they extend. + * + * @param list List of symbols to collect child hashes of. + * @param bool True to collect children of extended atoms, + * as well. + * @return map Hashes of atoms' children. + */ + private function getAllChildHashes(array $symbols, $recurse_up) { + assert_instances_of($symbols, 'DivinerLiveSymbol'); + + $hashes = array(); + foreach ($symbols as $symbol) { + foreach ($symbol->getAtom()->getChildHashes() as $hash) { + $hashes[$hash] = $hash; + } + if ($recurse_up) { + $hashes += $this->getAllChildHashes($symbol->getExtends(), true); + } + } + + return $hashes; + } + + + /** + * Attach child atoms to existing atoms. In recursive mode, also attach child + * atoms to atoms that these atoms extend. + * + * @param list List of symbols to attach childeren to. + * @param map Map of symbols, keyed by node hash. + * @param bool True to attach children to extended atoms, as well. + * @return void + */ + private function attachAllChildren( + array $symbols, + array $children, + $recurse_up) { + + assert_instances_of($symbols, 'DivinerLiveSymbol'); + assert_instances_of($children, 'DivinerLiveSymbol'); + + foreach ($symbols as $symbol) { + $symbol_children = array(); + foreach ($symbol->getAtom()->getChildHashes() as $hash) { + if (isset($children[$hash])) { + $symbol_children[] = $children[$hash]; + } + } + $symbol->attachChildren($symbol_children); + if ($recurse_up) { + $this->attachAllChildren($symbol->getExtends(), $children, true); + } + } + } + } diff --git a/src/applications/diviner/storage/DivinerLiveSymbol.php b/src/applications/diviner/storage/DivinerLiveSymbol.php index 8a8a4bb79f..51bad9b94e 100644 --- a/src/applications/diviner/storage/DivinerLiveSymbol.php +++ b/src/applications/diviner/storage/DivinerLiveSymbol.php @@ -21,6 +21,7 @@ final class DivinerLiveSymbol extends DivinerDAO private $book = self::ATTACHABLE; private $atom = self::ATTACHABLE; private $extends = self::ATTACHABLE; + private $children = self::ATTACHABLE; public function getConfiguration() { return array( @@ -115,6 +116,16 @@ final class DivinerLiveSymbol extends DivinerDAO return $this->assertAttached($this->extends); } + public function attachChildren(array $children) { + assert_instances_of($children, 'DivinerLiveSymbol'); + $this->children = $children; + return $this; + } + + public function getChildren() { + return $this->assertAttached($this->children); + } + /* -( PhabricatorPolicyInterface )----------------------------------------- */