Passed
Push — master ( 893d1e...31005e )
by Kirill
03:35
created

ReflectionFile   F

Complexity

Total Complexity 108

Size/Duplication

Total Lines 740
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 108
eloc 263
c 0
b 0
f 0
dl 0
loc 740
rs 2

25 Methods

Rating   Name   Duplication   Size   Complexity  
A importSchema() 0 3 1
A getFunctions() 0 3 1
A hasIncludes() 0 3 1
A registerUse() 0 25 5
A getInvocations() 0 7 2
A activeDeclaration() 0 12 5
A fetchContext() 0 22 5
A registerInvocation() 0 26 3
A getSource() 0 9 2
A registerDeclaration() 0 15 3
A __construct() 0 8 1
A lineNumber() 0 7 2
B registerNamespace() 0 36 7
A getInterfaces() 0 7 2
A getFilename() 0 3 1
A getTraits() 0 7 2
B registerFunction() 0 25 7
A endingToken() 0 20 5
A activeNamespace() 0 16 4
A getTokens() 0 3 1
A exportSchema() 0 3 1
D locateInvocations() 0 114 27
A isClassNameConst() 0 5 3
A getClasses() 0 7 2
C locateDeclarations() 0 43 15

How to fix   Complexity   

Complex Class

Complex classes like ReflectionFile often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ReflectionFile, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/**
4
 * Spiral Framework.
5
 *
6
 * @license   MIT
7
 * @author    Anton Titov (Wolfy-J)
8
 */
9
10
declare(strict_types=1);
11
12
namespace Spiral\Tokenizer\Reflection;
13
14
use Spiral\Tokenizer\Tokenizer;
15
16
/**
17
 * File reflections can fetch information about classes, interfaces, functions and traits declared
18
 * in file. In addition file reflection provides ability to fetch and describe every method/function
19
 * call.
20
 */
