Completed
Branch develop (c2aa4c)
by Anton
05:17
created

ReflectionFile::locateInvocations()   D

Complexity

Conditions 29
Paths 26

Size

Total Lines 119
Code Lines 63

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
c 2
b 0
f 0
dl 0
loc 119
rs 4.4524
cc 29
eloc 63
nc 26
nop 2

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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
     * @invisible
139
     * @var TokenizerInterface
140
     */
141
    protected $tokenizer = null;
142
143
    /**
144
     * @param string             $filename
145
     * @param TokenizerInterface $tokenizer
146
     * @param array              $cache     Tokenizer can construct reflection with pre-created
147
     *                                      cache to speed up indexation.
148
     */
149
    public function __construct($filename, TokenizerInterface $tokenizer = null, array $cache = [])
150
    {
151
        $this->filename = $filename;
152
        $this->tokenizer = $this->saturate($tokenizer, TokenizerInterface::class);
153
154
        if (!empty($cache)) {
155
            //Locating file schema from file, can speed up class location a LOT
156
            $this->importSchema($cache);
157
158
            return;
159
        }
160
161
        //Looking for declarations
162
        $this->locateDeclarations();
163
    }
164
165
    /**
166
     * {@inheritdoc}
167
     */
168
    public function getFilename()
169
    {
170
        return $this->filename;
171
    }
172
173
    /**
174
     * {@inheritdoc}
175
     */
176
    public function getFunctions()
177
    {
178
        return array_keys($this->functions);
179
    }
180
181
    /**
182
     * {@inheritdoc}
183
     */
184
    public function getClasses()
185
    {
186
        if (!isset($this->declarations['T_CLASS'])) {
187
            return [];
188
        }
189
190
        return array_keys($this->declarations['T_CLASS']);
191
    }
192
193
    /**
194
     * {@inheritdoc}
195
     */
196
    public function getTraits()
197
    {
198
        if (!isset($this->declarations['T_TRAIT'])) {
199
            return [];
200
        }
201
202
        return array_keys($this->declarations['T_TRAIT']);
203
    }
204
205
    /**
206
     * {@inheritdoc}
207
     */
208
    public function getInterfaces()
209
    {
210
        if (!isset($this->declarations['T_INTERFACE'])) {
211
            return [];
212
        }
213
214
        return array_keys($this->declarations['T_INTERFACE']);
215
    }
216
217
    /**
218
     * Get list of tokens associated with given file.
219
     *
220
     * @return array
221
     */
222
    public function getTokens()
223
    {
224
        if (empty($this->tokens)) {
225
            //Happens when reflection created from cache
226
            $this->tokens = $this->tokenizer->fetchTokens($this->filename);
227
            $this->countTokens = count($this->tokens);
228
        }
229
230
        return $this->tokens;
231
    }
232
233
    /**
234
     * {@inheritdoc}
235
     */
236
    public function hasIncludes()
237
    {
238
        return $this->hasIncludes;
239
    }
240
241
    /**
242
     * {@inheritdoc}
243
     */
244
    public function getInvocations()
245
    {
246
        if (empty($this->invocations)) {
247
            $this->locateInvocations($this->getTokens());
248
        }
249
250
        return $this->invocations;
251
    }
252
253
    /**
254
     * Export found declaration as array for caching purposes.
255
     *
256
     * @return array
257
     */
258
    public function exportSchema()
259
    {
260
        return [$this->hasIncludes, $this->declarations, $this->functions, $this->namespaces];
261
    }
262
263
    /**
264
     * Import cached reflection schema.
265
     *
266
     * @param array $cache
267
     */
268
    protected function importSchema(array $cache)
269
    {
270
        list($this->hasIncludes, $this->declarations, $this->functions, $this->namespaces) = $cache;
271
    }
272
273
    /**
274
     * Locate every class, interface, trait or function definition.
275
     */
276
    protected function locateDeclarations()
277
    {
278
        foreach ($this->getTokens() as $tokenID => $token) {
279
            if (!in_array($token[self::TOKEN_TYPE], self::$processTokens)) {
280
                continue;
281
            }
282
283
            switch ($token[self::TOKEN_TYPE]) {
284
                case T_NAMESPACE:
285
                    $this->registerNamespace($tokenID);
286
                    break;
287
288
                case T_USE:
289
                    $this->registerUse($tokenID);
290
                    break;
291
292
                case T_FUNCTION:
293
                    $this->registerFunction($tokenID);
294
                    break;
295
296
                case T_CLASS:
297
                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...
298
                case T_INTERFACE:
299
                    if (
300
                        $this->tokens[$tokenID][self::TOKEN_TYPE] == T_CLASS
301
                        && isset($this->tokens[$tokenID - 1])
302
                        && $this->tokens[$tokenID - 1][self::TOKEN_TYPE] == T_PAAMAYIM_NEKUDOTAYIM
303
                    ) {
304
                        //PHP5.5 ClassName::class constant
305
                        continue;
306
                    }
307
308
                    $this->registerDeclaration($tokenID, $token[self::TOKEN_TYPE]);
309
                    break;
310
311
                case T_INCLUDE:
312
                case T_INCLUDE_ONCE:
313
                case T_REQUIRE:
314
                case T_REQUIRE_ONCE:
315
                    $this->hasIncludes = true;
316
            }
317
        }
318
319
        //Dropping empty namespace
320
        if (isset($this->namespaces[''])) {
321
            $this->namespaces['\\'] = $this->namespaces[''];
322
            unset($this->namespaces['']);
323
        }
324
    }
