diff --git a/src/applications/diviner/atomizer/DivinerPHPAtomizer.php b/src/applications/diviner/atomizer/DivinerPHPAtomizer.php index 30928fe319..7b3f2d639b 100644 --- a/src/applications/diviner/atomizer/DivinerPHPAtomizer.php +++ b/src/applications/diviner/atomizer/DivinerPHPAtomizer.php @@ -1,313 +1,313 @@ setLanguage('php'); + } + + protected function executeAtomize($file_name, $file_data) { $future = xhpast_get_parser_future($file_data); $tree = XHPASTTree::newFromDataAndResolvedExecFuture( $file_data, $future->resolve()); $atoms = array(); $root = $tree->getRootNode(); $func_decl = $root->selectDescendantsOfType('n_FUNCTION_DECLARATION'); foreach ($func_decl as $func) { $name = $func->getChildByIndex(2); - $atom = id(new DivinerAtom()) - ->setType('function') + $atom = $this->newAtom(DivinerAtom::TYPE_FUNCTION) ->setName($name->getConcreteString()) ->setLine($func->getLineNumber()) ->setFile($file_name); $this->findAtomDocblock($atom, $func); $this->parseParams($atom, $func); $this->parseReturnType($atom, $func); $atoms[] = $atom; } $class_types = array( - 'class' => 'n_CLASS_DECLARATION', - 'interface' => 'n_INTERFACE_DECLARATION', + DivinerAtom::TYPE_CLASS => 'n_CLASS_DECLARATION', + DivinerAtom::TYPE_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) + $atom = $this->newAtom($atom_type) ->setName($name->getConcreteString()) ->setFile($file_name) ->setLine($class->getLineNumber()); + // TODO: Parse "abstract" and "final". + // 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(), - ))); + $this->newRef( + DivinerAtom::TYPE_CLASS, + $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->newRef( + DivinerAtom::TYPE_INTERFACE, + $iface_name->getConcreteString())); } $this->findAtomDocblock($atom, $class); $methods = $class->selectDescendantsOfType('n_METHOD_DECLARATION'); foreach ($methods as $method) { - $matom = id(new DivinerAtom()) - ->setType('method'); + $matom = $this->newAtom(DivinerAtom::TYPE_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; } private function parseParams(DivinerAtom $atom, AASTNode $func) { $params = $func ->getChildByIndex(3, 'n_DECLARATAION_PARAMETER_LIST') ->selectDescendantsOfType('n_DECLARATION_PARAMETER'); $param_spec = array(); if ($atom->getDocblockRaw()) { $metadata = $atom->getDocblockMeta(); } else { $metadata = array(); } $docs = idx($metadata, 'param'); if ($docs) { $docs = explode("\n", $docs); $docs = array_filter($docs); } else { $docs = array(); } if (count($docs)) { if (count($docs) < count($params)) { $atom->addWarning( pht( 'This call takes %d parameters, but only %d are documented.', count($params), count($docs))); } } foreach ($params as $param) { $name = $param->getChildByIndex(1)->getConcreteString(); $dict = array( 'type' => $param->getChildByIndex(0)->getConcreteString(), 'default' => $param->getChildByIndex(2)->getConcreteString(), ); if ($docs) { $doc = array_shift($docs); if ($doc) { $dict += $this->parseParamDoc($atom, $doc, $name); } } $param_spec[] = array( 'name' => $name, ) + $dict; } if ($docs) { foreach ($docs as $doc) { if ($doc) { $param_spec[] = $this->parseParamDoc($atom, $doc, null); } } } // TODO: Find `assert_instances_of()` calls in the function body and // add their type information here. See T1089. $atom->setProperty('parameters', $param_spec); } private function findAtomDocblock(DivinerAtom $atom, XHPASTNode $node) { $token = $node->getDocblockToken(); if ($token) { $atom->setDocblockRaw($token->getValue()); return true; } else { $tokens = $node->getTokens(); if ($tokens) { $prev = head($tokens); while ($prev = $prev->getPrevToken()) { if ($prev->isAnyWhitespace()) { continue; } break; } if ($prev && $prev->isComment()) { $value = $prev->getValue(); $matches = null; if (preg_match('/@(return|param|task|author)/', $value, $matches)) { $atom->addWarning( pht( 'Atom "%s" is preceded by a comment containing "@%s", but the '. 'comment is not a documentation comment. Documentation '. 'comments must begin with "/**", followed by a newline. Did '. 'you mean to use a documentation comment? (As the comment is '. 'not a documentation comment, it will be ignored.)', $atom->getName(), $matches[1])); } } } $atom->setDocblockRaw(''); return false; } } protected function parseParamDoc(DivinerAtom $atom, $doc, $name) { $dict = array(); $split = preg_split('/\s+/', trim($doc), $limit = 2); if (!empty($split[0])) { $dict['doctype'] = $split[0]; } if (!empty($split[1])) { $docs = $split[1]; // If the parameter is documented like "@param int $num Blah blah ..", // get rid of the `$num` part (which Diviner considers optional). If it // is present and different from the declared name, raise a warning. $matches = null; if (preg_match('/^(\\$\S+)\s+/', $docs, $matches)) { if ($name !== null) { if ($matches[1] !== $name) { $atom->addWarning( pht( 'Parameter "%s" is named "%s" in the documentation. The '. 'documentation may be out of date.', $name, $matches[1])); } } $docs = substr($docs, strlen($matches[0])); } $dict['docs'] = $docs; } return $dict; } private function parseReturnType(DivinerAtom $atom, XHPASTNode $decl) { $return_spec = array(); $metadata = $atom->getDocblockMeta(); $return = idx($metadata, 'return'); if (!$return) { $return = idx($metadata, 'returns'); if ($return) { $atom->addWarning( pht('Documentation uses `@returns`, but should use `@return`.')); } } if ($atom->getName() == '__construct' && $atom->getType() == 'method') { $return_spec = array( 'doctype' => 'this', 'docs' => '//Implicit.//', ); if ($return) { $atom->addWarning( 'Method __construct() has explicitly documented @return. The '. '__construct() method always returns $this. Diviner documents '. 'this implicitly.'); } } else if ($return) { $split = preg_split('/\s+/', trim($return), $limit = 2); if (!empty($split[0])) { $type = $split[0]; } if ($decl->getChildByIndex(1)->getTypeName() == 'n_REFERENCE') { $type = $type.' &'; } $docs = null; if (!empty($split[1])) { $docs = $split[1]; } $return_spec = array( 'doctype' => $type, 'docs' => $docs, ); } else { $return_spec = array( 'type' => 'wild', ); } $atom->setProperty('return', $return_spec); } } diff --git a/src/applications/diviner/controller/DivinerAtomController.php b/src/applications/diviner/controller/DivinerAtomController.php index d923d3d641..3117b3e9e2 100644 --- a/src/applications/diviner/controller/DivinerAtomController.php +++ b/src/applications/diviner/controller/DivinerAtomController.php @@ -1,424 +1,587 @@ 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(); $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->addCrumb( id(new PhabricatorCrumbView()) ->setName($book->getShortTitle()) ->setHref('/book/'.$book->getName().'/')); $atom_short_title = $atom->getDocblockMetaValue( 'short', $symbol->getTitle()); $crumbs->addCrumb( id(new PhabricatorCrumbView()) ->setName($atom_short_title)); $header = id(new PhabricatorHeaderView()) ->setHeader($symbol->getTitle()) ->addTag( id(new PhabricatorTagView()) ->setType(PhabricatorTagView::TYPE_STATE) ->setBackgroundColor(PhabricatorTagView::COLOR_BLUE) - ->setName(DivinerAtom::getAtomTypeNameString($atom->getType()))); + ->setName(DivinerAtom::getAtomTypeNameString($atom->getType()))) + ->setSubheader($this->renderFullSignature($symbol)); $properties = id(new PhabricatorPropertyListView()); $group = $atom->getDocblockMetaValue('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) - ->process(); - - $content = $engine->getOutput($symbol, $field); - - if (strlen(trim($symbol->getMarkupText($field)))) { - $content = phutil_tag( - 'div', - array( - 'class' => 'phabricator-remarkup', - ), - array( - $content, - )); - } else { - $undoc = DivinerAtom::getThisAtomIsNotDocumentedString($atom->getType()); - $content = id(new AphrontErrorView()) - ->appendChild($undoc) - ->setSeverity(AphrontErrorView::SEVERITY_NODATA); + ->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) ->appendChild($properties) ->appendChild($warnings) ->appendChild($content); - $parameters = $atom->getProperty('parameters'); - if ($parameters !== null) { - $document->appendChild( - id(new PhabricatorHeaderView()) - ->setHeader(pht('Parameters'))); - - $document->appendChild( - id(new DivinerParameterTableView()) - ->setParameters($parameters)); - } + $document->appendChild($this->buildParametersAndReturn(array($symbol))); - $return = $atom->getProperty('return'); - if ($return !== null) { - $document->appendChild( - id(new PhabricatorHeaderView()) - ->setHeader(pht('Return'))); - $document->appendChild( - id(new DivinerReturnTableView()) - ->setReturn($return)); - } - - $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 ($methods as $spec) { + $matom = last($spec['atoms']); $method_header = id(new PhabricatorHeaderView()) - ->setHeader(last($spec['atoms'])->getName()); + ->setHeader($matom->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); + $method_header->setSubheader( + $this->renderFullSignature($matom)); + + $document->appendChild( + array( + $method_header, + $this->renderMethodDocumentationText($symbol, $spec, $engine), + $this->buildParametersAndReturn($spec['atoms']), + )); } } 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( 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; } private function buildDefined( PhabricatorPropertyListView $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, ); } } return $task_specs + $extends_task_specs; } + private function renderFullSignature(DivinerLiveSymbol $symbol) { + switch ($symbol->getType()) { + case DivinerAtom::TYPE_CLASS: + case DivinerAtom::TYPE_INTERFACE: + case DivinerAtom::TYPE_METHOD: + case DivinerAtom::TYPE_FUNCTION: + break; + default: + return null; + } + + $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; + } + + $out[] = $symbol->getName(); + + $out = implode(' ', $out); + + $parameters = $atom->getProperty('parameters'); + if ($parameters !== null) { + $pout = array(); + foreach ($parameters as $parameter) { + $pout[] = $parameter['name']; + } + $out .= '('.implode(', ', $pout).')'; + } + + return $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 ($parameters !== null) { + $out[] = id(new PhabricatorHeaderView()) + ->setHeader(pht('Parameters')); + $out[] = id(new DivinerParameterTableView()) + ->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 ($return !== null) { + $out[] = id(new PhabricatorHeaderView()) + ->setHeader(pht('Return')); + $out[] = id(new DivinerReturnTableView()) + ->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(); + $undoc = DivinerAtom::getThisAtomIsNotDocumentedString($atom->getType()); + $content = id(new AphrontErrorView()) + ->appendChild($undoc) + ->setSeverity(AphrontErrorView::SEVERITY_NODATA); + } + + 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; + } + $out[] = phutil_tag( + 'div', + array(), + pht('From parent implementation in %s:', $impl->getName())); + } else if ($out) { + $out[] = phutil_tag( + 'div', + array(), + pht('From this implementation:')); + } + $out[] = $this->renderDocumentationText($symbol, $engine); + } + + // 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; + } + }