21
final class ReflectionFile
22
{
23
    /**
24
     * Namespace separator.
25
     */
26
    public const NS_SEPARATOR = '\\';
27
28
    /**
29
     * Constants for convenience.
30
     */
31
    public const TOKEN_TYPE = Tokenizer::TYPE;
32
    public const TOKEN_CODE = Tokenizer::CODE;
33
    public const TOKEN_LINE = Tokenizer::LINE;
34
35
    /**
36
     * Opening and closing token ids.
37
     */
38
    public const O_TOKEN = 0;
39
    public const C_TOKEN = 1;
40
41
    /**
42
     * Namespace uses.
43
     */
44
    public const N_USES = 2;
45
46
    /**
47
     * Set of tokens required to detect classes, traits, interfaces and function declarations. We
48
     * don't need any other token for that.
49
     *
50
     * @var array
51
     */
52
    private static $processTokens = [
53
        '{',
54
        '}',
55
        ';',
56
        T_PAAMAYIM_NEKUDOTAYIM,
57
        T_NAMESPACE,
58
        T_STRING,
59
        T_CLASS,
60
        T_INTERFACE,
61
        T_TRAIT,
62
        T_FUNCTION,
63
        T_NS_SEPARATOR,
64
        T_INCLUDE,
65
        T_INCLUDE_ONCE,
66
        T_REQUIRE,
67
        T_REQUIRE_ONCE,
68
        T_USE,
69
        T_AS,
70
    ];
71
72
    /** @var string */
73
    private $filename = '';
74
75
    /**
76
     * Parsed tokens array.
77
     *
78
     * @internal
79
     * @var array
80
     */
81
    private $tokens = [];
82
83
    /**
84
     * Total tokens count.
85
     *
86
     * @internal
87
     * @var int
88
     */
89
    private $countTokens = 0;
90
91
    /**
92
     * Indicator that file has external includes.
93
     *
94
     * @internal
95
     * @var bool
96
     */
97
    private $hasIncludes = false;
98
99
    /**
100
     * Namespaces used in file and their token positions.
101
     *
102
     * @internal
103
     * @var array
104
     */
105
    private $namespaces = [];
106
107
    /**
108
     * Declarations of classes, interfaces and traits.
109
     *
110
     * @internal
111
     * @var array
112
     */
113
    private $declarations = [];
114
115
    /**
116
     * Declarations of new functions.
117
     *
118
     * @internal
119
     * @var array
120
     */
121
    private $functions = [];
122
123
    /**
124
     * Every found method/function invocation.
125
     *
126
     * @internal
127
     * @var ReflectionInvocation[]
128
     */
129
    private $invocations = [];
130
131
    /**
132
     * @param string $filename
133
     */
134
    public function __construct(string $filename)
135
    {
136
        $this->filename = $filename;
137
        $this->tokens = Tokenizer::getTokens($filename);
138
        $this->countTokens = count($this->tokens);
139
140
        //Looking for declarations
141
        $this->locateDeclarations();
142
    }
143
144
    /**
145
     * Filename.
146
     *
147
     * @return string
148
     */
149
    public function getFilename(): string
150
    {
151
        return $this->filename;
152
    }
153
154
    /**
155
     * List of declared function names
156
     *
157
     * @return array
158
     */
159
    public function getFunctions(): array
160
    {
161
        return array_keys($this->functions);
162
    }
163
164
    /**
165
     * List of declared class names
166
     *
167
     * @return array
168
     */
169
    public function getClasses(): array
170
    {
171
        if (!isset($this->declarations['T_CLASS'])) {
172
            return [];
173
        }
174
175
        return array_keys($this->declarations['T_CLASS']);
176
    }
177
178
    /**
179
     * List of declared trait names
180
     *
181
     * @return array
182
     */
183
    public function getTraits(): array
184
    {
185
        if (!isset($this->declarations['T_TRAIT'])) {
186
            return [];
187
        }
188
189
        return array_keys($this->declarations['T_TRAIT']);
190
    }
191
192
    /**
193
     * List of declared interface names
194
     *
195
     * @return array
196
     */
197
    public function getInterfaces(): array
198
    {
199
        if (!isset($this->declarations['T_INTERFACE'])) {
200
            return [];
201
        }
202
203
        return array_keys($this->declarations['T_INTERFACE']);
204
    }
205
206
    /**
207
     * Get list of tokens associated with given file.
208
     *
209
     * @return array
210
     */
211
    public function getTokens(): array
212
    {
213
        return $this->tokens;
214
    }
215
216
    /**
217
     * Indication that file contains require/include statements
218
     *
219
     * @return bool
220
     */
221
    public function hasIncludes(): bool
222
    {
223
        return $this->hasIncludes;
224
    }
225
226
    /**
227
     * Locate and return list of every method or function call in specified file. Only static and
228
     * $this calls will be indexed
229
     *
230
     * @return ReflectionInvocation[]
231
     */
232
    public function getInvocations(): array
233
    {
234
        if (empty($this->invocations)) {
235
            $this->locateInvocations($this->getTokens());
236
        }
237
238
        return $this->invocations;
239
    }
240
241
    /**
242
     * Export found declaration as array for caching purposes.
243
     *
244
     * @return array
245
     */
246
    public function exportSchema(): array
247
    {
248
        return [$this->hasIncludes, $this->declarations, $this->functions, $this->namespaces];
249
    }
250
251
    /**
252
     * Import cached reflection schema.
253
     *
254
     * @param array $cache
255
     */
256
    protected function importSchema(array $cache)
257
    {
258
        list($this->hasIncludes, $this->declarations, $this->functions, $this->namespaces) = $cache;
259
    }
260
261
    /**
262
     * Locate every class, interface, trait or function definition.
263
     */
264
    protected function locateDeclarations()
265
    {
266
        foreach ($this->getTokens() as $tokenID => $token) {
267
            if (!in_array($token[self::TOKEN_TYPE], self::$processTokens)) {
268
                continue;
269
            }
270
271
            switch ($token[self::TOKEN_TYPE]) {
272
                case T_NAMESPACE:
273
                    $this->registerNamespace($tokenID);
274
                    break;
275
276
                case T_USE:
277
                    $this->registerUse($tokenID);
278
                    break;
279
280
                case T_FUNCTION:
281
                    $this->registerFunction($tokenID);
282
                    break;
283
284
                case T_CLASS:
285
                case T_TRAIT:
286
                case T_INTERFACE:
287
                    if ($this->isClassNameConst($tokenID)) {
288
                        //PHP5.5 ClassName::class constant
289
                        continue 2;
290
                    }
291
292
                    $this->registerDeclaration($tokenID, $token[self::TOKEN_TYPE]);
293
                    break;
294
295
                case T_INCLUDE:
296
                case T_INCLUDE_ONCE:
297
                case T_REQUIRE:
298
                case T_REQUIRE_ONCE:
299
                    $this->hasIncludes = true;
300
            }
301
        }
302
303
        //Dropping empty namespace
304
        if (isset($this->namespaces[''])) {
305
            $this->namespaces['\\'] = $this->namespaces[''];
306
            unset($this->namespaces['']);
307
        }
308
    }
309
310
    /**
311
     * Handle namespace declaration.
312
     *
313
     * @param int $tokenID
314
     */
315
    private function registerNamespace(int $tokenID)
316
    {
317
        $namespace = '';
318
        $localID = $tokenID + 1;
319
320
        do {
321
            $token = $this->tokens[$localID++];
322
            if ($token[self::TOKEN_CODE] == '{') {
323
                break;
324
            }
325
326
            $namespace .= $token[self::TOKEN_CODE];
327
        } while (
328
            isset($this->tokens[$localID])
329
            && $this->tokens[$localID][self::TOKEN_CODE] != '{'
330
            && $this->tokens[$localID][self::TOKEN_CODE] != ';'
331
        );
332
333
        //Whitespaces
334
        $namespace = trim($namespace);
335
336
        $uses = [];
337
        if (isset($this->namespaces[$namespace])) {
338
            $uses = $this->namespaces[$namespace];
339
        }
340
341
        if ($this->tokens[$localID][self::TOKEN_CODE] == ';') {
342
            $endingID = count($this->tokens) - 1;
343
        } else {
344
            $endingID = $this->endingToken($tokenID);
345
        }
346
347
        $this->namespaces[$namespace] = [
348
            self::O_TOKEN => $tokenID,
349
            self::C_TOKEN => $endingID,
350
            self::N_USES  => $uses,
351
        ];
352
    }
353
354
    /**
355
     * Handle use (import class from another namespace).
356
     *
357
     * @param int $tokenID
358
     */
359
    private function registerUse(int $tokenID)
360
    {
361
        $namespace = rtrim($this->activeNamespace($tokenID), '\\');
362
363
        $class = '';
364
        $localAlias = null;
365
        for ($localID = $tokenID + 1; $this->tokens[$localID][self::TOKEN_CODE] != ';'; ++$localID) {
366
            if ($this->tokens[$localID][self::TOKEN_TYPE] == T_AS) {
367
                $localAlias = '';
368
                continue;
369
            }
370
371
            if ($localAlias === null) {
372
                $class .= $this->tokens[$localID][self::TOKEN_CODE];
373
            } else {
374
                $localAlias .= $this->tokens[$localID][self::TOKEN_CODE];
375
            }
376
        }
377
378
        if (empty($localAlias)) {
379
            $names = explode('\\', $class);
380
            $localAlias = end($names);
381
        }
382
383
        $this->namespaces[$namespace][self::N_USES][trim($localAlias)] = trim($class);
384
    }
385
386
    /**
387
     * Handle function declaration (function creation).
388
     *
389
     * @param int $tokenID
390
     */
391
    private function registerFunction(int $tokenID)
392
    {
393
        foreach ($this->declarations as $declarations) {
394
            foreach ($declarations as $location) {
395
                if ($tokenID >= $location[self::O_TOKEN] && $tokenID <= $location[self::C_TOKEN]) {
396
                    //We are inside class, function is method
397
                    return;
398
                }
399
            }
400
        }
401
402
        $localID = $tokenID + 1;
403
        while ($this->tokens[$localID][self::TOKEN_TYPE] != T_STRING) {
404
            //Fetching function name
405
            ++$localID;
406
        }
407
408
        $name = $this->tokens[$localID][self::TOKEN_CODE];
409
        if (!empty($namespace = $this->activeNamespace($tokenID))) {
410
            $name = $namespace . self::NS_SEPARATOR . $name;
411
        }
412
413
        $this->functions[$name] = [
414
            self::O_TOKEN => $tokenID,
415
            self::C_TOKEN => $this->endingToken($tokenID),
416
        ];
417
    }
418
419
    /**
420
     * Handle declaration of class, trait of interface. Declaration will be stored under it's token
421
     * type in declarations array.
422
     *
423
     * @param int $tokenID
424
     * @param int $tokenType
425
     */
426
    private function registerDeclaration(int $tokenID, int $tokenType)
427
    {
428
        $localID = $tokenID + 1;
429
        while ($this->tokens[$localID][self::TOKEN_TYPE] != T_STRING) {
430
            ++$localID;
431
        }
432
433
        $name = $this->tokens[$localID][self::TOKEN_CODE];
434
        if (!empty($namespace = $this->activeNamespace($tokenID))) {
435
            $name = $namespace . self::NS_SEPARATOR . $name;
436
        }
437
438
        $this->declarations[token_name($tokenType)][$name] = [
439
            self::O_TOKEN => $tokenID,
440
            self::C_TOKEN => $this->endingToken($tokenID),
441
        ];
442
    }
443
444
    /**
445
     * Check if token ID represents `ClassName::class` constant statement.
446
     *
447
     * @param int $tokenID
448
     *
449
     * @return bool
450
     */
451
    private function isClassNameConst(int $tokenID): bool
452
    {
453
        return $this->tokens[$tokenID][self::TOKEN_TYPE] == T_CLASS
454
            && isset($this->tokens[$tokenID - 1])
455
            && $this->tokens[$tokenID - 1][self::TOKEN_TYPE] == T_PAAMAYIM_NEKUDOTAYIM;
456
    }
457
458
    /**
459
     * Locate every function or static method call (including $this calls).
460
     *
461
     * This is pretty old code, potentially to be improved using AST.
462
     *
463
     * @param array $tokens
464
     * @param int   $invocationLevel
465
     */
466
    private function locateInvocations(array $tokens, int $invocationLevel = 0)
467
    {
468
        //Multiple "(" and ")" statements nested.
469
        $level = 0;
470
471
        //Skip all tokens until next function
472
        $ignore = false;
473
474
        //Were function was found
475
        $invocationTID = 0;
476
477
        //Parsed arguments and their first token id
478
        $arguments = [];
479
        $argumentsTID = false;
480
481
        //Tokens used to re-enable token detection
482
        $stopTokens = [T_STRING, T_WHITESPACE, T_DOUBLE_COLON, T_OBJECT_OPERATOR, T_NS_SEPARATOR];
483
        foreach ($tokens as $tokenID => $token) {
484
            $tokenType = $token[self::TOKEN_TYPE];
485
486
            //We are not indexing function declarations or functions called from $objects.
487
            if (in_array($tokenType, [T_FUNCTION, T_OBJECT_OPERATOR, T_NEW])) {
488
                if (
489
                    empty($argumentsTID)
490
                    && (
491
                        empty($invocationTID)
492
                        || $this->getSource($invocationTID, $tokenID - 1) != '$this'
493
                    )
494
                ) {
495
                    //Not a call, function declaration, or object method
496
                    $ignore = true;
497
                    continue;
498
                }
499
            } elseif ($ignore) {
500
                if (!in_array($tokenType, $stopTokens)) {
501
                    //Returning to search
502
                    $ignore = false;
503
                }
504
                continue;
505
            }
506
507
            //We are inside function, and there is "(", indexing arguments.
508
            if (!empty($invocationTID) && ($tokenType == '(' || $tokenType == '[')) {
509
                if (empty($argumentsTID)) {
510
                    $argumentsTID = $tokenID;
511
                }
512
513
                ++$level;
514
                if ($level != 1) {
515
                    //Not arguments beginning, but arguments part
516
                    $arguments[$tokenID] = $token;
517
                }
518
519
                continue;
520
            }
521
522
            //We are inside function arguments and ")" met.
523
            if (!empty($invocationTID) && ($tokenType == ')' || $tokenType == ']')) {
524
                --$level;
525
                if ($level == -1) {
526
                    $invocationTID = false;
527
                    $level = 0;
528
                    continue;
529
                }
530
531
                //Function fully indexed, we can process it now.
532
                if ($level == 0) {
533
                    $this->registerInvocation(
534
                        $invocationTID,
535
                        $argumentsTID,
536
                        $tokenID,
537
                        $arguments,
538
                        $invocationLevel
539
                    );
540
541
                    //Closing search
542
                    $arguments = [];
543
                    $argumentsTID = $invocationTID = false;
544
                } else {
545
                    //Not arguments beginning, but arguments part
546
                    $arguments[$tokenID] = $token;
547
                }
548
549
                continue;
550
            }
551
552
            //Still inside arguments.
553
            if (!empty($invocationTID) && !empty($level)) {
554
                $arguments[$tokenID] = $token;
555
                continue;
556
            }
557
558
            //Nothing valuable to remember, will be parsed later.
559
            if (!empty($invocationTID) && in_array($tokenType, $stopTokens)) {
560
                continue;
561
            }
562
563
            //Seems like we found function/method call
564
            if (
565
                $tokenType == T_STRING
566
                || $tokenType == T_STATIC
567
                || $tokenType == T_NS_SEPARATOR
568
                || ($tokenType == T_VARIABLE && $token[self::TOKEN_CODE] == '$this')
569
            ) {
570
                $invocationTID = $tokenID;
571
                $level = 0;
572
573
                $argumentsTID = false;
574
                continue;
575
            }
576
577
            //Returning to search
578
            $invocationTID = false;
579
            $arguments = [];
580
        }
581
    }
582
583
    /**
584
     * Registering invocation.
585
     *
586
     * @param int   $invocationID
587
     * @param int   $argumentsID
588
     * @param int   $endID
589
     * @param array $arguments
590
     * @param int   $invocationLevel
591
     */
592
    private function registerInvocation(
593
        int $invocationID,
594
        int $argumentsID,
595
        int $endID,
596
        array $arguments,
597
        int $invocationLevel
598
    ) {
599
        //Nested invocations
600
        $this->locateInvocations($arguments, $invocationLevel + 1);
601
602
        list($class, $operator, $name) = $this->fetchContext($invocationID, $argumentsID);
603
604
        if (!empty($operator) && empty($class)) {
605
            //Non detectable
606
            return;
607
        }
608
609
        $this->invocations[] = new ReflectionInvocation(
610
            $this->filename,
611
            $this->lineNumber($invocationID),
612
            $class,
613
            $operator,
614
            $name,
615
            ReflectionArgument::locateArguments($arguments),
616
            $this->getSource($invocationID, $endID),
617
            $invocationLevel
618
        );
619
    }
620
621
    /**
622
     * Fetching invocation context.
623
     *
624
     * @param int $invocationTID
625
     * @param int $argumentsTID
626
     *
627
     * @return array
628
     */
629
    private function fetchContext(int $invocationTID, int $argumentsTID): array
630
    {
631
        $class = $operator = '';
632
        $name = trim($this->getSource($invocationTID, $argumentsTID), '( ');
633
634
        //Let's try to fetch all information we need
635
        if (strpos($name, '->') !== false) {
636
            $operator = '->';
637
        } elseif (strpos($name, '::') !== false) {
638
            $operator = '::';
639
        }
640
641
        if (!empty($operator)) {
642
            list($class, $name) = explode($operator, $name);
643
644
            //We now have to clarify class name
645
            if (in_array($class, ['self', 'static', '$this'])) {
646
                $class = $this->activeDeclaration($invocationTID);
647
            }
648
        }
649
650
        return [$class, $operator, $name];
651
    }
652
653
    /**
654
     * Get declaration which is active in given token position.
655
     *
656
     * @param int $tokenID
657
     *
658
     * @return string|null
659
     */
660
    private function activeDeclaration(int $tokenID): string
661
    {
662
        foreach ($this->declarations as $declarations) {
663
            foreach ($declarations as $name => $position) {
664
                if ($tokenID >= $position[self::O_TOKEN] && $tokenID <= $position[self::C_TOKEN]) {
665
                    return $name;
666
                }
667
            }
668
        }
669
670
        //Can not be detected
671
        return '';
672
    }
673
674
    /**
675
     * Get namespace name active at specified token position.
676
     *
677
     * @param int $tokenID
678
     *
679
     * @return string
680
     */
681
    private function activeNamespace(int $tokenID): string
682
    {
683
        foreach ($this->namespaces as $namespace => $position) {
684
            if ($tokenID >= $position[self::O_TOKEN] && $tokenID <= $position[self::C_TOKEN]) {
685
                return $namespace;
686
            }
687
        }
688
689
        //Seems like no namespace declaration
690
        $this->namespaces[''] = [
691
            self::O_TOKEN => 0,
692
            self::C_TOKEN => count($this->tokens),
693
            self::N_USES  => [],
694
        ];
695
696
        return '';
697
    }
698
699
    /**
700
     * Find token ID of ending brace.
701
     *
702
     * @param int $tokenID
703
     *
704
     * @return int
705
     */
706
    private function endingToken(int $tokenID): int
707
    {
708
        $level = null;
709
        for ($localID = $tokenID; $localID < $this->countTokens; ++$localID) {
710
            $token = $this->tokens[$localID];
711
            if ($token[self::TOKEN_CODE] == '{') {
712
                ++$level;
713
                continue;
714
            }
715
716
            if ($token[self::TOKEN_CODE] == '}') {
717
                --$level;
718
            }
719
720
            if ($level === 0) {
721
                break;
722
            }
723
        }
724
725
        return $localID;
726
    }
727
728
    /**
729
     * Get line number associated with token.
730
     *
731
     * @param int $tokenID
732
     *
733
     * @return int
734
     */
735
    private function lineNumber(int $tokenID): int
736
    {
737
        while (empty($this->tokens[$tokenID][self::TOKEN_LINE])) {
738
            --$tokenID;
739
        }
740
741
        return $this->tokens[$tokenID][self::TOKEN_LINE];
742
    }
743
744
    /**
745
     * Get src located between two tokens.
746
     *
747
     * @param int $startID
748
     * @param int $endID
749
     *
750
     * @return string
751
     */
752
    private function getSource(int $startID, int $endID): string
753
    {
754
        $result = '';
755
        for ($tokenID = $startID; $tokenID <= $endID; ++$tokenID) {
756
            //Collecting function usage src
757
            $result .= $this->tokens[$tokenID][self::TOKEN_CODE];
758
        }
759
760
        return $result;
761
    }
762
}
763