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