Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
freebsd
GitHub Repository: freebsd/phabricator
Path: blob/master/src/applications/diviner/controller/DivinerAtomController.php
12256 views
1
<?php
2
3
final 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 (strlen($tasks)) {
441
$tasks = array($tasks);
442
} else {
443
$tasks = array();
444
}
445
}
446
447
if ($tasks) {
448
foreach ($tasks as $task) {
449
list($name, $title) = explode(' ', $task, 2);
450
$name = trim($name);
451
$title = trim($title);
452
453
$task_specs[$name] = array(
454
'name' => $name,
455
'title' => $title,
456
'defined' => $symbol,
457
);
458
}
459
}
460
461
$specs = $task_specs + $extends_task_specs;
462
463
// Reorder "@tasks" in original declaration order. Basically, we want to
464
// use the documentation of the closest subclass, but put tasks which
465
// were declared by parents first.
466
$keys = array_keys($extends_task_specs);
467
$specs = array_select_keys($specs, $keys) + $specs;
468
469
return $specs;
470
}
471
472
private function renderFullSignature(
473
DivinerLiveSymbol $symbol,
474
$is_link = false) {
475
476
switch ($symbol->getType()) {
477
case DivinerAtom::TYPE_CLASS:
478
case DivinerAtom::TYPE_INTERFACE:
479
case DivinerAtom::TYPE_METHOD:
480
case DivinerAtom::TYPE_FUNCTION:
481
break;
482
default:
483
return $symbol->getTitle();
484
}
485
486
$atom = $symbol->getAtom();
487
488
$out = array();
489
490
if ($atom) {
491
if ($atom->getProperty('final')) {
492
$out[] = 'final';
493
}
494
495
if ($atom->getProperty('abstract')) {
496
$out[] = 'abstract';
497
}
498
499
if ($atom->getProperty('access')) {
500
$out[] = $atom->getProperty('access');
501
}
502
503
if ($atom->getProperty('static')) {
504
$out[] = 'static';
505
}
506
}
507
508
switch ($symbol->getType()) {
509
case DivinerAtom::TYPE_CLASS:
510
case DivinerAtom::TYPE_INTERFACE:
511
$out[] = $symbol->getType();
512
break;
513
case DivinerAtom::TYPE_FUNCTION:
514
switch ($atom->getLanguage()) {
515
case 'php':
516
$out[] = $symbol->getType();
517
break;
518
}
519
break;
520
case DivinerAtom::TYPE_METHOD:
521
switch ($atom->getLanguage()) {
522
case 'php':
523
$out[] = DivinerAtom::TYPE_FUNCTION;
524
break;
525
}
526
break;
527
}
528
529
$anchor = null;
530
switch ($symbol->getType()) {
531
case DivinerAtom::TYPE_METHOD:
532
$anchor = $symbol->getType().'/'.$symbol->getName();
533
break;
534
default:
535
break;
536
}
537
538
$out[] = phutil_tag(
539
$anchor ? 'a' : 'span',
540
array(
541
'class' => 'diviner-atom-signature-name',
542
'href' => $anchor ? '#'.$anchor : null,
543
'name' => $is_link ? null : $anchor,
544
),
545
$symbol->getName());
546
547
$out = phutil_implode_html(' ', $out);
548
549
if ($atom) {
550
$parameters = $atom->getProperty('parameters');
551
if ($parameters !== null) {
552
$pout = array();
553
foreach ($parameters as $parameter) {
554
$pout[] = idx($parameter, 'name', '...');
555
}
556
$out = array($out, '('.implode(', ', $pout).')');
557
}
558
}
559
560
return phutil_tag(
561
'span',
562
array(
563
'class' => 'diviner-atom-signature',
564
),
565
$out);
566
}
567
568
private function buildParametersAndReturn(array $symbols) {
569
assert_instances_of($symbols, 'DivinerLiveSymbol');
570
571
$symbols = array_reverse($symbols);
572
$out = array();
573
574
$collected_parameters = null;
575
foreach ($symbols as $symbol) {
576
$parameters = $symbol->getAtom()->getProperty('parameters');
577
if ($parameters !== null) {
578
if ($collected_parameters === null) {
579
$collected_parameters = array();
580
}
581
foreach ($parameters as $key => $parameter) {
582
if (isset($collected_parameters[$key])) {
583
$collected_parameters[$key] += $parameter;
584
} else {
585
$collected_parameters[$key] = $parameter;
586
}
587
}
588
}
589
}
590
591
if (nonempty($parameters)) {
592
$out[] = id(new DivinerParameterTableView())
593
->setHeader(pht('Parameters'))
594
->setParameters($parameters);
595
}
596
597
$collected_return = null;
598
foreach ($symbols as $symbol) {
599
$return = $symbol->getAtom()->getProperty('return');
600
if ($return) {
601
if ($collected_return) {
602
$collected_return += $return;
603
} else {
604
$collected_return = $return;
605
}
606
}
607
}
608
609
if (nonempty($return)) {
610
$out[] = id(new DivinerReturnTableView())
611
->setHeader(pht('Return'))
612
->setReturn($collected_return);
613
}
614
615
return $out;
616
}
617
618
private function renderDocumentationText(
619
DivinerLiveSymbol $symbol,
620
PhabricatorMarkupEngine $engine) {
621
622
$field = 'default';
623
$content = $engine->getOutput($symbol, $field);
624
625
if (strlen(trim($symbol->getMarkupText($field)))) {
626
$content = phutil_tag(
627
'div',
628
array(
629
'class' => 'phabricator-remarkup diviner-remarkup-section',
630
),
631
$content);
632
} else {
633
$atom = $symbol->getAtom();
634
$content = phutil_tag(
635
'div',
636
array(
637
'class' => 'diviner-message-not-documented',
638
),
639
DivinerAtom::getThisAtomIsNotDocumentedString($atom->getType()));
640
}
641
642
return $content;
643
}
644
645
private function renderMethodDocumentationText(
646
DivinerLiveSymbol $parent,
647
array $spec,
648
PhabricatorMarkupEngine $engine) {
649
650
$symbols = array_values($spec['atoms']);
651
$implementations = array_values($spec['implementations']);
652
653
$field = 'default';
654
655
$out = array();
656
foreach ($symbols as $key => $symbol) {
657
$impl = $implementations[$key];
658
if ($impl !== $parent) {
659
if (!strlen(trim($symbol->getMarkupText($field)))) {
660
continue;
661
}
662
}
663
664
$doc = $this->renderDocumentationText($symbol, $engine);
665
666
if (($impl !== $parent) || $out) {
667
$where = id(new PHUIBoxView())
668
->addClass('diviner-method-implementation-header')
669
->appendChild($impl->getName());
670
$doc = array($where, $doc);
671
672
if ($impl !== $parent) {
673
$doc = phutil_tag(
674
'div',
675
array(
676
'class' => 'diviner-method-implementation-inherited',
677
),
678
$doc);
679
}
680
}
681
682
$out[] = $doc;
683
}
684
685
// If we only have inherited implementations but none have documentation,
686
// render the last one here so we get the "this thing has no documentation"
687
// element.
688
if (!$out) {
689
$out[] = $this->renderDocumentationText($symbol, $engine);
690
}
691
692
return $out;
693
}
694
695
}
696
697