Completed
Push — master ( 9df40c...785346 )
by Anton
06:07
created

ReflectionFile::getFilename()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 4
rs 10
cc 1
eloc 2
nc 1
nop 0
1
<?php
2
/**
3
 * Spiral Framework.
4
 *
5
 * @license   MIT
6
 * @author    Anton Titov (Wolfy-J)
7
 */
8
namespace Spiral\Tokenizer\Reflections;
9
10
use Spiral\Core\Component;
11
use Spiral\Core\Traits\SaturateTrait;
12
use Spiral\Tokenizer\ReflectionFileInterface;
13
use Spiral\Tokenizer\TokenizerInterface;
14
15
/**
16
 * File reflections can fetch information about classes, interfaces, functions and traits declared
17
 * in file. In addition file reflection provides ability to fetch and describe every method/function
18
 * call.
19
 */
20
class ReflectionFile extends Component implements ReflectionFileInterface
21
{
22
    /**
23
     * Development sugar.
24
     */
25
    use SaturateTrait;
26
27
    /**
28
     * Namespace separator.
29
     */
30
    const NS_SEPARATOR = '\\';
31
32
    /**
33
     * Constants for convenience.
34
     */
35
    const TOKEN_TYPE = TokenizerInterface::TYPE;
36
    const TOKEN_CODE = TokenizerInterface::CODE;
37
    const TOKEN_LINE = TokenizerInterface::LINE;
38
39
    /**
40
     * Opening and closing token ids.
41
     */
42
    const O_TOKEN = 0;
43
    const C_TOKEN = 1;
44
45
    /**
46
     * Namespace uses.
47
     */
48
    const N_USES = 2;
49
50
    /**
51
     * Set of tokens required to detect classes, traits, interfaces and function declarations. We
52
     * don't need any other token for that.
53
     *
54
     * @var array
55
     */
56
    static private $processTokens = [
57
        '{',
58
        '}',
59
        ';',
60
        T_PAAMAYIM_NEKUDOTAYIM,
61
        T_NAMESPACE,
62
        T_STRING,
63
        T_CLASS,
64
        T_INTERFACE,
65
        T_TRAIT,
66
        T_FUNCTION,
67
        T_NS_SEPARATOR,
68
        T_INCLUDE,
69
        T_INCLUDE_ONCE,
70
        T_REQUIRE,
71
        T_REQUIRE_ONCE,
72
        T_USE,
73
        T_AS
74
    ];
75
76
    /**
77
     * @var string
78
     */
79
    private $filename = '';
80
81
    /**
82
     * Parsed tokens array.
83
     *
84
     * @invisible
85
     * @var array
86
     */
87
    private $tokens = [];
88
89
    /**
90
     * Total tokens count.
91
     *
92
     * @invisible
93
     * @var int
94
     */
95
    private $countTokens = 0;
96
97
    /**
98
     * Indicator that file has external includes.
99
     *
100
     * @invisible
101
     * @var bool
102
     */
103
    private $hasIncludes = false;
104
105
    /**
106
     * Namespaces used in file and their token positions.
107
     *
108
     * @invisible
109
     * @var array
110
     */
111
    private $namespaces = [];
112
113
    /**
114
     * Declarations of classes, interfaces and traits.
115
     *
116
     * @invisible
117
     * @var array
118
     */
119
    private $declarations = [];
120
121
    /**
122
     * Declarations of new functions.
123
     *
124
     * @invisible
125
     * @var array
126
     */
127
    private $functions = [];
128
129
    /**
130
     * Every found method/function invocation.
131
     *
132
     * @invisible
133
     * @var ReflectionInvocation[]
134
     */
135
    private $invocations = [];
136
137
    /**
138
     * @param array $tokens
139
     * @param array $cache Tokenizer can construct reflection with pre-created cache to speed up
140
     *                     indexation.
141
     */
142
    public function __construct(array $tokens, array $cache = [])
143
    {
144
        $this->tokens = $tokens;
145
146
        if (!empty($cache)) {
147
            //Locating file schema from file, can speed up class location a LOT
148
            $this->importSchema($cache);
149
150
            return;
151
        }
152
153
        //Looking for declarations
154
        $this->locateDeclarations();
155
    }
156
157
    /**
158
     * {@inheritdoc}
159
     */
160
    public function getFunctions()
161
    {
162
        return array_keys($this->functions);
163
    }
164
165
    /**
166
     * {@inheritdoc}
167
     */
168
    public function getClasses()
169
    {
170
        if (!isset($this->declarations['T_CLASS'])) {
171
            return [];
172
        }
173
174
        return array_keys($this->declarations['T_CLASS']);
175
    }
176
177
    /**
178
     * {@inheritdoc}
179
     */
180
    public function getTraits()
181
    {
182
        if (!isset($this->declarations['T_TRAIT'])) {
183
            return [];
184
        }
185
186
        return array_keys($this->declarations['T_TRAIT']);
187
    }
188
189
    /**
190
     * {@inheritdoc}
191
     */
192
    public function getInterfaces()
193
    {
194
        if (!isset($this->declarations['T_INTERFACE'])) {
195
            return [];
196
        }
197
198
        return array_keys($this->declarations['T_INTERFACE']);
199
    }
200
201
    /**
202
     * Get list of tokens associated with given file.
203
     *
204
     * @return array
205
     */
206
    public function getTokens()
207
    {
208
        return $this->tokens;
209
    }
210
211
    /**
212
     * {@inheritdoc}
213
     */
214
    public function hasIncludes()
215
    {
216
        return $this->hasIncludes;
217
    }
218
219
    /**
220
     * {@inheritdoc}
221
     */
222
    public function getInvocations()
223
    {
224
        if (empty($this->invocations)) {
225
            $this->locateInvocations($this->getTokens());
226
        }
227
228
        return $this->invocations;
229
    }
230
231
    /**
232
     * Export found declaration as array for caching purposes.
233
     *
234
     * @return array
235
     */
236
    public function exportSchema()
237
    {
238
        return [$this->hasIncludes, $this->declarations, $this->functions, $this->namespaces];
239
    }
240
241
    /**
242
     * Import cached reflection schema.
243
     *
244
     * @param array $cache
245
     */
246
    protected function importSchema(array $cache)
247
    {
248
        list($this->hasIncludes, $this->declarations, $this->functions, $this->namespaces) = $cache;
249
    }
250
251
    /**
252
     * Locate every class, interface, trait or function definition.
253
     */
254
    protected function locateDeclarations()
255
    {
256
        foreach ($this->getTokens() as $tokenID => $token) {
257
            if (!in_array($token[self::TOKEN_TYPE], self::$processTokens)) {
258
                continue;
259
            }
260
261
            switch ($token[self::TOKEN_TYPE]) {
262
                case T_NAMESPACE:
263
                    $this->registerNamespace($tokenID);
264
                    break;
265
266
                case T_USE:
267
                    $this->registerUse($tokenID);
268
                    break;
269
270
                case T_FUNCTION:
271
                    $this->registerFunction($tokenID);
272
                    break;
273
274
                case T_CLASS:
275
                case T_TRAIT;
0 ignored issues
show
Coding Style introduced by
case statements should not use curly braces.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
276
                case T_INTERFACE:
277
                    if (
278
                        $this->tokens[$tokenID][self::TOKEN_TYPE] == T_CLASS
279
                        && isset($this->tokens[$tokenID - 1])
280
                        && $this->tokens[$tokenID - 1][self::TOKEN_TYPE] == T_PAAMAYIM_NEKUDOTAYIM
281
                    ) {
282
                        //PHP5.5 ClassName::class constant
283
                        continue;
284
                    }
285
286
                    $this->registerDeclaration($tokenID, $token[self::TOKEN_TYPE]);
287
                    break;
288
289
                case T_INCLUDE:
290
                case T_INCLUDE_ONCE:
291
                case T_REQUIRE:
292
                case T_REQUIRE_ONCE:
293
                    $this->hasIncludes = true;
294
            }
295
        }
296
297
        //Dropping empty namespace
298
        if (isset($this->namespaces[''])) {
299
            $this->namespaces['\\'] = $this->namespaces[''];
300
            unset($this->namespaces['']);
301
        }
302
    }
303
304
    /**
305
     * Handle namespace declaration.
306
     *
307
     * @param int $tokenID
308
     */
309
    private function registerNamespace($tokenID)
310
    {
311
        $namespace = '';
312
        $localID = $tokenID + 1;
313
314
        do {
315
            $token = $this->tokens[$localID++];
316
            if ($token[self::TOKEN_CODE] == '{') {
317
                break;
318
            }
319
320
            $namespace .= $token[self::TOKEN_CODE];
321
        } while (
322
            isset($this->tokens[$localID])
323
            && $this->tokens[$localID][self::TOKEN_CODE] != '{'
324
            && $this->tokens[$localID][self::TOKEN_CODE] != ';'
325
        );
326
327
        //Whitespaces
328
        $namespace = trim($namespace);
329
330
        $uses = [];
331
        if (isset($this->namespaces[$namespace])) {
332
            $uses = $this->namespaces[$namespace];
333
        }
334
335
        if ($this->tokens[$localID][self::TOKEN_CODE] == ';') {
336
            $endingID = count($this->tokens) - 1;
337
        } else {
338
            $endingID = $this->endingToken($tokenID);
339
        }
340
341
        $this->namespaces[$namespace] = [
342
            self::O_TOKEN => $tokenID,
343
            self::C_TOKEN => $endingID,
344
            self::N_USES  => $uses
345
        ];
346
    }
347
348
    /**
349
     * Handle use (import class from another namespace).
350
     *
351
     * @param int $tokenID
352
     */
353
    private function registerUse($tokenID)
354
    {
355
        $namespace = rtrim($this->activeNamespace($tokenID), '\\');
356
357
        $class = '';
358
        $localAlias = null;
359
        for ($localID = $tokenID + 1; $this->tokens[$localID][self::TOKEN_CODE] != ';'; $localID++) {
360
            if ($this->tokens[$localID][self::TOKEN_TYPE] == T_AS) {
361
                $localAlias = '';
362
                continue;
363
            }
364
365
            if ($localAlias === null) {
366
                $class .= $this->tokens[$localID][self::TOKEN_CODE];
367
            } else {
368
                $localAlias .= $this->tokens[$localID][self::TOKEN_CODE];
369
            }
370
        }
371
372
        if (empty($localAlias)) {
373
            $names = explode('\\', $class);
374
            $localAlias = end($names);
375
        }
376
377
        $this->namespaces[$namespace][self::N_USES][trim($localAlias)] = trim($class);
378
    }
379
380
    /**
381
     * Handle function declaration (function creation).
382
     *
383
     * @param int $tokenID
384
     */
385
    private function registerFunction($tokenID)
386
    {
387 View Code Duplication
        foreach ($this->declarations as $declarations) {
388
            foreach ($declarations as $location) {
389
                if ($tokenID >= $location[self::O_TOKEN] && $tokenID <= $location[self::C_TOKEN]) {
390
                    //We are inside class, function is method
391
                    return;
392
                }
393
            }
394
        }
395
396
        $localID = $tokenID + 1;
397
        while ($this->tokens[$localID][self::TOKEN_TYPE] != T_STRING) {
398
            //Fetching function name
399
            $localID++;
400
        }
401
402
        $name = $this->tokens[$localID][self::TOKEN_CODE];
403 View Code Duplication
        if (!empty($namespace = $this->activeNamespace($tokenID))) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
404
            $name = $namespace . self::NS_SEPARATOR . $name;
405
        }
406
407
        $this->functions[$name] = [
408
            self::O_TOKEN => $tokenID,
409
            self::C_TOKEN => $this->endingToken($tokenID)
410
        ];
411
    }
