@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 * @phutil-external-symbol class Peast\Peast
5 * @phutil-external-symbol class Peast\Syntax\Node\Node
6 * @phutil-external-symbol class Peast\Syntax\Node\AssignmentExpression
7 * @phutil-external-symbol class Peast\Syntax\Node\CallExpression
8 * @phutil-external-symbol class Peast\Syntax\Node\FunctionExpression
9 * @phutil-external-symbol class Peast\Syntax\Node\Identifier
10 * @phutil-external-symbol class Peast\Syntax\Node\LogicalExpression
11 * @phutil-external-symbol class Peast\Syntax\Node\MemberExpression
12 * @phutil-external-symbol class Peast\Syntax\Node\ObjectExpression
13 * @phutil-external-symbol class Peast\Syntax\Node\StringLiteral
14 */
15final class DivinerJavelinAtomizer extends DivinerAtomizer {
16
17 protected function newAtom($type) {
18 return parent::newAtom($type)->setLanguage('js');
19 }
20
21 protected function executeAtomize($file_name, $file_data) {
22 JavelinPeastLibrary::loadLibrary();
23
24 $atoms = array();
25
26 $ast = Peast\Peast::latest($file_data)->parse();
27 $ast->traverse(function (Peast\Syntax\Node\Node $node) use (&$atoms) {
28 if ($node instanceof Peast\Syntax\Node\CallExpression) {
29 foreach ($this->parseCall($node) as $atom) {
30 $atoms[] = $atom;
31 }
32 } else if ($node instanceof Peast\Syntax\Node\AssignmentExpression) {
33 $atom = $this->parseAssignment($node);
34
35 if ($atom) {
36 $atoms[] = $atom;
37 }
38 }
39 });
40
41 $dparser = new PhutilDocblockParser();
42 $blocks = $dparser->extractDocblocks($file_data);
43
44 // Reject the first docblock as a header block.
45 array_shift($blocks);
46
47 $map = array();
48 foreach ($blocks as $data) {
49 list($block, $line) = $data;
50 $map[$line] = $block;
51 }
52
53 $atoms = mpull($atoms, null, 'getLine');
54 ksort($atoms);
55 end($atoms);
56 $last = key($atoms);
57
58 $block_map = array();
59 $pointer = null;
60 for ($ii = 1; $ii <= $last; $ii++) {
61 if (isset($map[$ii])) {
62 $pointer = $ii;
63 }
64 $block_map[$ii] = $pointer;
65 }
66
67 /** @var DivinerAtom $atom */
68 foreach ($atoms as $atom) {
69 $block_id = $block_map[$atom->getLine()];
70 if ($block_id !== null && isset($map[$block_id])) {
71 $atom->setDocblockRaw($map[$block_id]);
72 unset($map[$block_id]);
73 } else {
74 continue;
75 }
76
77 if (
78 $atom->getType() === DivinerAtom::TYPE_METHOD ||
79 $atom->getType() === DivinerAtom::TYPE_FUNCTION) {
80
81 $this->parseReturnDoc($atom);
82 $this->parseParametersDoc($atom);
83 }
84 }
85
86 return $atoms;
87 }
88
89 private function parseCall(
90 Peast\Syntax\Node\CallExpression $call): array {
91
92 $callee = $call->getCallee();
93
94 if (!($callee instanceof Peast\Syntax\Node\MemberExpression)) {
95 return array();
96 }
97
98 $object = $callee->getObject();
99 $property = $callee->getProperty();
100 if (
101 !($object instanceof Peast\Syntax\Node\Identifier) ||
102 !($property instanceof Peast\Syntax\Node\Identifier)) {
103 return array();
104 }
105
106 if ($object->getName() !== 'JX') {
107 return array();
108 }
109
110 if ($property->getName() !== 'install') {
111 return array();
112 }
113
114 $arguments = $call->getArguments();
115 if (count($arguments) < 2) {
116 return array();
117 }
118
119 list($install_name, $definition) = $arguments;
120 if (
121 !($install_name instanceof Peast\Syntax\Node\StringLiteral) ||
122 !($definition instanceof Peast\Syntax\Node\ObjectExpression)) {
123
124 return array();
125 }
126
127 list($class, $methods) = $this->parseClassDefinition($definition);
128 $class->setLine($call->getLocation()->getStart()->getLine())
129 ->setName('JX.'.$install_name->getValue());
130
131 if (!$class->getExtends() && $class->getName() !== 'JX.Base') {
132 $class->addExtends(
133 $this->newRef(DivinerAtom::TYPE_CLASS, 'JX.Base'));
134 }
135
136 $atoms = $methods;
137 $atoms[] = $class;
138
139 return $atoms;
140 }
141
142 private function parseAssignment(
143 Peast\Syntax\Node\AssignmentExpression $assignment): ?DivinerAtom {
144
145 $left = $assignment->getLeft();
146 $right = $assignment->getRight();
147
148 if (!($left instanceof Peast\Syntax\Node\MemberExpression)) {
149 return null;
150 }
151
152 $object = $left->getObject();
153 if (
154 !($object instanceof Peast\Syntax\Node\Identifier) ||
155 $object->getName() !== 'JX') {
156
157 return null;
158 }
159
160 // This supports constructions such as x || y || function () {}.
161 if (
162 $right instanceof Peast\Syntax\Node\LogicalExpression &&
163 $right->getOperator() === '||') {
164
165 // By associativity rules, this selects the rightmost expression.
166 $right = $right->getRight();
167 }
168
169 if (!($right instanceof Peast\Syntax\Node\FunctionExpression)) {
170 return null;
171 }
172
173 return $this->parseFunction($right, false)
174 ->setName('JX.'.$left->getProperty()->getName())
175 ->setLine($assignment->getLocation()->getStart()->getLine());
176 }
177
178 private function parseClassDefinition(
179 Peast\Syntax\Node\ObjectExpression $definition): array {
180
181 $methods = array();
182
183 $class = $this->newAtom(DivinerAtom::TYPE_CLASS);
184
185 foreach ($definition->getProperties() as $property) {
186 $key = $property->getKey();
187 $this->expectNode($key, Peast\Syntax\Node\Identifier::class);
188
189 $name = $key->getName();
190 $value = $property->getValue();
191 $start_line = $key->getLocation()->getStart()->getLine();
192
193 switch ($name) {
194 case 'members':
195 case 'statics':
196 $this->expectNode($value, Peast\Syntax\Node\ObjectExpression::class);
197
198 foreach ($this->parseInstallationEntries($value) as $atom) {
199 $atom
200 ->setProperty(
201 'static',
202 $name === 'statics')
203 ->setLine($start_line);
204 $class->addChild($atom);
205 $methods[] = $atom;
206 }
207 break;
208 case 'construct':
209 case 'initialize':
210 $this->expectNode(
211 $value,
212 Peast\Syntax\Node\FunctionExpression::class);
213
214 $atom = $this->parseFunction($value, true)
215 ->setName($name)
216 ->setLine($start_line)
217 ->setProperty(
218 'static',
219 $name === 'initialize');
220 $class->addChild($atom);
221 $methods[] = $atom;
222 break;
223 case 'extend':
224 $this->expectNode($value, Peast\Syntax\Node\StringLiteral::class);
225 $class->addExtends(
226 $this->newRef(
227 DivinerAtom::TYPE_CLASS,
228 $value->getValue()));
229 break;
230 case 'properties':
231 // Diviner doesn't document these yet.
232 case 'events':
233 case 'canCallAsFunction':
234 // These have not been implemented yet.
235 break;
236 default:
237 throw new Exception(
238 pht(
239 'Unexpected property "%s" in Javelin class definition!',
240 $name));
241 }
242 }
243
244 return array($class, $methods);
245 }
246
247 /**
248 * @param Peast\Syntax\Node\ObjectExpression $object_expression
249 * @return Generator<DivinerAtom>
250 */
251 private function parseInstallationEntries(
252 Peast\Syntax\Node\ObjectExpression $object_expression): Generator {
253
254 foreach ($object_expression->getProperties() as $property) {
255 $key = $property->getKey();
256 $this->expectNode($key, Peast\Syntax\Node\Identifier::class);
257 $start_line = $key->getLocation()->getStart()->getLine();
258
259 $value = $property->getValue();
260 if ($value instanceof Peast\Syntax\Node\FunctionExpression) {
261 $name = $key->getName();
262
263 $method = $this->parseFunction($value, true)
264 ->setName($name)
265 ->setLine($start_line);
266
267 if (!strncmp($name, '_', 1)) {
268 $method->setProperty('access', 'private');
269 }
270
271 yield $method;
272 }
273 }
274 }
275
276 private function parseFunction(
277 Peast\Syntax\Node\FunctionExpression $node,
278 bool $class_function): DivinerAtom {
279
280 $param_spec = array();
281
282 foreach ($node->getParams() as $param) {
283 $this->expectNode($param, Peast\Syntax\Node\Identifier::class);
284
285 $param_spec[] = array(
286 'name' => $param->getName(),
287 );
288 }
289
290 if ($class_function) {
291 $type = DivinerAtom::TYPE_METHOD;
292 } else {
293 $type = DivinerAtom::TYPE_FUNCTION;
294 }
295
296 return $this->newAtom($type)
297 ->setProperty('parameters', $param_spec);
298 }
299
300 private function parseReturnDoc(DivinerAtom $atom) {
301 $return = $atom->getDocblockMetaValue('return');
302
303 if ($return) {
304 $return = (array)$return;
305 if (count($return) > 1) {
306 $atom->addWarning(
307 pht(
308 'Documentation specifies `%s` multiple times.',
309 '@return'));
310 }
311 $return = head($return);
312
313 $split = preg_split('/\s+/', trim($return), $limit = 2);
314 if (!empty($split[0])) {
315 $type = $split[0];
316 } else {
317 $type = 'wild';
318 }
319
320 $docs = null;
321 if (!empty($split[1])) {
322 $docs = $split[1];
323 }
324
325 $return_spec = array(
326 'doctype' => $type,
327 'docs' => $docs,
328 );
329
330 $atom->setProperty('return', $return_spec);
331 }
332 }
333
334 private function parseParametersDoc(DivinerAtom $atom) {
335 $docs = $atom->getDocblockMetaValue('param');
336
337 if ($docs) {
338 $docs = (array)$docs;
339 $param_spec = array();
340
341 foreach ($atom->getProperty('parameters') as $dict) {
342 $doc = array_shift($docs);
343 if ($doc) {
344 $dict += $this->parseParamDoc($doc, $dict['name']);
345 }
346 $param_spec[] = $dict;
347 }
348
349 // Add extra parameters retrieved by arguments variable.
350 foreach ($docs as $doc) {
351 if ($doc) {
352 $param_spec[] = array(
353 'name' => '',
354 ) + $this->parseParamDoc($doc, '');
355 }
356 }
357
358 $atom->setProperty('parameters', $param_spec);
359 }
360 }
361
362 private function parseParamDoc(string $doc, string $name): array {
363 $dict = array();
364 $split = preg_split('/(?<!,)\s+/', trim($doc), 2);
365 if (!empty($split[0])) {
366 $dict['doctype'] = $split[0];
367 }
368
369 if (!empty($split[1])) {
370 $docs = $split[1];
371
372 // If the parameter is documented like `@param int num Blah blah ..`,
373 // get rid of the `num` part (which Diviner considers optional).
374 // Unlike PHP, where the $ is a good identifier, for JavaScript we'll only
375 // remove it if it matches the name of the parameter.
376 // False positives should be unlikely, as these should be lowercase.
377 if (!strncmp($docs, $name, strlen($name))) {
378 $docs = trim(substr($docs, strlen($name)));
379 }
380
381 $dict['docs'] = $docs;
382 }
383
384 return $dict;
385 }
386
387 private function expectNode($node, string $class) {
388 if (!($node instanceof $class)) {
389 $position = $node->getLocation()->getStart();
390
391 throw new Exception(
392 pht(
393 'Expected "%s" node but found "%s" (on line %d:%d).',
394 id(new $class())->getType(),
395 $node->getType(),
396 $position->getLine(),
397 $position->getColumn()));
398 }
399 }
400
401}