@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<?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}