Test Failed
Pull Request — master (#902)
by butschster
08:55
created

ReflectionFile::isAnonymousClass()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 3

Importance

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