@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
fork

Configure Feed

Select the types of activity you want to include in your feed.

Port the DivinerJavelinAtomizer from Javelin

Summary:
Bring back documentation for Javelin by porting the DivinerJavelinAtomizer class to Phorge.
It has been rewritten to support modern Diviner, and to use Peast, rather than jsast.
This allows it to support modern JavaScript syntax without relying on strange binaries
distributed from ponds in some farcical aquatic ceremony.

Ref T15123

Test Plan:
* Run `./bin/diviner generate --book webroot/rsrc/externals/javelin/docs/javelin.book`
* Look at http://phorge.localhost/book/javelin/ and see class documentation under "Free Radicals"

Reviewers: O1 Blessed Committers, avivey

Reviewed By: O1 Blessed Committers, avivey

Subscribers: tobiaswiese, valerio.bozzolan, Matthew, Cigaryno

Maniphest Tasks: T15123

Differential Revision: https://we.phorge.it/D26787

+411 -2
+1
.gitignore
··· 4 4 # Diviner 5 5 /docs/ 6 6 /.divinercache/ 7 + /webroot/rsrc/externals/javelin/.divinercache/ 7 8 /webroot/rsrc/externals/javelin/docs/.divinercache/ 8 9 /src/.cache/ 9 10
+2
src/__phutil_library_map__.php
··· 1114 1114 'DivinerFileAtomizer' => 'applications/diviner/atomizer/DivinerFileAtomizer.php', 1115 1115 'DivinerFindController' => 'applications/diviner/controller/DivinerFindController.php', 1116 1116 'DivinerGenerateWorkflow' => 'applications/diviner/workflow/DivinerGenerateWorkflow.php', 1117 + 'DivinerJavelinAtomizer' => 'applications/diviner/atomizer/DivinerJavelinAtomizer.php', 1117 1118 'DivinerLiveAtom' => 'applications/diviner/storage/DivinerLiveAtom.php', 1118 1119 'DivinerLiveBook' => 'applications/diviner/storage/DivinerLiveBook.php', 1119 1120 'DivinerLiveBookEditor' => 'applications/diviner/editor/DivinerLiveBookEditor.php', ··· 7211 7212 'DivinerFileAtomizer' => 'DivinerAtomizer', 7212 7213 'DivinerFindController' => 'DivinerController', 7213 7214 'DivinerGenerateWorkflow' => 'DivinerWorkflow', 7215 + 'DivinerJavelinAtomizer' => 'DivinerAtomizer', 7214 7216 'DivinerLiveAtom' => 'DivinerDAO', 7215 7217 'DivinerLiveBook' => array( 7216 7218 'DivinerDAO',
+401
src/applications/diviner/atomizer/DivinerJavelinAtomizer.php
··· 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 + */ 15 + final 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 (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 + }
+7 -2
webroot/rsrc/externals/javelin/docs/javelin.book
··· 4 4 "short" : "Javelin Docs", 5 5 "preface" : "Documentation for JavaScript developers using Javelin.", 6 6 "uri.source": 7 - "https://we.phorge.it/diffusion/P/browse/master/%f$%l", 7 + "https://we.phorge.it/diffusion/P/browse/master/webroot/rsrc/externals/javelin/%f$%l", 8 + "root": "../", 8 9 "rules": { 9 - "(\\.diviner$)": "DivinerArticleAtomizer" 10 + "(\\.diviner$)": "DivinerArticleAtomizer", 11 + "(\\.js)": "DivinerJavelinAtomizer" 10 12 }, 13 + "exclude": [ 14 + "(/__tests__/)" 15 + ], 11 16 "groups": { 12 17 "introduction": { 13 18 "name": "Introduction"