PageRenderTime 62ms CodeModel.GetById 30ms RepoModel.GetById 1ms app.codeStats 0ms

/src/applications/diviner/controller/DivinerAtomController.php

https://github.com/navyuginfo/phabricator
PHP | 674 lines | 546 code | 111 blank | 17 comment | 63 complexity | be4e178dba572dd3c0c1864094f5651b MD5 | raw file
Possible License(s): Apache-2.0, LGPL-2.0, LGPL-3.0, MIT, MPL-2.0-no-copyleft-exception, BSD-3-Clause
  1. <?php
  2. final class DivinerAtomController extends DivinerController {
  3. private $bookName;
  4. private $atomType;
  5. private $atomName;
  6. private $atomContext;
  7. private $atomIndex;
  8. public function shouldAllowPublic() {
  9. return true;
  10. }
  11. public function willProcessRequest(array $data) {
  12. $this->bookName = $data['book'];
  13. $this->atomType = $data['type'];
  14. $this->atomName = $data['name'];
  15. $this->atomContext = nonempty(idx($data, 'context'), null);
  16. $this->atomIndex = nonempty(idx($data, 'index'), null);
  17. }
  18. public function processRequest() {
  19. $request = $this->getRequest();
  20. $viewer = $request->getUser();
  21. require_celerity_resource('diviner-shared-css');
  22. $book = id(new DivinerBookQuery())
  23. ->setViewer($viewer)
  24. ->withNames(array($this->bookName))
  25. ->executeOne();
  26. if (!$book) {
  27. return new Aphront404Response();
  28. }
  29. // TODO: This query won't load ghosts, because they'll fail `needAtoms()`.
  30. // Instead, we might want to load ghosts and render a message like
  31. // "this thing existed in an older version, but no longer does", especially
  32. // if we add content like comments.
  33. $symbol = id(new DivinerAtomQuery())
  34. ->setViewer($viewer)
  35. ->withBookPHIDs(array($book->getPHID()))
  36. ->withTypes(array($this->atomType))
  37. ->withNames(array($this->atomName))
  38. ->withContexts(array($this->atomContext))
  39. ->withIndexes(array($this->atomIndex))
  40. ->needAtoms(true)
  41. ->needExtends(true)
  42. ->needChildren(true)
  43. ->executeOne();
  44. if (!$symbol) {
  45. return new Aphront404Response();
  46. }
  47. $atom = $symbol->getAtom();
  48. $crumbs = $this->buildApplicationCrumbs();
  49. $crumbs->addTextCrumb(
  50. $book->getShortTitle(),
  51. '/book/'.$book->getName().'/');
  52. $atom_short_title = $atom->getDocblockMetaValue(
  53. 'short',
  54. $symbol->getTitle());
  55. $crumbs->addTextCrumb($atom_short_title);
  56. $header = id(new PHUIHeaderView())
  57. ->setHeader($this->renderFullSignature($symbol))
  58. ->addTag(
  59. id(new PHUITagView())
  60. ->setType(PHUITagView::TYPE_STATE)
  61. ->setBackgroundColor(PHUITagView::COLOR_BLUE)
  62. ->setName(DivinerAtom::getAtomTypeNameString($atom->getType())));
  63. $properties = id(new PHUIPropertyListView());
  64. $group = $atom->getProperty('group');
  65. if ($group) {
  66. $group_name = $book->getGroupName($group);
  67. } else {
  68. $group_name = null;
  69. }
  70. $this->buildDefined($properties, $symbol);
  71. $this->buildExtendsAndImplements($properties, $symbol);
  72. $warnings = $atom->getWarnings();
  73. if ($warnings) {
  74. $warnings = id(new AphrontErrorView())
  75. ->setErrors($warnings)
  76. ->setTitle(pht('Documentation Warnings'))
  77. ->setSeverity(AphrontErrorView::SEVERITY_WARNING);
  78. }
  79. $methods = $this->composeMethods($symbol);
  80. $field = 'default';
  81. $engine = id(new PhabricatorMarkupEngine())
  82. ->setViewer($viewer)
  83. ->addObject($symbol, $field);
  84. foreach ($methods as $method) {
  85. foreach ($method['atoms'] as $matom) {
  86. $engine->addObject($matom, $field);
  87. }
  88. }
  89. $engine->process();
  90. $content = $this->renderDocumentationText($symbol, $engine);
  91. $toc = $engine->getEngineMetadata(
  92. $symbol,
  93. $field,
  94. PhutilRemarkupEngineRemarkupHeaderBlockRule::KEY_HEADER_TOC,
  95. array());
  96. $document = id(new PHUIDocumentView())
  97. ->setBook($book->getTitle(), $group_name)
  98. ->setHeader($header)
  99. ->setFontKit(PHUIDocumentView::FONT_SOURCE_SANS)
  100. ->appendChild($properties)
  101. ->appendChild($warnings)
  102. ->appendChild($content);
  103. $document->appendChild($this->buildParametersAndReturn(array($symbol)));
  104. if ($methods) {
  105. $tasks = $this->composeTasks($symbol);
  106. if ($tasks) {
  107. $methods_by_task = igroup($methods, 'task');
  108. // Add phantom tasks for methods which have a "@task" name that isn't
  109. // documented anywhere, or methods that have no "@task" name.
  110. foreach ($methods_by_task as $task => $ignored) {
  111. if (empty($tasks[$task])) {
  112. $tasks[$task] = array(
  113. 'name' => $task,
  114. 'title' => $task ? $task : pht('Other Methods'),
  115. 'defined' => $symbol,
  116. );
  117. }
  118. }
  119. $section = id(new DivinerSectionView())
  120. ->setHeader(pht('Tasks'));
  121. foreach ($tasks as $spec) {
  122. $section->addContent(
  123. id(new PHUIHeaderView())
  124. ->setNoBackground(true)
  125. ->setHeader($spec['title']));
  126. $task_methods = idx($methods_by_task, $spec['name'], array());
  127. $inner_box = id(new PHUIBoxView())
  128. ->addPadding(PHUI::PADDING_LARGE_LEFT)
  129. ->addPadding(PHUI::PADDING_LARGE_RIGHT)
  130. ->addPadding(PHUI::PADDING_LARGE_BOTTOM);
  131. $box_content = array();
  132. if ($task_methods) {
  133. $list_items = array();
  134. foreach ($task_methods as $task_method) {
  135. $atom = last($task_method['atoms']);
  136. $item = $this->renderFullSignature($atom, true);
  137. if (strlen($atom->getSummary())) {
  138. $item = array(
  139. $item,
  140. " \xE2\x80\x94 ",
  141. $atom->getSummary());
  142. }
  143. $list_items[] = phutil_tag('li', array(), $item);
  144. }
  145. $box_content[] = phutil_tag(
  146. 'ul',
  147. array(
  148. 'class' => 'diviner-list',
  149. ),
  150. $list_items);
  151. } else {
  152. $no_methods = pht('No methods for this task.');
  153. $box_content = phutil_tag('em', array(), $no_methods);
  154. }
  155. $inner_box->appendChild($box_content);
  156. $section->addContent($inner_box);
  157. }
  158. $document->appendChild($section);
  159. }
  160. $section = id(new DivinerSectionView())
  161. ->setHeader(pht('Methods'));
  162. foreach ($methods as $spec) {
  163. $matom = last($spec['atoms']);
  164. $method_header = id(new PHUIHeaderView())
  165. ->setNoBackground(true);
  166. $inherited = $spec['inherited'];
  167. if ($inherited) {
  168. $method_header->addTag(
  169. id(new PHUITagView())
  170. ->setType(PHUITagView::TYPE_STATE)
  171. ->setBackgroundColor(PHUITagView::COLOR_GREY)
  172. ->setName(pht('Inherited')));
  173. }
  174. $method_header->setHeader($this->renderFullSignature($matom));
  175. $section->addContent(
  176. array(
  177. $method_header,
  178. $this->renderMethodDocumentationText($symbol, $spec, $engine),
  179. $this->buildParametersAndReturn($spec['atoms']),
  180. ));
  181. }
  182. $document->appendChild($section);
  183. }
  184. if ($toc) {
  185. $side = new PHUIListView();
  186. $side->addMenuItem(
  187. id(new PHUIListItemView())
  188. ->setName(pht('Contents'))
  189. ->setType(PHUIListItemView::TYPE_LABEL));
  190. foreach ($toc as $key => $entry) {
  191. $side->addMenuItem(
  192. id(new PHUIListItemView())
  193. ->setName($entry[1])
  194. ->setHref('#'.$key));
  195. }
  196. $document->setSideNav($side, PHUIDocumentView::NAV_TOP);
  197. }
  198. return $this->buildApplicationPage(
  199. array(
  200. $crumbs,
  201. $document,
  202. ),
  203. array(
  204. 'title' => $symbol->getTitle(),
  205. 'device' => true,
  206. ));
  207. }
  208. private function buildExtendsAndImplements(
  209. PHUIPropertyListView $view,
  210. DivinerLiveSymbol $symbol) {
  211. $lineage = $this->getExtendsLineage($symbol);
  212. if ($lineage) {
  213. $tags = array();
  214. foreach ($lineage as $item) {
  215. $tags[] = $this->renderAtomTag($item);
  216. }
  217. $caret = phutil_tag('span', array('class' => 'caret-right msl msr'));
  218. $tags = phutil_implode_html($caret, $tags);
  219. $view->addProperty(pht('Extends'), $tags);
  220. }
  221. $implements = $this->getImplementsLineage($symbol);
  222. if ($implements) {
  223. $items = array();
  224. foreach ($implements as $spec) {
  225. $via = $spec['via'];
  226. $iface = $spec['interface'];
  227. if ($via == $symbol) {
  228. $items[] = $this->renderAtomTag($iface);
  229. } else {
  230. $items[] = array(
  231. $this->renderAtomTag($iface),
  232. " \xE2\x97\x80 ",
  233. $this->renderAtomTag($via));
  234. }
  235. }
  236. $view->addProperty(
  237. pht('Implements'),
  238. phutil_implode_html(phutil_tag('br'), $items));
  239. }
  240. }
  241. private function renderAtomTag(DivinerLiveSymbol $symbol) {
  242. return id(new PHUITagView())
  243. ->setType(PHUITagView::TYPE_OBJECT)
  244. ->setName($symbol->getName())
  245. ->setHref($symbol->getURI());
  246. }
  247. private function getExtendsLineage(DivinerLiveSymbol $symbol) {
  248. foreach ($symbol->getExtends() as $extends) {
  249. if ($extends->getType() == 'class') {
  250. $lineage = $this->getExtendsLineage($extends);
  251. $lineage[] = $extends;
  252. return $lineage;
  253. }
  254. }
  255. return array();
  256. }
  257. private function getImplementsLineage(DivinerLiveSymbol $symbol) {
  258. $implements = array();
  259. // Do these first so we get interfaces ordered from most to least specific.
  260. foreach ($symbol->getExtends() as $extends) {
  261. if ($extends->getType() == 'interface') {
  262. $implements[$extends->getName()] = array(
  263. 'interface' => $extends,
  264. 'via' => $symbol,
  265. );
  266. }
  267. }
  268. // Now do parent interfaces.
  269. foreach ($symbol->getExtends() as $extends) {
  270. if ($extends->getType() == 'class') {
  271. $implements += $this->getImplementsLineage($extends);
  272. }
  273. }
  274. return $implements;
  275. }
  276. private function buildDefined(
  277. PHUIPropertyListView $view,
  278. DivinerLiveSymbol $symbol) {
  279. $atom = $symbol->getAtom();
  280. $defined = $atom->getFile().':'.$atom->getLine();
  281. $link = $symbol->getBook()->getConfig('uri.source');
  282. if ($link) {
  283. $link = strtr(
  284. $link,
  285. array(
  286. '%%' => '%',
  287. '%f' => phutil_escape_uri($atom->getFile()),
  288. '%l' => phutil_escape_uri($atom->getLine()),
  289. ));
  290. $defined = phutil_tag(
  291. 'a',
  292. array(
  293. 'href' => $link,
  294. 'target' => '_blank',
  295. ),
  296. $defined);
  297. }
  298. $view->addProperty(pht('Defined'), $defined);
  299. }
  300. private function composeMethods(DivinerLiveSymbol $symbol) {
  301. $methods = $this->findMethods($symbol);
  302. if (!$methods) {
  303. return $methods;
  304. }
  305. foreach ($methods as $name => $method) {
  306. // Check for "@task" on each parent, to find the most recently declared
  307. // "@task".
  308. $task = null;
  309. foreach ($method['atoms'] as $key => $method_symbol) {
  310. $atom = $method_symbol->getAtom();
  311. if ($atom->getDocblockMetaValue('task')) {
  312. $task = $atom->getDocblockMetaValue('task');
  313. }
  314. }
  315. $methods[$name]['task'] = $task;
  316. // Set 'inherited' if this atom has no implementation of the method.
  317. if (last($method['implementations']) !== $symbol) {
  318. $methods[$name]['inherited'] = true;
  319. } else {
  320. $methods[$name]['inherited'] = false;
  321. }
  322. }
  323. return $methods;
  324. }
  325. private function findMethods(DivinerLiveSymbol $symbol) {
  326. $child_specs = array();
  327. foreach ($symbol->getExtends() as $extends) {
  328. if ($extends->getType() == DivinerAtom::TYPE_CLASS) {
  329. $child_specs = $this->findMethods($extends);
  330. }
  331. }
  332. foreach ($symbol->getChildren() as $child) {
  333. if ($child->getType() == DivinerAtom::TYPE_METHOD) {
  334. $name = $child->getName();
  335. if (isset($child_specs[$name])) {
  336. $child_specs[$name]['atoms'][] = $child;
  337. $child_specs[$name]['implementations'][] = $symbol;
  338. } else {
  339. $child_specs[$name] = array(
  340. 'atoms' => array($child),
  341. 'defined' => $symbol,
  342. 'implementations' => array($symbol),
  343. );
  344. }
  345. }
  346. }
  347. return $child_specs;
  348. }
  349. private function composeTasks(DivinerLiveSymbol $symbol) {
  350. $extends_task_specs = array();
  351. foreach ($symbol->getExtends() as $extends) {
  352. $extends_task_specs += $this->composeTasks($extends);
  353. }
  354. $task_specs = array();
  355. $tasks = $symbol->getAtom()->getDocblockMetaValue('task');
  356. if (strlen($tasks)) {
  357. $tasks = phutil_split_lines($tasks, $retain_endings = false);
  358. foreach ($tasks as $task) {
  359. list($name, $title) = explode(' ', $task, 2);
  360. $name = trim($name);
  361. $title = trim($title);
  362. $task_specs[$name] = array(
  363. 'name' => $name,
  364. 'title' => $title,
  365. 'defined' => $symbol,
  366. );
  367. }
  368. }
  369. $specs = $task_specs + $extends_task_specs;
  370. // Reorder "@tasks" in original declaration order. Basically, we want to
  371. // use the documentation of the closest subclass, but put tasks which
  372. // were declared by parents first.
  373. $keys = array_keys($extends_task_specs);
  374. $specs = array_select_keys($specs, $keys) + $specs;
  375. return $specs;
  376. }
  377. private function renderFullSignature(
  378. DivinerLiveSymbol $symbol,
  379. $is_link = false) {
  380. switch ($symbol->getType()) {
  381. case DivinerAtom::TYPE_CLASS:
  382. case DivinerAtom::TYPE_INTERFACE:
  383. case DivinerAtom::TYPE_METHOD:
  384. case DivinerAtom::TYPE_FUNCTION:
  385. break;
  386. default:
  387. return $symbol->getTitle();
  388. }
  389. $atom = $symbol->getAtom();
  390. $out = array();
  391. if ($atom->getProperty('final')) {
  392. $out[] = 'final';
  393. }
  394. if ($atom->getProperty('abstract')) {
  395. $out[] = 'abstract';
  396. }
  397. if ($atom->getProperty('access')) {
  398. $out[] = $atom->getProperty('access');
  399. }
  400. if ($atom->getProperty('static')) {
  401. $out[] = 'static';
  402. }
  403. switch ($symbol->getType()) {
  404. case DivinerAtom::TYPE_CLASS:
  405. case DivinerAtom::TYPE_INTERFACE:
  406. $out[] = $symbol->getType();
  407. break;
  408. case DivinerAtom::TYPE_FUNCTION:
  409. switch ($atom->getLanguage()) {
  410. case 'php':
  411. $out[] = $symbol->getType();
  412. break;
  413. }
  414. break;
  415. case DivinerAtom::TYPE_METHOD:
  416. switch ($atom->getLanguage()) {
  417. case 'php':
  418. $out[] = DivinerAtom::TYPE_FUNCTION;
  419. break;
  420. }
  421. break;
  422. }
  423. $anchor = null;
  424. switch ($symbol->getType()) {
  425. case DivinerAtom::TYPE_METHOD:
  426. $anchor = $symbol->getType().'/'.$symbol->getName();
  427. break;
  428. default:
  429. break;
  430. }
  431. $out[] = phutil_tag(
  432. $anchor ? 'a' : 'span',
  433. array(
  434. 'class' => 'diviner-atom-signature-name',
  435. 'href' => $anchor ? '#'.$anchor : null,
  436. 'name' => $is_link ? null : $anchor,
  437. ),
  438. $symbol->getName());
  439. $out = phutil_implode_html(' ', $out);
  440. $parameters = $atom->getProperty('parameters');
  441. if ($parameters !== null) {
  442. $pout = array();
  443. foreach ($parameters as $parameter) {
  444. $pout[] = idx($parameter, 'name', '...');
  445. }
  446. $out = array($out, '('.implode(', ', $pout).')');
  447. }
  448. return phutil_tag(
  449. 'span',
  450. array(
  451. 'class' => 'diviner-atom-signature',
  452. ),
  453. $out);
  454. }
  455. private function buildParametersAndReturn(array $symbols) {
  456. assert_instances_of($symbols, 'DivinerLiveSymbol');
  457. $symbols = array_reverse($symbols);
  458. $out = array();
  459. $collected_parameters = null;
  460. foreach ($symbols as $symbol) {
  461. $parameters = $symbol->getAtom()->getProperty('parameters');
  462. if ($parameters !== null) {
  463. if ($collected_parameters === null) {
  464. $collected_parameters = array();
  465. }
  466. foreach ($parameters as $key => $parameter) {
  467. if (isset($collected_parameters[$key])) {
  468. $collected_parameters[$key] += $parameter;
  469. } else {
  470. $collected_parameters[$key] = $parameter;
  471. }
  472. }
  473. }
  474. }
  475. if (nonempty($parameters)) {
  476. $out[] = id(new DivinerParameterTableView())
  477. ->setHeader(pht('Parameters'))
  478. ->setParameters($parameters);
  479. }
  480. $collected_return = null;
  481. foreach ($symbols as $symbol) {
  482. $return = $symbol->getAtom()->getProperty('return');
  483. if ($return) {
  484. if ($collected_return) {
  485. $collected_return += $return;
  486. } else {
  487. $collected_return = $return;
  488. }
  489. }
  490. }
  491. if (nonempty($return)) {
  492. $out[] = id(new DivinerReturnTableView())
  493. ->setHeader(pht('Return'))
  494. ->setReturn($collected_return);
  495. }
  496. return $out;
  497. }
  498. private function renderDocumentationText(
  499. DivinerLiveSymbol $symbol,
  500. PhabricatorMarkupEngine $engine) {
  501. $field = 'default';
  502. $content = $engine->getOutput($symbol, $field);
  503. if (strlen(trim($symbol->getMarkupText($field)))) {
  504. $content = phutil_tag(
  505. 'div',
  506. array(
  507. 'class' => 'phabricator-remarkup',
  508. ),
  509. $content);
  510. } else {
  511. $atom = $symbol->getAtom();
  512. $content = phutil_tag(
  513. 'div',
  514. array(
  515. 'class' => 'diviner-message-not-documented',
  516. ),
  517. DivinerAtom::getThisAtomIsNotDocumentedString($atom->getType()));
  518. }
  519. return $content;
  520. }
  521. private function renderMethodDocumentationText(
  522. DivinerLiveSymbol $parent,
  523. array $spec,
  524. PhabricatorMarkupEngine $engine) {
  525. $symbols = array_values($spec['atoms']);
  526. $implementations = array_values($spec['implementations']);
  527. $field = 'default';
  528. $out = array();
  529. foreach ($symbols as $key => $symbol) {
  530. $impl = $implementations[$key];
  531. if ($impl !== $parent) {
  532. if (!strlen(trim($symbol->getMarkupText($field)))) {
  533. continue;
  534. }
  535. }
  536. $doc = $this->renderDocumentationText($symbol, $engine);
  537. if (($impl !== $parent) || $out) {
  538. $where = id(new PHUIBoxView())
  539. ->addPadding(PHUI::PADDING_MEDIUM_LEFT)
  540. ->addPadding(PHUI::PADDING_MEDIUM_RIGHT)
  541. ->addClass('diviner-method-implementation-header')
  542. ->appendChild($impl->getName());
  543. $doc = array($where, $doc);
  544. if ($impl !== $parent) {
  545. $doc = phutil_tag(
  546. 'div',
  547. array(
  548. 'class' => 'diviner-method-implementation-inherited',
  549. ),
  550. $doc);
  551. }
  552. }
  553. $out[] = $doc;
  554. }
  555. // If we only have inherited implementations but none have documentation,
  556. // render the last one here so we get the "this thing has no documentation"
  557. // element.
  558. if (!$out) {
  559. $out[] = $this->renderDocumentationText($symbol, $engine);
  560. }
  561. return $out;
  562. }
  563. }