412
413
    /**
414
     * Handle declaration of class, trait of interface. Declaration will be stored under it's token
415
     * type in declarations array.
416
     *
417
     * @param int $tokenID
418
     * @param int $tokenType
419
     */
420
    private function registerDeclaration($tokenID, $tokenType)
421
    {
422
        $localID = $tokenID + 1;
423
        while ($this->tokens[$localID][self::TOKEN_TYPE] != T_STRING) {
424
            $localID++;
425
        }
426
427
        $name = $this->tokens[$localID][self::TOKEN_CODE];
428 View Code Duplication
        if (!empty($namespace = $this->activeNamespace($tokenID))) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
429
            $name = $namespace . self::NS_SEPARATOR . $name;
430
        }
431
432
        $this->declarations[token_name($tokenType)][$name] = [
433
            self::O_TOKEN => $tokenID,
434
            self::C_TOKEN => $this->endingToken($tokenID)
435
        ];
436
    }
437
438
    /**
439
     * Locate every function or static method call (including $this calls).
440
     *
441
     * @param array $tokens
442
     * @param int   $invocationLevel
443
     */
444
    private function locateInvocations(array $tokens, $invocationLevel = 0)
445
    {
446
        //Multiple "(" and ")" statements nested.
447
        $level = 0;
448
449
        //Skip all tokens until next function
450
        $ignore = false;
451
452
        //Were function was found
453
        $invocationTID = 0;
454
455
        //Parsed arguments and their first token id
456
        $arguments = [];
457
        $argumentsTID = false;
458
459
        //Tokens used to re-enable token detection
460
        $stopTokens = [T_STRING, T_WHITESPACE, T_DOUBLE_COLON, T_OBJECT_OPERATOR, T_NS_SEPARATOR];
461
        foreach ($tokens as $tokenID => $token) {
462
            $tokenType = $token[self::TOKEN_TYPE];
463
464
            //We are not indexing function declarations or functions called from $objects.
465
            if ($tokenType == T_FUNCTION || $tokenType == T_OBJECT_OPERATOR || $tokenType == T_NEW) {
466
                if (
467
                    empty($argumentsTID)
468
                    && (
469
                        empty($invocationTID)
470
                        || $this->getSource($invocationTID, $tokenID - 1) != '$this'
471
                    )
472
                ) {
473
                    //Not a call, function declaration, or object method
474
                    $ignore = true;
475
                    continue;
476
                }
477
            } elseif ($ignore) {
478
                if (!in_array($tokenType, $stopTokens)) {
479
                    //Returning to search
480
                    $ignore = false;
481
                }
482
                continue;
483
            }
484
485
            //We are inside function, and there is "(", indexing arguments.
486
            if (!empty($invocationTID) && ($tokenType == '(' || $tokenType == '[')) {
487
                if (empty($argumentsTID)) {
488
                    $argumentsTID = $tokenID;
489
                }
490
491
                $level++;
492
                if ($level != 1) {
493
                    //Not arguments beginning, but arguments part
494
                    $arguments[$tokenID] = $token;
495
                }
496
497
                continue;
498
            }
499
500
            //We are inside function arguments and ")" met.
501
            if (!empty($invocationTID) && ($tokenType == ')' || $tokenType == ']')) {
502
                $level--;
503
                if ($level == -1) {
504
                    $invocationTID = false;
505
                    $level = 0;
506
                    continue;
507
                }
508
509
                //Function fully indexed, we can process it now.
510
                if ($level == 0) {
511
                    $this->registerInvocation(
512
                        $invocationTID,
513
                        $argumentsTID,
514
                        $tokenID,
515
                        $arguments,
516
                        $invocationLevel
517
                    );
518
519
                    //Closing search
520
                    $arguments = [];
521
                    $argumentsTID = $invocationTID = false;
522
                } else {
523
                    //Not arguments beginning, but arguments part
524
                    $arguments[$tokenID] = $token;
525
                }
526
527
                continue;
528
            }
529
530
            //Still inside arguments.
531
            if (!empty($invocationTID) && !empty($level)) {
532
                $arguments[$tokenID] = $token;
533
                continue;
534
            }
535
536
            //Nothing valuable to remember, will be parsed later.
537
            if (!empty($invocationTID) && in_array($tokenType, $stopTokens)) {
538
                continue;
539
            }
540
541
            //Seems like we found function/method call
542
            if (
543
                $tokenType == T_STRING
544
                || $tokenType == T_STATIC
545
                || $tokenType == T_NS_SEPARATOR
546
                || ($tokenType == T_VARIABLE && $token[self::TOKEN_CODE] == '$this')
547
            ) {
548
                $invocationTID = $tokenID;
549
                $level = 0;
550
551
                $argumentsTID = false;
552
                continue;
553
            }
554
555
            //Returning to search
556
            $invocationTID = false;
557
            $arguments = [];
558
        }
559
    }
