@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
at upstream/main 702 lines 19 kB view raw
1<?php 2 3final class DivinerAtomController extends DivinerController { 4 5 public function shouldAllowPublic() { 6 return true; 7 } 8 9 public function handleRequest(AphrontRequest $request) { 10 $viewer = $request->getUser(); 11 12 $book_name = $request->getURIData('book'); 13 $atom_type = $request->getURIData('type'); 14 $atom_name = $request->getURIData('name'); 15 $atom_context = nonempty($request->getURIData('context'), null); 16 $atom_index = nonempty($request->getURIData('index'), null); 17 18 require_celerity_resource('diviner-shared-css'); 19 20 $book = id(new DivinerBookQuery()) 21 ->setViewer($viewer) 22 ->withNames(array($book_name)) 23 ->executeOne(); 24 25 if (!$book) { 26 return new Aphront404Response(); 27 } 28 29 $symbol = id(new DivinerAtomQuery()) 30 ->setViewer($viewer) 31 ->withBookPHIDs(array($book->getPHID())) 32 ->withTypes(array($atom_type)) 33 ->withNames(array($atom_name)) 34 ->withContexts(array($atom_context)) 35 ->withIndexes(array($atom_index)) 36 ->withIsDocumentable(true) 37 ->needAtoms(true) 38 ->needExtends(true) 39 ->needChildren(true) 40 ->executeOne(); 41 42 if (!$symbol) { 43 return new Aphront404Response(); 44 } 45 46 $atom = $symbol->getAtom(); 47 $crumbs = $this->buildApplicationCrumbs(); 48 $crumbs->setBorder(true); 49 50 $crumbs->addTextCrumb( 51 $book->getShortTitle(), 52 '/book/'.$book->getName().'/'); 53 54 $atom_short_title = $atom 55 ? $atom->getDocblockMetaValue('short', $symbol->getTitle()) 56 : $symbol->getTitle(); 57 58 $crumbs->addTextCrumb($atom_short_title); 59 60 $header = id(new PHUIHeaderView()) 61 ->setHeader($this->renderFullSignature($symbol)); 62 63 $properties = new PHUIPropertyListView(); 64 65 $group = $atom ? $atom->getProperty('group') : $symbol->getGroupName(); 66 if ($group) { 67 $group_name = $book->getGroupName($group); 68 } else { 69 $group_name = null; 70 } 71 72 $prop_list = new PHUIPropertyGroupView(); 73 $prop_list->addPropertyList($properties); 74 75 $document = id(new PHUIDocumentView()) 76 ->setBook($book->getTitle(), $group_name) 77 ->setHeader($header) 78 ->addClass('diviner-view'); 79 80 if ($atom) { 81 $this->buildDefined($properties, $symbol); 82 $this->buildExtendsAndImplements($properties, $symbol); 83 $this->buildRepository($properties, $symbol); 84 85 $warnings = $atom->getWarnings(); 86 if ($warnings) { 87 $warnings = id(new PHUIInfoView()) 88 ->setErrors($warnings) 89 ->setTitle(pht('Documentation Warnings')) 90 ->setSeverity(PHUIInfoView::SEVERITY_WARNING); 91 } 92 93 $document->appendChild($warnings); 94 } 95 96 $methods = $this->composeMethods($symbol); 97 98 $field = 'default'; 99 $engine = id(new PhabricatorMarkupEngine()) 100 ->setViewer($viewer) 101 ->addObject($symbol, $field); 102 foreach ($methods as $method) { 103 foreach ($method['atoms'] as $matom) { 104 $engine->addObject($matom, $field); 105 } 106 } 107 $engine->process(); 108 109 if ($atom) { 110 $content = $this->renderDocumentationText($symbol, $engine); 111 $document->appendChild($content); 112 } 113 114 $toc = $engine->getEngineMetadata( 115 $symbol, 116 $field, 117 PhutilRemarkupHeaderBlockRule::KEY_HEADER_TOC, 118 array()); 119 120 if (!$atom) { 121 $document->appendChild( 122 id(new PHUIInfoView()) 123 ->setSeverity(PHUIInfoView::SEVERITY_NOTICE) 124 ->appendChild(pht('This atom no longer exists.'))); 125 } 126 127 if ($atom) { 128 $document->appendChild($this->buildParametersAndReturn(array($symbol))); 129 } 130 131 if ($methods) { 132 $tasks = $this->composeTasks($symbol); 133 134 if ($tasks) { 135 $methods_by_task = igroup($methods, 'task'); 136 137 // Add phantom tasks for methods which have a "@task" name that isn't 138 // documented anywhere, or methods that have no "@task" name. 139 foreach ($methods_by_task as $task => $ignored) { 140 if (empty($tasks[$task])) { 141 $tasks[$task] = array( 142 'name' => $task, 143 'title' => $task ? $task : pht('Other Methods'), 144 'defined' => $symbol, 145 ); 146 } 147 } 148 149 $section = id(new DivinerSectionView()) 150 ->setHeader(pht('Tasks')); 151 152 foreach ($tasks as $spec) { 153 $section->addContent( 154 id(new PHUIHeaderView()) 155 ->setNoBackground(true) 156 ->setHeader($spec['title'])); 157 158 $task_methods = idx($methods_by_task, $spec['name'], array()); 159 160 $box_content = array(); 161 if ($task_methods) { 162 $list_items = array(); 163 foreach ($task_methods as $task_method) { 164 $atom = last($task_method['atoms']); 165 166 $item = $this->renderFullSignature($atom, true); 167 168 if (strlen($atom->getSummary())) { 169 $item = array( 170 $item, 171 " \xE2\x80\x94 ", 172 $atom->getSummary(), 173 ); 174 } 175 176 $list_items[] = phutil_tag('li', array(), $item); 177 } 178 179 $box_content[] = phutil_tag( 180 'ul', 181 array( 182 'class' => 'diviner-list', 183 ), 184 $list_items); 185 } else { 186 $no_methods = pht('No methods for this task.'); 187 $box_content = phutil_tag('em', array(), $no_methods); 188 } 189 190 $inner_box = phutil_tag_div('diviner-task-items', $box_content); 191 $section->addContent($inner_box); 192 } 193 $document->appendChild($section); 194 } 195 196 $section = id(new DivinerSectionView()) 197 ->setHeader(pht('Methods')); 198 199 foreach ($methods as $spec) { 200 $matom = last($spec['atoms']); 201 $method_header = id(new PHUIHeaderView()) 202 ->setNoBackground(true); 203 204 $inherited = $spec['inherited']; 205 if ($inherited) { 206 $method_header->addTag( 207 id(new PHUITagView()) 208 ->setType(PHUITagView::TYPE_STATE) 209 ->setBackgroundColor(PHUITagView::COLOR_GREY) 210 ->setName(pht('Inherited'))); 211 } 212 213 $method_header->setHeader($this->renderFullSignature($matom)); 214 215 $section->addContent( 216 array( 217 $method_header, 218 $this->renderMethodDocumentationText($symbol, $spec, $engine), 219 $this->buildParametersAndReturn($spec['atoms']), 220 )); 221 } 222 $document->appendChild($section); 223 } 224 225 if ($toc) { 226 $side = new PHUIListView(); 227 $side->addMenuItem( 228 id(new PHUIListItemView()) 229 ->setName(pht('Contents')) 230 ->setType(PHUIListItemView::TYPE_LABEL)); 231 foreach ($toc as $key => $entry) { 232 $side->addMenuItem( 233 id(new PHUIListItemView()) 234 ->setName($entry[1]) 235 ->setHref('#'.$key)); 236 } 237 238 $document->setToc($side); 239 } 240 241 $prop_list = phutil_tag_div('phui-document-view-pro-box', $prop_list); 242 243 return $this->newPage() 244 ->setTitle($symbol->getTitle()) 245 ->setCrumbs($crumbs) 246 ->appendChild(array( 247 $document, 248 $prop_list, 249 )); 250 } 251 252 private function buildExtendsAndImplements( 253 PHUIPropertyListView $view, 254 DivinerLiveSymbol $symbol) { 255 256 $lineage = $this->getExtendsLineage($symbol); 257 if ($lineage) { 258 $tags = array(); 259 foreach ($lineage as $item) { 260 $tags[] = $this->renderAtomTag($item); 261 } 262 263 $caret = phutil_tag('span', array('class' => 'caret-right msl msr')); 264 $tags = phutil_implode_html($caret, $tags); 265 $view->addProperty(pht('Extends'), $tags); 266 } 267 268 $implements = $this->getImplementsLineage($symbol); 269 if ($implements) { 270 $items = array(); 271 foreach ($implements as $spec) { 272 $via = $spec['via']; 273 $iface = $spec['interface']; 274 if ($via == $symbol) { 275 $items[] = $this->renderAtomTag($iface); 276 } else { 277 $items[] = array( 278 $this->renderAtomTag($iface), 279 " \xE2\x97\x80 ", 280 $this->renderAtomTag($via), 281 ); 282 } 283 } 284 285 $view->addProperty( 286 pht('Implements'), 287 phutil_implode_html(phutil_tag('br'), $items)); 288 } 289 } 290 291 private function buildRepository( 292 PHUIPropertyListView $view, 293 DivinerLiveSymbol $symbol) { 294 295 if (!$symbol->getRepositoryPHID()) { 296 return; 297 } 298 299 $view->addProperty( 300 pht('Repository'), 301 $this->getViewer()->renderHandle($symbol->getRepositoryPHID())); 302 } 303 304 private function renderAtomTag(DivinerLiveSymbol $symbol) { 305 return id(new PHUITagView()) 306 ->setType(PHUITagView::TYPE_OBJECT) 307 ->setName($symbol->getName()) 308 ->setHref($symbol->getURI()); 309 } 310 311 private function getExtendsLineage(DivinerLiveSymbol $symbol) { 312 foreach ($symbol->getExtends() as $extends) { 313 if ($extends->getType() == 'class') { 314 $lineage = $this->getExtendsLineage($extends); 315 $lineage[] = $extends; 316 return $lineage; 317 } 318 } 319 return array(); 320 } 321 322 private function getImplementsLineage(DivinerLiveSymbol $symbol) { 323 $implements = array(); 324 325 // Do these first so we get interfaces ordered from most to least specific. 326 foreach ($symbol->getExtends() as $extends) { 327 if ($extends->getType() == 'interface') { 328 $implements[$extends->getName()] = array( 329 'interface' => $extends, 330 'via' => $symbol, 331 ); 332 } 333 } 334 335 // Now do parent interfaces. 336 foreach ($symbol->getExtends() as $extends) { 337 if ($extends->getType() == 'class') { 338 $implements += $this->getImplementsLineage($extends); 339 } 340 } 341 342 return $implements; 343 } 344 345 private function buildDefined( 346 PHUIPropertyListView $view, 347 DivinerLiveSymbol $symbol) { 348 349 $atom = $symbol->getAtom(); 350 $defined = $atom->getFile().':'.$atom->getLine(); 351 352 $link = $symbol->getBook()->getConfig('uri.source'); 353 if ($link) { 354 $link = strtr( 355 $link, 356 array( 357 '%%' => '%', 358 '%f' => phutil_escape_uri($atom->getFile()), 359 '%l' => phutil_escape_uri($atom->getLine()), 360 )); 361 $defined = phutil_tag( 362 'a', 363 array( 364 'href' => $link, 365 'target' => '_blank', 366 ), 367 $defined); 368 } 369 370 $view->addProperty(pht('Defined'), $defined); 371 } 372 373 private function composeMethods(DivinerLiveSymbol $symbol) { 374 $methods = $this->findMethods($symbol); 375 if (!$methods) { 376 return $methods; 377 } 378 379 foreach ($methods as $name => $method) { 380 // Check for "@task" on each parent, to find the most recently declared 381 // "@task". 382 $task = null; 383 foreach ($method['atoms'] as $key => $method_symbol) { 384 $atom = $method_symbol->getAtom(); 385 if ($atom->getDocblockMetaValue('task')) { 386 $task = $atom->getDocblockMetaValue('task'); 387 } 388 } 389 $methods[$name]['task'] = $task; 390 391 // Set 'inherited' if this atom has no implementation of the method. 392 if (last($method['implementations']) !== $symbol) { 393 $methods[$name]['inherited'] = true; 394 } else { 395 $methods[$name]['inherited'] = false; 396 } 397 } 398 399 return $methods; 400 } 401 402 private function findMethods(DivinerLiveSymbol $symbol) { 403 $child_specs = array(); 404 foreach ($symbol->getExtends() as $extends) { 405 if ($extends->getType() == DivinerAtom::TYPE_CLASS) { 406 $child_specs = $this->findMethods($extends); 407 } 408 } 409 410 foreach ($symbol->getChildren() as $child) { 411 if ($child->getType() == DivinerAtom::TYPE_METHOD) { 412 $name = $child->getName(); 413 if (isset($child_specs[$name])) { 414 $child_specs[$name]['atoms'][] = $child; 415 $child_specs[$name]['implementations'][] = $symbol; 416 } else { 417 $child_specs[$name] = array( 418 'atoms' => array($child), 419 'defined' => $symbol, 420 'implementations' => array($symbol), 421 ); 422 } 423 } 424 } 425 426 return $child_specs; 427 } 428 429 private function composeTasks(DivinerLiveSymbol $symbol) { 430 $extends_task_specs = array(); 431 foreach ($symbol->getExtends() as $extends) { 432 $extends_task_specs += $this->composeTasks($extends); 433 } 434 435 $task_specs = array(); 436 437 $tasks = $symbol->getAtom()->getDocblockMetaValue('task'); 438 439 if (!is_array($tasks)) { 440 if (phutil_nonempty_string($tasks)) { 441 $tasks = array($tasks); 442 } else { 443 $tasks = array(); 444 } 445 } 446 447 if ($tasks) { 448 foreach ($tasks as $task) { 449 if (strpos($task, ' ') !== false) { 450 list($name, $title) = explode(' ', $task, 2); 451 } else { 452 list($name, $title) = array($task, ''); 453 } 454 $name = trim($name); 455 $title = trim($title); 456 457 $task_specs[$name] = array( 458 'name' => $name, 459 'title' => $title, 460 'defined' => $symbol, 461 ); 462 } 463 } 464 465 $specs = $task_specs + $extends_task_specs; 466 467 // Reorder "@tasks" in original declaration order. Basically, we want to 468 // use the documentation of the closest subclass, but put tasks which 469 // were declared by parents first. 470 $keys = array_keys($extends_task_specs); 471 $specs = array_select_keys($specs, $keys) + $specs; 472 473 return $specs; 474 } 475 476 private function renderFullSignature( 477 DivinerLiveSymbol $symbol, 478 $is_link = false) { 479 480 switch ($symbol->getType()) { 481 case DivinerAtom::TYPE_CLASS: 482 case DivinerAtom::TYPE_INTERFACE: 483 case DivinerAtom::TYPE_METHOD: 484 case DivinerAtom::TYPE_FUNCTION: 485 break; 486 default: 487 return $symbol->getTitle(); 488 } 489 490 $atom = $symbol->getAtom(); 491 492 $out = array(); 493 494 if ($atom) { 495 if ($atom->getProperty('final')) { 496 $out[] = 'final'; 497 } 498 499 if ($atom->getProperty('abstract')) { 500 $out[] = 'abstract'; 501 } 502 503 if ($atom->getProperty('access')) { 504 $out[] = $atom->getProperty('access'); 505 } 506 507 if ($atom->getProperty('static')) { 508 $out[] = 'static'; 509 } 510 } 511 512 switch ($symbol->getType()) { 513 case DivinerAtom::TYPE_CLASS: 514 case DivinerAtom::TYPE_INTERFACE: 515 $out[] = $symbol->getType(); 516 break; 517 case DivinerAtom::TYPE_FUNCTION: 518 switch ($atom->getLanguage()) { 519 case 'php': 520 $out[] = $symbol->getType(); 521 break; 522 } 523 break; 524 case DivinerAtom::TYPE_METHOD: 525 switch ($atom->getLanguage()) { 526 case 'php': 527 $out[] = DivinerAtom::TYPE_FUNCTION; 528 break; 529 } 530 break; 531 } 532 533 $anchor = null; 534 switch ($symbol->getType()) { 535 case DivinerAtom::TYPE_METHOD: 536 $anchor = $symbol->getType().'/'.$symbol->getName(); 537 break; 538 default: 539 break; 540 } 541 542 $out[] = phutil_tag( 543 $anchor ? 'a' : 'span', 544 array( 545 'class' => 'diviner-atom-signature-name', 546 'href' => $anchor ? '#'.$anchor : null, 547 'name' => $is_link ? null : $anchor, 548 ), 549 $symbol->getName()); 550 551 $out = phutil_implode_html(' ', $out); 552 553 if ($atom) { 554 $parameters = $atom->getProperty('parameters'); 555 if ($parameters !== null) { 556 $pout = array(); 557 foreach ($parameters as $parameter) { 558 $pout[] = idx($parameter, 'name', '...'); 559 } 560 $out = array($out, '('.implode(', ', $pout).')'); 561 } 562 } 563 564 return phutil_tag( 565 'span', 566 array( 567 'class' => 'diviner-atom-signature', 568 ), 569 $out); 570 } 571 572 /** 573 * @param array<DivinerLiveSymbol> $symbols 574 */ 575 private function buildParametersAndReturn(array $symbols) { 576 assert_instances_of($symbols, DivinerLiveSymbol::class); 577 578 $symbols = array_reverse($symbols); 579 $out = array(); 580 581 $collected_parameters = null; 582 foreach ($symbols as $symbol) { 583 $parameters = $symbol->getAtom()->getProperty('parameters'); 584 if ($parameters !== null) { 585 if ($collected_parameters === null) { 586 $collected_parameters = array(); 587 } 588 foreach ($parameters as $key => $parameter) { 589 if (isset($collected_parameters[$key])) { 590 $collected_parameters[$key] += $parameter; 591 } else { 592 $collected_parameters[$key] = $parameter; 593 } 594 } 595 } 596 } 597 598 if (nonempty($parameters)) { 599 $out[] = id(new DivinerParameterTableView()) 600 ->setHeader(pht('Parameters')) 601 ->setParameters($parameters); 602 } 603 604 $collected_return = null; 605 foreach ($symbols as $symbol) { 606 $return = $symbol->getAtom()->getProperty('return'); 607 if ($return) { 608 if ($collected_return) { 609 $collected_return += $return; 610 } else { 611 $collected_return = $return; 612 } 613 } 614 } 615 616 if (nonempty($return)) { 617 $out[] = id(new DivinerReturnTableView()) 618 ->setHeader(pht('Return')) 619 ->setReturn($collected_return); 620 } 621 622 return $out; 623 } 624 625 private function renderDocumentationText( 626 DivinerLiveSymbol $symbol, 627 PhabricatorMarkupEngine $engine) { 628 629 $field = 'default'; 630 $content = $engine->getOutput($symbol, $field); 631 632 if (strlen(trim($symbol->getMarkupText($field)))) { 633 $content = phutil_tag( 634 'div', 635 array( 636 'class' => 'phabricator-remarkup diviner-remarkup-section', 637 ), 638 $content); 639 } else { 640 $atom = $symbol->getAtom(); 641 $content = phutil_tag( 642 'div', 643 array( 644 'class' => 'diviner-message-not-documented', 645 ), 646 DivinerAtom::getThisAtomIsNotDocumentedString($atom->getType())); 647 } 648 649 return $content; 650 } 651 652 private function renderMethodDocumentationText( 653 DivinerLiveSymbol $parent, 654 array $spec, 655 PhabricatorMarkupEngine $engine) { 656 657 $symbols = array_values($spec['atoms']); 658 $implementations = array_values($spec['implementations']); 659 660 $field = 'default'; 661 662 $out = array(); 663 foreach ($symbols as $key => $symbol) { 664 $impl = $implementations[$key]; 665 if ($impl !== $parent) { 666 if (!strlen(trim($symbol->getMarkupText($field)))) { 667 continue; 668 } 669 } 670 671 $doc = $this->renderDocumentationText($symbol, $engine); 672 673 if (($impl !== $parent) || $out) { 674 $where = id(new PHUIBoxView()) 675 ->addClass('diviner-method-implementation-header') 676 ->appendChild($impl->getName()); 677 $doc = array($where, $doc); 678 679 if ($impl !== $parent) { 680 $doc = phutil_tag( 681 'div', 682 array( 683 'class' => 'diviner-method-implementation-inherited', 684 ), 685 $doc); 686 } 687 } 688 689 $out[] = $doc; 690 } 691 692 // If we only have inherited implementations but none have documentation, 693 // render the last one here so we get the "this thing has no documentation" 694 // element. 695 if (!$out) { 696 $out[] = $this->renderDocumentationText($symbol, $engine); 697 } 698 699 return $out; 700 } 701 702}