diff --git a/src/applications/diviner/controller/DivinerAtomController.php b/src/applications/diviner/controller/DivinerAtomController.php index 798c524367..10ebcccb3e 100644 --- a/src/applications/diviner/controller/DivinerAtomController.php +++ b/src/applications/diviner/controller/DivinerAtomController.php @@ -1,677 +1,679 @@ 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)) + ->withGhosts(false) + ->withIsDocumentable(true) ->needAtoms(true) ->needExtends(true) ->needChildren(true) ->executeOne(); if (!$symbol) { return new Aphront404Response(); } $atom = $symbol->getAtom(); $crumbs = $this->buildApplicationCrumbs(); $crumbs->setBorder(true); $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 PHUIInfoView()) ->setErrors($warnings) ->setTitle(pht('Documentation Warnings')) ->setSeverity(PHUIInfoView::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, PhutilRemarkupHeaderBlockRule::KEY_HEADER_TOC, array()); $document = id(new PHUIDocumentView()) ->setBook($book->getTitle(), $group_name) ->setHeader($header) ->addClass('diviner-view') ->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(), )); } 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; } } diff --git a/src/applications/diviner/controller/DivinerBookController.php b/src/applications/diviner/controller/DivinerBookController.php index fcc9aa822b..125edbb6ab 100644 --- a/src/applications/diviner/controller/DivinerBookController.php +++ b/src/applications/diviner/controller/DivinerBookController.php @@ -1,101 +1,103 @@ bookName = $data['book']; } public function processRequest() { $request = $this->getRequest(); $viewer = $request->getUser(); $book = id(new DivinerBookQuery()) ->setViewer($viewer) ->withNames(array($this->bookName)) ->executeOne(); if (!$book) { return new Aphront404Response(); } $crumbs = $this->buildApplicationCrumbs(); $crumbs->setBorder(true); $crumbs->addTextCrumb( $book->getShortTitle(), '/book/'.$book->getName().'/'); $header = id(new PHUIHeaderView()) ->setHeader($book->getTitle()) ->setUser($viewer) ->setPolicyObject($book) ->setEpoch($book->getDateModified()); $document = new PHUIDocumentView(); $document->setHeader($header); $document->addClass('diviner-view'); $document->setFontKit(PHUIDocumentView::FONT_SOURCE_SANS); $atoms = id(new DivinerAtomQuery()) ->setViewer($viewer) ->withBookPHIDs(array($book->getPHID())) + ->withGhosts(false) + ->withIsDocumentable(true) ->execute(); $atoms = msort($atoms, 'getSortKey'); $group_spec = $book->getConfig('groups'); if (!is_array($group_spec)) { $group_spec = array(); } $groups = mgroup($atoms, 'getGroupName'); $groups = array_select_keys($groups, array_keys($group_spec)) + $groups; if (isset($groups[''])) { $no_group = $groups['']; unset($groups['']); $groups[''] = $no_group; } $out = array(); foreach ($groups as $group => $atoms) { $group_name = $book->getGroupName($group); if (!strlen($group_name)) { $group_name = pht('Free Radicals'); } $section = id(new DivinerSectionView()) ->setHeader($group_name); $section->addContent($this->renderAtomList($atoms)); $out[] = $section; } $preface = $book->getPreface(); $preface_view = null; if (strlen($preface)) { $preface_view = PhabricatorMarkupEngine::renderOneObject( id(new PhabricatorMarkupOneOff())->setContent($preface), 'default', $viewer); } $document->appendChild($preface_view); $document->appendChild($out); return $this->buildApplicationPage( array( $crumbs, $document, ), array( 'title' => $book->getTitle(), )); } } diff --git a/src/applications/diviner/controller/DivinerFindController.php b/src/applications/diviner/controller/DivinerFindController.php index 0f73164da6..061e357b9f 100644 --- a/src/applications/diviner/controller/DivinerFindController.php +++ b/src/applications/diviner/controller/DivinerFindController.php @@ -1,91 +1,94 @@ getRequest(); $viewer = $request->getUser(); $book_name = $request->getStr('book'); $query_text = $request->getStr('name'); $book = null; if ($book_name) { $book = id(new DivinerBookQuery()) ->setViewer($viewer) ->withNames(array($book_name)) ->executeOne(); if (!$book) { return new Aphront404Response(); } } $query = id(new DivinerAtomQuery()) ->setViewer($viewer); if ($book) { $query->withBookPHIDs(array($book->getPHID())); } $context = $request->getStr('context'); if (strlen($context)) { $query->withContexts(array($context)); } $type = $request->getStr('type'); if (strlen($type)) { $query->withTypes(array($type)); } + $query->withGhosts(false); + $query->withIsDocumentable(true); + $name_query = clone $query; $name_query->withNames( array( $query_text, // TODO: This could probably be more smartly normalized in the DB, // but just fake it for now. phutil_utf8_strtolower($query_text), )); $atoms = $name_query->execute(); if (!$atoms) { $title_query = clone $query; $title_query->withTitles(array($query_text)); $atoms = $title_query->execute(); } $not_found_uri = $this->getApplicationURI(); if (!$atoms) { $dialog = id(new AphrontDialogView()) ->setUser($viewer) ->setTitle(pht('Documentation Not Found')) ->appendChild( pht( 'Unable to find the specified documentation. You may have '. 'followed a bad or outdated link.')) ->addCancelButton($not_found_uri, pht('Read More Documentation')); return id(new AphrontDialogResponse())->setDialog($dialog); } if (count($atoms) == 1 && $request->getBool('jump')) { $atom_uri = head($atoms)->getURI(); return id(new AphrontRedirectResponse())->setURI($atom_uri); } $list = $this->renderAtomList($atoms); return $this->buildApplicationPage( $list, array( 'title' => array(pht('Find'), pht('"%s"', $query_text)), )); } } diff --git a/src/applications/diviner/publisher/DivinerLivePublisher.php b/src/applications/diviner/publisher/DivinerLivePublisher.php index 9e3ea7d1ef..71002ccd2a 100644 --- a/src/applications/diviner/publisher/DivinerLivePublisher.php +++ b/src/applications/diviner/publisher/DivinerLivePublisher.php @@ -1,152 +1,152 @@ book) { $book_name = $this->getConfig('name'); $book = id(new DivinerLiveBook())->loadOneWhere('name = %s', $book_name); if (!$book) { $book = id(new DivinerLiveBook()) ->setName($book_name) ->setViewPolicy(PhabricatorPolicies::POLICY_USER) ->save(); } $book->setConfigurationData($this->getConfigurationData())->save(); $this->book = $book; } return $this->book; } private function loadSymbolForAtom(DivinerAtom $atom) { $symbol = id(new DivinerAtomQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withBookPHIDs(array($this->loadBook()->getPHID())) ->withTypes(array($atom->getType())) ->withNames(array($atom->getName())) ->withContexts(array($atom->getContext())) ->withIndexes(array($this->getAtomSimilarIndex($atom))) ->withIncludeUndocumentable(true) ->withIncludeGhosts(true) ->executeOne(); if ($symbol) { return $symbol; } return id(new DivinerLiveSymbol()) ->setBookPHID($this->loadBook()->getPHID()) ->setType($atom->getType()) ->setName($atom->getName()) ->setContext($atom->getContext()) ->setAtomIndex($this->getAtomSimilarIndex($atom)); } private function loadAtomStorageForSymbol(DivinerLiveSymbol $symbol) { $storage = id(new DivinerLiveAtom())->loadOneWhere( 'symbolPHID = %s', $symbol->getPHID()); if ($storage) { return $storage; } return id(new DivinerLiveAtom()) ->setSymbolPHID($symbol->getPHID()); } protected function loadAllPublishedHashes() { $symbols = id(new DivinerAtomQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withBookPHIDs(array($this->loadBook()->getPHID())) - ->withIncludeUndocumentable(true) + ->withGhosts(false) ->execute(); return mpull($symbols, 'getGraphHash'); } protected function deleteDocumentsByHash(array $hashes) { $atom_table = new DivinerLiveAtom(); $symbol_table = new DivinerLiveSymbol(); $conn_w = $symbol_table->establishConnection('w'); $strings = array(); foreach ($hashes as $hash) { $strings[] = qsprintf($conn_w, '%s', $hash); } foreach (PhabricatorLiskDAO::chunkSQL($strings, ', ') as $chunk) { queryfx( $conn_w, 'UPDATE %T SET graphHash = NULL, nodeHash = NULL WHERE graphHash IN (%Q)', $symbol_table->getTableName(), $chunk); } queryfx( $conn_w, 'DELETE a FROM %T a LEFT JOIN %T s ON a.symbolPHID = s.phid WHERE s.graphHash IS NULL', $atom_table->getTableName(), $symbol_table->getTableName()); } protected function createDocumentsByHash(array $hashes) { foreach ($hashes as $hash) { $atom = $this->getAtomFromGraphHash($hash); $ref = $atom->getRef(); $symbol = $this->loadSymbolForAtom($atom); $is_documentable = $this->shouldGenerateDocumentForAtom($atom); $symbol ->setGraphHash($hash) ->setIsDocumentable((int)$is_documentable) ->setTitle($ref->getTitle()) ->setGroupName($ref->getGroup()) ->setNodeHash($atom->getHash()); if ($atom->getType() !== DivinerAtom::TYPE_FILE) { $renderer = $this->getRenderer(); $summary = $renderer->getAtomSummary($atom); $symbol->setSummary($summary); } else { $symbol->setSummary(''); } $symbol->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? // TODO: Yeah do that soon ^^^ if ($atom->getType() !== DivinerAtom::TYPE_FILE) { $storage = $this->loadAtomStorageForSymbol($symbol) ->setAtomData($atom->toDictionary()) ->setContent(null) ->save(); } } } public function findAtomByRef(DivinerAtomRef $ref) { // TODO: Actually implement this. return null; } } diff --git a/src/applications/diviner/query/DivinerAtomQuery.php b/src/applications/diviner/query/DivinerAtomQuery.php index 70759a2cc1..12600927ff 100644 --- a/src/applications/diviner/query/DivinerAtomQuery.php +++ b/src/applications/diviner/query/DivinerAtomQuery.php @@ -1,447 +1,447 @@ ids = $ids; return $this; } public function withPHIDs(array $phids) { $this->phids = $phids; return $this; } public function withBookPHIDs(array $phids) { $this->bookPHIDs = $phids; return $this; } public function withTypes(array $types) { $this->types = $types; return $this; } public function withNames(array $names) { $this->names = $names; return $this; } public function withContexts(array $contexts) { $this->contexts = $contexts; return $this; } public function withIndexes(array $indexes) { $this->indexes = $indexes; return $this; } public function withNodeHashes(array $hashes) { $this->nodeHashes = $hashes; return $this; } public function withTitles($titles) { $this->titles = $titles; return $this; } public function withNameContains($text) { $this->nameContains = $text; return $this; } public function needAtoms($need) { $this->needAtoms = $need; return $this; } public function needChildren($need) { $this->needChildren = $need; return $this; } /** - * 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 - * the codebase but was deleted). + * Include or exclude "ghosts", which are symbols which used to exist but do + * not exist currently (for example, a function which existed in an older + * version of the codebase but was deleted). * * These symbols had PHIDs assigned to them, and may have other sorts of * metadata that we don't want to lose (like comments or flags), so we don't * delete them outright. They might also come back in the future: the change * which deleted the symbol might be reverted, or the documentation might * have been generated incorrectly by accident. In these cases, we can * restore the original data. * - * However, most callers are not interested in these symbols, so they are - * excluded by default. You can use this method to include them in results. - * - * @param bool True to include ghosts. + * @param bool * @return this */ - public function withIncludeGhosts($include) { - $this->includeGhosts = $include; + public function withGhosts($ghosts) { + $this->isGhost = $ghosts; return $this; } public function needExtends($need) { $this->needExtends = $need; return $this; } - public function withIncludeUndocumentable($include) { - $this->includeUndocumentable = $include; + public function withIsDocumentable($documentable) { + $this->isDocumentable = $documentable; return $this; } protected function loadPage() { $table = new DivinerLiveSymbol(); $conn_r = $table->establishConnection('r'); $data = queryfx_all( $conn_r, 'SELECT * FROM %T %Q %Q %Q', $table->getTableName(), $this->buildWhereClause($conn_r), $this->buildOrderClause($conn_r), $this->buildLimitClause($conn_r)); return $table->loadAllFromArray($data); } protected function willFilterPage(array $atoms) { $books = array_unique(mpull($atoms, 'getBookPHID')); $books = id(new DivinerBookQuery()) ->setViewer($this->getViewer()) ->withPHIDs($books) ->execute(); $books = mpull($books, null, 'getPHID'); foreach ($atoms as $key => $atom) { $book = idx($books, $atom->getBookPHID()); if (!$book) { unset($atoms[$key]); continue; } $atom->attachBook($book); } if ($this->needAtoms) { $atom_data = id(new DivinerLiveAtom())->loadAllWhere( 'symbolPHID IN (%Ls)', mpull($atoms, 'getPHID')); $atom_data = mpull($atom_data, null, 'getSymbolPHID'); foreach ($atoms as $key => $atom) { $data = idx($atom_data, $atom->getPHID()); if (!$data) { unset($atoms[$key]); continue; } $atom->attachAtom($data); } } // 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) ->needChildren($this->needChildren) ->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); } } 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; } protected function buildWhereClause(AphrontDatabaseConnection $conn_r) { $where = array(); if ($this->ids) { $where[] = qsprintf( $conn_r, 'id IN (%Ld)', $this->ids); } if ($this->phids) { $where[] = qsprintf( $conn_r, 'phid IN (%Ls)', $this->phids); } if ($this->bookPHIDs) { $where[] = qsprintf( $conn_r, 'bookPHID IN (%Ls)', $this->bookPHIDs); } if ($this->types) { $where[] = qsprintf( $conn_r, 'type IN (%Ls)', $this->types); } if ($this->names) { $where[] = qsprintf( $conn_r, 'name IN (%Ls)', $this->names); } if ($this->titles) { $hashes = array(); foreach ($this->titles as $title) { $slug = DivinerAtomRef::normalizeTitleString($title); $hash = PhabricatorHash::digestForIndex($slug); $hashes[] = $hash; } $where[] = qsprintf( $conn_r, 'titleSlugHash in (%Ls)', $hashes); } if ($this->contexts) { $with_null = false; $contexts = $this->contexts; foreach ($contexts as $key => $value) { if ($value === null) { unset($contexts[$key]); $with_null = true; continue; } } if ($contexts && $with_null) { $where[] = qsprintf( $conn_r, 'context IN (%Ls) OR context IS NULL', $contexts); } else if ($contexts) { $where[] = qsprintf( $conn_r, 'context IN (%Ls)', $contexts); } else if ($with_null) { $where[] = qsprintf( $conn_r, 'context IS NULL'); } } if ($this->indexes) { $where[] = qsprintf( $conn_r, 'atomIndex IN (%Ld)', $this->indexes); } - if (!$this->includeUndocumentable) { + if ($this->isDocumentable !== null) { $where[] = qsprintf( $conn_r, - 'isDocumentable = 1'); + 'isDocumentable = %d', + (int)$this->isDocumentable); } - if (!$this->includeGhosts) { - $where[] = qsprintf( - $conn_r, - 'graphHash IS NOT NULL'); + if ($this->isGhost !== null) { + if ($this->isGhost) { + $where[] = qsprintf($conn_r, 'graphHash IS NULL'); + } else { + $where[] = qsprintf($conn_r, 'graphHash IS NOT NULL'); + } } if ($this->nodeHashes) { $where[] = qsprintf( $conn_r, 'nodeHash IN (%Ls)', $this->nodeHashes); } if ($this->nameContains) { // NOTE: This CONVERT() call makes queries case-insensitive, since the // column has binary collation. Eventually, this should move into // fulltext. $where[] = qsprintf( $conn_r, 'CONVERT(name USING utf8) LIKE %~', $this->nameContains); } $where[] = $this->buildPagingClause($conn_r); 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 children 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); } } } public function getQueryApplicationClass() { return 'PhabricatorDivinerApplication'; } } diff --git a/src/applications/diviner/storage/DivinerLiveBook.php b/src/applications/diviner/storage/DivinerLiveBook.php index ef7a9bb277..41e6ab5b21 100644 --- a/src/applications/diviner/storage/DivinerLiveBook.php +++ b/src/applications/diviner/storage/DivinerLiveBook.php @@ -1,108 +1,106 @@ true, self::CONFIG_SERIALIZATION => array( 'configurationData' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'name' => 'text64', ), self::CONFIG_KEY_SCHEMA => array( 'key_phid' => null, 'phid' => array( 'columns' => array('phid'), 'unique' => true, ), 'name' => array( 'columns' => array('name'), 'unique' => true, ), ), ) + parent::getConfiguration(); } public function getConfig($key, $default = null) { return idx($this->configurationData, $key, $default); } public function setConfig($key, $value) { $this->configurationData[$key] = $value; return $this; } public function generatePHID() { return PhabricatorPHID::generateNewPHID( DivinerBookPHIDType::TYPECONST); } public function getTitle() { return $this->getConfig('title', $this->getName()); } public function getShortTitle() { return $this->getConfig('short', $this->getTitle()); } public function getPreface() { return $this->getConfig('preface'); } public function getGroupName($group) { $groups = $this->getConfig('groups', array()); $spec = idx($groups, $group, array()); return idx($spec, 'name', $group); } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, ); } public function getPolicy($capability) { return PhabricatorPolicies::getMostOpenPolicy(); } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return false; } public function describeAutomaticCapability($capability) { return null; } /* -( PhabricatorDestructibleInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { $this->openTransaction(); $atoms = id(new DivinerAtomQuery()) ->setViewer($engine->getViewer()) ->withBookPHIDs(array($this->getPHID())) - ->withIncludeGhosts(true) - ->withIncludeUndocumentable(true) ->execute(); foreach ($atoms as $atom) { $engine->destroyObject($atom); } $this->delete(); $this->saveTransaction(); } }