Path: blob/master/src/applications/diviner/atomizer/DivinerPHPAtomizer.php
12256 views
<?php12final class DivinerPHPAtomizer extends DivinerAtomizer {34protected function newAtom($type) {5return parent::newAtom($type)->setLanguage('php');6}78protected function executeAtomize($file_name, $file_data) {9$future = PhutilXHPASTBinary::getParserFuture($file_data);10$tree = XHPASTTree::newFromDataAndResolvedExecFuture(11$file_data,12$future->resolve());1314$atoms = array();15$root = $tree->getRootNode();1617$func_decl = $root->selectDescendantsOfType('n_FUNCTION_DECLARATION');18foreach ($func_decl as $func) {19$name = $func->getChildByIndex(2);2021// Don't atomize closures22if ($name->getTypeName() === 'n_EMPTY') {23continue;24}2526$atom = $this->newAtom(DivinerAtom::TYPE_FUNCTION)27->setName($name->getConcreteString())28->setLine($func->getLineNumber())29->setFile($file_name);3031$this->findAtomDocblock($atom, $func);32$this->parseParams($atom, $func);33$this->parseReturnType($atom, $func);3435$atoms[] = $atom;36}3738$class_types = array(39DivinerAtom::TYPE_CLASS => 'n_CLASS_DECLARATION',40DivinerAtom::TYPE_INTERFACE => 'n_INTERFACE_DECLARATION',41);42foreach ($class_types as $atom_type => $node_type) {43$class_decls = $root->selectDescendantsOfType($node_type);4445foreach ($class_decls as $class) {46$name = $class->getChildByIndex(1, 'n_CLASS_NAME');4748$atom = $this->newAtom($atom_type)49->setName($name->getConcreteString())50->setFile($file_name)51->setLine($class->getLineNumber());5253// This parses `final` and `abstract`.54$attributes = $class->getChildByIndex(0, 'n_CLASS_ATTRIBUTES');55foreach ($attributes->selectDescendantsOfType('n_STRING') as $attr) {56$atom->setProperty($attr->getConcreteString(), true);57}5859// If this exists, it is `n_EXTENDS_LIST`.60$extends = $class->getChildByIndex(2);61$extends_class = $extends->selectDescendantsOfType('n_CLASS_NAME');62foreach ($extends_class as $parent_class) {63$atom->addExtends(64$this->newRef(65DivinerAtom::TYPE_CLASS,66$parent_class->getConcreteString()));67}6869// If this exists, it is `n_IMPLEMENTS_LIST`.70$implements = $class->getChildByIndex(3);71$iface_names = $implements->selectDescendantsOfType('n_CLASS_NAME');72foreach ($iface_names as $iface_name) {73$atom->addExtends(74$this->newRef(75DivinerAtom::TYPE_INTERFACE,76$iface_name->getConcreteString()));77}7879$this->findAtomDocblock($atom, $class);8081$methods = $class->selectDescendantsOfType('n_METHOD_DECLARATION');82foreach ($methods as $method) {83$matom = $this->newAtom(DivinerAtom::TYPE_METHOD);8485$this->findAtomDocblock($matom, $method);8687$attribute_list = $method->getChildByIndex(0);88$attributes = $attribute_list->selectDescendantsOfType('n_STRING');89if ($attributes) {90foreach ($attributes as $attribute) {91$attr = strtolower($attribute->getConcreteString());92switch ($attr) {93case 'final':94case 'abstract':95case 'static':96$matom->setProperty($attr, true);97break;98case 'public':99case 'protected':100case 'private':101$matom->setProperty('access', $attr);102break;103}104}105} else {106$matom->setProperty('access', 'public');107}108109$this->parseParams($matom, $method);110111$matom->setName($method->getChildByIndex(2)->getConcreteString());112$matom->setLine($method->getLineNumber());113$matom->setFile($file_name);114115$this->parseReturnType($matom, $method);116$atom->addChild($matom);117118$atoms[] = $matom;119}120121$atoms[] = $atom;122}123}124125return $atoms;126}127128private function parseParams(DivinerAtom $atom, AASTNode $func) {129$params = $func130->getChildOfType(3, 'n_DECLARATION_PARAMETER_LIST')131->selectDescendantsOfType('n_DECLARATION_PARAMETER');132133$param_spec = array();134135if ($atom->getDocblockRaw()) {136$metadata = $atom->getDocblockMeta();137} else {138$metadata = array();139}140141$docs = idx($metadata, 'param');142if ($docs) {143$docs = (array)$docs;144$docs = array_filter($docs);145} else {146$docs = array();147}148149if (count($docs)) {150if (count($docs) < count($params)) {151$atom->addWarning(152pht(153'This call takes %s parameter(s), but only %s are documented.',154phutil_count($params),155phutil_count($docs)));156}157}158159foreach ($params as $param) {160$name = $param->getChildByIndex(1)->getConcreteString();161$dict = array(162'type' => $param->getChildByIndex(0)->getConcreteString(),163'default' => $param->getChildByIndex(2)->getConcreteString(),164);165166if ($docs) {167$doc = array_shift($docs);168if ($doc) {169$dict += $this->parseParamDoc($atom, $doc, $name);170}171}172173$param_spec[] = array(174'name' => $name,175) + $dict;176}177178if ($docs) {179foreach ($docs as $doc) {180if ($doc) {181$param_spec[] = $this->parseParamDoc($atom, $doc, null);182}183}184}185186// TODO: Find `assert_instances_of()` calls in the function body and187// add their type information here. See T1089.188189$atom->setProperty('parameters', $param_spec);190}191192private function findAtomDocblock(DivinerAtom $atom, XHPASTNode $node) {193$token = $node->getDocblockToken();194if ($token) {195$atom->setDocblockRaw($token->getValue());196return true;197} else {198$tokens = $node->getTokens();199if ($tokens) {200$prev = head($tokens);201while ($prev = $prev->getPrevToken()) {202if ($prev->isAnyWhitespace()) {203continue;204}205break;206}207208if ($prev && $prev->isComment()) {209$value = $prev->getValue();210$matches = null;211if (preg_match('/@(return|param|task|author)/', $value, $matches)) {212$atom->addWarning(213pht(214'Atom "%s" is preceded by a comment containing `%s`, but '.215'the comment is not a documentation comment. Documentation '.216'comments must begin with `%s`, followed by a newline. Did '.217'you mean to use a documentation comment? (As the comment is '.218'not a documentation comment, it will be ignored.)',219$atom->getName(),220'@'.$matches[1],221'/**'));222}223}224}225226$atom->setDocblockRaw('');227return false;228}229}230231protected function parseParamDoc(DivinerAtom $atom, $doc, $name) {232$dict = array();233$split = preg_split('/\s+/', trim($doc), 2);234if (!empty($split[0])) {235$dict['doctype'] = $split[0];236}237238if (!empty($split[1])) {239$docs = $split[1];240241// If the parameter is documented like `@param int $num Blah blah ..`,242// get rid of the `$num` part (which Diviner considers optional). If it243// is present and different from the declared name, raise a warning.244$matches = null;245if (preg_match('/^(\\$\S+)\s+/', $docs, $matches)) {246if ($name !== null) {247if ($matches[1] !== $name) {248$atom->addWarning(249pht(250'Parameter "%s" is named "%s" in the documentation. '.251'The documentation may be out of date.',252$name,253$matches[1]));254}255}256$docs = substr($docs, strlen($matches[0]));257}258259$dict['docs'] = $docs;260}261262return $dict;263}264265private function parseReturnType(DivinerAtom $atom, XHPASTNode $decl) {266$return_spec = array();267268$metadata = $atom->getDocblockMeta();269$return = idx($metadata, 'return');270271$type = null;272$docs = null;273274if (!$return) {275$return = idx($metadata, 'returns');276if ($return) {277$atom->addWarning(278pht(279'Documentation uses `%s`, but should use `%s`.',280'@returns',281'@return'));282}283}284285$return = (array)$return;286if (count($return) > 1) {287$atom->addWarning(288pht(289'Documentation specifies `%s` multiple times.',290'@return'));291}292$return = head($return);293294if ($atom->getName() == '__construct' && $atom->getType() == 'method') {295$return_spec = array(296'doctype' => 'this',297'docs' => '//Implicit.//',298);299300if ($return) {301$atom->addWarning(302pht(303'Method `%s` has explicitly documented `%s`. The `%s` method '.304'always returns `%s`. Diviner documents this implicitly.',305'__construct()',306'@return',307'__construct()',308'$this'));309}310} else if ($return) {311$split = preg_split('/(?<!,)\s+/', trim($return), 2);312if (!empty($split[0])) {313$type = $split[0];314}315316if ($decl->getChildByIndex(1)->getTypeName() == 'n_REFERENCE') {317$type = $type.' &';318}319320if (!empty($split[1])) {321$docs = $split[1];322}323324$return_spec = array(325'doctype' => $type,326'docs' => $docs,327);328} else {329$return_spec = array(330'type' => 'wild',331);332}333334$atom->setProperty('return', $return_spec);335}336337}338339340