@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

at recaptime-dev/main 372 lines 11 kB view raw
1<?php 2 3/** 4 * @phutil-external-symbol class PhpParser\Node 5 * @phutil-external-symbol class PhpParser\NodeTraverser 6 * @phutil-external-symbol class PhpParser\Node\FunctionLike 7 * @phutil-external-symbol class PhpParser\NodeVisitor\FindingVisitor 8 * @phutil-external-symbol class PhpParser\NodeVisitor\NameResolver 9 * @phutil-external-symbol class PhpParser\Node\Stmt\Class_ 10 * @phutil-external-symbol class PhpParser\Node\Stmt\ClassLike 11 * @phutil-external-symbol class PhpParser\Node\Stmt\Enum_ 12 * @phutil-external-symbol class PhpParser\Node\Stmt\Function_ 13 * @phutil-external-symbol class PhpParser\Node\Stmt\Interface_ 14 * @phutil-external-symbol class PhpParser\Node\Stmt\Trait_ 15 * @phutil-external-symbol class PhpParser\PrettyPrinter\Standard 16 */ 17final class DivinerPHPAtomizer extends DivinerAtomizer { 18 19 protected function newAtom($type) { 20 return parent::newAtom($type)->setLanguage('php'); 21 } 22 23 protected function executeAtomize($file_name, $file_data) { 24 $parser = PhutilPHPParserLibrary::getParser(); 25 $ast = $parser->parse($file_data); 26 27 $classlike_finder = new PhpParser\NodeVisitor\FindingVisitor( 28 function ($node) { 29 return $node instanceof PhpParser\Node\Stmt\ClassLike; 30 }); 31 $function_finder = new PhpParser\NodeVisitor\FindingVisitor( 32 function ($node) { 33 return $node instanceof PhpParser\Node\Stmt\Function_; 34 }); 35 36 $namespace_resolver = new PhpParser\NodeVisitor\NameResolver(); 37 $traverser = new PhpParser\NodeTraverser(); 38 $traverser->addVisitor($namespace_resolver); 39 $traverser->addVisitor($classlike_finder); 40 $traverser->addVisitor($function_finder); 41 $traverser->traverse($ast); 42 43 $atoms = array(); 44 45 foreach ($function_finder->getFoundNodes() as $func) { 46 $atom = $this->newAtom(DivinerAtom::TYPE_FUNCTION) 47 ->setName($func->namespacedName->toString()) 48 ->setLine($func->getStartLine()) 49 ->setFile($file_name); 50 51 $this->findAtomDocblock($atom, $func); 52 $this->parseParams($atom, $func); 53 $this->parseReturnType($atom, $func); 54 55 $atoms[] = $atom; 56 } 57 58 $class_types = array( 59 PhpParser\Node\Stmt\Class_::class => DivinerAtom::TYPE_CLASS, 60 PhpParser\Node\Stmt\Interface_::class => DivinerAtom::TYPE_INTERFACE, 61 PhpParser\Node\Stmt\Trait_::class => DivinerAtom::TYPE_TRAIT, 62 PhpParser\Node\Stmt\Enum_::class => DivinerAtom::TYPE_ENUM, 63 ); 64 65 foreach ($classlike_finder->getFoundNodes() as $class) { 66 $atom_type = $class_types[get_class($class)]; 67 68 // Don't analyze anonymous classes. 69 if (!$class->name) { 70 continue; 71 } 72 73 $atom = $this->newAtom($atom_type) 74 ->setName($class->namespacedName->toString()) 75 ->setFile($file_name) 76 ->setLine($class->getStartLine()); 77 78 if ($class instanceof PhpParser\Node\Stmt\Class_) { 79 if ($class->isAbstract()) { 80 $atom->setProperty('abstract', true); 81 } else if ($class->isFinal()) { 82 $atom->setProperty('final', true); 83 } else if ($class->isReadonly()) { 84 $atom->setProperty('readonly', true); 85 } 86 87 if ($class->extends) { 88 $atom->addExtends( 89 $this->newRef( 90 DivinerAtom::TYPE_CLASS, 91 $class->extends->toString())); 92 } 93 94 foreach ($class->implements as $implement) { 95 $atom->addExtends( 96 $this->newRef( 97 DivinerAtom::TYPE_INTERFACE, 98 $implement->toString())); 99 } 100 } else if ($class instanceof PhpParser\Node\Stmt\Interface_) { 101 foreach ($class->extends as $extend) { 102 $atom->addExtends( 103 $this->newRef( 104 DivinerAtom::TYPE_INTERFACE, 105 $extend->toString())); 106 } 107 } else if ($class instanceof PhpParser\Node\Stmt\Enum_) { 108 foreach ($class->implements as $implement) { 109 $atom->addExtends( 110 $this->newRef( 111 DivinerAtom::TYPE_INTERFACE, 112 $implement->toString())); 113 } 114 } 115 116 $this->findAtomDocblock($atom, $class); 117 118 foreach ($class->getMethods() as $method) { 119 $matom = $this->newAtom(DivinerAtom::TYPE_METHOD) 120 ->setName($method->name->toString()) 121 ->setLine($method->getStartLine()) 122 ->setFile($file_name); 123 124 $this->findAtomDocblock($matom, $method); 125 126 if ($method->isFinal()) { 127 $matom->setProperty('final', true); 128 } 129 130 if ($method->isAbstract()) { 131 $matom->setProperty('abstract', true); 132 } 133 134 if ($method->isStatic()) { 135 $matom->setProperty('static', true); 136 } 137 138 if ($method->isPrivate()) { 139 $matom->setProperty('access', 'private'); 140 } else if ($method->isProtected()) { 141 $matom->setProperty('access', 'protected'); 142 } else { 143 $matom->setProperty('access', 'public'); 144 } 145 146 $this->parseParams($matom, $method); 147 148 $this->parseReturnType($matom, $method); 149 $atom->addChild($matom); 150 151 $atoms[] = $matom; 152 } 153 154 $atoms[] = $atom; 155 } 156 157 return $atoms; 158 } 159 160 private function parseParams( 161 DivinerAtom $atom, 162 PhpParser\Node\FunctionLike $func) { 163 164 $params = $func->getParams(); 165 166 $param_spec = array(); 167 168 if ($atom->getDocblockRaw()) { 169 $metadata = $atom->getDocblockMeta(); 170 } else { 171 $metadata = array(); 172 } 173 174 $docs = idx($metadata, 'param'); 175 if ($docs) { 176 $docs = (array)$docs; 177 $docs = array_filter($docs); 178 } else { 179 $docs = array(); 180 } 181 182 if (count($docs)) { 183 if (count($docs) < count($params)) { 184 $atom->addWarning( 185 pht( 186 'This call takes %s parameter(s), but only %s are documented.', 187 phutil_count($params), 188 phutil_count($docs))); 189 } 190 } 191 192 foreach ($params as $param) { 193 $name = '$'.$param->var->name; 194 $dict = array( 195 'type' => $this->stringify($param->type), 196 'default' => $this->stringify($param->default), 197 ); 198 199 if ($docs) { 200 $doc = array_shift($docs); 201 if ($doc) { 202 $dict += $this->parseParamDoc($atom, $doc, $name); 203 } 204 } 205 206 $param_spec[] = array( 207 'name' => $name, 208 ) + $dict; 209 } 210 211 if ($docs) { 212 foreach ($docs as $doc) { 213 if ($doc) { 214 $param_spec[] = $this->parseParamDoc($atom, $doc, null); 215 } 216 } 217 } 218 219 // TODO: Find `assert_instances_of()` calls in the function body and 220 // add their type information here. See T1089. 221 222 $atom->setProperty('parameters', $param_spec); 223 } 224 225 private function findAtomDocblock(DivinerAtom $atom, PhpParser\Node $node) { 226 $doc_comment = $node->getDocComment(); 227 228 if ($doc_comment) { 229 $atom->setDocblockRaw($doc_comment->getText()); 230 } else { 231 $comments = $node->getComments(); 232 233 foreach ($comments as $comment) { 234 $value = $comment->getText(); 235 $matches = null; 236 if (preg_match('/@(return|param|task|author)/', $value, $matches)) { 237 $atom->addWarning( 238 pht( 239 'Atom "%s" is preceded by a comment containing `%s`, but '. 240 'the comment is not a documentation comment. Documentation '. 241 'comments must begin with `%s`, followed by a newline. Did '. 242 'you mean to use a documentation comment? (As the comment is '. 243 'not a documentation comment, it will be ignored.)', 244 $atom->getName(), 245 '@'.$matches[1], 246 '/**')); 247 } 248 } 249 250 $atom->setDocblockRaw(''); 251 } 252 } 253 254 protected function parseParamDoc(DivinerAtom $atom, $doc, $name) { 255 $dict = array(); 256 $split = preg_split('/(?<!,)\s+/', trim($doc), 2); 257 if (!empty($split[0])) { 258 $dict['doctype'] = $split[0]; 259 } 260 261 if (!empty($split[1])) { 262 $docs = $split[1]; 263 264 // If the parameter is documented like `@param int $num Blah blah ..`, 265 // get rid of the `$num` part (which Diviner considers optional). If it 266 // is present and different from the declared name, raise a warning. 267 $matches = null; 268 if (preg_match('/^(\\$\S+)\s+/', $docs, $matches)) { 269 if ($name !== null) { 270 if ($matches[1] !== $name) { 271 $atom->addWarning( 272 pht( 273 'Parameter "%s" is named "%s" in the documentation. '. 274 'The documentation may be out of date.', 275 $name, 276 $matches[1])); 277 } 278 } 279 $docs = substr($docs, strlen($matches[0])); 280 } 281 282 $dict['docs'] = $docs; 283 } 284 285 return $dict; 286 } 287 288 private function parseReturnType( 289 DivinerAtom $atom, 290 PhpParser\Node\FunctionLike $decl) { 291 292 $return_spec = array(); 293 294 $metadata = $atom->getDocblockMeta(); 295 $return = idx($metadata, 'return'); 296 297 $type = null; 298 $docs = null; 299 300 if (!$return) { 301 $return = idx($metadata, 'returns'); 302 if ($return) { 303 $atom->addWarning( 304 pht( 305 'Documentation uses `%s`, but should use `%s`.', 306 '@returns', 307 '@return')); 308 } 309 } 310 311 $return = (array)$return; 312 if (count($return) > 1) { 313 $atom->addWarning( 314 pht( 315 'Documentation specifies `%s` multiple times.', 316 '@return')); 317 } 318 $return = head($return); 319 320 if ($atom->getName() == '__construct' && $atom->getType() == 'method') { 321 $return_spec = array( 322 'doctype' => 'this', 323 'docs' => '//Implicit.//', 324 ); 325 326 if ($return) { 327 $atom->addWarning( 328 pht( 329 'Method `%s` has explicitly documented `%s`. The `%s` method '. 330 'always returns `%s`. Diviner documents this implicitly.', 331 '__construct()', 332 '@return', 333 '__construct()', 334 '$this')); 335 } 336 } else if ($return) { 337 $split = preg_split('/(?<!,)\s+/', trim($return), 2); 338 if (!empty($split[0])) { 339 $type = $split[0]; 340 } 341 342 if ($decl->returnsByRef()) { 343 $type = $type.' &'; 344 } 345 346 if (!empty($split[1])) { 347 $docs = $split[1]; 348 } 349 350 $return_spec = array( 351 'doctype' => $type, 352 'docs' => $docs, 353 ); 354 } else { 355 $return_spec = array( 356 'type' => 'wild', 357 ); 358 } 359 360 $atom->setProperty('return', $return_spec); 361 } 362 363 private function stringify(?PhpParser\Node $node) { 364 if (!$node) { 365 return ''; 366 } 367 368 return id(new PhpParser\PrettyPrinter\Standard()) 369 ->prettyPrint(array($node)); 370 } 371 372}