public function process(XHPASTNode $root) { $parser = new PhutilDocblockParser(); $classes = $root->selectDescendantsOfType('n_CLASS_DECLARATION'); foreach ($classes as $class) { $is_final = false; $is_abstract = false; $is_concrete_extensible = false; $attributes = $class->getChildOfType(0, 'n_CLASS_ATTRIBUTES'); foreach ($attributes->getChildren() as $child) { if ($child->getConcreteString() == 'final') { $is_final = true; } if ($child->getConcreteString() == 'abstract') { $is_abstract = true; } } $docblock = $class->getDocblockToken(); if ($docblock) { list($text, $specials) = $parser->parse($docblock->getValue()); $is_concrete_extensible = idx($specials, 'concrete-extensible'); } if (!$is_final && !$is_abstract && !$is_concrete_extensible) { $this->raiseLintAtNode($class->getChildOfType(1, 'n_CLASS_NAME'), pht('This class is neither `%s` nor `%s`, and does not have ' . 'a docblock marking it `%s`.', 'final', 'abstract', '@concrete-extensible')); } } }
public function setDocblockRaw($docblock_raw) { $this->docblockRaw = $docblock_raw; $parser = new PhutilDocblockParser(); list($text, $meta) = $parser->parse($docblock_raw); $this->docblockText = $text; $this->docblockMeta = $meta; return $this; }
private function parseDocblock($doc_file) { $contents = Filesystem::readFile($doc_file); $file = basename($doc_file); $parser = new PhutilDocblockParser(); list($docblock, $specials) = $parser->parse($contents); switch ($file) { case 'embedded-specials.docblock': $this->assertEqual(array(), $specials); $this->assertEqual("So long as a @special does not appear at the beginning of a line,\n" . "it is parsed as normal text.", $docblock); break; case 'indented-block.docblock': $this->assertEqual(array(), $specials); $this->assertEqual('Cozy lummox gives smart squid who asks for job pen.', $docblock); break; case 'indented-text.docblock': $this->assertEqual(array(), $specials); $this->assertEqual('Cozy lummox gives smart squid who asks for job pen.', $docblock); break; case 'multiline-special.docblock': $this->assertEqual(array('special' => 'x y z'), $specials); $this->assertEqual('', $docblock); break; case 'multi-specials.docblock': $this->assertEqual(array('special' => array('north', 'south'), 'stable' => true), $specials); $this->assertEqual('', $docblock); break; case 'specials.docblock': $this->assertEqual(array('type' => 'type', 'task' => 'task', 'special' => array('dot', 'dot', 'dash')), $specials); $this->assertEqual('', $docblock); break; case 'linebreak-breaks-specials.docblock': $this->assertEqual(array('title' => 'title'), $specials); $this->assertEqual('This is normal text, not part of the @title.', $docblock); break; case 'specials-with-hyphen.docblock': $this->assertEqual(array('repeat-hyphen' => array('a', 'b'), 'multiline-hyphen' => 'mmm nnn', 'normal-hyphen' => 'x'), $specials); break; case 'indented-specials.docblock': $this->assertEqual(array('title' => 'sendmail', 'special' => 'only a little bit indented'), $specials); break; case 'flag-specials.docblock': $this->assertEqual("stuff above\n\nstuff in the middle\n\nstuff below", $docblock); $this->assertEqual(array('flag' => true, 'stuff' => true, 'zebra' => true, 'apple' => true), $specials); break; case 'mixed-types.docblock': $this->assertEqual(array('special' => array('squirrels', true)), $specials); break; default: throw new Exception(pht("No test case to handle file '%s'!", $file)); } }
private function parseDocblock($doc_file) { $contents = Filesystem::readFile($doc_file); $file = basename($doc_file); $parser = new PhutilDocblockParser(); list($docblock, $specials) = $parser->parse($contents); switch ($file) { case 'embedded-specials.docblock': $this->assertEqual(array(), $specials); $this->assertEqual("So long as a @special does not appear at the beginning of a line,\n" . "it is parsed as normal text.", $docblock); break; case 'indented-block.docblock': $this->assertEqual(array(), $specials); $this->assertEqual("Cozy lummox gives smart squid who asks for job pen.", $docblock); break; case 'indented-text.docblock': $this->assertEqual(array(), $specials); $this->assertEqual("Cozy lummox gives smart squid who asks for job pen.", $docblock); break; case 'multiline-special.docblock': $this->assertEqual(array('special' => "x y z"), $specials); $this->assertEqual("", $docblock); break; case 'multi-specials.docblock': $this->assertEqual(array('special' => "north\nsouth"), $specials); $this->assertEqual("", $docblock); break; case 'specials.docblock': $this->assertEqual(array('type' => 'type', 'task' => 'task'), $specials); $this->assertEqual("", $docblock); break; case 'linebreak-breaks-specials.docblock': $this->assertEqual(array('title' => 'title'), $specials); $this->assertEqual("This is normal text, not part of the @title.", $docblock); break; case 'specials-with-hyphen.docblock': $this->assertEqual(array('repeat-hyphen' => "a\nb", 'multiline-hyphen' => "mmm nnn", 'normal-hyphen' => "x"), $specials); break; case 'indented-specials.docblock': $this->assertEqual(array('title' => 'sendmail'), $specials); break; case 'flag-specials.docblock': $this->assertEqual("stuff above\n\nstuff in the middle\n\nstuff below", $docblock); $this->assertEqual(array('flag' => true, 'stuff' => true, 'zebra' => true, 'apple' => true), $specials); break; default: throw new Exception("No test case to handle file '{$file}'!"); } }
$files = $finder->find(); echo "Processing " . count($files) . " files"; $file_map = array(); foreach ($files as $path => $raw_hash) { echo "."; $path = '/' . Filesystem::readablePath($path, $root); $data = Filesystem::readFile($root . $path); $data = $xformer->transformResource($path, $data); $hash = md5($data); $hash = md5($hash . $path . $resource_hash); $file_map[$path] = array('hash' => $hash, 'disk' => $path); } echo "\n"; $resource_graph = array(); $hash_map = array(); $parser = new PhutilDocblockParser(); foreach ($file_map as $path => $info) { $type = CelerityResourceTransformer::getResourceType($path); $data = Filesystem::readFile($root . $info['disk']); $matches = array(); $ok = preg_match('@/[*][*].*?[*]/@s', $data, $matches); if (!$ok) { throw new Exception("File {$path} does not have a header doc comment. Encode dependency " . "data in a header docblock."); } list($description, $metadata) = $parser->parse($matches[0]); $provides = preg_split('/\\s+/', trim(idx($metadata, 'provides'))); $requires = preg_split('/\\s+/', trim(idx($metadata, 'requires'))); $provides = array_filter($provides); $requires = array_filter($requires); if (!$provides) { // Tests and documentation-only JS is permitted to @provide no targets.
private function lintRaggedClasstreeEdges($root) { $parser = new PhutilDocblockParser(); $classes = $root->selectDescendantsOfType('n_CLASS_DECLARATION'); foreach ($classes as $class) { $is_final = false; $is_abstract = false; $is_concrete_extensible = false; $attributes = $class->getChildOfType(0, 'n_CLASS_ATTRIBUTES'); foreach ($attributes->getChildren() as $child) { if ($child->getConcreteString() == 'final') { $is_final = true; } if ($child->getConcreteString() == 'abstract') { $is_abstract = true; } } $docblock = $class->getDocblockToken(); if ($docblock) { list($text, $specials) = $parser->parse($docblock->getValue()); $is_concrete_extensible = idx($specials, 'concrete-extensible'); } if (!$is_final && !$is_abstract && !$is_concrete_extensible) { $this->raiseLintAtNode($class->getChildOfType(1, 'n_CLASS_NAME'), self::LINT_RAGGED_CLASSTREE_EDGE, "This class is neither 'final' nor 'abstract', and does not have " . "a docblock marking it '@concrete-extensible'."); } } }
phutil_require_module('phutil', 'parser/xhpast/api/tree'); phutil_require_module('arcanist', 'lint/linter/phutilmodule'); phutil_require_module('arcanist', 'lint/message'); phutil_require_module('arcanist', 'parser/phutilmodule'); $data = array(); $futures = array(); foreach (Filesystem::listDirectory($dir, $hidden_files = false) as $file) { if (!preg_match('/.php$/', $file)) { continue; } $data[$file] = Filesystem::readFile($dir . '/' . $file); $futures[$file] = xhpast_get_parser_future($data[$file]); } $requirements = new PhutilModuleRequirements(); $requirements->addBuiltins($builtin); $doc_parser = new PhutilDocblockParser(); $has_init = false; $has_files = false; foreach (Futures($futures) as $file => $future) { try { $tree = XHPASTTree::newFromDataAndResolvedExecFuture($data[$file], $future->resolve()); } catch (XHPASTSyntaxErrorException $ex) { echo "Syntax Error! In '{$file}': " . $ex->getMessage() . "\n"; exit(1); } $root = $tree->getRootNode(); $requirements->setCurrentFile($file); if ($file == '__init__.php') { $has_init = true; $calls = $root->selectDescendantsOfType('n_FUNCTION_CALL'); foreach ($calls as $call) {
public function parseFile($file, $data) { $ast = $this->trees[$file]; $this->expectNode($ast, 'Program'); $atoms = $this->parseAST($ast); $file_atom = new DivinerFileAtom(); foreach ($atoms as $atom) { $file_atom->addChild($atom); } $file_atom->setName($file); $file_atom->setFile($file); $dparser = new PhutilDocblockParser(); $blocks = $dparser->extractDocblocks($data); // Reject the first docblock as a header block. array_shift($blocks); $map = array(); foreach ($blocks as $data) { list($block, $line) = $data; $map[$line] = $block; } $atoms = $file_atom->getAllChildren(); $atoms = mpull($atoms, null, 'getLine'); ksort($atoms); end($atoms); $last = key($atoms); $block_map = array(); $pointer = null; for ($ii = 1; $ii <= $last; $ii++) { if (isset($map[$ii])) { $pointer = $ii; } $block_map[$ii] = $pointer; } foreach ($atoms as $atom) { $block_id = $block_map[$atom->getLine()]; if (isset($map[$block_id])) { $atom->setRawDocblock($map[$block_id]); unset($map[$block_id]); } if ($atom instanceof DivinerMethodAtom || $atom instanceof DivinerFunctionAtom) { $metadata = $atom->getDocblockMetadata(); $return = idx($metadata, 'return'); if ($return) { $split = preg_split('/\\s+/', trim($return), $limit = 2); if (!empty($split[0])) { $type = $split[0]; } else { $type = 'wild'; } $docs = null; if (!empty($split[1])) { $docs = $split[1]; } $dict = array('doctype' => $type, 'docs' => $docs); $atom->setReturnTypeAttributes($dict); } $docs = idx($metadata, 'param', ''); if ($docs) { $docs = explode("\n", $docs); foreach ($atom->getParameters() as $param => $dict) { $doc = array_shift($docs); if ($doc) { $dict += $this->parseParamDoc($doc); } $atom->addParameter($param, $dict); } // Add extra parameters retrieved by arguments variable. foreach ($docs as $doc) { if ($doc) { $atom->addParameter('', $this->parseParamDoc($doc)); } } } } } foreach ($atoms as $atom) { $atom->setLanguage('js'); $atom->setFile($file); } $file_atom->setLanguage('js'); return array($file_atom); }
/** * Parse the `@provides` and `@requires` symbols out of a text resource, like * JS or CSS. * * @param string Resource name. * @param string Resource data. * @return pair<string|null, list<string>|null> The `@provides` symbol and the * list of `@requires` symbols. If the resource is not part of the * dependency graph, both are null. */ private function getProvidesAndRequires($name, $data) { $parser = new PhutilDocblockParser(); $matches = array(); $ok = preg_match('@/[*][*].*?[*]/@s', $data, $matches); if (!$ok) { throw new Exception(pht('Resource "%s" does not have a header doc comment. Encode ' . 'dependency data in a header docblock.', $name)); } list($description, $metadata) = $parser->parse($matches[0]); $provides = preg_split('/\\s+/', trim(idx($metadata, 'provides'))); $requires = preg_split('/\\s+/', trim(idx($metadata, 'requires'))); $provides = array_filter($provides); $requires = array_filter($requires); if (!$provides) { // Tests and documentation-only JS is permitted to @provide no targets. return array(null, null); } if (count($provides) > 1) { throw new Exception(pht('Resource "%s" must @provide at most one Celerity target.', $name)); } return array(head($provides), $requires); }