325
326
    /**
327
     * Handle namespace declaration.
328
     *
329
     * @param int $tokenID
330
     */
331
    private function registerNamespace($tokenID)
332
    {
333
        $namespace = '';
334
        $localID = $tokenID + 1;
335
336
        do {
337
            $token = $this->tokens[$localID++];
338
            if ($token[self::TOKEN_CODE] == '{') {
339
                break;
340
            }
341
342
            $namespace .= $token[self::TOKEN_CODE];
343
        } while (
344
            isset($this->tokens[$localID])
345
            && $this->tokens[$localID][self::TOKEN_CODE] != '{'
346
            && $this->tokens[$localID][self::TOKEN_CODE] != ';'
347
        );
348
349
        //Whitespaces
350
        $namespace = trim($namespace);
351
352
        $uses = [];
353
        if (isset($this->namespaces[$namespace])) {
354
            $uses = $this->namespaces[$namespace];
355
        }
356
357
        if ($this->tokens[$localID][self::TOKEN_CODE] == ';') {
358
            $endingID = count($this->tokens) - 1;
359
        } else {
360
            $endingID = $this->endingToken($tokenID);
361
        }
362
363
        $this->namespaces[$namespace] = [
364
            self::O_TOKEN => $tokenID,
365
            self::C_TOKEN => $endingID,
366
            self::N_USES  => $uses
367
        ];
368
    }
369
370
    /**
371
     * Handle use (import class from another namespace).
372
     *
373
     * @param int $tokenID
374
     */
375
    private function registerUse($tokenID)
376
    {
377
        $namespace = rtrim($this->activeNamespace($tokenID), '\\');
378
379
        $class = '';
380
        $localAlias = null;
381
        for ($localID = $tokenID + 1; $this->tokens[$localID][self::TOKEN_CODE] != ';'; $localID++) {
382
            if ($this->tokens[$localID][self::TOKEN_TYPE] == T_AS) {
383
                $localAlias = '';
384
                continue;
385
            }
386
387
            if ($localAlias === null) {
388
                $class .= $this->tokens[$localID][self::TOKEN_CODE];
389
            } else {
390
                $localAlias .= $this->tokens[$localID][self::TOKEN_CODE];
391
            }
392
        }
393
394
        if (empty($localAlias)) {
395
            $names = explode('\\', $class);
396
            $localAlias = end($names);
397
        }
398
399
        $this->namespaces[$namespace][self::N_USES][trim($localAlias)] = trim($class);
400
    }
401
402
    /**
403
     * Handle function declaration (function creation).
404
     *
405
     * @param int $tokenID
406
     */
407
    private function registerFunction($tokenID)
408
    {
409
        foreach ($this->declarations as $declarations) {
410
            foreach ($declarations as $location) {
411
                if ($tokenID >= $location[self::O_TOKEN] && $tokenID <= $location[self::C_TOKEN]) {
412
                    //We are inside class, function is method
413
                    return;
414
                }
415
            }
416
        }
417
418
        $localID = $tokenID + 1;
419
        while ($this->tokens[$localID][self::TOKEN_TYPE] != T_STRING) {
420
            //Fetching function name
421
            $localID++;
422
        }
423
424
        $name = $this->tokens[$localID][self::TOKEN_CODE];
425 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...
426
            $name = $namespace . self::NS_SEPARATOR . $name;
427
        }
428
429
        $this->functions[$name] = [
430
            self::O_TOKEN => $tokenID,
431
            self::C_TOKEN => $this->endingToken($tokenID)
432
        ];
433
    }
434
435
    /**
436
     * Handle declaration of class, trait of interface. Declaration will be stored under it's token
437
     * type in declarations array.
438
     *
439
     * @param int $tokenID
440
     * @param int $tokenType
441
     */
442
    private function registerDeclaration($tokenID, $tokenType)
443
    {
444
        $localID = $tokenID + 1;
445
        while ($this->tokens[$localID][self::TOKEN_TYPE] != T_STRING) {
446
            $localID++;
447
        }
448
449
        $name = $this->tokens[$localID][self::TOKEN_CODE];
450 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...
451
            $name = $namespace . self::NS_SEPARATOR . $name;
452
        }
453
454
        $this->declarations[token_name($tokenType)][$name] = [
455
            self::O_TOKEN => $tokenID,
456
            self::C_TOKEN => $this->endingToken($tokenID)
457
        ];
458
    }
459
460
    /**
461
     * Locate every function or static method call (including $this calls).
462
     *
463
     * @param array $tokens
464
     * @param int   $invocationLevel
465
     */
466
    private function locateInvocations(array $tokens, $invocationLevel = 0)
467
    {
468
        //Multiple "(" and ")" statements nested.
469
        $level = 0;
470
471
        //Inside array arguments
472
        $arrayLevel = 0;
0 ignored issues
show
Unused Code introduced by
$arrayLevel is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

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