Overview

Namespaces

  • Pharborist
    • Constants
    • ControlStructures
    • Exceptions
    • Functions
    • Generators
    • Namespaces
    • Objects
    • Operators
    • Types
    • Variables

Classes

  • Pharborist\Variables\CompoundVariableNode
  • Pharborist\Variables\GlobalStatementNode
  • Pharborist\Variables\ReferenceVariableNode
  • Pharborist\Variables\StaticVariableNode
  • Pharborist\Variables\StaticVariableStatementNode
  • Pharborist\Variables\VariableNode
  • Pharborist\Variables\VariableVariableNode

Interfaces

  • Pharborist\Variables\VariableExpressionNode
  • Overview
  • Namespace
  • Class
   1: <?php
   2: namespace Pharborist;
   3: 
   4: use Pharborist\Constants\ConstantDeclarationNode;
   5: use Pharborist\Constants\ConstantDeclarationStatementNode;
   6: use Pharborist\Constants\ConstantNode;
   7: use Pharborist\ControlStructures\BreakStatementNode;
   8: use Pharborist\ControlStructures\CaseNode;
   9: use Pharborist\ControlStructures\ContinueStatementNode;
  10: use Pharborist\ControlStructures\DeclareDirectiveNode;
  11: use Pharborist\ControlStructures\DeclareNode;
  12: use Pharborist\ControlStructures\DefaultNode;
  13: use Pharborist\ControlStructures\DoWhileNode;
  14: use Pharborist\ControlStructures\ElseIfNode;
  15: use Pharborist\ControlStructures\ExitNode;
  16: use Pharborist\ControlStructures\ForeachNode;
  17: use Pharborist\ControlStructures\ForNode;
  18: use Pharborist\ControlStructures\GotoLabelNode;
  19: use Pharborist\ControlStructures\GotoStatementNode;
  20: use Pharborist\ControlStructures\IfNode;
  21: use Pharborist\ControlStructures\IncludeNode;
  22: use Pharborist\ControlStructures\IncludeOnceNode;
  23: use Pharborist\ControlStructures\RequireNode;
  24: use Pharborist\ControlStructures\RequireOnceNode;
  25: use Pharborist\ControlStructures\ReturnStatementNode;
  26: use Pharborist\ControlStructures\SwitchNode;
  27: use Pharborist\ControlStructures\WhileNode;
  28: use Pharborist\Exceptions\CatchNode;
  29: use Pharborist\Exceptions\ThrowStatementNode;
  30: use Pharborist\Exceptions\TryCatchNode;
  31: use Pharborist\Functions\AnonymousFunctionNode;
  32: use Pharborist\Functions\CallbackCallNode;
  33: use Pharborist\Functions\CallNode;
  34: use Pharborist\Functions\DefineNode;
  35: use Pharborist\Functions\EmptyNode;
  36: use Pharborist\Functions\EvalNode;
  37: use Pharborist\Functions\FunctionCallNode;
  38: use Pharborist\Functions\FunctionDeclarationNode;
  39: use Pharborist\Functions\HaltCompilerNode;
  40: use Pharborist\Functions\IssetNode;
  41: use Pharborist\Functions\ListNode;
  42: use Pharborist\Functions\ParameterNode;
  43: use Pharborist\Functions\UnsetNode;
  44: use Pharborist\Generators\YieldNode;
  45: use Pharborist\Generators\YieldStatementNode;
  46: use Pharborist\Namespaces\NameNode;
  47: use Pharborist\Namespaces\NamespaceNode;
  48: use Pharborist\Namespaces\UseDeclarationBlockNode;
  49: use Pharborist\Namespaces\UseDeclarationNode;
  50: use Pharborist\Namespaces\UseDeclarationStatementNode;
  51: use Pharborist\Objects\ClassConstantLookupNode;
  52: use Pharborist\Objects\ClassMemberListNode;
  53: use Pharborist\Objects\ClassMemberLookupNode;
  54: use Pharborist\Objects\ClassMemberNode;
  55: use Pharborist\Objects\ClassMethodCallNode;
  56: use Pharborist\Objects\ClassMethodNode;
  57: use Pharborist\Objects\ClassNameScalarNode;
  58: use Pharborist\Objects\ClassNode;
  59: use Pharborist\Objects\InterfaceMethodNode;
  60: use Pharborist\Objects\InterfaceNode;
  61: use Pharborist\Objects\ModifiersNode;
  62: use Pharborist\Objects\NameExpressionNode;
  63: use Pharborist\Objects\NewNode;
  64: use Pharborist\Objects\ObjectMethodCallNode;
  65: use Pharborist\Objects\ObjectPropertyNode;
  66: use Pharborist\Objects\TraitAliasNode;
  67: use Pharborist\Objects\TraitMethodReferenceNode;
  68: use Pharborist\Objects\TraitNode;
  69: use Pharborist\Objects\TraitPrecedenceNode;
  70: use Pharborist\Objects\TraitUseNode;
  71: use Pharborist\Types\ArrayNode;
  72: use Pharborist\Types\ArrayPairNode;
  73: use Pharborist\Types\FalseNode;
  74: use Pharborist\Types\HeredocNode;
  75: use Pharborist\Types\InterpolatedStringNode;
  76: use Pharborist\Types\NullNode;
  77: use Pharborist\Types\StringVariableNode;
  78: use Pharborist\Types\TrueNode;
  79: use Pharborist\Variables\CompoundVariableNode;
  80: use Pharborist\Variables\GlobalStatementNode;
  81: use Pharborist\Variables\ReferenceVariableNode;
  82: use Pharborist\Variables\StaticVariableNode;
  83: use Pharborist\Variables\StaticVariableStatementNode;
  84: use Pharborist\Variables\VariableVariableNode;
  85: 
  86: /**
  87:  * Parses PHP tokens into syntax tree.
  88:  */
  89: class Parser {
  90:   /**
  91:    * @var array
  92:    */
  93:   private static $namespacePathTypes = [T_STRING, T_NS_SEPARATOR, T_NAMESPACE];
  94: 
  95:   /**
  96:    * @var array
  97:    */
  98:   private static $visibilityTypes = [T_PUBLIC, T_PROTECTED, T_PRIVATE];
  99: 
 100:   /**
 101:    * Iterator over PHP tokens.
 102:    * @var TokenIterator
 103:    */
 104:   private $iterator;
 105: 
 106:   /**
 107:    * ParentNode to capture document comment if the child does not capture it.
 108:    * @var ParentNode
 109:    */
 110:   private $skipParent = NULL;
 111: 
 112:   /**
 113:    * Skipped hidden tokens.
 114:    * @var TokenNode[]
 115:    */
 116:   private $skipped = [];
 117: 
 118:   /**
 119:    * Skipped document comment.
 120:    * @var DocCommentNode
 121:    */
 122:   private $docComment = NULL;
 123: 
 124:   /**
 125:    * Skipped hidden tokens after document comment.
 126:    */
 127:   private $skippedDocComment = [];
 128: 
 129:   /**
 130:    * The root node of the syntax tree.
 131:    * @var RootNode
 132:    */
 133:   private $top;
 134: 
 135:   /**
 136:    * Parser used for parsing expressions.
 137:    * @var ExpressionParser
 138:    */
 139:   private $expressionParser;
 140: 
 141:   /**
 142:    * @var TokenNode
 143:    */
 144:   private $current;
 145: 
 146:   /**
 147:    * @var int
 148:    */
 149:   private $currentType;
 150: 
 151:   /**
 152:    * Constructor.
 153:    */
 154:   public function __construct() {
 155:     // Define future PHP constants if not already defined.
 156:     if (!defined('T_FINALLY')) {
 157:       define('T_FINALLY', 'finally');
 158:     }
 159:     if (!defined('T_YIELD')) {
 160:       define('T_YIELD', 'yield');
 161:     }
 162:     if (!defined('T_ELLIPSIS')) {
 163:       define('T_ELLIPSIS', '...');
 164:     }
 165:     if (!defined('T_POW')) {
 166:       define('T_POW', '**');
 167:     }
 168:     if (!defined('T_POW_EQUAL')) {
 169:       define('T_POW_EQUAL', '**=');
 170:     }
 171:     $this->expressionParser = new ExpressionParser();
 172:   }
 173: 
 174:   /**
 175:    * Build a syntax tree from the token iterator.
 176:    * @param TokenIterator $iterator
 177:    * @return RootNode Root node of the tree
 178:    */
 179:   public function buildTree(TokenIterator $iterator) {
 180:     $this->iterator = $iterator;
 181:     $this->current = $this->iterator->current();
 182:     $this->currentType = $this->current ? $this->current->getType() : NULL;
 183:     $top = new RootNode();
 184:     $this->top = $top;
 185:     if ($this->currentType && $this->currentType !== T_OPEN_TAG) {
 186:       $node = new TemplateNode();
 187:       // Parse any template statements that proceed the opening PHP tag.
 188:       $this->templateStatementList($node);
 189:       $top->addChild($node);
 190:     }
 191:     if ($this->tryMatch(T_OPEN_TAG, $top, NULL, TRUE, TRUE)) {
 192:       $this->topStatementList($top);
 193:     }
 194:     return $top;
 195:   }
 196: 
 197:   /**
 198:    * Parse a file and return the parsed tree
 199:    * @param string $filename Path to file
 200:    * @return RootNode|bool
 201:    *   The top-level node of the parsed tree or FALSE if the file contents
 202:    *   could not be read.
 203:    */
 204:   public static function parseFile($filename) {
 205:     $source = @file_get_contents($filename);
 206:     if ($source === FALSE) {
 207:       return FALSE;
 208:     }
 209:     return self::parseSource($source);
 210:   }
 211: 
 212:   /**
 213:    * Parse PHP source code and return the parsed tree.
 214:    * @param string $source PHP source code
 215:    * @return RootNode
 216:    *   The top-level node of the parsed tree
 217:    */
 218:   public static function parseSource($source) {
 219:     static $tokenizer, $parser = NULL;
 220:     if (!isset($parser)) {
 221:       $tokenizer = new Tokenizer();
 222:       $parser = new self();
 223:     }
 224:     $tokens = $tokenizer->getAll($source);
 225:     return $parser->buildTree(new TokenIterator($tokens));
 226:   }
 227: 
 228:   /**
 229:    * Parse a snippet of PHP and return the node of first element.
 230:    * @param string $snippet PHP snippet without the opening PHP tag
 231:    * @return Node
 232:    *   The first node in the snippet.
 233:    */
 234:   public static function parseSnippet($snippet) {
 235:     $tree = self::parseSource('<?php ' . $snippet);
 236:     return $tree->firstChild()->next();
 237:   }
 238: 
 239:   /**
 240:    * Parse a PHP expression.
 241:    *
 242:    * @param string $expression
 243:    *   PHP expression snippet.
 244:    *
 245:    * @return ExpressionNode
 246:    *   The expression.
 247:    */
 248:   public static function parseExpression($expression) {
 249:     $tree = self::parseSource('<?php ' . $expression . ';');
 250:     /** @var ExpressionStatementNode $statement_node */
 251:     $statement_node = $tree->firstChild()->next();
 252:     return $statement_node->getExpression()->remove();
 253:   }
 254: 
 255:   /**
 256:    * Parse zero or more template statements.
 257:    * @param ParentNode $node Node to append matches to.
 258:    * @throws ParserException
 259:    */
 260:   private function templateStatementList(ParentNode $node) {
 261:     while ($this->current) {
 262:       if ($this->currentType === T_OPEN_TAG) {
 263:         return;
 264:       }
 265:       elseif ($this->currentType === T_INLINE_HTML) {
 266:         $node->addChild($this->mustMatchToken(T_INLINE_HTML));
 267:       }
 268:       elseif ($this->currentType === T_OPEN_TAG_WITH_ECHO) {
 269:         $node->addChild($this->echoTagStatement());
 270:       }
 271:       else {
 272:         throw new ParserException($this->iterator->getSourcePosition(),
 273:           'expected PHP opening tag, but got ' . $this->iterator->current()->getText());
 274:       }
 275:     }
 276:   }
 277: 
 278:   /**
 279:    * Parse an echo PHP (eg. <?=$a?>) statement.
 280:    * @return EchoTagStatementNode
 281:    */
 282:   private function echoTagStatement() {
 283:     $node = new EchoTagStatementNode();
 284:     $this->mustMatch(T_OPEN_TAG_WITH_ECHO, $node);
 285:     $expressions = new CommaListNode();
 286:     do {
 287:       $expressions->addChild($this->expr());
 288:     } while ($this->tryMatch(',', $expressions));
 289:     $node->addChild($expressions, 'expressions');
 290:     $this->tryMatch(';', $node);
 291:     $this->mustMatch(T_CLOSE_TAG, $node, NULL, TRUE);
 292:     return $node;
 293:   }
 294: 
 295:   /**
 296:    * Parse a list of top level statements.
 297:    * @param StatementBlockNode $node Node to append matches to
 298:    * @param string $terminator Character that ends the statement list
 299:    */
 300:   private function topStatementList(StatementBlockNode $node, $terminator = '') {
 301:     $this->matchHidden($node);
 302:     while ($this->currentType !== NULL && $this->currentType !== $terminator) {
 303:       $node->addChild($this->topStatement());
 304:       $this->matchHidden($node);
 305:     }
 306:     $this->matchHidden($node);
 307:     $this->matchDocComment($node, NULL);
 308:   }
 309: 
 310:   /**
 311:    * Parse a top level statement.
 312:    * @return Node
 313:    */
 314:   private function topStatement() {
 315:     switch ($this->currentType) {
 316:       case T_USE:
 317:         return $this->useBlock();
 318:       case T_CONST:
 319:         return $this->_const();
 320:       case T_ABSTRACT:
 321:       case T_FINAL:
 322:       case T_CLASS:
 323:         return $this->classDeclaration();
 324:       case T_INTERFACE:
 325:         return $this->interfaceDeclaration();
 326:       case T_TRAIT:
 327:         return $this->traitDeclaration();
 328:       case T_HALT_COMPILER:
 329:         $node = new HaltCompilerNode();
 330:         $this->mustMatch(T_HALT_COMPILER, $node, 'name');
 331:         $this->mustMatch('(', $node, 'openParen');
 332:         $this->mustMatch(')', $node, 'closeParen');
 333:         $this->endStatement($node);
 334:         return $node;
 335:       default:
 336:         if ($this->currentType === T_FUNCTION && $this->isLookAhead(T_STRING, '&')) {
 337:           return $this->functionDeclaration();
 338:         }
 339:         elseif ($this->currentType === T_NAMESPACE && !$this->isLookAhead(T_NS_SEPARATOR)) {
 340:           return $this->_namespace();
 341:         }
 342:         return $this->statement();
 343:     }
 344:   }
 345: 
 346:   private function endStatement(ParentNode $node) {
 347:     if ($this->currentType === T_CLOSE_TAG) {
 348:       // http://php.net/manual/en/language.basic-syntax.instruction-separation.php
 349:       // The closing tag of a block of PHP code automatically implies a
 350:       // semicolon; you do not need to have a semicolon terminating the last
 351:       // line of a PHP block.
 352:       return;
 353:     }
 354:     $this->mustMatch(';', $node, NULL, TRUE, TRUE);
 355:   }
 356: 
 357:   /**
 358:    * Parse a constant declaration list.
 359:    * @return ConstantDeclarationStatementNode
 360:    */
 361:   private function _const() {
 362:     $node = new ConstantDeclarationStatementNode();
 363:     $this->matchDocComment($node);
 364:     $this->mustMatch(T_CONST, $node);
 365:     $declarations = new CommaListNode();
 366:     do {
 367:       $declarations->addChild($this->constDeclaration());
 368:     } while ($this->tryMatch(',', $declarations));
 369:     $node->addChild($declarations, 'declarations');
 370:     $this->endStatement($node);
 371:     return $node;
 372:   }
 373: 
 374:   /**
 375:    * Parse a constant declaration.
 376:    * @return ConstantDeclarationNode
 377:    */
 378:   private function constDeclaration() {
 379:     $node = new ConstantDeclarationNode();
 380:     $name_node = new NameNode();
 381:     $this->mustMatch(T_STRING, $name_node, NULL, TRUE);
 382:     $node->addChild($name_node, 'name');
 383:     if ($this->mustMatch('=', $node)) {
 384:       $node->addChild($this->staticScalar(), 'value');
 385:     }
 386:     return $node;
 387:   }
 388: 
 389:   /**
 390:    * Parse a statement.
 391:    * @return Node
 392:    */
 393:   private function statement() {
 394:     switch ($this->currentType) {
 395:       case T_CLOSE_TAG:
 396:         // A close tag escapes into template mode.
 397:         $node = new TemplateNode();
 398:         $this->mustMatch(T_CLOSE_TAG, $node);
 399:         $this->templateStatementList($node);
 400:         if ($this->iterator->hasNext()) {
 401:           $this->mustMatch(T_OPEN_TAG, $node, NULL, TRUE, TRUE);
 402:         }
 403:         return $node;
 404:       case T_IF:
 405:         return $this->_if();
 406:       case T_WHILE:
 407:         return $this->_while();
 408:       case T_DO:
 409:         return $this->doWhile();
 410:       case T_FOR:
 411:         return $this->_for();
 412:       case T_SWITCH:
 413:         return $this->_switch();
 414:       case T_BREAK:
 415:         return $this->_break();
 416:       case T_CONTINUE:
 417:         return $this->_continue();
 418:       case T_RETURN:
 419:         return $this->_return();
 420:       case T_YIELD:
 421:         $node = new YieldStatementNode();
 422:         $node->addChild($this->_yield());
 423:         $this->endStatement($node);
 424:         return $node;
 425:       case T_GLOBAL:
 426:         return $this->_global();
 427:       case T_ECHO:
 428:         return $this->_echo();
 429:       case T_UNSET:
 430:         return $this->_unset();
 431:       case T_FOREACH:
 432:         return $this->_foreach();
 433:       case T_DECLARE:
 434:         return $this->_declare();
 435:       case T_TRY:
 436:         return $this->_try();
 437:       case T_THROW:
 438:         return $this->_throw();
 439:       case T_GOTO:
 440:         return $this->_goto();
 441:       case '{':
 442:         return $this->innerStatementBlock();
 443:       case ';':
 444:         $node = new BlankStatementNode();
 445:         $this->endStatement($node);
 446:         return $node;
 447:       case T_STATIC:
 448:         if ($this->isLookAhead(T_VARIABLE)) {
 449:           return $this->staticVariableList();
 450:         }
 451:         else {
 452:           return $this->exprStatement();
 453:         }
 454:       case T_STRING:
 455:         if ($this->isLookAhead(':')) {
 456:           $node = new GotoLabelNode();
 457:           $this->mustMatch(T_STRING, $node, 'label');
 458:           $this->mustMatch(':', $node, NULL, TRUE, TRUE);
 459:           return $node;
 460:         }
 461:         else {
 462:           return $this->exprStatement();
 463:         }
 464:       default:
 465:         return $this->exprStatement();
 466:     }
 467:   }
 468: 
 469:   /**
 470:    * Parse a static variable list.
 471:    * @return StaticVariableStatementNode
 472:    */
 473:   private function staticVariableList() {
 474:     $node = new StaticVariableStatementNode();
 475:     $this->matchDocComment($node);
 476:     $this->mustMatch(T_STATIC, $node);
 477:     $variables = new CommaListNode();
 478:     do {
 479:       $variables->addChild($this->staticVariable());
 480:     } while ($this->tryMatch(',', $variables));
 481:     $node->addChild($variables, 'variables');
 482:     $this->endStatement($node);
 483:     return $node;
 484:   }
 485: 
 486:   /**
 487:    * Parse a static variable.
 488:    * @return StaticVariableNode
 489:    */
 490:   private function staticVariable() {
 491:     $node = new StaticVariableNode();
 492:     $this->mustMatch(T_VARIABLE, $node, 'name', TRUE);
 493:     if ($this->tryMatch('=', $node)) {
 494:       $node->addChild($this->staticScalar(), 'initialValue');
 495:     }
 496:     return $node;
 497:   }
 498: 
 499:   /**
 500:    * Parse expression statement.
 501:    * @return ExpressionStatementNode
 502:    */
 503:   private function exprStatement() {
 504:     $node = new ExpressionStatementNode();
 505:     $this->matchDocComment($node);
 506:     $node->addChild($this->expr(), 'expression');
 507:     $this->endStatement($node);
 508:     return $node;
 509:   }
 510: 
 511:   /**
 512:    * Parse condition.
 513:    * @param ParentNode $node
 514:    * @param string $property_name
 515:    */
 516:   private function condition(ParentNode $node, $property_name) {
 517:     $this->mustMatch('(', $node, 'openParen');
 518:     if ($this->currentType === T_YIELD) {
 519:       $node->addChild($this->_yield(), $property_name);
 520:     }
 521:     else {
 522:       $node->addChild($this->expr(), $property_name);
 523:     }
 524:     $this->mustMatch(')', $node, 'closeParen');
 525:   }
 526: 
 527:   /**
 528:    * Parse if control structure.
 529:    * @return IfNode
 530:    */
 531:   private function _if() {
 532:     $node = new IfNode();
 533:     $this->mustMatch(T_IF, $node);
 534:     $this->condition($node, 'condition');
 535:     if ($this->tryMatch(':', $node, 'openColon', FALSE, TRUE)) {
 536:       $node->addChild($this->innerIfInnerStatementList(), 'then');
 537:       while ($this->currentType === T_ELSEIF) {
 538:         $this->matchHidden($node);
 539:         $elseIf = new ElseIfNode();
 540:         $this->mustMatch(T_ELSEIF, $elseIf);
 541:         $this->condition($elseIf, 'condition');
 542:         $this->mustMatch(':', $elseIf, 'openColon', FALSE, TRUE);
 543:         $elseIf->addChild($this->innerIfInnerStatementList(), 'then');
 544:         $node->addChild($elseIf);
 545:       }
 546:       if ($this->tryMatch(T_ELSE, $node, 'elseKeyword')) {
 547:         $this->mustMatch(':', $node, 'elseColon', FALSE, TRUE);
 548:         $node->addChild($this->innerStatementListNode(T_ENDIF), 'else');
 549:       }
 550:       $this->mustMatch(T_ENDIF, $node, 'endKeyword');
 551:       $this->endStatement($node);
 552:       return $node;
 553:     }
 554:     else {
 555:       $this->matchHidden($node);
 556:       $node->addChild($this->statement(), 'then');
 557:       while ($this->currentType === T_ELSEIF) {
 558:         $this->matchHidden($node);
 559:         $elseIf = new ElseIfNode();
 560:         $this->mustMatch(T_ELSEIF, $elseIf);
 561:         $this->condition($elseIf, 'condition');
 562:         $this->matchHidden($elseIf);
 563:         $elseIf->addChild($this->statement(), 'then');
 564:         $node->addChild($elseIf);
 565:       }
 566:       if ($this->tryMatch(T_ELSE, $node, 'elseKeyword', FALSE, TRUE)) {
 567:         $node->addChild($this->statement(), 'else');
 568:       }
 569:       return $node;
 570:     }
 571:   }
 572: 
 573:   /**
 574:    * Parse statements for alternative if syntax.
 575:    * @return Node
 576:    */
 577:   private function innerIfInnerStatementList() {
 578:     static $terminators = [T_ELSEIF, T_ELSE, T_ENDIF];
 579:     $node = new StatementBlockNode();
 580:     while ($this->currentType !== NULL && !in_array($this->currentType, $terminators)) {
 581:       $this->matchHidden($node);
 582:       $node->addChild($this->innerStatement());
 583:     }
 584:     return $node;
 585:   }
 586: 
 587:   /**
 588:    * Parse while control structure.
 589:    * @return WhileNode
 590:    */
 591:   private function _while() {
 592:     $node = new WhileNode();
 593:     $this->mustMatch(T_WHILE, $node);
 594:     $this->condition($node, 'condition');
 595:     if ($this->tryMatch(':', $node, 'openColon', FALSE, TRUE)) {
 596:       $node->addChild($this->innerStatementListNode(T_ENDWHILE), 'body');
 597:       $this->mustMatch(T_ENDWHILE, $node, 'endKeyword');
 598:       $this->endStatement($node);
 599:       return $node;
 600:     }
 601:     else {
 602:       $this->matchHidden($node);
 603:       $node->addChild($this->statement(), 'body');
 604:       return $node;
 605:     }
 606:   }
 607: 
 608:   /**
 609:    * Parse do while control stucture.
 610:    * @return DoWhileNode
 611:    */
 612:   private function doWhile() {
 613:     $node = new DoWhileNode();
 614:     $this->mustMatch(T_DO, $node, NULL, FALSE, TRUE);
 615:     $node->addChild($this->statement(), 'body');
 616:     $this->mustMatch(T_WHILE, $node, 'whileKeyword');
 617:     $this->condition($node, 'condition');
 618:     $this->endStatement($node);
 619:     return $node;
 620:   }
 621: 
 622:   /**
 623:    * Parse for control structure.
 624:    * @return ForNode
 625:    */
 626:   private function _for() {
 627:     $node = new ForNode();
 628:     $this->mustMatch(T_FOR, $node);
 629:     $this->mustMatch('(', $node, 'openParen');
 630:     $this->forExpr($node, ';', 'initial');
 631:     $this->forExpr($node, ';', 'condition');
 632:     $this->forExpr($node, ')', 'step', TRUE, 'closeParen');
 633:     if ($this->tryMatch(':', $node, 'openColon', FALSE, TRUE)) {
 634:       $node->addChild($this->innerStatementListNode(T_ENDFOR), 'body');
 635:       $this->mustMatch(T_ENDFOR, $node, 'endKeyword');
 636:       $this->endStatement($node);
 637:       return $node;
 638:     }
 639:     else {
 640:       $this->matchHidden($node);
 641:       $node->addChild($this->statement(), 'body');
 642:       return $node;
 643:     }
 644:   }
 645: 
 646:   /**
 647:    * Parse a for expression.
 648:    * @param ForNode $parent Parent for node
 649:    * @param int|string $terminator Token type that terminates the for expression
 650:    * @param string $property_name
 651:    * @param bool $is_last TRUE if last for expression
 652:    * @param string $terminator_name
 653:    */
 654:   private function forExpr(ForNode $parent, $terminator, $property_name, $is_last = FALSE, $terminator_name = NULL) {
 655:     if ($this->tryMatch($terminator, $parent)) {
 656:       $parent->addChild(new CommaListNode(), $property_name);
 657:       return;
 658:     }
 659:     $parent->addChild($this->exprList(), $property_name);
 660:     $this->mustMatch($terminator, $parent, $terminator_name, $is_last, FALSE);
 661:   }
 662: 
 663:   /**
 664:    * Parse a switch control structure.
 665:    * @return SwitchNode
 666:    */
 667:   private function _switch() {
 668:     $node = new SwitchNode();
 669:     $this->mustMatch(T_SWITCH, $node);
 670:     $this->condition($node, 'switchOn');
 671:     if ($this->tryMatch(':', $node, 'openColon')) {
 672:       $this->tryMatch(';', $node);
 673:       while ($this->currentType !== NULL && $this->currentType !== T_ENDSWITCH) {
 674:         $node->addChild($this->caseStatement(T_ENDSWITCH));
 675:         $this->matchHidden($node);
 676:       }
 677:       $this->mustMatch(T_ENDSWITCH, $node, 'endKeyword');
 678:       $this->endStatement($node);
 679:       return $node;
 680:     }
 681:     else {
 682:       $this->mustMatch('{', $node);
 683:       $this->tryMatch(';', $node);
 684:       while ($this->currentType !== NULL && $this->currentType !== '}') {
 685:         $node->addChild($this->caseStatement('}'));
 686:         $this->matchHidden($node);
 687:       }
 688:       $this->mustMatch('}', $node, NULL, TRUE);
 689:       return $node;
 690:     }
 691:   }
 692: 
 693:   /**
 694:    * Parse a case statement.
 695:    * @param int|string $terminator Token type that terminates statement list
 696:    * @return CaseNode|DefaultNode
 697:    * @throws ParserException
 698:    */
 699:   private function caseStatement($terminator) {
 700:     static $terminators = [T_CASE, T_DEFAULT];
 701:     if ($this->currentType === T_CASE) {
 702:       $node = new CaseNode();
 703:       $this->mustMatch(T_CASE, $node);
 704:       $node->addChild($this->expr(), 'matchOn');
 705:       if (!$this->tryMatch(':', $node, NULL, TRUE, TRUE) && !$this->tryMatch(';', $node, NULL, TRUE, TRUE)) {
 706:         throw new ParserException($this->iterator->getSourcePosition(), 'expected :');
 707:       }
 708:       if ($this->currentType !== $terminator && !in_array($this->currentType, $terminators)) {
 709:         $this->matchHidden($node);
 710:         $node->addChild($this->innerCaseStatementList($terminator), 'body');
 711:       }
 712:       return $node;
 713:     }
 714:     elseif ($this->currentType === T_DEFAULT) {
 715:       $node = new DefaultNode();
 716:       $this->mustMatch(T_DEFAULT, $node);
 717:       if (!$this->tryMatch(':', $node, NULL, TRUE, TRUE) && !$this->tryMatch(';', $node, NULL, TRUE, TRUE)) {
 718:         throw new ParserException($this->iterator->getSourcePosition(), 'expected :');
 719:       }
 720:       if ($this->currentType !== $terminator && !in_array($this->currentType, $terminators)) {
 721:         $this->matchHidden($node);
 722:         $node->addChild($this->innerCaseStatementList($terminator), 'body');
 723:       }
 724:       return $node;
 725:     }
 726:     throw new ParserException($this->iterator->getSourcePosition(), "expected case or default");
 727:   }
 728: 
 729:   /**
 730:    * Parse the inner statements for a case statement.
 731:    * @param int|string $terminator Token type that terminates statement list
 732:    * @return Node
 733:    */
 734:   private function innerCaseStatementList($terminator) {
 735:     static $terminators = [T_CASE, T_DEFAULT];
 736:     $node = new StatementBlockNode();
 737:     while ($this->currentType !== NULL && $this->currentType !== $terminator && !in_array($this->currentType, $terminators)) {
 738:       $this->matchHidden($node);
 739:       $node->addChild($this->innerStatement());
 740:     }
 741:     return $node;
 742:   }
 743: 
 744:   /**
 745:    * Parse a break statement.
 746:    * @return BreakStatementNode
 747:    */
 748:   private function _break() {
 749:     $node = new BreakStatementNode();
 750:     $this->mustMatch(T_BREAK, $node);
 751:     if ($this->tryMatch(';', $node, NULL, TRUE, TRUE) || $this->currentType === T_CLOSE_TAG) {
 752:       return $node;
 753:     }
 754:     $this->parseLevel($node);
 755:     $this->endStatement($node);
 756:     return $node;
 757:   }
 758: 
 759:   /**
 760:    * Parse a continue statement.
 761:    * @return ContinueStatementNode
 762:    */
 763:   private function _continue() {
 764:     $node = new ContinueStatementNode();
 765:     $this->mustMatch(T_CONTINUE, $node);
 766:     if ($this->tryMatch(';', $node, NULL, TRUE, TRUE) || $this->currentType === T_CLOSE_TAG) {
 767:       return $node;
 768:     }
 769:     $this->parseLevel($node);
 770:     $this->endStatement($node);
 771:     return $node;
 772:   }
 773: 
 774:   /**
 775:    * Parse a break/continue level.
 776:    * @param BreakStatementNode|ContinueStatementNode $node
 777:    */
 778:   private function parseLevel($node) {
 779:     if ($this->tryMatch('(', $node, 'openParen')) {
 780:       $this->mustMatch(T_LNUMBER, $node, 'level');
 781:       $this->mustMatch(')', $node, 'closeParen');
 782:     }
 783:     else {
 784:       $this->mustMatch(T_LNUMBER, $node, 'level');
 785:     }
 786:   }
 787: 
 788:   /**
 789:    * Parse a return statement.
 790:    * @return ReturnStatementNode
 791:    */
 792:   private function _return() {
 793:     $node = new ReturnStatementNode();
 794:     $this->mustMatch(T_RETURN, $node);
 795:     if ($this->tryMatch(';', $node, NULL, TRUE, TRUE) || $this->currentType === T_CLOSE_TAG) {
 796:       return $node;
 797:     }
 798:     $node->addChild($this->expr(), 'expression');
 799:     $this->endStatement($node);
 800:     return $node;
 801:   }
 802: 
 803:   /**
 804:    * Parse a yield expression.
 805:    * @return YieldNode
 806:    */
 807:   private function _yield() {
 808:     $node = new YieldNode();
 809:     $this->mustMatch(T_YIELD, $node);
 810:     $expr = $this->expr();
 811:     if ($this->tryMatch(T_DOUBLE_ARROW, $node)) {
 812:       $node->addChild($expr, 'key');
 813:       $node->addChild($this->expr(), 'value');
 814:     }
 815:     else {
 816:       $node->addChild($expr, 'value');
 817:     }
 818:     return $node;
 819:   }
 820: 
 821:   /**
 822:    * Parse a global variable declaration list.
 823:    * @return GlobalStatementNode
 824:    */
 825:   private function _global() {
 826:     $node = new GlobalStatementNode();
 827:     $this->mustMatch(T_GLOBAL, $node);
 828:     $variables = new CommaListNode();
 829:     do {
 830:       $variables->addChild($this->globalVar());
 831:     } while ($this->tryMatch(',', $variables));
 832:     $node->addChild($variables, 'variables');
 833:     $this->endStatement($node);
 834:     return $node;
 835:   }
 836: 
 837:   /**
 838:    * Parse global variable.
 839:    * @return Node
 840:    * @throws ParserException
 841:    */
 842:   private function globalVar() {
 843:     if ($this->currentType === T_VARIABLE) {
 844:       return $this->mustMatchToken(T_VARIABLE);
 845:     }
 846:     elseif ($this->currentType === '$') {
 847:       if ($this->isLookAhead('{')) {
 848:         return $this->_compoundVariable();
 849:       }
 850:       else {
 851:         $node = new VariableVariableNode();
 852:         $this->mustMatch('$', $node);
 853:         $node->addChild($this->variable());
 854:         return $node;
 855:       }
 856:     }
 857:     throw new ParserException($this->iterator->getSourcePosition(), 'expected a global variable (eg. T_VARIABLE)');
 858:   }
 859: 
 860:   /**
 861:    * Parse echo statement.
 862:    * @return EchoStatementNode
 863:    */
 864:   private function _echo() {
 865:     $node = new EchoStatementNode();
 866:     $this->mustMatch(T_ECHO, $node);
 867:     $expressions = new CommaListNode();
 868:     do {
 869:       $expressions->addChild($this->expr());
 870:     } while ($this->tryMatch(',', $expressions));
 871:     $node->addChild($expressions, 'expressions');
 872:     $this->endStatement($node);
 873:     return $node;
 874:   }
 875: 
 876:   /**
 877:    * Parse an unset statement.
 878:    * @return UnsetStatementNode
 879:    */
 880:   private function _unset() {
 881:     $statement_node = new UnsetStatementNode();
 882:     $node = new UnsetNode();
 883:     $this->mustMatch(T_UNSET, $node, 'name');
 884:     $arguments = new CommaListNode();
 885:     $this->mustMatch('(', $node, 'openParen');
 886:     $node->addChild($arguments, 'arguments');
 887:     do {
 888:       $arguments->addChild($this->variable());
 889:     } while ($this->tryMatch(',', $arguments));
 890:     $this->mustMatch(')', $node, 'closeParen', FALSE);
 891:     $statement_node->addChild($node, 'functionCall');
 892:     $this->endStatement($statement_node);
 893:     return $statement_node;
 894:   }
 895: 
 896:   /**
 897:    * Parse foreach control structure.
 898:    * @return ForeachNode
 899:    */
 900:   private function _foreach() {
 901:     $node = new ForeachNode();
 902:     $this->mustMatch(T_FOREACH, $node);
 903:     $this->mustMatch('(', $node, 'openParen');
 904:     $node->addChild($this->expr(), 'onEach');
 905:     $this->mustMatch(T_AS, $node);
 906:     $value = $this->foreachVariable();
 907:     if ($this->currentType === T_DOUBLE_ARROW) {
 908:       $node->addChild($value, 'key');
 909:       $this->mustMatch(T_DOUBLE_ARROW, $node);
 910:       $node->addChild($this->foreachVariable(), 'value');
 911:     }
 912:     else {
 913:       $node->addChild($value, 'value');
 914:     }
 915:     $this->mustMatch(')', $node, 'closeParen', FALSE, TRUE);
 916:     if ($this->tryMatch(':', $node, NULL, FALSE, TRUE)) {
 917:       $node->addChild($this->innerStatementListNode(T_ENDFOREACH), 'body');
 918:       $this->mustMatch(T_ENDFOREACH, $node);
 919:       $this->endStatement($node);
 920:       return $node;
 921:     }
 922:     else {
 923:       $node->addChild($this->statement(), 'body');
 924:       return $node;
 925:     }
 926:   }
 927: 
 928:   /**
 929:    * Parse a foreach variable.
 930:    * @return Node
 931:    */
 932:   private function foreachVariable() {
 933:     if ($this->currentType === T_LIST) {
 934:       return $this->_list();
 935:     }
 936:     else {
 937:       if ($this->currentType === '&') {
 938:         return $this->writeVariable();
 939:       }
 940:       else {
 941:         return $this->variable();
 942:       }
 943:     }
 944:   }
 945: 
 946:   /**
 947:    * Parse a list() expression.
 948:    * @return ListNode
 949:    */
 950:   private function _list() {
 951:     $node = new ListNode();
 952:     $this->mustMatch(T_LIST, $node, 'name');
 953:     $arguments = new CommaListNode();
 954:     $this->mustMatch('(', $node, 'openParen');
 955:     $node->addChild($arguments, 'arguments');
 956:     do {
 957:       if ($this->currentType === ')') {
 958:         break;
 959:       }
 960:       if ($this->currentType !== ',') {
 961:         $arguments->addChild($this->listElement());
 962:       }
 963:     } while ($this->tryMatch(',', $arguments));
 964:     $this->mustMatch(')', $node, 'closeParen', TRUE);
 965:     return $node;
 966:   }
 967: 
 968:   /**
 969:    * Parse an element from list() expression.
 970:    * @return Node
 971:    */
 972:   private function listElement() {
 973:     if ($this->currentType === T_LIST) {
 974:       return $this->_list();
 975:     }
 976:     else {
 977:       return $this->variable();
 978:     }
 979:   }
 980: 
 981:   /**
 982:    * Parse a declare statement.
 983:    * @return DeclareNode
 984:    */
 985:   private function _declare() {
 986:     $node = new DeclareNode();
 987:     $this->mustMatch(T_DECLARE, $node);
 988:     $this->mustMatch('(', $node, 'openParen');
 989:     $directives = new CommaListNode();
 990:     $node->addChild($directives, 'directives');
 991:     if (!$this->tryMatch(')', $node, 'closeParen', FALSE, TRUE)) {
 992:       do {
 993:         $declare_directive = new DeclareDirectiveNode();
 994:         $this->tryMatch(T_STRING, $declare_directive, 'name');
 995:         if ($this->tryMatch('=', $declare_directive)) {
 996:           $declare_directive->addChild($this->staticScalar(), 'value');
 997:         }
 998:         $directives->addChild($declare_directive);
 999:       } while ($this->tryMatch(',', $directives));
1000:       $this->mustMatch(')', $node, 'closeParen', FALSE, TRUE);
1001:     }
1002:     if ($this->tryMatch(':', $node, NULL, FALSE, TRUE)) {
1003:       $node->addChild($this->innerStatementListNode(T_ENDDECLARE), 'body');
1004:       $this->mustMatch(T_ENDDECLARE, $node);
1005:       $this->endStatement($node);
1006:       return $node;
1007:     }
1008:     else {
1009:       $node->addChild($this->statement(), 'body');
1010:       return $node;
1011:     }
1012:   }
1013: 
1014:   /**
1015:    * Parse a try control structure.
1016:    * @return TryCatchNode
1017:    */
1018:   private function _try() {
1019:     $node = new TryCatchNode();
1020:     $this->mustMatch(T_TRY, $node);
1021:     $node->addChild($this->innerStatementBlock(), 'try');
1022:     $catch_node = new CatchNode();
1023:     while ($this->tryMatch(T_CATCH, $catch_node)) {
1024:       $this->mustMatch('(', $catch_node, 'openParen');
1025:       $catch_node->addChild($this->name(), 'exceptionType');
1026:       $this->mustMatch(T_VARIABLE, $catch_node, 'variable');
1027:       $this->mustMatch(')', $catch_node, 'closeParen', FALSE, TRUE);
1028:       $catch_node->addChild($this->innerStatementBlock(), 'body');
1029:       $node->addChild($catch_node);
1030:       $catch_node = new CatchNode();
1031:     }
1032:     if ($this->tryMatch(T_FINALLY, $node)) {
1033:       $node->addChild($this->innerStatementBlock(), 'finally');
1034:     }
1035:     return $node;
1036:   }
1037: 
1038:   /**
1039:    * Parse a throw statement.
1040:    * @return ThrowStatementNode
1041:    */
1042:   private function _throw() {
1043:     $node = new ThrowStatementNode();
1044:     $this->mustMatch(T_THROW, $node);
1045:     $node->addChild($this->expr(), 'expression');
1046:     $this->endStatement($node);
1047:     return $node;
1048:   }
1049: 
1050:   /**
1051:    * Parse a goto statement.
1052:    * @return GotoStatementNode
1053:    */
1054:   private function _goto() {
1055:     $node = new GotoStatementNode();
1056:     $this->mustMatch(T_GOTO, $node);
1057:     $this->mustMatch(T_STRING, $node, 'label');
1058:     $this->endStatement($node);
1059:     return $node;
1060:   }
1061: 
1062:   /**
1063:    * Parse a list of expressions.
1064:    * @return CommaListNode
1065:    */
1066:   private function exprList() {
1067:     $node = new CommaListNode();
1068:     do {
1069:       $node->addChild($this->expr());
1070:     } while ($this->tryMatch(',', $node));
1071:     return $node;
1072:   }
1073: 
1074:   /**
1075:    * Parse a static scalar expression.
1076:    * @return Node
1077:    * @throws ParserException
1078:    */
1079:   private function staticScalar() {
1080:     if ($this->currentType === T_ARRAY) {
1081:       $node = new ArrayNode();
1082:       $this->mustMatch(T_ARRAY, $node);
1083:       $this->mustMatch('(', $node, 'openParen');
1084:       $this->staticArrayPairList($node, ')');
1085:       $this->mustMatch(')', $node, 'closeParen', TRUE);
1086:       return $node;
1087:     }
1088:     elseif ($this->currentType === '[') {
1089:       $node = new ArrayNode();
1090:       $this->mustMatch('[', $node);
1091:       $this->staticArrayPairList($node, ']');
1092:       $this->mustMatch(']', $node, NULL, TRUE);
1093:       return $node;
1094:     }
1095:     else {
1096:       return $this->expr(TRUE);
1097:     }
1098:   }
1099: 
1100:   /**
1101:    * Parse static operand.
1102:    * @return Node
1103:    */
1104:   private function staticOperand() {
1105:     static $scalar_types = [
1106:       T_STRING_VARNAME,
1107:       T_CLASS_C,
1108:       T_LNUMBER,
1109:       T_DNUMBER,
1110:       T_CONSTANT_ENCAPSED_STRING,
1111:       T_LINE,
1112:       T_FILE,
1113:       T_DIR,
1114:       T_TRAIT_C,
1115:       T_METHOD_C,
1116:       T_FUNC_C,
1117:       T_NS_C,
1118:     ];
1119:     if ($scalar = $this->tryMatchToken($scalar_types)) {
1120:       return $scalar;
1121:     }
1122:     elseif ($this->currentType === '(') {
1123:       $node = new ParenthesisNode();
1124:       $this->mustMatch('(', $node, 'openParen');
1125:       $node->addChild($this->staticScalar(), 'expression');
1126:       $this->mustMatch(')', $node, 'closeParen', TRUE);
1127:       return $node;
1128:     }
1129:     elseif (in_array($this->currentType, self::$namespacePathTypes)) {
1130:       $namespace_path = $this->name();
1131:       if ($this->currentType === T_DOUBLE_COLON) {
1132:         $colon_node = new PartialNode();
1133:         $this->mustMatch(T_DOUBLE_COLON, $colon_node);
1134:         if ($this->currentType === T_CLASS) {
1135:           return $this->classNameScalar($namespace_path, $colon_node);
1136:         }
1137:         else {
1138:           $class_constant = $this->mustMatchToken(T_STRING);
1139:           return $this->classConstant($namespace_path, $colon_node, $class_constant);
1140:         }
1141:       }
1142:       else {
1143:         $node = new ConstantNode();
1144:         $node->addChild($namespace_path, 'constantName');
1145:         return $node;
1146:       }
1147:     }
1148:     elseif ($this->currentType === T_STATIC) {
1149:       $static_node = $this->mustMatchToken(T_STATIC);
1150:       $colon_node = new PartialNode();
1151:       $this->mustMatch(T_DOUBLE_COLON, $colon_node);
1152:       if ($this->currentType === T_CLASS) {
1153:         return $this->classNameScalar($static_node, $colon_node);
1154:       }
1155:       else {
1156:         $class_constant = $this->mustMatchToken(T_STRING);
1157:         return $this->classConstant($static_node, $colon_node, $class_constant);
1158:       }
1159:     }
1160:     elseif ($this->currentType === T_START_HEREDOC) {
1161:       $node = new HeredocNode();
1162:       $this->mustMatch(T_START_HEREDOC, $node);
1163:       if ($this->tryMatch(T_END_HEREDOC, $node)) {
1164:         return $node;
1165:       }
1166:       $this->mustMatch(T_ENCAPSED_AND_WHITESPACE, $node);
1167:       $this->mustMatch(T_END_HEREDOC, $node);
1168:       return $node;
1169:     }
1170:     else {
1171:       return NULL;
1172:     }
1173:   }
1174: 
1175:   /**
1176:    * Parse an expression.
1177:    * @param bool $static TRUE if static expression
1178:    * @return Node
1179:    * @throws ParserException
1180:    */
1181:   private function expr($static = FALSE) {
1182:     static $end_expression_types = [':', ';', ',', ')', ']', '}', T_AS, T_DOUBLE_ARROW, T_CLOSE_TAG];
1183:     // Group tokens into operands & operators to pass to the expression parser
1184:     $expression_nodes = [];
1185:     while ($this->currentType !== NULL && !in_array($this->currentType, $end_expression_types)) {
1186:       if ($op = $this->exprOperator($static)) {
1187:         $expression_nodes[] = $op;
1188:         if ($op->type === T_INSTANCEOF) {
1189:           $expression_nodes[] = $this->classNameReference();
1190:         }
1191:       }
1192:       elseif ($operand = ($static ? $this->staticOperand() : $this->exprOperand())) {
1193:         $expression_nodes[] = $operand;
1194:       }
1195:       else {
1196:         throw new ParserException($this->iterator->getSourcePosition(), "invalid expression");
1197:       }
1198:     }
1199:     return $this->expressionParser->parse($expression_nodes);
1200:   }
1201: 
1202:   /**
1203:    * Parse an expression operator.
1204:    * @param bool $static Static operator
1205:    * @return Operator
1206:    */
1207:   private function exprOperator($static = FALSE) {
1208:     $token_type = $this->currentType;
1209:     if ($operator = OperatorFactory::createOperator($token_type, $static)) {
1210:       $this->mustMatch($token_type, $operator, 'operator');
1211:       if ($token_type === '?') {
1212:         if ($this->currentType === ':') {
1213:           $colon = new PartialNode();
1214:           $this->mustMatch(':', $colon);
1215:           return OperatorFactory::createElvisOperator($operator, $colon);
1216:         }
1217:         else {
1218:           $operator->then = $static ? $this->staticScalar() : $this->expr();
1219:           $colon = new PartialNode();
1220:           $this->mustMatch(':', $colon);
1221:           $operator->colon = $colon;
1222:           return $operator;
1223:         }
1224:       }
1225:       elseif ($token_type === '=' && $this->currentType === '&') {
1226:         $by_ref_node = new PartialNode();
1227:         $this->mustMatch('&', $by_ref_node);
1228:         return OperatorFactory::createAssignReferenceOperator($operator, $by_ref_node);
1229:       }
1230:       return $operator;
1231:     }
1232:     return NULL;
1233:   }
1234: 
1235:   /**
1236:    * Parse an expression operand.
1237:    * @return Node
1238:    * @throws ParserException
1239:    */
1240:   private function exprOperand() {
1241:     switch ($this->currentType) {
1242:       case T_STRING_VARNAME:
1243:       case T_CLASS_C:
1244:       case T_LNUMBER:
1245:       case T_DNUMBER:
1246:       case T_LINE:
1247:       case T_FILE:
1248:       case T_DIR:
1249:       case T_TRAIT_C:
1250:       case T_METHOD_C:
1251:       case T_FUNC_C:
1252:       case T_NS_C:
1253:       case T_YIELD:
1254:         return $this->mustMatchToken($this->currentType);
1255:       case T_CONSTANT_ENCAPSED_STRING:
1256:         return $this->arrayDeference($this->mustMatchToken(T_CONSTANT_ENCAPSED_STRING));
1257:       case T_ARRAY:
1258:         $node = new ArrayNode();
1259:         $this->mustMatch(T_ARRAY, $node);
1260:         $this->mustMatch('(', $node, 'openParen');
1261:         $this->arrayPairList($node, ')');
1262:         $this->mustMatch(')', $node, 'closeParen', TRUE);
1263:         return $this->arrayDeference($node);
1264:       case '[':
1265:         $node = new ArrayNode();
1266:         $this->mustMatch('[', $node);
1267:         $this->arrayPairList($node, ']');
1268:         $this->mustMatch(']', $node, NULL, TRUE);
1269:         return $this->arrayDeference($node);
1270:       case '(':
1271:         $node = new ParenthesisNode();
1272:         $this->mustMatch('(', $node, 'openParen');
1273:         if ($this->currentType === T_NEW) {
1274:           $node->addChild($this->newExpr(), 'expression');
1275:           $this->mustMatch(')', $node, 'closeParen', TRUE);
1276:           $node = $this->objectDereference($this->arrayDeference($node));
1277:         }
1278:         elseif ($this->currentType === T_YIELD) {
1279:           $node->addChild($this->_yield());
1280:           $this->mustMatch(')', $node, 'closeParen', TRUE);
1281:         }
1282:         else {
1283:           $node->addChild($this->expr());
1284:           $this->mustMatch(')', $node, 'closeParen', TRUE);
1285:         }
1286:         return $node;
1287:       case T_START_HEREDOC:
1288:         $node = new HeredocNode();
1289:         $this->mustMatch(T_START_HEREDOC, $node);
1290:         if ($this->tryMatch(T_END_HEREDOC, $node, NULL, TRUE)) {
1291:           return $node;
1292:         }
1293:         else {
1294:           $this->encapsList($node, T_END_HEREDOC, TRUE);
1295:           $this->mustMatch(T_END_HEREDOC, $node, NULL, TRUE);
1296:           return $node;
1297:         }
1298:       case '"':
1299:         $node = new InterpolatedStringNode();
1300:         $this->mustMatch('"', $node);
1301:         $this->encapsList($node, '"');
1302:         $this->mustMatch('"', $node);
1303:         return $node;
1304:       case T_STRING:
1305:       case T_NS_SEPARATOR:
1306:       case T_NAMESPACE:
1307:         $namespace_path = $this->name();
1308:         if ($this->currentType === T_DOUBLE_COLON) {
1309:           return $this->exprClass($namespace_path);
1310:         }
1311:         elseif ($this->currentType === '(') {
1312:           return $this->functionCall($namespace_path);
1313:         }
1314:         else {
1315:           $constant_name = strtolower($namespace_path->getText());
1316:           if ($constant_name === 'true') {
1317:             $node = new TrueNode();
1318:           }
1319:           elseif ($constant_name === 'false') {
1320:             $node = new FalseNode();
1321:           }
1322:           elseif ($constant_name === 'null') {
1323:             $node = new NullNode();
1324:           }
1325:           else {
1326:             $node = new ConstantNode();
1327:           }
1328:           $node->addChild($namespace_path, 'constantName');
1329:           return $node;
1330:         }
1331:       case T_STATIC:
1332:         $static = $this->mustMatchToken(T_STATIC);
1333:         if ($this->currentType === T_FUNCTION) {
1334:           return $this->anonymousFunction($static);
1335:         } else {
1336:           return $this->exprClass($static);
1337:         }
1338:       case '$':
1339:       case T_VARIABLE:
1340:         $operand = $this->indirectReference();
1341:         if (!($operand instanceof VariableVariableNode) && $this->currentType === T_DOUBLE_COLON) {
1342:           return $this->exprClass($operand);
1343:         }
1344:         elseif ($this->currentType === '(') {
1345:           return $this->functionCall($operand, TRUE);
1346:         }
1347:         else {
1348:           return $this->objectDereference($operand);
1349:         }
1350:       case T_ISSET:
1351:         $node = new IssetNode();
1352:         $this->mustMatch(T_ISSET, $node, 'name');
1353:         $arguments = new CommaListNode();
1354:         $this->mustMatch('(', $node, 'openParen');
1355:         $node->addChild($arguments, 'arguments');
1356:         do {
1357:           $arguments->addChild($this->variable());
1358:         } while ($this->tryMatch(',', $arguments));
1359:         $this->mustMatch(')', $node, 'closeParen', TRUE);
1360:         return $node;
1361:       case T_EMPTY:
1362:       case T_EVAL:
1363:         if ($this->currentType === T_EMPTY) {
1364:           $node = new EmptyNode();
1365:         }
1366:         else {
1367:           $node = new EvalNode();
1368:         }
1369:         $this->mustMatch($this->currentType, $node, 'name');
1370:         $arguments = new CommaListNode();
1371:         $this->mustMatch('(', $node, 'openParen');
1372:         $node->addChild($arguments, 'arguments');
1373:         $arguments->addChild($this->expr());
1374:         $this->mustMatch(')', $node, 'closeParen', TRUE);
1375:         return $node;
1376:       case T_INCLUDE:
1377:       case T_REQUIRE:
1378:       case T_INCLUDE_ONCE:
1379:       case T_REQUIRE_ONCE:
1380:         if ($this->currentType === T_INCLUDE) {
1381:           $node = new IncludeNode();
1382:         }
1383:         elseif ($this->currentType === T_INCLUDE_ONCE) {
1384:           $node = new IncludeOnceNode();
1385:         }
1386:         elseif ($this->currentType === T_REQUIRE) {
1387:           $node = new RequireNode();
1388:         }
1389:         else {
1390:           $node = new RequireOnceNode();
1391:         }
1392:         $this->mustMatch($this->currentType, $node);
1393:         $node->addChild($this->expr(), 'expression');
1394:         return $node;
1395:       case T_NEW:
1396:         return $this->newExpr();
1397:       case T_LIST:
1398:         return $this->_list();
1399:       case T_EXIT:
1400:         $node = new ExitNode();
1401:         $this->mustMatch(T_EXIT, $node, NULL, TRUE);
1402:         if ($this->currentType !== '(') {
1403:           return $node;
1404:         }
1405:         $this->mustMatch('(', $node, 'openParen');
1406:         if ($this->tryMatch(')', $node, 'closeParen', TRUE)) {
1407:           return $node;
1408:         }
1409:         if ($this->currentType === T_YIELD) {
1410:           $node->addChild($this->_yield(), 'expression');
1411:         }
1412:         else {
1413:           $node->addChild($this->expr(), 'expression');
1414:         }
1415:         $this->mustMatch(')', $node, 'closeParen', TRUE);
1416:         return $node;
1417:       case T_FUNCTION:
1418:         return $this->anonymousFunction();
1419:       case '`':
1420:         return $this->backtick();
1421:     }
1422:     throw new ParserException($this->iterator->getSourcePosition(), "expression operand");
1423:   }
1424: 
1425:   /**
1426:    * Parse a backtick expression.
1427:    * @return BacktickNode
1428:    */
1429:   private function backtick() {
1430:     $node = new BacktickNode();
1431:     $this->mustMatch('`', $node);
1432:     $this->encapsList($node, '`', TRUE);
1433:     $this->mustMatch('`', $node, NULL, TRUE);
1434:     return $node;
1435:   }
1436: 
1437:   /**
1438:    * Parse an anonymous function declaration.
1439:    * @param Node $static
1440:    * @return AnonymousFunctionNode
1441:    */
1442:   private function anonymousFunction(Node $static = NULL) {
1443:     $node = new AnonymousFunctionNode();
1444:     if ($static) {
1445:       $node->addChild($static);
1446:     }
1447:     $this->mustMatch(T_FUNCTION, $node);
1448:     $this->tryMatch('&', $node, 'reference');
1449:     $this->parameterList($node);
1450:     if ($this->tryMatch(T_USE, $node, 'lexicalUse')) {
1451:       $this->mustMatch('(', $node, 'lexicalOpenParen');
1452:       $lexical_vars_node = new CommaListNode();
1453:       do {
1454:         if ($this->currentType === '&') {
1455:           $var = new ReferenceVariableNode();
1456:           $this->mustMatch('&', $var);
1457:           $this->mustMatch(T_VARIABLE, $var, 'variable', TRUE);
1458:           $lexical_vars_node->addChild($var);
1459:         }
1460:         else {
1461:           $this->mustMatch(T_VARIABLE, $lexical_vars_node, TRUE);
1462:         }
1463:       } while ($this->tryMatch(',', $lexical_vars_node));
1464:       $node->addChild($lexical_vars_node, 'lexicalVariables');
1465:       $this->mustMatch(')', $node, 'lexicalCloseParen');
1466:     }
1467:     $this->matchHidden($node);
1468:     $node->addChild($this->innerStatementBlock(), 'body');
1469:     return $node;
1470:   }
1471: 
1472:   /**
1473:    * Parse a new expression.
1474:    * @return NewNode
1475:    */
1476:   private function newExpr() {
1477:     $node = new NewNode();
1478:     $this->mustMatch(T_NEW, $node);
1479:     $node->addChild($this->classNameReference(), 'className');
1480:     if ($this->currentType === '(') {
1481:       $this->functionCallParameterList($node);
1482:     }
1483:     return $node;
1484:   }
1485: 
1486:   /**
1487:    * Parse a class name reference.
1488:    * @return Node
1489:    */
1490:   private function classNameReference() {
1491:     switch ($this->currentType) {
1492:       case T_STRING:
1493:       case T_NS_SEPARATOR:
1494:       case T_NAMESPACE:
1495:         $namespace_path = $this->name();
1496:         if ($this->currentType === T_DOUBLE_COLON) {
1497:           $node = $this->staticMember($namespace_path);
1498:           return $this->dynamicClassNameReference($node);
1499:         }
1500:         else {
1501:           return $namespace_path;
1502:         }
1503:       case T_STATIC:
1504:         $static_node = $this->mustMatchToken(T_STATIC);
1505:         if ($this->currentType === T_DOUBLE_COLON) {
1506:           $node = $this->staticMember($static_node);
1507:           return $this->dynamicClassNameReference($node);
1508:         }
1509:         else {
1510:           return $static_node;
1511:         }
1512:       default:
1513:         if ($this->currentType === '$' && !$this->isLookAhead('{')) {
1514:           return $this->dynamicClassNameReference($this->indirectReference());
1515:         }
1516:         $var_node = $this->referenceVariable();
1517:         if ($this->currentType === T_DOUBLE_COLON) {
1518:           $var_node = $this->staticMember($var_node);
1519:         }
1520:         return $this->dynamicClassNameReference($var_node);
1521:     }
1522:   }
1523: 
1524:   /**
1525:    * Parse static member.
1526:    * @param Node $var_node
1527:    * @return ClassMemberLookupNode
1528:    */
1529:   private function staticMember($var_node) {
1530:     $node = new ClassMemberLookupNode();
1531:     $node->addChild($var_node, 'className');
1532:     $this->mustMatch(T_DOUBLE_COLON, $node);
1533:     $node->addChild($this->indirectReference(), 'memberName');
1534:     return $node;
1535:   }
1536: 
1537:   /**
1538:    * Parse a dynamic class name reference.
1539:    * @param Node $object
1540:    * @return Node
1541:    */
1542:   private function dynamicClassNameReference(Node $object) {
1543:     $node = $object;
1544:     while ($this->currentType === T_OBJECT_OPERATOR) {
1545:       $node = new ObjectPropertyNode();
1546:       $node->addChild($object, 'object');
1547:       $this->mustMatch(T_OBJECT_OPERATOR, $node);
1548:       $node->addChild($this->objectProperty(), 'property');
1549:       $object = $this->offsetVariable($node);
1550:     }
1551:     return $node;
1552:   }
1553: 
1554:   /**
1555:    * Parse array pair list.
1556:    * @param ArrayNode $node the parent ArrayNode
1557:    * @param int|string $terminator Token type that ends the pair list
1558:    */
1559:   private function arrayPairList(ArrayNode $node, $terminator) {
1560:     $elements = new CommaListNode();
1561:     do {
1562:       if ($this->currentType === $terminator) {
1563:         break;
1564:       }
1565:       $this->matchHidden($elements);
1566:       $elements->addChild($this->arrayPair());
1567:     } while ($this->tryMatch(',', $elements, NULL, TRUE));
1568:     $node->addChild($elements, 'elements');
1569:   }
1570: 
1571:   /**
1572:    * Parse static array pair list.
1573:    * @param ArrayNode $node Array node to add elements to
1574:    * @param int|string $terminator Token type that terminates the array pair list
1575:    */
1576:   private function staticArrayPairList(ArrayNode $node, $terminator) {
1577:     $elements = new CommaListNode();
1578:     do {
1579:       if ($this->currentType === $terminator) {
1580:         break;
1581:       }
1582:       $this->matchHidden($elements);
1583:       $value = $this->staticScalar();
1584:       if ($this->currentType === T_DOUBLE_ARROW) {
1585:         $pair = new ArrayPairNode();
1586:         $pair->addChild($value, 'key');
1587:         $this->mustMatch(T_DOUBLE_ARROW, $pair);
1588:         $pair->addChild($this->staticScalar(), 'value');
1589:         $elements->addChild($pair);
1590:       }
1591:       else {
1592:         $elements->addChild($value);
1593:       }
1594:     } while ($this->tryMatch(',', $elements, NULL, TRUE));
1595:     $node->addChild($elements, 'elements');
1596:   }
1597: 
1598:   /**
1599:    * Parse an array pair.
1600:    * @return Node
1601:    */
1602:   private function arrayPair() {
1603:     if ($this->currentType === '&') {
1604:       return $this->writeVariable();
1605:     }
1606:     $node = $this->expr();
1607:     if ($this->currentType === T_DOUBLE_ARROW) {
1608:       $expr = $node;
1609:       $node = new ArrayPairNode();
1610:       $node->addChild($expr, 'key');
1611:       $this->mustMatch(T_DOUBLE_ARROW, $node);
1612:       if ($this->currentType === '&') {
1613:         $node->addChild($this->writeVariable(), 'value');
1614:       }
1615:       else {
1616:         $node->addChild($this->expr(), 'value');
1617:       }
1618:     }
1619:     return $node;
1620:   }
1621: 
1622:   /**
1623:    * Parse a write variable.
1624:    * @return ReferenceVariableNode
1625:    */
1626:   private function writeVariable() {
1627:     $node = new ReferenceVariableNode();
1628:     $this->mustMatch('&', $node);
1629:     $node->addChild($this->variable(), 'variable');
1630:     return $node;
1631:   }
1632: 
1633:   /**
1634:    * Parse an encaps list.
1635:    * @param InterpolatedStringNode|HeredocNode|BacktickNode $node Interpolated string.
1636:    * @param int|string $terminator Token type that terminates the encaps list
1637:    * @param bool $encaps_whitespace_allowed
1638:    */
1639:   private function encapsList($node, $terminator, $encaps_whitespace_allowed = FALSE) {
1640:     if (!$encaps_whitespace_allowed) {
1641:       if ($this->tryMatch(T_ENCAPSED_AND_WHITESPACE, $node)) {
1642:         $node->addChild($this->encapsVar());
1643:       }
1644:     }
1645:     while ($this->currentType !== NULL && $this->currentType !== $terminator) {
1646:       $this->tryMatch(T_ENCAPSED_AND_WHITESPACE, $node) ||
1647:         $node->addChild($this->encapsVar());
1648:     }
1649:   }
1650: 
1651:   /**
1652:    * Parse an encaps variable.
1653:    * @return StringVariableNode
1654:    * @throws ParserException
1655:    */
1656:   private function encapsVar() {
1657:     static $offset_types = [T_STRING, T_NUM_STRING, T_VARIABLE];
1658:     $node = new StringVariableNode();
1659:     if ($this->tryMatch(T_DOLLAR_OPEN_CURLY_BRACES, $node)) {
1660:       if ($this->tryMatch(T_STRING_VARNAME, $node)) {
1661:         if ($this->tryMatch('[', $node)) {
1662:           $node->addChild($this->expr());
1663:           $this->mustMatch(']', $node);
1664:         }
1665:       }
1666:       else {
1667:         $node->addChild($this->expr());
1668:       }
1669:       $this->mustMatch('}', $node);
1670:       return $node;
1671:     }
1672:     elseif ($this->tryMatch(T_CURLY_OPEN, $node)) {
1673:       $node->addChild($this->variable());
1674:       $this->mustMatch('}', $node);
1675:       return $node;
1676:     }
1677:     elseif ($this->mustMatch(T_VARIABLE, $node)) {
1678:       if ($this->tryMatch('[', $node)) {
1679:         if (!in_array($this->currentType, $offset_types)) {
1680:           throw new ParserException($this->iterator->getSourcePosition(),
1681:             'expected encaps_var_offset (T_STRING or T_NUM_STRING or T_VARIABLE)');
1682:         }
1683:         $node->addChild($this->tryMatchToken($offset_types));
1684:         $this->mustMatch(']', $node);
1685:       }
1686:       elseif ($this->tryMatch(T_OBJECT_OPERATOR, $node)) {
1687:         $this->mustMatch(T_STRING, $node);
1688:       }
1689:       return $node;
1690:     }
1691:     throw new ParserException($this->iterator->getSourcePosition(), 'expected encaps variable');
1692:   }
1693: 
1694:   /**
1695:    * Parse expression operand given class name.
1696:    * @param Node $class_name
1697:    * @return Node
1698:    */
1699:   private function exprClass(Node $class_name) {
1700:     $colon_node = new PartialNode();
1701:     $this->mustMatch(T_DOUBLE_COLON, $colon_node);
1702:     if ($this->currentType === T_STRING) {
1703:       $class_constant = $this->mustMatchToken(T_STRING);
1704:       if ($this->currentType === '(') {
1705:         return $this->classMethodCall($class_name, $colon_node, $class_constant);
1706:       }
1707:       else {
1708:         return $this->classConstant($class_name, $colon_node, $class_constant);
1709:       }
1710:     }
1711:     elseif ($this->currentType === T_CLASS) {
1712:       return $this->classNameScalar($class_name, $colon_node);
1713:     }
1714:     else {
1715:       return $this->classVariable($class_name, $colon_node);
1716:     }
1717:   }
1718: 
1719:   /**
1720:    * Construct a class constant.
1721:    * @param $class_name
1722:    * @param $colon_node
1723:    * @param $class_constant
1724:    * @return ClassConstantLookupNode
1725:    */
1726:   private function classConstant($class_name, $colon_node, $class_constant) {
1727:     $node = new ClassConstantLookupNode();
1728:     $node->addChild($class_name, 'className');
1729:     $node->mergeNode($colon_node);
1730:     $node->addChild($class_constant, 'constantName');
1731:     return $node;
1732:   }
1733: 
1734:   /**
1735:    * Construct a class method call.
1736:    * @param Node $class_name
1737:    * @param ParentNode $colon_node
1738:    * @param Node $method_name
1739:    * @return Node
1740:    */
1741:   private function classMethodCall($class_name, $colon_node, $method_name) {
1742:     $node = new ClassMethodCallNode();
1743:     $node->addChild($class_name, 'className');
1744:     $node->mergeNode($colon_node);
1745:     $node->addChild($method_name, 'methodName');
1746:     $this->functionCallParameterList($node);
1747:     return $this->objectDereference($this->arrayDeference($node));
1748:   }
1749: 
1750:   /**
1751:    * Construct a class name scalar.
1752:    * @param $class_name
1753:    * @param $colon_node
1754:    * @return ClassNameScalarNode
1755:    */
1756:   private function classNameScalar($class_name, $colon_node) {
1757:     $node = new ClassNameScalarNode();
1758:     $node->addChild($class_name, 'className');
1759:     $node->mergeNode($colon_node);
1760:     $this->mustMatch(T_CLASS, $node, NULL, TRUE);
1761:     return $node;
1762:   }
1763: 
1764:   /**
1765:    * Parse a class variable given $class_name::
1766:    * @param $class_name
1767:    * @param $colon_node
1768:    * @return Node
1769:    */
1770:   private function classVariable($class_name, $colon_node) {
1771:     if ($this->currentType === '{') {
1772:       // Must be $class_name::{expr()}().
1773:       return $this->classMethodCall($class_name, $colon_node, $this->bracesExpr());
1774:     }
1775: 
1776:     /*
1777:      * Note $var_node contains possible array lookup, eg. $a[0]. This must
1778:      * happen cause $class::$var[0]() is treated as $class::($var[0])();
1779:      * However $class::$var[0] is treated as ($class::$var)[0], but in order
1780:      * to determine between these two cases, the [0] has to be matched.
1781:      */
1782:     $var_node = $this->indirectReference();
1783:     if ($this->currentType === '(') {
1784:       return $this->classMethodCall($class_name, $colon_node, $var_node);
1785:     }
1786:     else {
1787:       /*
1788:        * Since $class::$var[0] is treated as ($class::$var)[0] then have
1789:        * to replace the $var in $var[0] with $class::$var.
1790:        */
1791:       if ($var_node instanceof ArrayLookupNode) {
1792:         // Find the member name, eg. $var
1793:         $member_name = $var_node;
1794:         while ($member_name instanceof ArrayLookupNode) {
1795:           $member_name = $member_name->getArray();
1796:         }
1797:         // Replace the member name with ClassMemberLookupNode, eg. $class::$var
1798:         $node = new ClassMemberLookupNode();
1799:         $node->addChild($class_name, 'className');
1800:         $node->mergeNode($colon_node);
1801:         $node->addChild(clone $member_name, 'memberName');
1802:         $member_name->replaceWith($node);
1803:         return $this->objectDereference($var_node);
1804:       }
1805:       else {
1806:         $node = new ClassMemberLookupNode();
1807:         $node->addChild($class_name, 'className');
1808:         $node->mergeNode($colon_node);
1809:         $node->addChild($var_node, 'memberName');
1810:         return $this->objectDereference($node);
1811:       }
1812:     }
1813:   }
1814: 
1815:   /**
1816:    * Parse variable.
1817:    * @return Node
1818:    * @throws ParserException
1819:    */
1820:   private function variable() {
1821:     switch ($this->currentType) {
1822:       case T_STRING:
1823:       case T_NS_SEPARATOR:
1824:       case T_NAMESPACE:
1825:         $namespace_path = $this->name();
1826:         if ($this->currentType === '(') {
1827:           return $this->functionCall($namespace_path);
1828:         }
1829:         elseif ($this->currentType === T_DOUBLE_COLON) {
1830:           return $this->varClass($namespace_path);
1831:         }
1832:         break;
1833:       case T_STATIC:
1834:         $class_name = $this->mustMatchToken(T_STATIC);
1835:         return $this->varClass($class_name);
1836:       case '$':
1837:       case T_VARIABLE:
1838:         $var = $this->indirectReference();
1839:         if ($this->currentType === '(') {
1840:           return $this->functionCall($var, TRUE);
1841:         }
1842:         elseif (!($var instanceof VariableVariableNode) && $this->currentType === T_DOUBLE_COLON) {
1843:           return $this->varClass($var);
1844:         }
1845:         else {
1846:           return $this->objectDereference($var);
1847:         }
1848:     }
1849:     throw new ParserException($this->iterator->getSourcePosition(), "expected variable");
1850:   }
1851: 
1852:   /**
1853:    * Parse variable given class name.
1854:    * @param Node $class_name
1855:    * @return Node
1856:    */
1857:   private function varClass(Node $class_name) {
1858:     $colon_node = new PartialNode();
1859:     $this->mustMatch(T_DOUBLE_COLON, $colon_node);
1860:     if ($this->currentType === T_STRING) {
1861:       $method_name = $this->mustMatchToken(T_STRING);
1862:       return $this->classMethodCall($class_name, $colon_node, $method_name);
1863:     }
1864:     else {
1865:       return $this->classVariable($class_name, $colon_node);
1866:     }
1867:   }
1868: 
1869:   /**
1870:    * Apply any function call, array and object deference.
1871:    * @param Node $function_reference
1872:    * @param bool $dynamic TRUE if the function call is dynamic
1873:    * @return Node
1874:    */
1875:   private function functionCall(Node $function_reference, $dynamic = FALSE) {
1876:     if ($dynamic) {
1877:       $node = new CallbackCallNode();
1878:       $node->addChild($function_reference, 'callback');
1879:     }
1880:     else {
1881:       if ($function_reference instanceof NameNode && $function_reference->childCount() === 1 && $function_reference == 'define') {
1882:         $node = new DefineNode();
1883:       }
1884:       else {
1885:         $node = new FunctionCallNode();
1886:       }
1887:       $node->addChild($function_reference, 'name');
1888:     }
1889:     $this->functionCallParameterList($node);
1890:     return $this->objectDereference($this->arrayDeference($node));
1891:   }
1892: 
1893:   /**
1894:    * Apply any object dereference to object operand.
1895:    * @param Node $object
1896:    * @return Node
1897:    */
1898:   private function objectDereference(Node $object) {
1899:     if ($this->currentType !== T_OBJECT_OPERATOR) {
1900:       return $object;
1901:     }
1902:     $operator_node = new PartialNode();
1903:     $this->mustMatch(T_OBJECT_OPERATOR, $operator_node, 'operator');
1904: 
1905:     $object_property = $this->objectProperty();
1906:     if ($this->currentType === '(') {
1907:       $node = new ObjectMethodCallNode();
1908:       $node->addChild($object, 'object');
1909:       $node->mergeNode($operator_node);
1910:       $node->addChild($object_property, 'methodName');
1911:       $this->functionCallParameterList($node);
1912:       $node = $this->arrayDeference($node);
1913:     }
1914:     else {
1915:       $node = new ObjectPropertyNode();
1916:       $node->addChild($object, 'object');
1917:       $node->mergeNode($operator_node);
1918:       $node->addChild($object_property, 'property');
1919:       $node = $this->offsetVariable($node);
1920:       if ($this->currentType === '(') {
1921:         $call = new CallbackCallNode();
1922:         $call->addChild($node, 'callback');
1923:         $this->functionCallParameterList($call);
1924:         $node = $this->arrayDeference($call);
1925:       }
1926:     }
1927: 
1928:     return $this->objectDereference($node);
1929:   }
1930: 
1931:   /**
1932:    * Parse object property.
1933:    * @return Node
1934:    */
1935:   private function objectProperty() {
1936:     if ($this->currentType === T_STRING) {
1937:       return $this->mustMatchToken(T_STRING);
1938:     }
1939:     elseif ($this->currentType === '{') {
1940:       return $this->bracesExpr();
1941:     }
1942:     else {
1943:       return $this->indirectReference();
1944:     }
1945:   }
1946: 
1947:   /**
1948:    * Parse indirect variable reference.
1949:    * @return Node
1950:    */
1951:   private function indirectReference() {
1952:     if ($this->currentType === '$' && !$this->isLookAhead('{')) {
1953:       $node = new VariableVariableNode();
1954:       $this->mustMatch('$', $node);
1955:       $node->addChild($this->indirectReference(), 'variable');
1956:       return $node;
1957:     }
1958:     return $this->referenceVariable();
1959:   }
1960: 
1961:   /**
1962:    * Parse variable reference.
1963:    * @return Node
1964:    */
1965:   private function referenceVariable() {
1966:     return $this->offsetVariable($this->compoundVariable());
1967:   }
1968: 
1969:   /**
1970:    * Apply any offset to variable.
1971:    * @param Node $var
1972:    * @return Node
1973:    */
1974:   private function offsetVariable(Node $var) {
1975:     if ($this->currentType === '{') {
1976:       $node = new ArrayLookupNode();
1977:       $node->addChild($var, 'array');
1978:       $this->mustMatch('{', $node);
1979:       $node->addChild($this->expr(), 'key');
1980:       $this->mustMatch('}', $node, NULL, TRUE);
1981:       return $this->offsetVariable($node);
1982:     }
1983:     elseif ($this->currentType === '[') {
1984:       $node = new ArrayLookupNode();
1985:       $node->addChild($var, 'array');
1986:       $this->dimOffset($node);
1987:       return $this->offsetVariable($node);
1988:     }
1989:     else {
1990:       return $var;
1991:     }
1992:   }
1993: 
1994:   /**
1995:    * Parse compound variable.
1996:    * @return Node
1997:    */
1998:   private function compoundVariable() {
1999:     if ($this->currentType === '$') {
2000:       return $this->_compoundVariable();
2001:     }
2002:     else {
2003:       return $this->mustMatchToken(T_VARIABLE);
2004:     }
2005:   }
2006: 
2007:   /**
2008:    * Parse compound variable.
2009:    * @return CompoundVariableNode
2010:    */
2011:   private function _compoundVariable() {
2012:     $node = new CompoundVariableNode();
2013:     $this->mustMatch('$', $node);
2014:     $this->mustMatch('{', $node);
2015:     $node->addChild($this->expr(), 'expression');
2016:     $this->mustMatch('}', $node, NULL, TRUE);
2017:     return $node;
2018:   }
2019: 
2020:   /**
2021:    * Parse braces expression.
2022:    * @return NameExpressionNode
2023:    */
2024:   private function bracesExpr() {
2025:     $node = new NameExpressionNode();
2026:     $this->mustMatch('{', $node);
2027:     $node->addChild($this->expr());
2028:     $this->mustMatch('}', $node, NULL, TRUE);
2029:     return $node;
2030:   }
2031: 
2032:   /**
2033:    * Parse dimensional offset.
2034:    * @param ArrayLookupNode $node Node to append to
2035:    */
2036:   private function dimOffset(ArrayLookupNode $node) {
2037:     $this->mustMatch('[', $node);
2038:     if ($this->currentType !== ']') {
2039:       $node->addChild($this->expr(), 'key');
2040:     }
2041:     $this->mustMatch(']', $node, NULL, TRUE);
2042:   }
2043: 
2044:   /**
2045:    * Parse function call parameter list.
2046:    * @param NewNode|CallNode $node
2047:    */
2048:   private function functionCallParameterList($node) {
2049:     $arguments = new CommaListNode();
2050:     $this->mustMatch('(', $node, 'openParen');
2051:     $node->addChild($arguments, 'arguments');
2052:     if ($this->tryMatch(')', $node, 'closeParen', TRUE)) {
2053:       return;
2054:     }
2055:     if ($this->currentType === T_YIELD) {
2056:       $arguments->addChild($this->_yield());
2057:     } else {
2058:       do {
2059:         $arguments->addChild($this->functionCallParameter());
2060:       } while ($this->tryMatch(',', $arguments));
2061:     }
2062:     $this->mustMatch(')', $node, 'closeParen', TRUE);
2063:   }
2064: 
2065:   /**
2066:    * Parse function call parameter.
2067:    * @return Node
2068:    */
2069:   private function functionCallParameter() {
2070:     switch ($this->currentType) {
2071:       case '&':
2072:         return $this->writeVariable();
2073:       case T_ELLIPSIS:
2074:         $node = new SplatNode();
2075:         $this->mustMatch(T_ELLIPSIS, $node);
2076:         $node->addChild($this->expr(), 'expression');
2077:         return $node;
2078:       default:
2079:         return $this->expr();
2080:     }
2081:   }
2082: 
2083:   /**
2084:    * Apply any array deference to operand.
2085:    * @param Node $node
2086:    * @return Node
2087:    */
2088:   private function arrayDeference(Node $node) {
2089:     while ($this->currentType === '[') {
2090:       $n = $node;
2091:       $node = new ArrayLookupNode();
2092:       $node->addChild($n, 'array');
2093:       $this->dimOffset($node);
2094:     }
2095:     return $node;
2096:   }
2097: 
2098:   /**
2099:    * Parse function declaration.
2100:    * @return FunctionDeclarationNode
2101:    */
2102:   private function functionDeclaration() {
2103:     $node = new FunctionDeclarationNode();
2104:     $this->matchDocComment($node);
2105:     $this->mustMatch(T_FUNCTION, $node);
2106:     $this->tryMatch('&', $node, 'reference');
2107:     $name_node = new NameNode();
2108:     $this->mustMatch(T_STRING, $name_node, NULL, TRUE);
2109:     $node->addChild($name_node, 'name');
2110:     $this->parameterList($node);
2111:     $this->matchHidden($node);
2112:     $this->body($node);
2113:     return $node;
2114:   }
2115: 
2116:   /**
2117:    * Parse parameter list.
2118:    * @param ParentNode $parent
2119:    */
2120:   private function parameterList(ParentNode $parent) {
2121:     $node = new CommaListNode();
2122:     $this->mustMatch('(', $parent, 'openParen');
2123:     $parent->addChild($node, 'parameters');
2124:     if ($this->tryMatch(')', $parent, 'closeParen', TRUE)) {
2125:       return;
2126:     }
2127:     do {
2128:       $node->addChild($this->parameter());
2129:     } while ($this->tryMatch(',', $node));
2130:     $this->mustMatch(')', $parent, 'closeParen', TRUE);
2131:   }
2132: 
2133:   /**
2134:    * Parse parameter.
2135:    * @return ParameterNode
2136:    */
2137:   private function parameter() {
2138:     $node = new ParameterNode();
2139:     if ($type = $this->optionalTypeHint()) {
2140:       $node->addChild($type, 'typeHint');
2141:     }
2142:     $this->tryMatch('&', $node, 'reference');
2143:     $this->tryMatch(T_ELLIPSIS, $node, 'variadic');
2144:     $this->mustMatch(T_VARIABLE, $node, 'name', TRUE);
2145:     if ($this->tryMatch('=', $node)) {
2146:       $node->addChild($this->staticScalar(), 'value');
2147:     }
2148:     return $node;
2149:   }
2150: 
2151:   /**
2152:    * Parse optional class type for parameter.
2153:    * @return Node
2154:    */
2155:   private function optionalTypeHint() {
2156:     static $array_callable_types = [T_ARRAY, T_CALLABLE];
2157:     $node = NULL;
2158:     if ($node = $this->tryMatchToken($array_callable_types)) {
2159:       return $node;
2160:     }
2161:     elseif (in_array($this->currentType, self::$namespacePathTypes)) {
2162:       return $this->name();
2163:     }
2164:     return NULL;
2165:   }
2166: 
2167:   /**
2168:    * Parse function/method body.
2169:    *
2170:    * @param FunctionDeclarationNode|ClassMethodNode $function
2171:    *   Function or method.
2172:    */
2173:   private function body($function) {
2174:     $function->addChild($this->innerStatementBlock(), 'body');
2175:   }
2176: 
2177:   /**
2178:    * Parse inner statement list.
2179:    * @param StatementBlockNode $parent Node to append statements to
2180:    * @param int|string $terminator Token type that terminates the statement list
2181:    */
2182:   private function innerStatementList(StatementBlockNode $parent, $terminator) {
2183:     while ($this->currentType !== NULL && $this->currentType !== $terminator) {
2184:       $this->matchHidden($parent);
2185:       $parent->addChild($this->innerStatement());
2186:     }
2187:   }
2188: 
2189:   /**
2190:    * Parse inner statement block.
2191:    * @return Node
2192:    */
2193:   private function innerStatementBlock() {
2194:     $node = new StatementBlockNode();
2195:     $this->mustMatch('{', $node, NULL, FALSE, TRUE);
2196:     $this->innerStatementList($node, '}');
2197:     $this->mustMatch('}', $node, NULL, TRUE, TRUE);
2198:     return $node;
2199:   }
2200: 
2201:   /**
2202:    * Parse inner statement list for alternative control structures.
2203:    * @param $terminator
2204:    * @return Node
2205:    */
2206:   private function innerStatementListNode($terminator) {
2207:     $node = new StatementBlockNode();
2208:     $this->innerStatementList($node, $terminator);
2209:     return $node;
2210:   }
2211: 
2212:   /**
2213:    * Parse an inner statement.
2214:    * @return Node
2215:    * @throws ParserException
2216:    */
2217:   private function innerStatement() {
2218:     switch ($this->currentType) {
2219:       case T_HALT_COMPILER:
2220:         throw new ParserException($this->iterator->getSourcePosition(),
2221:           "__halt_compiler can only be used from the outermost scope");
2222:       case T_ABSTRACT:
2223:       case T_FINAL:
2224:       case T_CLASS:
2225:         return $this->classDeclaration();
2226:       case T_INTERFACE:
2227:         return $this->interfaceDeclaration();
2228:       case T_TRAIT:
2229:         return $this->traitDeclaration();
2230:       default:
2231:         if ($this->currentType === T_FUNCTION && $this->isLookAhead(T_STRING, '&')) {
2232:           return $this->functionDeclaration();
2233:         }
2234:         return $this->statement();
2235:     }
2236:   }
2237: 
2238:   /**
2239:    * Parse a namespace path.
2240:    * @return NameNode
2241:    */
2242:   private function name() {
2243:     $node = new NameNode();
2244:     if ($this->tryMatch(T_NAMESPACE, $node)) {
2245:       $this->mustMatch(T_NS_SEPARATOR, $node);
2246:     }
2247:     elseif ($this->tryMatch(T_NS_SEPARATOR, $node)) {
2248:       // Absolute path
2249:     }
2250:     $this->mustMatch(T_STRING, $node, NULL, TRUE);
2251:     while ($this->tryMatch(T_NS_SEPARATOR, $node)) {
2252:       $this->mustMatch(T_STRING, $node, NULL, TRUE);
2253:     }
2254:     return $node;
2255:   }
2256: 
2257:   /**
2258:    * Parse a namespace declaration.
2259:    * @return NamespaceNode
2260:    */
2261:   private function _namespace() {
2262:     $node = new NamespaceNode();
2263:     $this->matchDocComment($node);
2264:     $this->mustMatch(T_NAMESPACE, $node);
2265:     if ($this->currentType === T_STRING) {
2266:       $name = $this->namespaceName();
2267:       $node->addChild($name, 'name');
2268:     }
2269:     $this->matchHidden($node);
2270:     $body = new StatementBlockNode();
2271:     if ($this->tryMatch('{', $body)) {
2272:       $this->topStatementList($body, '}');
2273:       $this->mustMatch('}', $body);
2274:       $node->addChild($body, 'body');
2275:     }
2276:     else {
2277:       $this->endStatement($node);
2278:       $this->matchHidden($node);
2279:       $node->addChild($this->namespaceBlock(), 'body');
2280:     }
2281:     return $node;
2282:   }
2283: 
2284:   /**
2285:    * Parse a list of top level namespace statements.
2286:    * @return StatementBlockNode
2287:    */
2288:   private function namespaceBlock() {
2289:     $node = new StatementBlockNode();
2290:     $this->matchHidden($node);
2291:     while ($this->currentType !== NULL) {
2292:       if ($this->currentType === T_NAMESPACE && !$this->isLookAhead(T_NS_SEPARATOR)) {
2293:         break;
2294:       }
2295:       $node->addChild($this->topStatement());
2296:       $this->matchHidden($node);
2297:     }
2298:     $this->matchHidden($node);
2299:     return $node;
2300:   }
2301: 
2302:   /**
2303:    * Parse a namespace name.
2304:    * @return NameNode
2305:    */
2306:   private function namespaceName() {
2307:     $node = new NameNode();
2308:     $this->mustMatch(T_STRING, $node, NULL, TRUE);
2309:     while ($this->tryMatch(T_NS_SEPARATOR, $node)) {
2310:       $this->mustMatch(T_STRING, $node, NULL, TRUE);
2311:     }
2312:     return $node;
2313:   }
2314: 
2315:   /**
2316:    * Parse a block of use declaration statements.
2317:    * @return UseDeclarationBlockNode
2318:    */
2319:   private function useBlock() {
2320:     $node = new UseDeclarationBlockNode();
2321:     $node->addChild($this->_use());
2322:     while ($this->currentType === T_USE) {
2323:       $this->matchHidden($node);
2324:       $node->addChild($this->_use());
2325:     }
2326:     return $node;
2327:   }
2328: 
2329:   /**
2330:    * Parse a use declaration list.
2331:    * @return UseDeclarationStatementNode
2332:    */
2333:   private function _use() {
2334:     $node = new UseDeclarationStatementNode();
2335:     $this->mustMatch(T_USE, $node);
2336:     $this->tryMatch(T_FUNCTION, $node, 'useFunction') || $this->tryMatch(T_CONST, $node, 'useConst');
2337:     $declarations = new CommaListNode();
2338:     do {
2339:       $declarations->addChild($this->useDeclaration());
2340:     } while ($this->tryMatch(',', $declarations));
2341:     $node->addChild($declarations, 'declarations');
2342:     $this->endStatement($node);
2343:     return $node;
2344:   }
2345: 
2346:   /**
2347:    * Parse a use declaration.
2348:    * @return UseDeclarationNode
2349:    */
2350:   private function useDeclaration() {
2351:     $declaration = new UseDeclarationNode();
2352:     $node = new NameNode();
2353:     $this->tryMatch(T_NS_SEPARATOR, $node);
2354:     $this->mustMatch(T_STRING, $node, NULL, TRUE)->getText();
2355:     while ($this->tryMatch(T_NS_SEPARATOR, $node)) {
2356:       $this->mustMatch(T_STRING, $node, NULL, TRUE)->getText();
2357:     }
2358:     $declaration->addChild($node, 'name');
2359:     if ($this->tryMatch(T_AS, $declaration)) {
2360:       $this->mustMatch(T_STRING, $declaration, 'alias', TRUE)->getText();
2361:     }
2362:     return $declaration;
2363:   }
2364: 
2365:   /**
2366:    * Parse a class declaration.
2367:    * @return ClassNode
2368:    */
2369:   private function classDeclaration() {
2370:     $node = new ClassNode();
2371:     $this->matchDocComment($node);
2372:     $this->tryMatch(T_ABSTRACT, $node, 'abstract') || $this->tryMatch(T_FINAL, $node, 'final');
2373:     $this->mustMatch(T_CLASS, $node);
2374:     $name_node = new NameNode();
2375:     $this->mustMatch(T_STRING, $name_node, NULL, TRUE);
2376:     $node->addChild($name_node, 'name');
2377:     if ($this->tryMatch(T_EXTENDS, $node)) {
2378:       $node->addChild($this->name(), 'extends');
2379:     }
2380:     if ($this->tryMatch(T_IMPLEMENTS, $node)) {
2381:       $implements = new CommaListNode();
2382:       do {
2383:         $implements->addChild($this->name());
2384:       } while ($this->tryMatch(',', $implements));
2385:       $node->addChild($implements, 'implements');
2386:     }
2387:     $this->matchHidden($node);
2388:     $statement_block = new StatementBlockNode();
2389:     $this->mustMatch('{', $statement_block, NULL, FALSE, TRUE);
2390:     $is_abstract = $node->getAbstract() !== NULL;
2391:     while ($this->currentType !== NULL && $this->currentType !== '}') {
2392:       $this->matchHidden($statement_block);
2393:       $statement_block->addChild($this->classStatement($is_abstract));
2394:     }
2395:     $this->mustMatch('}', $statement_block, NULL, TRUE, TRUE);
2396:     $node->addChild($statement_block, 'statements');
2397:     return $node;
2398:   }
2399: 
2400:   /**
2401:    * Parse a class statement.
2402:    * @param bool $is_abstract TRUE if the class is abstract
2403:    * @return Node
2404:    * @throws ParserException
2405:    */
2406:   private function classStatement($is_abstract) {
2407:     if ($this->currentType === T_FUNCTION) {
2408:       $modifiers = new ModifiersNode();
2409:       $doc_comment = new PartialCommentNode();
2410:       $this->matchDocComment($doc_comment);
2411:       return $this->classMethod($doc_comment, $modifiers);
2412:     }
2413:     elseif ($this->currentType === T_VAR) {
2414:       $doc_comment = new PartialCommentNode();
2415:       $this->matchDocComment($doc_comment);
2416:       $modifiers = new ModifiersNode();
2417:       $this->mustMatch(T_VAR, $modifiers, 'visibility');
2418:       return $this->classMemberList($doc_comment, $modifiers);
2419:     }
2420:     elseif ($this->currentType === T_CONST) {
2421:       return $this->_const();
2422:     }
2423:     elseif ($this->currentType === T_USE) {
2424:       return $this->traitUse();
2425:     }
2426:     // Match modifiers
2427:     $doc_comment = new PartialCommentNode();
2428:     $this->matchDocComment($doc_comment);
2429:     $modifiers = new ModifiersNode();
2430:     while ($this->iterator->hasNext()) {
2431:       switch ($this->currentType) {
2432:         case T_PUBLIC:
2433:         case T_PROTECTED:
2434:         case T_PRIVATE:
2435:           if ($modifiers->getVisibility()) {
2436:             throw new ParserException(
2437:               $this->iterator->getSourcePosition(),
2438:               "can only have one visibility modifier on class member/method."
2439:             );
2440:           }
2441:           $this->mustMatch($this->currentType, $modifiers, 'visibility');
2442:           break;
2443:         case T_STATIC:
2444:           if ($modifiers->getStatic()) {
2445:             throw new ParserException(
2446:               $this->iterator->getSourcePosition(), "duplicate modifier");
2447:           }
2448:           $this->mustMatch(T_STATIC, $modifiers, 'static');
2449:           break;
2450:         case T_FINAL:
2451:           if ($modifiers->getFinal()) {
2452:             throw new ParserException(
2453:               $this->iterator->getSourcePosition(), "duplicate modifier");
2454:           }
2455:           if ($modifiers->getAbstract()) {
2456:             throw new ParserException(
2457:               $this->iterator->getSourcePosition(),
2458:               "can not use final modifier on abstract method");
2459:           }
2460:           $this->mustMatch(T_FINAL, $modifiers, 'final');
2461:           break;
2462:         case T_ABSTRACT:
2463:           if ($modifiers->getAbstract()) {
2464:             throw new ParserException(
2465:               $this->iterator->getSourcePosition(), "duplicate modifier");
2466:           }
2467:           if ($modifiers->getFinal()) {
2468:             throw new ParserException(
2469:               $this->iterator->getSourcePosition(),
2470:               "can not use abstract modifier on final method");
2471:           }
2472:           if (!$is_abstract) {
2473:             throw new ParserException(
2474:               $this->iterator->getSourcePosition(),
2475:               "can not use abstract modifier in non-abstract class");
2476:           }
2477:           $this->mustMatch(T_ABSTRACT, $modifiers, 'abstract');
2478:           break;
2479:         case T_FUNCTION:
2480:           return $this->classMethod($doc_comment, $modifiers);
2481:         case T_VARIABLE:
2482:           return $this->classMemberList($doc_comment, $modifiers);
2483:         default:
2484:           throw new ParserException(
2485:             $this->iterator->getSourcePosition(),
2486:             "invalid class statement");
2487:       }
2488:     }
2489:     throw new ParserException(
2490:       $this->iterator->getSourcePosition(),
2491:       "invalid class statement");
2492:   }
2493: 
2494:   /**
2495:    * Parse a class member list.
2496:    * @param PartialNode|null $doc_comment DocBlock associated with method
2497:    * @param ModifiersNode $modifiers Member modifiers
2498:    * @return ClassMemberListNode
2499:    * @throws ParserException
2500:    */
2501:   private function classMemberList($doc_comment, ModifiersNode $modifiers) {
2502:     // Modifier checks
2503:     if ($modifiers->getAbstract()) {
2504:       throw new ParserException(
2505:         $this->iterator->getSourcePosition(),
2506:         "members can not be declared abstract");
2507:     }
2508:     if ($modifiers->getFinal()) {
2509:       throw new ParserException(
2510:         $this->iterator->getSourcePosition(),
2511:         "members can not be declared final");
2512:     }
2513:     $node = new ClassMemberListNode();
2514:     $node->mergeNode($doc_comment);
2515:     $node->mergeNode($modifiers);
2516:     $members = new CommaListNode();
2517:     do {
2518:       $members->addChild($this->classMember());
2519:     } while ($this->tryMatch(',', $members));
2520:     $node->addChild($members, 'members');
2521:     $this->endStatement($node);
2522:     return $node;
2523:   }
2524: 
2525:   /**
2526:    * Parse a class member.
2527:    * @return ClassMemberNode
2528:    */
2529:   private function classMember() {
2530:     $node = new ClassMemberNode();
2531:     $this->mustMatch(T_VARIABLE, $node, 'name', TRUE);
2532:     if ($this->tryMatch('=', $node)) {
2533:       $node->addChild($this->staticScalar(), 'value');
2534:     }
2535:     return $node;
2536:   }
2537: 
2538:   /**
2539:    * Parse a class method
2540:    * @param PartialNode|null $doc_comment DocBlock associated with method
2541:    * @param ModifiersNode $modifiers Method modifiers
2542:    * @return ClassMethodNode
2543:    */
2544:   private function classMethod($doc_comment, ModifiersNode $modifiers) {
2545:     $node = new ClassMethodNode();
2546:     $node->mergeNode($doc_comment);
2547:     $node->mergeNode($modifiers);
2548:     $this->mustMatch(T_FUNCTION, $node);
2549:     $this->tryMatch('&', $node, 'reference');
2550:     $this->mustMatch(T_STRING, $node, 'name');
2551:     $this->parameterList($node);
2552:     if ($modifiers->getAbstract()) {
2553:       $this->endStatement($node);
2554:       return $node;
2555:     }
2556:     $this->matchHidden($node);
2557:     $this->body($node);
2558:     return $node;
2559:   }
2560: 
2561:   /**
2562:    * Parse a trait use statement.
2563:    * @return TraitUseNode
2564:    */
2565:   private function traitUse() {
2566:     $node = new TraitUseNode();
2567:     $this->mustMatch(T_USE, $node);
2568:     // trait_list
2569:     $traits = new CommaListNode();
2570:     do {
2571:       $traits->addChild($this->name());
2572:     } while ($this->tryMatch(',', $traits));
2573:     $node->addChild($traits, 'traits');
2574:     // trait_adaptations
2575:     if ($this->tryMatch('{', $node)) {
2576:       $adaptations = new StatementBlockNode();
2577:       while ($this->currentType !== NULL && $this->currentType !== '}') {
2578:         $adaptations->addChild($this->traitAdaptation());
2579:         $this->matchHidden($adaptations);
2580:       }
2581:       $node->addChild($adaptations, 'adaptations');
2582:       $this->mustMatch('}', $node, NULL, TRUE, TRUE);
2583:       return $node;
2584:     }
2585:     $this->endStatement($node);
2586:     return $node;
2587:   }
2588: 
2589:   /**
2590:    * Parse a trait adaptation statement.
2591:    * @return Node
2592:    */
2593:   private function traitAdaptation() {
2594:     /** @var NameNode $qualified_name */
2595:     $qualified_name = $this->name();
2596:     if ($qualified_name->childCount() === 1 && $this->currentType !== T_DOUBLE_COLON) {
2597:       return $this->traitAlias($qualified_name);
2598:     }
2599:     $node = new TraitMethodReferenceNode();
2600:     $node->addChild($qualified_name, 'traitName');
2601:     $this->mustMatch(T_DOUBLE_COLON, $node);
2602:     $this->mustMatch(T_STRING, $node, 'methodReference', TRUE);
2603:     if ($this->currentType === T_AS) {
2604:       return $this->traitAlias($node);
2605:     }
2606:     $method_reference_node = $node;
2607:     $node = new TraitPrecedenceNode();
2608:     $node->addChild($method_reference_node, 'traitMethodReference');
2609:     $this->mustMatch(T_INSTEADOF, $node);
2610:     $trait_names = new CommaListNode();
2611:     do {
2612:       $trait_names->addChild($this->name());
2613:     } while ($this->tryMatch(',', $trait_names));
2614:     $node->addChild($trait_names, 'traitNames');
2615:     $this->endStatement($node);
2616:     return $node;
2617:   }
2618: 
2619:   /**
2620:    * Parse a trait alias.
2621:    * @param TraitMethodReferenceNode|NameNode $trait_method_reference
2622:    * @return TraitAliasNode
2623:    */
2624:   private function traitAlias($trait_method_reference) {
2625:     $node = new TraitAliasNode();
2626:     $node->addChild($trait_method_reference, 'traitMethodReference');
2627:     $this->mustMatch(T_AS, $node);
2628:     if ($trait_modifier = $this->tryMatchToken(self::$visibilityTypes)) {
2629:       $node->addChild($trait_modifier, 'visibility');
2630:       $this->tryMatch(T_STRING, $node, 'alias');
2631:       $this->endStatement($node);
2632:       return $node;
2633:     }
2634:     $this->mustMatch(T_STRING, $node, 'alias');
2635:     $this->endStatement($node);
2636:     return $node;
2637:   }
2638: 
2639:   /**
2640:    * Parse an interface declaration.
2641:    * @return Node
2642:    */
2643:   private function interfaceDeclaration() {
2644:     $node = new InterfaceNode();
2645:     $this->matchDocComment($node);
2646:     $this->mustMatch(T_INTERFACE, $node);
2647:     $name_node = new NameNode();
2648:     $this->mustMatch(T_STRING, $name_node, NULL, TRUE);
2649:     $node->addChild($name_node, 'name');
2650:     if ($this->tryMatch(T_EXTENDS, $node)) {
2651:       $extends = new CommaListNode();
2652:       do {
2653:         $extends->addChild($this->name());
2654:       } while ($this->tryMatch(',', $extends));
2655:       $node->addChild($extends, 'extends');
2656:     }
2657:     $this->matchHidden($node);
2658:     $statement_block = new StatementBlockNode();
2659:     $this->mustMatch('{', $statement_block, NULL, FALSE, TRUE);
2660:     while ($this->currentType !== NULL && $this->currentType !== '}') {
2661:       $this->matchHidden($statement_block);
2662:       if ($this->currentType === T_CONST) {
2663:         $statement_block->addChild($this->_const());
2664:       }
2665:       else {
2666:         $statement_block->addChild($this->interfaceMethod());
2667:       }
2668:     }
2669:     $this->mustMatch('}', $statement_block, NULL, TRUE, TRUE);
2670:     $node->addChild($statement_block, 'statements');
2671:     return $node;
2672:   }
2673: 
2674:   /**
2675:    * Parse an interface method declaration.
2676:    * @return InterfaceMethodNode
2677:    * @throws ParserException
2678:    */
2679:   private function interfaceMethod() {
2680:     static $visibility_keyword_types = [T_PUBLIC, T_PROTECTED, T_PRIVATE];
2681:     $node = new InterfaceMethodNode();
2682:     $this->matchDocComment($node);
2683:     $is_static = $this->tryMatch(T_STATIC, $node, 'static');
2684:     while (in_array($this->currentType, $visibility_keyword_types)) {
2685:       if ($node->getVisibility()) {
2686:         throw new ParserException(
2687:           $this->iterator->getSourcePosition(),
2688:           "can only have one visibility modifier on interface method."
2689:         );
2690:       }
2691:       $this->mustMatch($this->currentType, $node, 'visibility');
2692:     }
2693:     !$is_static && $this->tryMatch(T_STATIC, $node, 'static');
2694:     $this->mustMatch(T_FUNCTION, $node);
2695:     $this->tryMatch('&', $node, 'reference');
2696:     $this->mustMatch(T_STRING, $node, 'name');
2697:     $this->parameterList($node);
2698:     $this->endStatement($node);
2699:     return $node;
2700:   }
2701: 
2702: 
2703:   /**
2704:    * Parse a trait declaration.
2705:    * @return TraitNode
2706:    */
2707:   private function traitDeclaration() {
2708:     $node = new TraitNode();
2709:     $this->matchDocComment($node);
2710:     $this->mustMatch(T_TRAIT, $node);
2711:     $name_node = new NameNode();
2712:     $this->mustMatch(T_STRING, $name_node, NULL, TRUE);
2713:     $node->addChild($name_node, 'name');
2714:     if ($this->tryMatch(T_EXTENDS, $node)) {
2715:       $node->addChild($this->name(), 'extends');
2716:     }
2717:     if ($this->tryMatch(T_IMPLEMENTS, $node)) {
2718:       $implements = new CommaListNode();
2719:       do {
2720:         $implements->addChild($this->name());
2721:       } while ($this->tryMatch(',', $implements));
2722:       $node->addChild($implements, 'implements');
2723:     }
2724:     $this->matchHidden($node);
2725:     $statement_block = new StatementBlockNode();
2726:     $this->mustMatch('{', $statement_block, NULL, FALSE, TRUE);
2727:     while ($this->currentType !== NULL && $this->currentType !== '}') {
2728:       $this->matchHidden($statement_block);
2729:       $statement_block->addChild($this->classStatement(TRUE));
2730:     }
2731:     $this->mustMatch('}', $statement_block, NULL, TRUE, TRUE);
2732:     $node->addChild($statement_block, 'statements');
2733:     return $node;
2734:   }
2735: 
2736:   /**
2737:    * Skip hidden tokens.
2738:    */
2739:   private function skipHidden() {
2740:     $token = $this->iterator->current();
2741:     while ($token && $token instanceof HiddenNode) {
2742:       $this->skipped[] = $token;
2743:       $token = $this->iterator->next();
2744:     }
2745:   }
2746: 
2747:   /**
2748:    * Skip hidden tokens.
2749:    */
2750:   private function skipHiddenCaptureDocComment() {
2751:     $token = $this->iterator->current();
2752:     // Skip whitespace and comment
2753:     while ($token && ($token->getType() === T_COMMENT || $token->getType() === T_WHITESPACE)) {
2754:       $this->skipped[] = $token;
2755:       $token = $this->iterator->next();
2756:     }
2757:     while ($token && $token instanceof DocCommentNode) {
2758:       $this->skippedDocComment = [];
2759:       $this->docComment = $token;
2760:       $token = $this->iterator->next();
2761:       while ($token && ($token->getType() === T_COMMENT || $token->getType() === T_WHITESPACE)) {
2762:         $this->skippedDocComment[] = $token;
2763:         $token = $this->iterator->next();
2764:       }
2765:       if ($token && $token instanceof DocCommentNode) {
2766:         // Merge skippedDocComment with skipped
2767:         $this->skipped[] = $this->docComment;
2768:         $this->skipped = array_merge($this->skipped, $this->skippedDocComment);
2769:       }
2770:     }
2771:   }
2772: 
2773:   private function getSkipNodes($skipped) {
2774:     $nodes = [];
2775:     for ($i = 0, $n = count($skipped); $i < $n; $i++) {
2776:       $token = $skipped[$i];
2777:       if ($token instanceof CommentNode && $token->isLineComment()) {
2778:         $comment = $token;
2779:         $end = $i;
2780:         for ($j = $i + 1; $j < $n; $j++) {
2781:           $token = $skipped[$j];
2782:           if ($token instanceof WhitespaceNode && $token->getNewlineCount() === 0) {
2783:             $j++;
2784:             $token = $j < $n ? $skipped[$j] : NULL;
2785:           }
2786:           if ($token instanceof CommentNode && $token->getCommentType() === $comment->getCommentType()) {
2787:             $end = $j;
2788:           }
2789:           else {
2790:             break;
2791:           }
2792:         }
2793:         if ($end > $i) {
2794:           $comment_block = new LineCommentBlockNode();
2795:           for ($j = $i; $j <= $end; $j++) {
2796:             $comment_block->addChild($skipped[$j]);
2797:           }
2798:           $i = $end;
2799:           $nodes[] = $comment_block;
2800:         }
2801:         else {
2802:           $nodes[] = $comment;
2803:         }
2804:       }
2805:       else {
2806:         $nodes[] = $token;
2807:       }
2808:     }
2809:     return $nodes;
2810:   }
2811: 
2812:   /**
2813:    * Add any previously skipped tokens to $parent.
2814:    * @param ParentNode $parent
2815:    */
2816:   private function addSkipped(ParentNode $parent) {
2817:     $parent->addChildren($this->getSkipNodes($this->skipped));
2818:     $this->skipped = [];
2819:     $this->matchDocComment($this->skipParent ?: $parent, NULL);
2820:     $this->skipParent = NULL;
2821:   }
2822: 
2823:   /**
2824:    * Match hidden tokens and add to $parent.
2825:    * @param ParentNode $parent
2826:    */
2827:   private function matchHidden(ParentNode $parent) {
2828:     $parent->addChildren($this->getSkipNodes($this->skipped));
2829:     $this->skipped = [];
2830:     $this->skipParent = $parent;
2831:   }
2832: 
2833:   /**
2834:    * Match doc comment and its following skipped tokens to $parent.
2835:    * @param ParentNode $parent
2836:    * @param string $property_name
2837:    */
2838:   private function matchDocComment(ParentNode $parent, $property_name = 'docComment') {
2839:     if ($this->docComment) {
2840:       $parent->addChild($this->docComment, $property_name);
2841:       $parent->addChildren($this->getSkipNodes($this->skippedDocComment));
2842:       $this->skippedDocComment = [];
2843:       $this->docComment = NULL;
2844:     }
2845:     $this->skipParent = NULL;
2846:   }
2847: 
2848:   /**
2849:    * @param int $expected_type
2850:    * @param ParentNode $parent
2851:    * @param string $property_name
2852:    * @param bool $maybe_last TRUE if this may be the last match for rule.
2853:    * @param bool $capture_doc_comment
2854:    * @return TokenNode
2855:    * @throws ParserException
2856:    */
2857:   private function mustMatch($expected_type, ParentNode $parent, $property_name = NULL, $maybe_last = FALSE, $capture_doc_comment = FALSE) {
2858:     if ($this->currentType !== $expected_type) {
2859:       throw new ParserException(
2860:         $this->iterator->getSourcePosition(),
2861:         'expected ' . TokenNode::typeName($expected_type));
2862:     }
2863:     $token_node = $this->current;
2864:     $this->addSkipped($parent);
2865:     $parent->addChild($token_node, $property_name);
2866:     $this->nextToken($capture_doc_comment);
2867:     if (!$maybe_last) {
2868:       if ($capture_doc_comment) {
2869:         $parent->addChildren($this->skipped);
2870:         $this->skipped = [];
2871:       }
2872:       else {
2873:         $this->addSkipped($parent);
2874:       }
2875:     }
2876:     return $token_node;
2877:   }
2878: 
2879:   /**
2880:    * @param int $expected_type
2881:    * @param ParentNode $parent
2882:    * @param string $property_name
2883:    * @param bool $maybe_last TRUE if this may be the last match for rule.
2884:    * @param bool $capture_doc_comment
2885:    * @return TokenNode
2886:    */
2887:   private function tryMatch($expected_type, ParentNode $parent, $property_name = NULL, $maybe_last = FALSE, $capture_doc_comment = FALSE) {
2888:     if ($this->currentType !== $expected_type) {
2889:       return NULL;
2890:     }
2891:     $token_node = $this->current;
2892:     $this->addSkipped($parent);
2893:     $parent->addChild($token_node, $property_name);
2894:     $this->nextToken($capture_doc_comment);
2895:     if (!$maybe_last) {
2896:       if ($capture_doc_comment) {
2897:         $parent->addChildren($this->skipped);
2898:         $this->skipped = [];
2899:       }
2900:       else {
2901:         $this->addSkipped($parent);
2902:       }
2903:     }
2904:     return $token_node;
2905:   }
2906: 
2907:   /**
2908:    * @param array $expected_types
2909:    * @return TokenNode
2910:    */
2911:   private function tryMatchToken($expected_types) {
2912:     if ($this->current === NULL) {
2913:       return NULL;
2914:     }
2915:     foreach ($expected_types as $expected_type) {
2916:       if ($expected_type === $this->currentType) {
2917:         $token_node = $this->current;
2918:         $this->nextToken();
2919:         return $token_node;
2920:       }
2921:     }
2922:     return NULL;
2923:   }
2924: 
2925:   /**
2926:    * @param int|string $expected_type Expected token type
2927:    * @return TokenNode
2928:    * @throws ParserException
2929:    */
2930:   private function mustMatchToken($expected_type) {
2931:     if ($this->currentType !== $expected_type) {
2932:       throw new ParserException(
2933:         $this->iterator->getSourcePosition(),
2934:         'expected ' . TokenNode::typeName($expected_type));
2935:     }
2936:     $token_node = $this->current;
2937:     $this->nextToken();
2938:     return $token_node;
2939:   }
2940: 
2941:   /**
2942:    * Move iterator to next non hidden token.
2943:    * @param bool $capture_doc_comment
2944:    */
2945:   private function nextToken($capture_doc_comment = FALSE) {
2946:     $this->iterator->next();
2947:     $capture_doc_comment ? $this->skipHiddenCaptureDocComment() : $this->skipHidden();
2948:     $this->current = $this->iterator->current();
2949:     if ($this->current) {
2950:       $this->currentType = $this->current->getType();
2951:     }
2952:     else {
2953:       $this->currentType = NULL;
2954:     }
2955:   }
2956: 
2957:   /**
2958:    * Look ahead from current position at tokens and check if the token at
2959:    * offset is of an expected type, where the offset ignores hidden tokens.
2960:    * @param int|string $expected_type Expected token type
2961:    * @param int|string $skip_type (Optional) Additional token type to ignore
2962:    * @return bool
2963:    */
2964:   private function isLookAhead($expected_type, $skip_type = NULL) {
2965:     $token = NULL;
2966:     for ($offset = 1; ; $offset++) {
2967:       $token = $this->iterator->peek($offset);
2968:       if ($token === NULL) {
2969:         return FALSE;
2970:       }
2971:       if (!($token instanceof HiddenNode) && $token->getType() !== $skip_type) {
2972:         return $expected_type === $token->getType();
2973:       }
2974:     }
2975:     return FALSE;
2976:   }
2977: }
2978: 
Pharborist API documentation generated by ApiGen