560
561
    /**
562
     * Registering invocation.
563
     *
564
     * @param int   $invocationID
565
     * @param int   $argumentsID
566
     * @param int   $endID
567
     * @param array $arguments
568
     * @param int   $invocationLevel
569
     */
570
    private function registerInvocation(
571
        $invocationID,
572
        $argumentsID,
573
        $endID,
574
        array $arguments,
575
        $invocationLevel
576
    ) {
577
        //Nested invocations
578
        $this->locateInvocations($arguments, $invocationLevel + 1);
579
580
        list($class, $operator, $name) = $this->fetchContext($invocationID, $argumentsID);
581
        if (!empty($operator) && empty($class)) {
582
            //Non detectable
583
            return;
584
        }
585
586
        $this->invocations[] = new ReflectionInvocation(
587
            $this->filename,
588
            $this->lineNumber($invocationID),
589
            $class,
590
            $operator,
591
            $name,
592
            ReflectionArgument::locateArguments($arguments),
593
            $this->getSource($invocationID, $endID),
594
            $invocationLevel
595
        );
596
    }
597
598
    /**
599
     * Fetching invocation context.
600
     *
601
     * @param int $invocationTID
602
     * @param int $argumentsTID
603
     * @return array
604
     */
605
    private function fetchContext($invocationTID, $argumentsTID)
