Passed
Pull Request — master (#34)
by Evgeniy
02:28
created

ClosureExporter::isNextTokenIsPartOfNamespace()   B

Complexity

Conditions 9
Paths 24

Size

Total Lines 11
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 9

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 9
eloc 7
c 1
b 0
f 0
nc 24
nop 1
dl 0
loc 11
ccs 8
cts 8
cp 1
crap 9
rs 8.0555
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\VarDumper;
6
7
use Closure;
8
use ReflectionException;
9
use ReflectionFunction;
10
11
use function array_key_exists;
12
use function array_slice;
13
use function defined;
14
use function in_array;
15
use function is_array;
16
17
/**
18
 * ClosureExporter exports PHP {@see \Closure} as a string containing PHP code.
19
 *
20
 * The string is a valid PHP expression that can be evaluated by PHP parser
21
 * and the evaluation result will give back the closure instance.
22
 */
23
final class ClosureExporter
24
{
25
    private UseStatementParser $useStatementParser;
26
27 1
    public function __construct()
28
    {
29 1
        $this->useStatementParser = new UseStatementParser();
30 1
    }
31
32
    /**
33
     * Export closure as a string containing PHP code.
34
     *
35
     * @param Closure $closure Closure to export.
36
     *
37
     * @throws ReflectionException
38
     *
39
     * @return string String containing PHP code.
40
     */
41 39
    public function export(Closure $closure): string
42
    {
43 39
        $reflection = new ReflectionFunction($closure);
44
45 39
        $fileName = $reflection->getFileName();
46 39
        $start = $reflection->getStartLine();
47 39
        $end = $reflection->getEndLine();
48
49 39
        if ($fileName === false || $start === false || $end === false || ($fileContent = file($fileName)) === false) {
50
            return 'function() {/* Error: unable to determine Closure source */}';
51
        }
52
53 39
        --$start;
54 39
        $uses = $this->useStatementParser->fromFile($fileName);
55
56 39
        $source = implode('', array_slice($fileContent, $start, $end - $start));
57 39
        $tokens = token_get_all('<?php ' . $source);
58 39
        array_shift($tokens);
59
60 39
        $closureTokens = [];
61 39
        $pendingParenthesisCount = 0;
62 39
        $isShortClosure = false;
63 39
        $buffer = '';
64 39
        foreach ($tokens as $token) {
65 39
            if (!isset($token[0])) {
66
                continue;
67
            }
68 39
            if (in_array($token[0], [T_FUNCTION, T_FN, T_STATIC], true)) {
69 39
                $closureTokens[] = $token[1];
70 39
                if (!$isShortClosure && $token[0] === T_FN) {
71 31
                    $isShortClosure = true;
72
                }
73 39
                continue;
74
            }
75 39
            if ($closureTokens !== []) {
76 39
                $readableToken = $token[1] ?? $token;
77 39
                if ($this->isNextTokenIsPartOfNamespace($token)) {
78 20
                    $buffer .= $token[1];
79
                    // HERE we need to match partially because now NS can be a single token in PHP 8
80 20
                    if (array_key_exists($buffer, $uses) && !$this->isNextTokenIsPartOfNamespace(next($tokens))) {
81 12
                        $readableToken = $uses[$buffer];
82 12
                        $buffer = '';
83
                    }
84
                }
85 39
                if ($token === '{' || $token === '[') {
86 12
                    $pendingParenthesisCount++;
87 39
                } elseif ($token === '}' || $token === ']') {
88 15
                    if ($pendingParenthesisCount === 0) {
89 3
                        break;
90
                    }
91 12
                    $pendingParenthesisCount--;
92 39
                } elseif ($token === ',' || $token === ';') {
93 36
                    if ($pendingParenthesisCount === 0) {
94 36
                        break;
95
                    }
96
                }
97 39
                $closureTokens[] = $readableToken;
98
            }
99
        }
100
101 39
        return implode('', $closureTokens);
102
    }
103
104 39
    private function isNextTokenIsPartOfNamespace($token): bool
105
    {
106 39
        if (!is_array($token)) {
107 39
            return false;
108
        }
109
110 39
        return $token[0] === T_STRING
111 39
            || $token[0] === T_NS_SEPARATOR
112 39
            || (defined('T_NAME_QUALIFIED') && $token[0] === T_NAME_QUALIFIED)
113 39
            || (defined('T_NAME_FULLY_QUALIFIED') && $token[0] === T_NAME_FULLY_QUALIFIED)
114 39
            || (defined('T_NAME_RELATIVE') && $token[0] === T_NAME_RELATIVE);
115
    }
116
}
117