/src/applications/diviner/controller/DivinerAtomController.php
PHP | 674 lines | 546 code | 111 blank | 17 comment | 63 complexity | be4e178dba572dd3c0c1864094f5651b MD5 | raw file
Possible License(s): Apache-2.0, LGPL-2.0, LGPL-3.0, MIT, MPL-2.0-no-copyleft-exception, BSD-3-Clause
- <?php
- final class DivinerAtomController extends DivinerController {
- private $bookName;
- private $atomType;
- private $atomName;
- private $atomContext;
- private $atomIndex;
- public function shouldAllowPublic() {
- return true;
- }
- public function willProcessRequest(array $data) {
- $this->bookName = $data['book'];
- $this->atomType = $data['type'];
- $this->atomName = $data['name'];
- $this->atomContext = nonempty(idx($data, 'context'), null);
- $this->atomIndex = nonempty(idx($data, 'index'), null);
- }
- public function processRequest() {
- $request = $this->getRequest();
- $viewer = $request->getUser();
- require_celerity_resource('diviner-shared-css');
- $book = id(new DivinerBookQuery())
- ->setViewer($viewer)
- ->withNames(array($this->bookName))
- ->executeOne();
- if (!$book) {
- return new Aphront404Response();
- }
- // TODO: This query won't load ghosts, because they'll fail `needAtoms()`.
- // Instead, we might want to load ghosts and render a message like
- // "this thing existed in an older version, but no longer does", especially
- // if we add content like comments.
- $symbol = id(new DivinerAtomQuery())
- ->setViewer($viewer)
- ->withBookPHIDs(array($book->getPHID()))
- ->withTypes(array($this->atomType))
- ->withNames(array($this->atomName))
- ->withContexts(array($this->atomContext))
- ->withIndexes(array($this->atomIndex))
- ->needAtoms(true)
- ->needExtends(true)
- ->needChildren(true)
- ->executeOne();
- if (!$symbol) {
- return new Aphront404Response();
- }
- $atom = $symbol->getAtom();
- $crumbs = $this->buildApplicationCrumbs();
- $crumbs->addTextCrumb(
- $book->getShortTitle(),
- '/book/'.$book->getName().'/');
- $atom_short_title = $atom->getDocblockMetaValue(
- 'short',
- $symbol->getTitle());
- $crumbs->addTextCrumb($atom_short_title);
- $header = id(new PHUIHeaderView())
- ->setHeader($this->renderFullSignature($symbol))
- ->addTag(
- id(new PHUITagView())
- ->setType(PHUITagView::TYPE_STATE)
- ->setBackgroundColor(PHUITagView::COLOR_BLUE)
- ->setName(DivinerAtom::getAtomTypeNameString($atom->getType())));
- $properties = id(new PHUIPropertyListView());
- $group = $atom->getProperty('group');
- if ($group) {
- $group_name = $book->getGroupName($group);
- } else {
- $group_name = null;
- }
- $this->buildDefined($properties, $symbol);
- $this->buildExtendsAndImplements($properties, $symbol);
- $warnings = $atom->getWarnings();
- if ($warnings) {
- $warnings = id(new AphrontErrorView())
- ->setErrors($warnings)
- ->setTitle(pht('Documentation Warnings'))
- ->setSeverity(AphrontErrorView::SEVERITY_WARNING);
- }
- $methods = $this->composeMethods($symbol);
- $field = 'default';
- $engine = id(new PhabricatorMarkupEngine())
- ->setViewer($viewer)
- ->addObject($symbol, $field);
- foreach ($methods as $method) {
- foreach ($method['atoms'] as $matom) {
- $engine->addObject($matom, $field);
- }
- }
- $engine->process();
- $content = $this->renderDocumentationText($symbol, $engine);
- $toc = $engine->getEngineMetadata(
- $symbol,
- $field,
- PhutilRemarkupEngineRemarkupHeaderBlockRule::KEY_HEADER_TOC,
- array());
- $document = id(new PHUIDocumentView())
- ->setBook($book->getTitle(), $group_name)
- ->setHeader($header)
- ->setFontKit(PHUIDocumentView::FONT_SOURCE_SANS)
- ->appendChild($properties)
- ->appendChild($warnings)
- ->appendChild($content);
- $document->appendChild($this->buildParametersAndReturn(array($symbol)));
- if ($methods) {
- $tasks = $this->composeTasks($symbol);
- if ($tasks) {
- $methods_by_task = igroup($methods, 'task');
- // Add phantom tasks for methods which have a "@task" name that isn't
- // documented anywhere, or methods that have no "@task" name.
- foreach ($methods_by_task as $task => $ignored) {
- if (empty($tasks[$task])) {
- $tasks[$task] = array(
- 'name' => $task,
- 'title' => $task ? $task : pht('Other Methods'),
- 'defined' => $symbol,
- );
- }
- }
- $section = id(new DivinerSectionView())
- ->setHeader(pht('Tasks'));
- foreach ($tasks as $spec) {
- $section->addContent(
- id(new PHUIHeaderView())
- ->setNoBackground(true)
- ->setHeader($spec['title']));
- $task_methods = idx($methods_by_task, $spec['name'], array());
- $inner_box = id(new PHUIBoxView())
- ->addPadding(PHUI::PADDING_LARGE_LEFT)
- ->addPadding(PHUI::PADDING_LARGE_RIGHT)
- ->addPadding(PHUI::PADDING_LARGE_BOTTOM);
- $box_content = array();
- if ($task_methods) {
- $list_items = array();
- foreach ($task_methods as $task_method) {
- $atom = last($task_method['atoms']);
- $item = $this->renderFullSignature($atom, true);
- if (strlen($atom->getSummary())) {
- $item = array(
- $item,
- " \xE2\x80\x94 ",
- $atom->getSummary());
- }
- $list_items[] = phutil_tag('li', array(), $item);
- }
- $box_content[] = phutil_tag(
- 'ul',
- array(
- 'class' => 'diviner-list',
- ),
- $list_items);
- } else {
- $no_methods = pht('No methods for this task.');
- $box_content = phutil_tag('em', array(), $no_methods);
- }
- $inner_box->appendChild($box_content);
- $section->addContent($inner_box);
- }
- $document->appendChild($section);
- }
- $section = id(new DivinerSectionView())
- ->setHeader(pht('Methods'));
- foreach ($methods as $spec) {
- $matom = last($spec['atoms']);
- $method_header = id(new PHUIHeaderView())
- ->setNoBackground(true);
- $inherited = $spec['inherited'];
- if ($inherited) {
- $method_header->addTag(
- id(new PHUITagView())
- ->setType(PHUITagView::TYPE_STATE)
- ->setBackgroundColor(PHUITagView::COLOR_GREY)
- ->setName(pht('Inherited')));
- }
- $method_header->setHeader($this->renderFullSignature($matom));
- $section->addContent(
- array(
- $method_header,
- $this->renderMethodDocumentationText($symbol, $spec, $engine),
- $this->buildParametersAndReturn($spec['atoms']),
- ));
- }
- $document->appendChild($section);
- }
- if ($toc) {
- $side = new PHUIListView();
- $side->addMenuItem(
- id(new PHUIListItemView())
- ->setName(pht('Contents'))
- ->setType(PHUIListItemView::TYPE_LABEL));
- foreach ($toc as $key => $entry) {
- $side->addMenuItem(
- id(new PHUIListItemView())
- ->setName($entry[1])
- ->setHref('#'.$key));
- }
- $document->setSideNav($side, PHUIDocumentView::NAV_TOP);
- }
- return $this->buildApplicationPage(
- array(
- $crumbs,
- $document,
- ),
- array(
- 'title' => $symbol->getTitle(),
- 'device' => true,
- ));
- }
- private function buildExtendsAndImplements(
- PHUIPropertyListView $view,
- DivinerLiveSymbol $symbol) {
- $lineage = $this->getExtendsLineage($symbol);
- if ($lineage) {
- $tags = array();
- foreach ($lineage as $item) {
- $tags[] = $this->renderAtomTag($item);
- }
- $caret = phutil_tag('span', array('class' => 'caret-right msl msr'));
- $tags = phutil_implode_html($caret, $tags);
- $view->addProperty(pht('Extends'), $tags);
- }
- $implements = $this->getImplementsLineage($symbol);
- if ($implements) {
- $items = array();
- foreach ($implements as $spec) {
- $via = $spec['via'];
- $iface = $spec['interface'];
- if ($via == $symbol) {
- $items[] = $this->renderAtomTag($iface);
- } else {
- $items[] = array(
- $this->renderAtomTag($iface),
- " \xE2\x97\x80 ",
- $this->renderAtomTag($via));
- }
- }
- $view->addProperty(
- pht('Implements'),
- phutil_implode_html(phutil_tag('br'), $items));
- }
- }
- private function renderAtomTag(DivinerLiveSymbol $symbol) {
- return id(new PHUITagView())
- ->setType(PHUITagView::TYPE_OBJECT)
- ->setName($symbol->getName())
- ->setHref($symbol->getURI());
- }
- 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;
- }
- private function buildDefined(
- PHUIPropertyListView $view,
- DivinerLiveSymbol $symbol) {
- $atom = $symbol->getAtom();
- $defined = $atom->getFile().':'.$atom->getLine();
- $link = $symbol->getBook()->getConfig('uri.source');
- if ($link) {
- $link = strtr(
- $link,
- array(
- '%%' => '%',
- '%f' => phutil_escape_uri($atom->getFile()),
- '%l' => phutil_escape_uri($atom->getLine()),
- ));
- $defined = phutil_tag(
- 'a',
- array(
- 'href' => $link,
- 'target' => '_blank',
- ),
- $defined);
- }
- $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,
- );
- }
- }
- $specs = $task_specs + $extends_task_specs;
- // Reorder "@tasks" in original declaration order. Basically, we want to
- // use the documentation of the closest subclass, but put tasks which
- // were declared by parents first.
- $keys = array_keys($extends_task_specs);
- $specs = array_select_keys($specs, $keys) + $specs;
- return $specs;
- }
- private function renderFullSignature(
- DivinerLiveSymbol $symbol,
- $is_link = false) {
- switch ($symbol->getType()) {
- case DivinerAtom::TYPE_CLASS:
- case DivinerAtom::TYPE_INTERFACE:
- case DivinerAtom::TYPE_METHOD:
- case DivinerAtom::TYPE_FUNCTION:
- break;
- default:
- return $symbol->getTitle();
- }
- $atom = $symbol->getAtom();
- $out = array();
- if ($atom->getProperty('final')) {
- $out[] = 'final';
- }
- if ($atom->getProperty('abstract')) {
- $out[] = 'abstract';
- }
- if ($atom->getProperty('access')) {
- $out[] = $atom->getProperty('access');
- }
- if ($atom->getProperty('static')) {
- $out[] = 'static';
- }
- switch ($symbol->getType()) {
- case DivinerAtom::TYPE_CLASS:
- case DivinerAtom::TYPE_INTERFACE:
- $out[] = $symbol->getType();
- break;
- case DivinerAtom::TYPE_FUNCTION:
- switch ($atom->getLanguage()) {
- case 'php':
- $out[] = $symbol->getType();
- break;
- }
- break;
- case DivinerAtom::TYPE_METHOD:
- switch ($atom->getLanguage()) {
- case 'php':
- $out[] = DivinerAtom::TYPE_FUNCTION;
- break;
- }
- break;
- }
- $anchor = null;
- switch ($symbol->getType()) {
- case DivinerAtom::TYPE_METHOD:
- $anchor = $symbol->getType().'/'.$symbol->getName();
- break;
- default:
- break;
- }
- $out[] = phutil_tag(
- $anchor ? 'a' : 'span',
- array(
- 'class' => 'diviner-atom-signature-name',
- 'href' => $anchor ? '#'.$anchor : null,
- 'name' => $is_link ? null : $anchor,
- ),
- $symbol->getName());
- $out = phutil_implode_html(' ', $out);
- $parameters = $atom->getProperty('parameters');
- if ($parameters !== null) {
- $pout = array();
- foreach ($parameters as $parameter) {
- $pout[] = idx($parameter, 'name', '...');
- }
- $out = array($out, '('.implode(', ', $pout).')');
- }
- return phutil_tag(
- 'span',
- array(
- 'class' => 'diviner-atom-signature',
- ),
- $out);
- }
- private function buildParametersAndReturn(array $symbols) {
- assert_instances_of($symbols, 'DivinerLiveSymbol');
- $symbols = array_reverse($symbols);
- $out = array();
- $collected_parameters = null;
- foreach ($symbols as $symbol) {
- $parameters = $symbol->getAtom()->getProperty('parameters');
- if ($parameters !== null) {
- if ($collected_parameters === null) {
- $collected_parameters = array();
- }
- foreach ($parameters as $key => $parameter) {
- if (isset($collected_parameters[$key])) {
- $collected_parameters[$key] += $parameter;
- } else {
- $collected_parameters[$key] = $parameter;
- }
- }
- }
- }
- if (nonempty($parameters)) {
- $out[] = id(new DivinerParameterTableView())
- ->setHeader(pht('Parameters'))
- ->setParameters($parameters);
- }
- $collected_return = null;
- foreach ($symbols as $symbol) {
- $return = $symbol->getAtom()->getProperty('return');
- if ($return) {
- if ($collected_return) {
- $collected_return += $return;
- } else {
- $collected_return = $return;
- }
- }
- }
- if (nonempty($return)) {
- $out[] = id(new DivinerReturnTableView())
- ->setHeader(pht('Return'))
- ->setReturn($collected_return);
- }
- return $out;
- }
- private function renderDocumentationText(
- DivinerLiveSymbol $symbol,
- PhabricatorMarkupEngine $engine) {
- $field = 'default';
- $content = $engine->getOutput($symbol, $field);
- if (strlen(trim($symbol->getMarkupText($field)))) {
- $content = phutil_tag(
- 'div',
- array(
- 'class' => 'phabricator-remarkup',
- ),
- $content);
- } else {
- $atom = $symbol->getAtom();
- $content = phutil_tag(
- 'div',
- array(
- 'class' => 'diviner-message-not-documented',
- ),
- DivinerAtom::getThisAtomIsNotDocumentedString($atom->getType()));
- }
- return $content;
- }
- private function renderMethodDocumentationText(
- DivinerLiveSymbol $parent,
- array $spec,
- PhabricatorMarkupEngine $engine) {
- $symbols = array_values($spec['atoms']);
- $implementations = array_values($spec['implementations']);
- $field = 'default';
- $out = array();
- foreach ($symbols as $key => $symbol) {
- $impl = $implementations[$key];
- if ($impl !== $parent) {
- if (!strlen(trim($symbol->getMarkupText($field)))) {
- continue;
- }
- }
- $doc = $this->renderDocumentationText($symbol, $engine);
- if (($impl !== $parent) || $out) {
- $where = id(new PHUIBoxView())
- ->addPadding(PHUI::PADDING_MEDIUM_LEFT)
- ->addPadding(PHUI::PADDING_MEDIUM_RIGHT)
- ->addClass('diviner-method-implementation-header')
- ->appendChild($impl->getName());
- $doc = array($where, $doc);
- if ($impl !== $parent) {
- $doc = phutil_tag(
- 'div',
- array(
- 'class' => 'diviner-method-implementation-inherited',
- ),
- $doc);
- }
- }
- $out[] = $doc;
- }
- // If we only have inherited implementations but none have documentation,
- // render the last one here so we get the "this thing has no documentation"
- // element.
- if (!$out) {
- $out[] = $this->renderDocumentationText($symbol, $engine);
- }
- return $out;
- }
- }