606
    {
607
        $class = $operator = '';
608
        $name = trim($this->getSource($invocationTID, $argumentsTID), '( ');
609
610
        //Let's try to fetch all information we need
611
        if (strpos($name, '->') !== false) {
612
            $operator = '->';
613
        } elseif (strpos($name, '::') !== false) {
614
            $operator = '::';
615
        }
616
617
        if (!empty($operator)) {
618
            list($class, $name) = explode($operator, $name);
619
620
            //We now have to clarify class name
621
            if (in_array($class, ['self', 'static', '$this'])) {
622
                $class = $this->activeDeclaration($invocationTID);
623
            }
624
        }
625
626
        return [$class, $operator, $name];
627
    }
628
629
    /**
630
     * Get declaration which is active in given token position.
631
     *
632
     * @param int $tokenID
633
     * @return string|null
634
     */
635
    private function activeDeclaration($tokenID)
636
    {
637 View Code Duplication
        foreach ($this->declarations as $declarations) {
638
            foreach ($declarations as $name => $position) {
639
                if ($tokenID >= $position[self::O_TOKEN] && $tokenID <= $position[self::C_TOKEN]) {
640
                    return $name;
641
                }
642
            }
643
        }
644
645
        //Can not be detected
646
        return null;
647
    }
648
649
    /**
650
     * Get namespace name active at specified token position.
651
     *
652
     * @param int $tokenID
653
     * @return string
654
     */
655
    private function activeNamespace($tokenID)
656
    {
657
        foreach ($this->namespaces as $namespace => $position) {
658
            if ($tokenID >= $position[self::O_TOKEN] && $tokenID <= $position[self::C_TOKEN]) {
659
                return $namespace;
660
            }
661
        }
662
663
        //Seems like no namespace declaration
664
        $this->namespaces[''] = [
665
            self::O_TOKEN => 0,
666
            self::C_TOKEN => count($this->tokens),
667
            self::N_USES  => []
668
        ];
669
670
        return '';
671
    }
672
673
    /**
674
     * Find token ID of ending brace.
675
     *
676
     * @param int $tokenID
677
     * @return mixed
678
     */
679
    private function endingToken($tokenID)
680
    {
681
        $level = null;
682
        for ($localID = $tokenID; $localID < $this->countTokens; $localID++) {
683
            $token = $this->tokens[$localID];
684
            if ($token[self::TOKEN_CODE] == '{') {
685
                $level++;
686
                continue;
687
            }
688
689
            if ($token[self::TOKEN_CODE] == '}') {
690
                $level--;
691
            }
692
693
            if ($level === 0) {
694
                break;
695
            }
696
        }
697
698
        return $localID;
699
    }
700
701
    /**
702
     * Get line number associated with token.
703
     *
704
     * @param int $tokenID
705
     * @return int
706
     */
707
    private function lineNumber($tokenID)
708
    {
709
        while (empty($this->tokens[$tokenID][self::TOKEN_LINE])) {
710
            $tokenID--;
711
        }
712
713
        return $this->tokens[$tokenID][self::TOKEN_LINE];
714
    }
715
716
    /**
717
     * Get source located between two tokens.
718
     *
719
     * @param int $startID
720
     * @param int $endID
721
     * @return string
722
     */
723
    private function getSource($startID, $endID)
724
    {
725
        $result = '';
726
        for ($tokenID = $startID; $tokenID <= $endID; $tokenID++) {
727
            //Collecting function usage source
728
            $result .= $this->tokens[$tokenID][self::TOKEN_CODE];
729
        }
730
731
        return $result;
732
    }
733
}