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: 88:
89: class Parser {
90: 91: 92:
93: private static $namespacePathTypes = [T_STRING, T_NS_SEPARATOR, T_NAMESPACE];
94:
95: 96: 97:
98: private static $visibilityTypes = [T_PUBLIC, T_PROTECTED, T_PRIVATE];
99:
100: 101: 102: 103:
104: private $iterator;
105:
106: 107: 108: 109:
110: private $skipParent = NULL;
111:
112: 113: 114: 115:
116: private $skipped = [];
117:
118: 119: 120: 121:
122: private $docComment = NULL;
123:
124: 125: 126:
127: private $skippedDocComment = [];
128:
129: 130: 131: 132:
133: private $top;
134:
135: 136: 137: 138:
139: private $expressionParser;
140:
141: 142: 143:
144: private $current;
145:
146: 147: 148:
149: private $currentType;
150:
151: 152: 153:
154: public function __construct() {
155:
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: 176: 177: 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:
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: 199: 200: 201: 202: 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: 214: 215: 216: 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: 230: 231: 232: 233:
234: public static function parseSnippet($snippet) {
235: $tree = self::parseSource('<?php ' . $snippet);
236: return $tree->firstChild()->next();
237: }
238:
239: 240: 241: 242: 243: 244: 245: 246: 247:
248: public static function parseExpression($expression) {
249: $tree = self::parseSource('<?php ' . $expression . ';');
250:
251: $statement_node = $tree->firstChild()->next();
252: return $statement_node->getExpression()->remove();
253: }
254:
255: 256: 257: 258: 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: 280: 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: 297: 298: 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: 312: 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:
349:
350:
351:
352: return;
353: }
354: $this->mustMatch(';', $node, NULL, TRUE, TRUE);
355: }
356:
357: 358: 359: 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: 376: 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: 391: 392:
393: private function statement() {
394: switch ($this->currentType) {
395: case T_CLOSE_TAG:
396:
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: 471: 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: 488: 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: 501: 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: 513: 514: 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: 529: 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: 575: 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: 589: 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: 610: 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: 624: 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: 648: 649: 650: 651: 652: 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: 665: 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: 695: 696: 697: 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: 731: 732: 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: 746: 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: 761: 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: 776: 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: 790: 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: 805: 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: 823: 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: 839: 840: 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: 862: 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: 878: 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: 898: 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: 930: 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: 948: 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: 970: 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: 983: 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: 1016: 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: 1040: 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: 1052: 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: 1064: 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: 1076: 1077: 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: 1102: 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: 1177: 1178: 1179: 1180:
1181: private function expr($static = FALSE) {
1182: static $end_expression_types = [':', ';', ',', ')', ']', '}', T_AS, T_DOUBLE_ARROW, T_CLOSE_TAG];
1183:
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: 1204: 1205: 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: 1237: 1238: 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: 1427: 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: 1439: 1440: 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: 1474: 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: 1488: 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: 1526: 1527: 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: 1539: 1540: 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: 1556: 1557: 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: 1573: 1574: 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: 1600: 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: 1624: 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: 1635: 1636: 1637: 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: 1653: 1654: 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: 1696: 1697: 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: 1721: 1722: 1723: 1724: 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: 1736: 1737: 1738: 1739: 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: 1752: 1753: 1754: 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: 1766: 1767: 1768: 1769:
1770: private function classVariable($class_name, $colon_node) {
1771: if ($this->currentType === '{') {
1772:
1773: return $this->classMethodCall($class_name, $colon_node, $this->bracesExpr());
1774: }
1775:
1776: 1777: 1778: 1779: 1780: 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: 1789: 1790:
1791: if ($var_node instanceof ArrayLookupNode) {
1792:
1793: $member_name = $var_node;
1794: while ($member_name instanceof ArrayLookupNode) {
1795: $member_name = $member_name->getArray();
1796: }
1797:
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: 1817: 1818: 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: 1854: 1855: 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: 1871: 1872: 1873: 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: 1895: 1896: 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: 1933: 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: 1949: 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: 1963: 1964:
1965: private function referenceVariable() {
1966: return $this->offsetVariable($this->compoundVariable());
1967: }
1968:
1969: 1970: 1971: 1972: 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: 1996: 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: 2009: 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: 2022: 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: 2034: 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: 2046: 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: 2067: 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: 2085: 2086: 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: 2100: 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: 2118: 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: 2135: 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: 2153: 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: 2169: 2170: 2171: 2172:
2173: private function body($function) {
2174: $function->addChild($this->innerStatementBlock(), 'body');
2175: }
2176:
2177: 2178: 2179: 2180: 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: 2191: 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: 2203: 2204: 2205:
2206: private function innerStatementListNode($terminator) {
2207: $node = new StatementBlockNode();
2208: $this->innerStatementList($node, $terminator);
2209: return $node;
2210: }
2211:
2212: 2213: 2214: 2215: 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: 2240: 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:
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: 2259: 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: 2286: 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: 2304: 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: 2317: 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: 2331: 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: 2348: 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: 2367: 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: 2402: 2403: 2404: 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:
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: 2496: 2497: 2498: 2499: 2500:
2501: private function classMemberList($doc_comment, ModifiersNode $modifiers) {
2502:
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: 2527: 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: 2540: 2541: 2542: 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: 2563: 2564:
2565: private function traitUse() {
2566: $node = new TraitUseNode();
2567: $this->mustMatch(T_USE, $node);
2568:
2569: $traits = new CommaListNode();
2570: do {
2571: $traits->addChild($this->name());
2572: } while ($this->tryMatch(',', $traits));
2573: $node->addChild($traits, 'traits');
2574:
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: 2591: 2592:
2593: private function traitAdaptation() {
2594:
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: 2621: 2622: 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: 2641: 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: 2676: 2677: 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: 2705: 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: 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: 2749:
2750: private function skipHiddenCaptureDocComment() {
2751: $token = $this->iterator->current();
2752:
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:
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: 2814: 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: 2825: 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: 2835: 2836: 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: 2850: 2851: 2852: 2853: 2854: 2855: 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: 2881: 2882: 2883: 2884: 2885: 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: 2909: 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: 2927: 2928: 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: 2943: 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: 2959: 2960: 2961: 2962: 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: