ReflectionFile::isAnonymousClass()   A
last analyzed

Complexity

Conditions 3
Paths 3

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 3

Importance

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