Passed
Pull Request — master (#1059)
by Maxim
12:16
created

ReflectionFile   F

Complexity

Total Complexity 120

Size/Duplication

Total Lines 707
Duplicated Lines 0 %

Test Coverage

Coverage 95.54%

Importance

Changes 0
Metric Value
wmc 120
eloc 277
dl 0
loc 707
ccs 257
cts 269
cp 0.9554
rs 2
c 0
b 0
f 0

28 Methods

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

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