1<?php
2/* ===========================================================================
3 * Copyright (c) 2018-2021 Zindex Software
4 *
5 * Licensed under the MIT License
6 * =========================================================================== */
7
8namespace Opis\Closure;
9
10defined('T_NAME_QUALIFIED') || define('T_NAME_QUALIFIED', -4);
11defined('T_NAME_FULLY_QUALIFIED') || define('T_NAME_FULLY_QUALIFIED', -5);
12defined('T_FN') || define('T_FN', -6);
13
14use Closure;
15use ReflectionFunction;
16
17class ReflectionClosure extends ReflectionFunction
18{
19    protected $code;
20    protected $tokens;
21    protected $hashedName;
22    protected $useVariables;
23    protected $isStaticClosure;
24    protected $isScopeRequired;
25    protected $isBindingRequired;
26    protected $isShortClosure;
27
28    protected static $files = array();
29    protected static $classes = array();
30    protected static $functions = array();
31    protected static $constants = array();
32    protected static $structures = array();
33
34
35    /**
36     * ReflectionClosure constructor.
37     * @param Closure $closure
38     * @param string|null $code This is ignored. Do not use it
39     * @throws \ReflectionException
40     */
41    public function __construct(Closure $closure, $code = null)
42    {
43        parent::__construct($closure);
44    }
45
46    /**
47     * @return bool
48     */
49    public function isStatic()
50    {
51        if ($this->isStaticClosure === null) {
52            $this->isStaticClosure = strtolower(substr($this->getCode(), 0, 6)) === 'static';
53        }
54
55        return $this->isStaticClosure;
56    }
57
58    public function isShortClosure()
59    {
60        if ($this->isShortClosure === null) {
61            $code = $this->getCode();
62            if ($this->isStatic()) {
63                $code = substr($code, 6);
64            }
65            $this->isShortClosure = strtolower(substr(trim($code), 0, 2)) === 'fn';
66        }
67
68        return $this->isShortClosure;
69    }
70
71    /**
72     * @return string
73     */
74    public function getCode()
75    {
76        if($this->code !== null){
77            return $this->code;
78        }
79
80        $fileName = $this->getFileName();
81        $line = $this->getStartLine() - 1;
82
83        $className = null;
84
85        if (null !== $className = $this->getClosureScopeClass()) {
86            $className = '\\' . trim($className->getName(), '\\');
87        }
88
89        $builtin_types = self::getBuiltinTypes();
90        $class_keywords = ['self', 'static', 'parent'];
91
92        $ns = $this->getNamespaceName();
93        $nsf = $ns == '' ? '' : ($ns[0] == '\\' ? $ns : '\\' . $ns);
94
95        $_file = var_export($fileName, true);
96        $_dir = var_export(dirname($fileName), true);
97        $_namespace = var_export($ns, true);
98        $_class = var_export(trim($className, '\\'), true);
99        $_function = $ns . ($ns == '' ? '' : '\\') . '{closure}';
100        $_method = ($className == '' ? '' : trim($className, '\\') . '::') . $_function;
101        $_function = var_export($_function, true);
102        $_method = var_export($_method, true);
103        $_trait = null;
104
105        $tokens = $this->getTokens();
106        $state = $lastState = 'start';
107        $inside_structure = false;
108        $isShortClosure = false;
109        $inside_structure_mark = 0;
110        $open = 0;
111        $code = '';
112        $id_start = $id_start_ci = $id_name = $context = '';
113        $classes = $functions = $constants = null;
114        $use = array();
115        $lineAdd = 0;
116        $isUsingScope = false;
117        $isUsingThisObject = false;
118
119        for($i = 0, $l = count($tokens); $i < $l; $i++) {
120            $token = $tokens[$i];
121            switch ($state) {
122                case 'start':
123                    if ($token[0] === T_FUNCTION || $token[0] === T_STATIC) {
124                        $code .= $token[1];
125                        $state = $token[0] === T_FUNCTION ? 'function' : 'static';
126                    } elseif ($token[0] === T_FN) {
127                        $isShortClosure = true;
128                        $code .= $token[1];
129                        $state = 'closure_args';
130                    }
131                    break;
132                case 'static':
133                    if ($token[0] === T_WHITESPACE || $token[0] === T_COMMENT || $token[0] === T_FUNCTION) {
134                        $code .= $token[1];
135                        if ($token[0] === T_FUNCTION) {
136                            $state = 'function';
137                        }
138                    } elseif ($token[0] === T_FN) {
139                        $isShortClosure = true;
140                        $code .= $token[1];
141                        $state = 'closure_args';
142                    } else {
143                        $code = '';
144                        $state = 'start';
145                    }
146                    break;
147                case 'function':
148                    switch ($token[0]){
149                        case T_STRING:
150                            $code = '';
151                            $state = 'named_function';
152                            break;
153                        case '(':
154                            $code .= '(';
155                            $state = 'closure_args';
156                            break;
157                        default:
158                            $code .= is_array($token) ? $token[1] : $token;
159                    }
160                    break;
161                case 'named_function':
162                    if($token[0] === T_FUNCTION || $token[0] === T_STATIC){
163                        $code = $token[1];
164                        $state = $token[0] === T_FUNCTION ? 'function' : 'static';
165                    } elseif ($token[0] === T_FN) {
166                        $isShortClosure = true;
167                        $code .= $token[1];
168                        $state = 'closure_args';
169                    }
170                    break;
171                case 'closure_args':
172                    switch ($token[0]){
173                        case T_NAME_QUALIFIED:
174                            list($id_start, $id_start_ci, $id_name) = $this->parseNameQualified($token[1]);
175                            $context = 'args';
176                            $state = 'id_name';
177                            $lastState = 'closure_args';
178                            break;
179                        case T_NS_SEPARATOR:
180                        case T_STRING:
181                            $id_start = $token[1];
182                            $id_start_ci = strtolower($id_start);
183                            $id_name = '';
184                            $context = 'args';
185                            $state = 'id_name';
186                            $lastState = 'closure_args';
187                            break;
188                        case T_USE:
189                            $code .= $token[1];
190                            $state = 'use';
191                            break;
192                        case T_DOUBLE_ARROW:
193                            $code .= $token[1];
194                            if ($isShortClosure) {
195                                $state = 'closure';
196                            }
197                            break;
198                        case ':':
199                            $code .= ':';
200                            $state = 'return';
201                            break;
202                        case '{':
203                            $code .= '{';
204                            $state = 'closure';
205                            $open++;
206                            break;
207                        default:
208                            $code .= is_array($token) ? $token[1] : $token;
209                    }
210                    break;
211                case 'use':
212                    switch ($token[0]){
213                        case T_VARIABLE:
214                            $use[] = substr($token[1], 1);
215                            $code .= $token[1];
216                            break;
217                        case '{':
218                            $code .= '{';
219                            $state = 'closure';
220                            $open++;
221                            break;
222                        case ':':
223                            $code .= ':';
224                            $state = 'return';
225                            break;
226                        default:
227                            $code .= is_array($token) ? $token[1] : $token;
228                            break;
229                    }
230                    break;
231                case 'return':
232                    switch ($token[0]){
233                        case T_WHITESPACE:
234                        case T_COMMENT:
235                        case T_DOC_COMMENT:
236                            $code .= $token[1];
237                            break;
238                        case T_NS_SEPARATOR:
239                        case T_STRING:
240                            $id_start = $token[1];
241                            $id_start_ci = strtolower($id_start);
242                            $id_name = '';
243                            $context = 'return_type';
244                            $state = 'id_name';
245                            $lastState = 'return';
246                            break 2;
247                        case T_NAME_QUALIFIED:
248                            list($id_start, $id_start_ci, $id_name) = $this->parseNameQualified($token[1]);
249                            $context = 'return_type';
250                            $state = 'id_name';
251                            $lastState = 'return';
252                            break 2;
253                        case T_DOUBLE_ARROW:
254                            $code .= $token[1];
255                            if ($isShortClosure) {
256                                $state = 'closure';
257                            }
258                            break;
259                        case '{':
260                            $code .= '{';
261                            $state = 'closure';
262                            $open++;
263                            break;
264                        default:
265                            $code .= is_array($token) ? $token[1] : $token;
266                            break;
267                    }
268                    break;
269                case 'closure':
270                    switch ($token[0]){
271                        case T_CURLY_OPEN:
272                        case T_DOLLAR_OPEN_CURLY_BRACES:
273                        case '{':
274                            $code .= is_array($token) ? $token[1] : $token;
275                            $open++;
276                            break;
277                        case '}':
278                            $code .= '}';
279                            if(--$open === 0 && !$isShortClosure){
280                                break 3;
281                            } elseif ($inside_structure) {
282                                $inside_structure = !($open === $inside_structure_mark);
283                            }
284                            break;
285                        case '(':
286                        case '[':
287                            $code .= $token[0];
288                            if ($isShortClosure) {
289                                $open++;
290                            }
291                            break;
292                        case ')':
293                        case ']':
294                            if ($isShortClosure) {
295                                if ($open === 0) {
296                                    break 3;
297                                }
298                                --$open;
299                            }
300                            $code .= $token[0];
301                            break;
302                        case ',':
303                        case ';':
304                            if ($isShortClosure && $open === 0) {
305                                break 3;
306                            }
307                            $code .= $token[0];
308                            break;
309                        case T_LINE:
310                            $code .= $token[2] - $line + $lineAdd;
311                            break;
312                        case T_FILE:
313                            $code .= $_file;
314                            break;
315                        case T_DIR:
316                            $code .= $_dir;
317                            break;
318                        case T_NS_C:
319                            $code .= $_namespace;
320                            break;
321                        case T_CLASS_C:
322                            $code .= $inside_structure ? $token[1] : $_class;
323                            break;
324                        case T_FUNC_C:
325                            $code .= $inside_structure ? $token[1] : $_function;
326                            break;
327                        case T_METHOD_C:
328                            $code .= $inside_structure ? $token[1] : $_method;
329                            break;
330                        case T_COMMENT:
331                            if (substr($token[1], 0, 8) === '#trackme') {
332                                $timestamp = time();
333                                $code .= '/**' . PHP_EOL;
334                                $code .= '* Date      : ' . date(DATE_W3C, $timestamp) . PHP_EOL;
335                                $code .= '* Timestamp : ' . $timestamp . PHP_EOL;
336                                $code .= '* Line      : ' . ($line + 1) . PHP_EOL;
337                                $code .= '* File      : ' . $_file . PHP_EOL . '*/' . PHP_EOL;
338                                $lineAdd += 5;
339                            } else {
340                                $code .= $token[1];
341                            }
342                            break;
343                        case T_VARIABLE:
344                            if($token[1] == '$this' && !$inside_structure){
345                                $isUsingThisObject = true;
346                            }
347                            $code .= $token[1];
348                            break;
349                        case T_STATIC:
350                        case T_NS_SEPARATOR:
351                        case T_STRING:
352                            $id_start = $token[1];
353                            $id_start_ci = strtolower($id_start);
354                            $id_name = '';
355                            $context = 'root';
356                            $state = 'id_name';
357                            $lastState = 'closure';
358                            break 2;
359                        case T_NAME_QUALIFIED:
360                            list($id_start, $id_start_ci, $id_name) = $this->parseNameQualified($token[1]);
361                            $context = 'root';
362                            $state = 'id_name';
363                            $lastState = 'closure';
364                            break 2;
365                        case T_NEW:
366                            $code .= $token[1];
367                            $context = 'new';
368                            $state = 'id_start';
369                            $lastState = 'closure';
370                            break 2;
371                        case T_USE:
372                            $code .= $token[1];
373                            $context = 'use';
374                            $state = 'id_start';
375                            $lastState = 'closure';
376                            break;
377                        case T_INSTANCEOF:
378                        case T_INSTEADOF:
379                            $code .= $token[1];
380                            $context = 'instanceof';
381                            $state = 'id_start';
382                            $lastState = 'closure';
383                            break;
384                        case T_OBJECT_OPERATOR:
385                        case T_DOUBLE_COLON:
386                            $code .= $token[1];
387                            $lastState = 'closure';
388                            $state = 'ignore_next';
389                            break;
390                        case T_FUNCTION:
391                            $code .= $token[1];
392                            $state = 'closure_args';
393                            if (!$inside_structure) {
394                                $inside_structure = true;
395                                $inside_structure_mark = $open;
396                            }
397                            break;
398                        case T_TRAIT_C:
399                            if ($_trait === null) {
400                                $startLine = $this->getStartLine();
401                                $endLine = $this->getEndLine();
402                                $structures = $this->getStructures();
403
404                                $_trait = '';
405
406                                foreach ($structures as &$struct) {
407                                    if ($struct['type'] === 'trait' &&
408                                        $struct['start'] <= $startLine &&
409                                        $struct['end'] >= $endLine
410                                    ) {
411                                        $_trait = ($ns == '' ? '' : $ns . '\\') . $struct['name'];
412                                        break;
413                                    }
414                                }
415
416                                $_trait = var_export($_trait, true);
417                            }
418
419                            $code .= $_trait;
420                            break;
421                        default:
422                            $code .= is_array($token) ? $token[1] : $token;
423                    }
424                    break;
425                case 'ignore_next':
426                    switch ($token[0]){
427                        case T_WHITESPACE:
428                        case T_COMMENT:
429                        case T_DOC_COMMENT:
430                            $code .= $token[1];
431                            break;
432                        case T_CLASS:
433                        case T_NEW:
434                        case T_STATIC:
435                        case T_VARIABLE:
436                        case T_STRING:
437                        case T_CLASS_C:
438                        case T_FILE:
439                        case T_DIR:
440                        case T_METHOD_C:
441                        case T_FUNC_C:
442                        case T_FUNCTION:
443                        case T_INSTANCEOF:
444                        case T_LINE:
445                        case T_NS_C:
446                        case T_TRAIT_C:
447                        case T_USE:
448                            $code .= $token[1];
449                            $state = $lastState;
450                            break;
451                        default:
452                            $state = $lastState;
453                            $i--;
454                    }
455                    break;
456                case 'id_start':
457                    switch ($token[0]){
458                        case T_WHITESPACE:
459                        case T_COMMENT:
460                        case T_DOC_COMMENT:
461                            $code .= $token[1];
462                            break;
463                        case T_NS_SEPARATOR:
464                        case T_NAME_FULLY_QUALIFIED:
465                        case T_STRING:
466                        case T_STATIC:
467                            $id_start = $token[1];
468                            $id_start_ci = strtolower($id_start);
469                            $id_name = '';
470                            $state = 'id_name';
471                            break 2;
472                        case T_NAME_QUALIFIED:
473                            list($id_start, $id_start_ci, $id_name) = $this->parseNameQualified($token[1]);
474                            $state = 'id_name';
475                            break 2;
476                        case T_VARIABLE:
477                            $code .= $token[1];
478                            $state = $lastState;
479                            break;
480                        case T_CLASS:
481                            $code .= $token[1];
482                            $state = 'anonymous';
483                            break;
484                        default:
485                            $i--;//reprocess last
486                            $state = 'id_name';
487                    }
488                    break;
489                case 'id_name':
490                    switch ($token[0]){
491                        case T_NAME_QUALIFIED:
492                        case T_NS_SEPARATOR:
493                        case T_STRING:
494                        case T_WHITESPACE:
495                        case T_COMMENT:
496                        case T_DOC_COMMENT:
497                            $id_name .= $token[1];
498                            break;
499                        case '(':
500                            if ($isShortClosure) {
501                                $open++;
502                            }
503                            if($context === 'new' || false !== strpos($id_name, '\\')){
504                                if($id_start_ci === 'self' || $id_start_ci === 'static') {
505                                    if (!$inside_structure) {
506                                        $isUsingScope = true;
507                                    }
508                                } elseif ($id_start !== '\\' && !in_array($id_start_ci, $class_keywords)) {
509                                    if ($classes === null) {
510                                        $classes = $this->getClasses();
511                                    }
512                                    if (isset($classes[$id_start_ci])) {
513                                        $id_start = $classes[$id_start_ci];
514                                    }
515                                    if($id_start[0] !== '\\'){
516                                        $id_start = $nsf . '\\' . $id_start;
517                                    }
518                                }
519                            } else {
520                                if($id_start !== '\\'){
521                                    if($functions === null){
522                                        $functions = $this->getFunctions();
523                                    }
524                                    if(isset($functions[$id_start_ci])){
525                                        $id_start = $functions[$id_start_ci];
526                                    } elseif ($nsf !== '\\' && function_exists($nsf . '\\' . $id_start)) {
527                                        $id_start = $nsf . '\\' . $id_start;
528                                        // Cache it to functions array
529                                        $functions[$id_start_ci] = $id_start;
530                                    }
531                                }
532                            }
533                            $code .= $id_start . $id_name . '(';
534                            $state = $lastState;
535                            break;
536                        case T_VARIABLE:
537                        case T_DOUBLE_COLON:
538                            if($id_start !== '\\') {
539                                if($id_start_ci === 'self' || $id_start_ci === 'parent'){
540                                    if (!$inside_structure) {
541                                        $isUsingScope = true;
542                                    }
543                                } elseif ($id_start_ci === 'static') {
544                                    if (!$inside_structure) {
545                                        $isUsingScope = $token[0] === T_DOUBLE_COLON;
546                                    }
547                                } elseif (!(\PHP_MAJOR_VERSION >= 7 && in_array($id_start_ci, $builtin_types))){
548                                    if ($classes === null) {
549                                        $classes = $this->getClasses();
550                                    }
551                                    if (isset($classes[$id_start_ci])) {
552                                        $id_start = $classes[$id_start_ci];
553                                    }
554                                    if($id_start[0] !== '\\'){
555                                        $id_start = $nsf . '\\' . $id_start;
556                                    }
557                                }
558                            }
559
560                            $code .= $id_start . $id_name . $token[1];
561                            $state = $token[0] === T_DOUBLE_COLON ? 'ignore_next' : $lastState;
562                            break;
563                        default:
564                            if($id_start !== '\\' && !defined($id_start)){
565                                if($constants === null){
566                                    $constants = $this->getConstants();
567                                }
568                                if(isset($constants[$id_start])){
569                                    $id_start = $constants[$id_start];
570                                } elseif($context === 'new'){
571                                    if(in_array($id_start_ci, $class_keywords)) {
572                                        if (!$inside_structure) {
573                                            $isUsingScope = true;
574                                        }
575                                    } else {
576                                        if ($classes === null) {
577                                            $classes = $this->getClasses();
578                                        }
579                                        if (isset($classes[$id_start_ci])) {
580                                            $id_start = $classes[$id_start_ci];
581                                        }
582                                        if ($id_start[0] !== '\\') {
583                                            $id_start = $nsf . '\\' . $id_start;
584                                        }
585                                    }
586                                } elseif($context === 'use' ||
587                                    $context === 'instanceof' ||
588                                    $context === 'args' ||
589                                    $context === 'return_type' ||
590                                    $context === 'extends' ||
591                                    $context === 'root'
592                                ){
593                                    if(in_array($id_start_ci, $class_keywords)){
594                                        if (!$inside_structure && !$id_start_ci === 'static') {
595                                            $isUsingScope = true;
596                                        }
597                                    } elseif (!(\PHP_MAJOR_VERSION >= 7 && in_array($id_start_ci, $builtin_types))){
598                                        if($classes === null){
599                                            $classes = $this->getClasses();
600                                        }
601                                        if(isset($classes[$id_start_ci])){
602                                            $id_start = $classes[$id_start_ci];
603                                        }
604                                        if($id_start[0] !== '\\'){
605                                            $id_start = $nsf . '\\' . $id_start;
606                                        }
607                                    }
608                                }
609                            }
610                            $code .= $id_start . $id_name;
611                            $state = $lastState;
612                            $i--;//reprocess last token
613                    }
614                    break;
615                case 'anonymous':
616                    switch ($token[0]) {
617                        case T_NS_SEPARATOR:
618                        case T_STRING:
619                            $id_start = $token[1];
620                            $id_start_ci = strtolower($id_start);
621                            $id_name = '';
622                            $state = 'id_name';
623                            $context = 'extends';
624                            $lastState = 'anonymous';
625                        break;
626                        case '{':
627                            $state = 'closure';
628                            if (!$inside_structure) {
629                                $inside_structure = true;
630                                $inside_structure_mark = $open;
631                            }
632                            $i--;
633                            break;
634                        default:
635                            $code .= is_array($token) ? $token[1] : $token;
636                    }
637                    break;
638            }
639        }
640
641        if ($isShortClosure) {
642            $this->useVariables = $this->getStaticVariables();
643        } else {
644            $this->useVariables = empty($use) ? $use : array_intersect_key($this->getStaticVariables(), array_flip($use));
645        }
646
647        $this->isShortClosure = $isShortClosure;
648        $this->isBindingRequired = $isUsingThisObject;
649        $this->isScopeRequired = $isUsingScope;
650        $this->code = $code;
651
652        return $this->code;
653    }
654
655    /**
656     * @return array
657     */
658    private static function getBuiltinTypes()
659    {
660        // PHP 5
661        if (\PHP_MAJOR_VERSION === 5) {
662            return ['array', 'callable'];
663        }
664
665        // PHP 8
666        if (\PHP_MAJOR_VERSION === 8) {
667            return ['array', 'callable', 'string', 'int', 'bool', 'float', 'iterable', 'void', 'object', 'mixed', 'false', 'null'];
668        }
669
670        // PHP 7
671        switch (\PHP_MINOR_VERSION) {
672            case 0:
673                return ['array', 'callable', 'string', 'int', 'bool', 'float'];
674            case 1:
675                return ['array', 'callable', 'string', 'int', 'bool', 'float', 'iterable', 'void'];
676            default:
677                return ['array', 'callable', 'string', 'int', 'bool', 'float', 'iterable', 'void', 'object'];
678        }
679    }
680
681    /**
682     * @return array
683     */
684    public function getUseVariables()
685    {
686        if($this->useVariables !== null){
687            return $this->useVariables;
688        }
689
690        $tokens = $this->getTokens();
691        $use = array();
692        $state = 'start';
693
694        foreach ($tokens as &$token) {
695            $is_array = is_array($token);
696
697            switch ($state) {
698                case 'start':
699                    if ($is_array && $token[0] === T_USE) {
700                        $state = 'use';
701                    }
702                    break;
703                case 'use':
704                    if ($is_array) {
705                        if ($token[0] === T_VARIABLE) {
706                            $use[] = substr($token[1], 1);
707                        }
708                    } elseif ($token == ')') {
709                        break 2;
710                    }
711                    break;
712            }
713        }
714
715        $this->useVariables = empty($use) ? $use : array_intersect_key($this->getStaticVariables(), array_flip($use));
716
717        return $this->useVariables;
718    }
719
720    /**
721     * return bool
722     */
723    public function isBindingRequired()
724    {
725        if($this->isBindingRequired === null){
726            $this->getCode();
727        }
728
729        return $this->isBindingRequired;
730    }
731
732    /**
733     * return bool
734     */
735    public function isScopeRequired()
736    {
737        if($this->isScopeRequired === null){
738            $this->getCode();
739        }
740
741        return $this->isScopeRequired;
742    }
743
744    /**
745     * @return string
746     */
747    protected function getHashedFileName()
748    {
749        if ($this->hashedName === null) {
750            $this->hashedName = sha1($this->getFileName());
751        }
752
753        return $this->hashedName;
754    }
755
756    /**
757     * @return array
758     */
759    protected function getFileTokens()
760    {
761        $key = $this->getHashedFileName();
762
763        if (!isset(static::$files[$key])) {
764            static::$files[$key] = token_get_all(file_get_contents($this->getFileName()));
765        }
766
767        return static::$files[$key];
768    }
769
770    /**
771     * @return array
772     */
773    protected function getTokens()
774    {
775        if ($this->tokens === null) {
776            $tokens = $this->getFileTokens();
777            $startLine = $this->getStartLine();
778            $endLine = $this->getEndLine();
779            $results = array();
780            $start = false;
781
782            foreach ($tokens as &$token) {
783                if (!is_array($token)) {
784                    if ($start) {
785                        $results[] = $token;
786                    }
787
788                    continue;
789                }
790
791                $line = $token[2];
792
793                if ($line <= $endLine) {
794                    if ($line >= $startLine) {
795                        $start = true;
796                        $results[] = $token;
797                    }
798
799                    continue;
800                }
801
802                break;
803            }
804
805            $this->tokens = $results;
806        }
807
808        return $this->tokens;
809    }
810
811    /**
812     * @return array
813     */
814    protected function getClasses()
815    {
816        $key = $this->getHashedFileName();
817
818        if (!isset(static::$classes[$key])) {
819            $this->fetchItems();
820        }
821
822        return static::$classes[$key];
823    }
824
825    /**
826     * @return array
827     */
828    protected function getFunctions()
829    {
830        $key = $this->getHashedFileName();
831
832        if (!isset(static::$functions[$key])) {
833            $this->fetchItems();
834        }
835
836        return static::$functions[$key];
837    }
838
839    /**
840     * @return array
841     */
842    protected function getConstants()
843    {
844        $key = $this->getHashedFileName();
845
846        if (!isset(static::$constants[$key])) {
847            $this->fetchItems();
848        }
849
850        return static::$constants[$key];
851    }
852
853    /**
854     * @return array
855     */
856    protected function getStructures()
857    {
858        $key = $this->getHashedFileName();
859
860        if (!isset(static::$structures[$key])) {
861            $this->fetchItems();
862        }
863
864        return static::$structures[$key];
865    }
866
867    protected function fetchItems()
868    {
869        $key = $this->getHashedFileName();
870
871        $classes = array();
872        $functions = array();
873        $constants = array();
874        $structures = array();
875        $tokens = $this->getFileTokens();
876
877        $open = 0;
878        $state = 'start';
879        $lastState = '';
880        $prefix = '';
881        $name = '';
882        $alias = '';
883        $isFunc = $isConst = false;
884
885        $startLine = $endLine = 0;
886        $structType = $structName = '';
887        $structIgnore = false;
888
889        foreach ($tokens as $token) {
890
891            switch ($state) {
892                case 'start':
893                    switch ($token[0]) {
894                        case T_CLASS:
895                        case T_INTERFACE:
896                        case T_TRAIT:
897                            $state = 'before_structure';
898                            $startLine = $token[2];
899                            $structType = $token[0] == T_CLASS
900                                                    ? 'class'
901                                                    : ($token[0] == T_INTERFACE ? 'interface' : 'trait');
902                            break;
903                        case T_USE:
904                            $state = 'use';
905                            $prefix = $name = $alias = '';
906                            $isFunc = $isConst = false;
907                            break;
908                        case T_FUNCTION:
909                            $state = 'structure';
910                            $structIgnore = true;
911                            break;
912                        case T_NEW:
913                            $state = 'new';
914                            break;
915                        case T_OBJECT_OPERATOR:
916                        case T_DOUBLE_COLON:
917                            $state = 'invoke';
918                            break;
919                    }
920                    break;
921                case 'use':
922                    switch ($token[0]) {
923                        case T_FUNCTION:
924                            $isFunc = true;
925                            break;
926                        case T_CONST:
927                            $isConst = true;
928                            break;
929                        case T_NS_SEPARATOR:
930                            $name .= $token[1];
931                            break;
932                        case T_STRING:
933                            $name .= $token[1];
934                            $alias = $token[1];
935                            break;
936                        case T_NAME_QUALIFIED:
937                            $name .= $token[1];
938                            $pieces = explode('\\', $token[1]);
939                            $alias = end($pieces);
940                            break;
941                        case T_AS:
942                            $lastState = 'use';
943                            $state = 'alias';
944                            break;
945                        case '{':
946                            $prefix = $name;
947                            $name = $alias = '';
948                            $state = 'use-group';
949                            break;
950                        case ',':
951                        case ';':
952                            if ($name === '' || $name[0] !== '\\') {
953                                $name = '\\' . $name;
954                            }
955
956                            if ($alias !== '') {
957                                if ($isFunc) {
958                                    $functions[strtolower($alias)] = $name;
959                                } elseif ($isConst) {
960                                    $constants[$alias] = $name;
961                                } else {
962                                    $classes[strtolower($alias)] = $name;
963                                }
964                            }
965                            $name = $alias = '';
966                            $state = $token === ';' ? 'start' : 'use';
967                            break;
968                    }
969                    break;
970                case 'use-group':
971                    switch ($token[0]) {
972                        case T_NS_SEPARATOR:
973                            $name .= $token[1];
974                            break;
975                        case T_NAME_QUALIFIED:
976                            $name .= $token[1];
977                            $pieces = explode('\\', $token[1]);
978                            $alias = end($pieces);
979                            break;
980                        case T_STRING:
981                            $name .= $token[1];
982                            $alias = $token[1];
983                            break;
984                        case T_AS:
985                            $lastState = 'use-group';
986                            $state = 'alias';
987                            break;
988                        case ',':
989                        case '}':
990
991                            if ($prefix === '' || $prefix[0] !== '\\') {
992                                $prefix = '\\' . $prefix;
993                            }
994
995                            if ($alias !== '') {
996                                if ($isFunc) {
997                                    $functions[strtolower($alias)] = $prefix . $name;
998                                } elseif ($isConst) {
999                                    $constants[$alias] = $prefix . $name;
1000                                } else {
1001                                    $classes[strtolower($alias)] = $prefix . $name;
1002                                }
1003                            }
1004                            $name = $alias = '';
1005                            $state = $token === '}' ? 'use' : 'use-group';
1006                            break;
1007                    }
1008                    break;
1009                case 'alias':
1010                    if ($token[0] === T_STRING) {
1011                        $alias = $token[1];
1012                        $state = $lastState;
1013                    }
1014                    break;
1015                case 'new':
1016                    switch ($token[0]) {
1017                        case T_WHITESPACE:
1018                        case T_COMMENT:
1019                        case T_DOC_COMMENT:
1020                            break 2;
1021                        case T_CLASS:
1022                            $state = 'structure';
1023                            $structIgnore = true;
1024                            break;
1025                        default:
1026                            $state = 'start';
1027                    }
1028                    break;
1029                case 'invoke':
1030                    switch ($token[0]) {
1031                        case T_WHITESPACE:
1032                        case T_COMMENT:
1033                        case T_DOC_COMMENT:
1034                            break 2;
1035                        default:
1036                            $state = 'start';
1037                    }
1038                    break;
1039                case 'before_structure':
1040                    if ($token[0] == T_STRING) {
1041                        $structName = $token[1];
1042                        $state = 'structure';
1043                    }
1044                    break;
1045                case 'structure':
1046                    switch ($token[0]) {
1047                        case '{':
1048                        case T_CURLY_OPEN:
1049                        case T_DOLLAR_OPEN_CURLY_BRACES:
1050                            $open++;
1051                            break;
1052                        case '}':
1053                            if (--$open == 0) {
1054                                if(!$structIgnore){
1055                                    $structures[] = array(
1056                                        'type' => $structType,
1057                                        'name' => $structName,
1058                                        'start' => $startLine,
1059                                        'end' => $endLine,
1060                                    );
1061                                }
1062                                $structIgnore = false;
1063                                $state = 'start';
1064                            }
1065                            break;
1066                        default:
1067                            if (is_array($token)) {
1068                                $endLine = $token[2];
1069                            }
1070                    }
1071                    break;
1072            }
1073        }
1074
1075        static::$classes[$key] = $classes;
1076        static::$functions[$key] = $functions;
1077        static::$constants[$key] = $constants;
1078        static::$structures[$key] = $structures;
1079    }
1080
1081    private function parseNameQualified($token)
1082    {
1083        $pieces = explode('\\', $token);
1084
1085        $id_start = array_shift($pieces);
1086
1087        $id_start_ci = strtolower($id_start);
1088
1089        $id_name = '\\' . implode('\\', $pieces);
1090
1091        return [$id_start, $id_start_ci, $id_name];
1092    }
1093}
1094