@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
3/**
4 * @extends PhabricatorCursorPagedPolicyAwareQuery<DivinerLiveSymbol>
5 */
6final class DivinerAtomQuery extends PhabricatorCursorPagedPolicyAwareQuery {
7
8 private $ids;
9 private $phids;
10 private $bookPHIDs;
11 private $names;
12 private $types;
13 private $contexts;
14 private $indexes;
15 private $isDocumentable;
16 private $isGhost;
17 private $nodeHashes;
18 private $titles;
19 private $nameContains;
20 private $repositoryPHIDs;
21
22 private $needAtoms;
23 private $needExtends;
24 private $needChildren;
25 private $needRepositories;
26
27 public function withIDs(array $ids) {
28 $this->ids = $ids;
29 return $this;
30 }
31
32 public function withPHIDs(array $phids) {
33 $this->phids = $phids;
34 return $this;
35 }
36
37 public function withBookPHIDs(array $phids) {
38 $this->bookPHIDs = $phids;
39 return $this;
40 }
41
42 public function withTypes(array $types) {
43 $this->types = $types;
44 return $this;
45 }
46
47 public function withNames(array $names) {
48 $this->names = $names;
49 return $this;
50 }
51
52 public function withContexts(array $contexts) {
53 $this->contexts = $contexts;
54 return $this;
55 }
56
57 public function withIndexes(array $indexes) {
58 $this->indexes = $indexes;
59 return $this;
60 }
61
62 public function withNodeHashes(array $hashes) {
63 $this->nodeHashes = $hashes;
64 return $this;
65 }
66
67 public function withTitles($titles) {
68 $this->titles = $titles;
69 return $this;
70 }
71
72 public function withNameContains($text) {
73 $this->nameContains = $text;
74 return $this;
75 }
76
77 public function needAtoms($need) {
78 $this->needAtoms = $need;
79 return $this;
80 }
81
82 public function needChildren($need) {
83 $this->needChildren = $need;
84 return $this;
85 }
86
87 /**
88 * Include or exclude "ghosts", which are symbols which used to exist but do
89 * not exist currently (for example, a function which existed in an older
90 * version of the codebase but was deleted).
91 *
92 * These symbols had PHIDs assigned to them, and may have other sorts of
93 * metadata that we don't want to lose (like comments or flags), so we don't
94 * delete them outright. They might also come back in the future: the change
95 * which deleted the symbol might be reverted, or the documentation might
96 * have been generated incorrectly by accident. In these cases, we can
97 * restore the original data.
98 *
99 * @param bool $ghosts
100 * @return $this
101 */
102 public function withGhosts($ghosts) {
103 $this->isGhost = $ghosts;
104 return $this;
105 }
106
107 public function needExtends($need) {
108 $this->needExtends = $need;
109 return $this;
110 }
111
112 public function withIsDocumentable($documentable) {
113 $this->isDocumentable = $documentable;
114 return $this;
115 }
116
117 public function withRepositoryPHIDs(array $repository_phids) {
118 $this->repositoryPHIDs = $repository_phids;
119 return $this;
120 }
121
122 public function needRepositories($need_repositories) {
123 $this->needRepositories = $need_repositories;
124 return $this;
125 }
126
127 protected function loadPage() {
128 $table = new DivinerLiveSymbol();
129 $conn_r = $table->establishConnection('r');
130
131 $data = queryfx_all(
132 $conn_r,
133 'SELECT * FROM %T %Q %Q %Q',
134 $table->getTableName(),
135 $this->buildWhereClause($conn_r),
136 $this->buildOrderClause($conn_r),
137 $this->buildLimitClause($conn_r));
138
139 return $table->loadAllFromArray($data);
140 }
141
142 /**
143 * @param array<DivinerLiveSymbol> $atoms
144 */
145 protected function willFilterPage(array $atoms) {
146 assert_instances_of($atoms, DivinerLiveSymbol::class);
147
148 $books = array_unique(mpull($atoms, 'getBookPHID'));
149
150 $books = id(new DivinerBookQuery())
151 ->setViewer($this->getViewer())
152 ->withPHIDs($books)
153 ->execute();
154 $books = mpull($books, null, 'getPHID');
155
156 foreach ($atoms as $key => $atom) {
157 $book = idx($books, $atom->getBookPHID());
158 if (!$book) {
159 $this->didRejectResult($atom);
160 unset($atoms[$key]);
161 continue;
162 }
163 $atom->attachBook($book);
164 }
165
166 if ($this->needAtoms) {
167 $atom_data = id(new DivinerLiveAtom())->loadAllWhere(
168 'symbolPHID IN (%Ls)',
169 mpull($atoms, 'getPHID'));
170 $atom_data = mpull($atom_data, null, 'getSymbolPHID');
171
172 foreach ($atoms as $key => $atom) {
173 $data = idx($atom_data, $atom->getPHID());
174 $atom->attachAtom($data);
175 }
176 }
177
178 // Load all of the symbols this symbol extends, recursively. Commonly,
179 // this means all the ancestor classes and interfaces it extends and
180 // implements.
181 if ($this->needExtends) {
182 // First, load all the matching symbols by name. This does 99% of the
183 // work in most cases, assuming things are named at all reasonably.
184 $names = array();
185 foreach ($atoms as $atom) {
186 if (!$atom->getAtom()) {
187 continue;
188 }
189
190 foreach ($atom->getAtom()->getExtends() as $xref) {
191 $names[] = $xref->getName();
192 }
193 }
194
195 if ($names) {
196 $xatoms = id(new DivinerAtomQuery())
197 ->setViewer($this->getViewer())
198 ->withNames($names)
199 ->withGhosts(false)
200 ->needExtends(true)
201 ->needAtoms(true)
202 ->needChildren($this->needChildren)
203 ->execute();
204 $xatoms = mgroup($xatoms, 'getName', 'getType', 'getBookPHID');
205 } else {
206 $xatoms = array();
207 }
208
209 foreach ($atoms as $atom) {
210 $atom_lang = null;
211 $atom_extends = array();
212
213 if ($atom->getAtom()) {
214 $atom_lang = $atom->getAtom()->getLanguage();
215 $atom_extends = $atom->getAtom()->getExtends();
216 }
217
218 $extends = array();
219
220 foreach ($atom_extends as $xref) {
221 // If there are no symbols of the matching name and type, we can't
222 // resolve this.
223 if (empty($xatoms[$xref->getName()][$xref->getType()])) {
224 continue;
225 }
226
227 // If we found matches in the same documentation book, prefer them
228 // over other matches. Otherwise, look at all the matches.
229 $matches = $xatoms[$xref->getName()][$xref->getType()];
230 if (isset($matches[$atom->getBookPHID()])) {
231 $maybe = $matches[$atom->getBookPHID()];
232 } else {
233 $maybe = array_mergev($matches);
234 }
235
236 if (!$maybe) {
237 continue;
238 }
239
240 // Filter out matches in a different language, since, e.g., PHP
241 // classes can not implement JS classes.
242 $same_lang = array();
243 foreach ($maybe as $xatom) {
244 if ($xatom->getAtom()->getLanguage() == $atom_lang) {
245 $same_lang[] = $xatom;
246 }
247 }
248
249 if (!$same_lang) {
250 continue;
251 }
252
253 // If we have duplicates remaining, just pick the first one. There's
254 // nothing more we can do to figure out which is the real one.
255 $extends[] = head($same_lang);
256 }
257
258 $atom->attachExtends($extends);
259 }
260 }
261
262 if ($this->needChildren) {
263 $child_hashes = $this->getAllChildHashes($atoms, $this->needExtends);
264
265 if ($child_hashes) {
266 $children = id(new DivinerAtomQuery())
267 ->setViewer($this->getViewer())
268 ->withNodeHashes($child_hashes)
269 ->needAtoms($this->needAtoms)
270 ->execute();
271
272 $children = mpull($children, null, 'getNodeHash');
273 } else {
274 $children = array();
275 }
276
277 $this->attachAllChildren($atoms, $children, $this->needExtends);
278 }
279
280 if ($this->needRepositories) {
281 $repositories = id(new PhabricatorRepositoryQuery())
282 ->setViewer($this->getViewer())
283 ->withPHIDs(mpull($atoms, 'getRepositoryPHID'))
284 ->execute();
285 $repositories = mpull($repositories, null, 'getPHID');
286
287 foreach ($atoms as $key => $atom) {
288 if ($atom->getRepositoryPHID() === null) {
289 $atom->attachRepository(null);
290 continue;
291 }
292
293 $repository = idx($repositories, $atom->getRepositoryPHID());
294
295 if (!$repository) {
296 $this->didRejectResult($atom);
297 unset($atoms[$key]);
298 continue;
299 }
300
301 $atom->attachRepository($repository);
302 }
303 }
304
305 return $atoms;
306 }
307
308 protected function buildWhereClause(AphrontDatabaseConnection $conn) {
309 $where = array();
310
311 if ($this->ids) {
312 $where[] = qsprintf(
313 $conn,
314 'id IN (%Ld)',
315 $this->ids);
316 }
317
318 if ($this->phids) {
319 $where[] = qsprintf(
320 $conn,
321 'phid IN (%Ls)',
322 $this->phids);
323 }
324
325 if ($this->bookPHIDs) {
326 $where[] = qsprintf(
327 $conn,
328 'bookPHID IN (%Ls)',
329 $this->bookPHIDs);
330 }
331
332 if ($this->types) {
333 $where[] = qsprintf(
334 $conn,
335 'type IN (%Ls)',
336 $this->types);
337 }
338
339 if ($this->names) {
340 $where[] = qsprintf(
341 $conn,
342 'name IN (%Ls)',
343 $this->names);
344 }
345
346 if ($this->titles) {
347 $hashes = array();
348
349 foreach ($this->titles as $title) {
350 $slug = DivinerAtomRef::normalizeTitleString($title);
351 $hash = PhabricatorHash::digestForIndex($slug);
352 $hashes[] = $hash;
353 }
354
355 $where[] = qsprintf(
356 $conn,
357 'titleSlugHash in (%Ls)',
358 $hashes);
359 }
360
361 if ($this->contexts) {
362 $with_null = false;
363 $contexts = $this->contexts;
364
365 foreach ($contexts as $key => $value) {
366 if ($value === null) {
367 unset($contexts[$key]);
368 $with_null = true;
369 continue;
370 }
371 }
372
373 if ($contexts && $with_null) {
374 $where[] = qsprintf(
375 $conn,
376 'context IN (%Ls) OR context IS NULL',
377 $contexts);
378 } else if ($contexts) {
379 $where[] = qsprintf(
380 $conn,
381 'context IN (%Ls)',
382 $contexts);
383 } else if ($with_null) {
384 $where[] = qsprintf(
385 $conn,
386 'context IS NULL');
387 }
388 }
389
390 if ($this->indexes) {
391 $where[] = qsprintf(
392 $conn,
393 'atomIndex IN (%Ld)',
394 $this->indexes);
395 }
396
397 if ($this->isDocumentable !== null) {
398 $where[] = qsprintf(
399 $conn,
400 'isDocumentable = %d',
401 (int)$this->isDocumentable);
402 }
403
404 if ($this->isGhost !== null) {
405 if ($this->isGhost) {
406 $where[] = qsprintf($conn, 'graphHash IS NULL');
407 } else {
408 $where[] = qsprintf($conn, 'graphHash IS NOT NULL');
409 }
410 }
411
412 if ($this->nodeHashes) {
413 $where[] = qsprintf(
414 $conn,
415 'nodeHash IN (%Ls)',
416 $this->nodeHashes);
417 }
418
419 if ($this->nameContains) {
420 // NOTE: This `CONVERT()` call makes queries case-insensitive, since
421 // the column has binary collation. Eventually, this should move into
422 // fulltext.
423 $where[] = qsprintf(
424 $conn,
425 'CONVERT(name USING utf8) LIKE %~',
426 $this->nameContains);
427 }
428
429 if ($this->repositoryPHIDs) {
430 $where[] = qsprintf(
431 $conn,
432 'repositoryPHID IN (%Ls)',
433 $this->repositoryPHIDs);
434 }
435
436 $where[] = $this->buildPagingClause($conn);
437
438 return $this->formatWhereClause($conn, $where);
439 }
440
441 /**
442 * Walk a list of atoms and collect all the node hashes of the atoms'
443 * children. When recursing, also walk up the tree and collect children of
444 * atoms they extend.
445 *
446 * @param array<DivinerLiveSymbol> $symbols List of symbols to collect child
447 * hashes of.
448 * @param bool $recurse_up True to collect children of
449 * extended atoms, as well.
450 * @return map<string, string> Hashes of atoms' children.
451 */
452 private function getAllChildHashes(array $symbols, $recurse_up) {
453 assert_instances_of($symbols, DivinerLiveSymbol::class);
454
455 $hashes = array();
456 foreach ($symbols as $symbol) {
457 $child_hashes = array();
458
459 if ($symbol->getAtom()) {
460 $child_hashes = $symbol->getAtom()->getChildHashes();
461 }
462
463 foreach ($child_hashes as $hash) {
464 $hashes[$hash] = $hash;
465 }
466
467 if ($recurse_up) {
468 $hashes += $this->getAllChildHashes($symbol->getExtends(), true);
469 }
470 }
471
472 return $hashes;
473 }
474
475 /**
476 * Attach child atoms to existing atoms. In recursive mode, also attach child
477 * atoms to atoms that these atoms extend.
478 *
479 * @param array<DivinerLiveSymbol> $symbols List of symbols to attach
480 * children to.
481 * @param map<string, DivinerLiveSymbol> $children Map of symbols, keyed by
482 * node hash.
483 * @param bool $recurse_up True to attach children to extended atoms, as
484 * well.
485 * @return void
486 */
487 private function attachAllChildren(
488 array $symbols,
489 array $children,
490 $recurse_up) {
491
492 assert_instances_of($symbols, DivinerLiveSymbol::class);
493 assert_instances_of($children, DivinerLiveSymbol::class);
494
495 foreach ($symbols as $symbol) {
496 $child_hashes = array();
497 $symbol_children = array();
498
499 if ($symbol->getAtom()) {
500 $child_hashes = $symbol->getAtom()->getChildHashes();
501 }
502
503 foreach ($child_hashes as $hash) {
504 if (isset($children[$hash])) {
505 $symbol_children[] = $children[$hash];
506 }
507 }
508
509 $symbol->attachChildren($symbol_children);
510
511 if ($recurse_up) {
512 $this->attachAllChildren($symbol->getExtends(), $children, true);
513 }
514 }
515 }
516
517 public function getQueryApplicationClass() {
518 return PhabricatorDivinerApplication::class;
519 }
520
521}