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

at recaptime-dev/main 401 lines 11 kB view raw
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}