Test Failed
Pull Request — master (#895)
by butschster
19:39 queued 10:00
created

ReflectionFile::hasIncludes()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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