Passed
Pull Request — master (#38)
by Alexander
11:39 queued 09:06
created

ReflectionFile::isClassNameConst()   A

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 1
Bugs 0 Features 0
Metric Value
cc 3
eloc 3
c 1
b 0
f 0
nc 3
nop 1
dl 0
loc 5
ccs 4
cts 4
cp 1
crap 3
rs 10
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Classifier;
6
7
use PhpToken;
8
9
/**
10
 * This file was copied from {@link https://github.com/spiral/tokenizer}.
11
 *
12
 * @internal
13
 *
14
 * @psalm-type TPosition = list{int, int}
15
 */
16
final class ReflectionFile
17
{
18
    /**
19
     * Namespace separator.
20
     */
21
    public const NS_SEPARATOR = '\\';
22
23
    /**
24
     * Opening and closing token ids.
25
     */
26
    public const O_TOKEN = 0;
27
    public const C_TOKEN = 1;
28
29
    public const T_OPEN_CURLY_BRACES = 123;
30
    public const T_CLOSE_CURLY_BRACES = 125;
31
    public const T_SEMICOLON = 59;
32
33
    /**
34
     * Set of tokens required to detect classes, traits, interfaces declarations. We
35
     * don't need any other token for that.
36
     */
37
    private const TOKENS = [
38
        self::T_OPEN_CURLY_BRACES,
39
        self::T_CLOSE_CURLY_BRACES,
40
        self::T_SEMICOLON,
41
        T_PAAMAYIM_NEKUDOTAYIM,
42
        T_NAMESPACE,
43
        T_STRING,
44
        T_CLASS,
45
        T_INTERFACE,
46
        T_TRAIT,
47
        T_ENUM,
0 ignored issues
show
Bug introduced by
The constant Yiisoft\Classifier\T_ENUM was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
48
        T_NS_SEPARATOR,
49
    ];
50
51
    /**
52
     * Parsed tokens array.
53
     *
54
     * @var array<int, PhpToken>
55
     */
56
    private array $tokens;
57
58
    /**
59
     * Total tokens count.
60
     */
61
    private int $countTokens;
62
63
    /**
64
     * Namespaces used in file and their token positions.
65
     *
66
     * @psalm-var array<string, TPosition>
67
     */
68
    private array $namespaces = [];
69
70
    /**
71
     * Declarations of classes, interfaces and traits.
72
     *
73
     * @psalm-var array<class-string|trait-string, TPosition>
74
     */
75
    private array $declarations = [];
76
77 25
    public function __construct(
78
        private string $filename
79
    ) {
80 25
        $this->tokens = PhpToken::tokenize(file_get_contents($this->filename));
81 25
        $this->countTokens = \count($this->tokens);
82
83
        //Looking for declarations
84 25
        $this->locateDeclarations();
85
    }
86
87
    /**
88
     * List of declarations names
89
     *
90
     * @return array<class-string|trait-string>
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<class-string|trait-string> at position 2 could not be parsed: Unknown type name 'class-string' at position 2 in array<class-string|trait-string>.
Loading history...
91
     */
92 25
    public function getDeclarations(): array
93
    {
94 25
        return \array_keys($this->declarations);
95
    }
96
97
    /**
98
     * Locate every class, interface, trait or enum definition.
99
     */
100 25
    private function locateDeclarations(): void
101
    {
102 25
        foreach ($this->tokens as $tokenIndex => $token) {
103 25
            if (!\in_array($token->id, self::TOKENS, true)) {
104 25
                continue;
105
            }
106
107 25
            switch ($token->id) {
108 25
                case T_NAMESPACE:
109 24
                    $this->registerNamespace($tokenIndex);
110 24
                    break;
111
112 25
                case T_CLASS:
113 25
                case T_TRAIT:
114 25
                case T_INTERFACE:
115 25
                case T_ENUM:
0 ignored issues
show
Bug introduced by
The constant Yiisoft\Classifier\T_ENUM was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
116 25
                    if ($this->isClassNameConst($tokenIndex)) {
117
                        // PHP5.5 ClassName::class constant
118 13
                        continue 2;
119
                    }
120
121 25
                    if ($this->isAnonymousClass($tokenIndex)) {
122
                        // PHP7.0 Anonymous classes new class ('foo', 'bar')
123 14
                        continue 2;
124
                    }
125
126 25
                    if (!$this->isCorrectDeclaration($tokenIndex)) {
127
                        // PHP8.0 Named parameters ->foo(class: 'bar')
128 14
                        continue 2;
129
                    }
130
131 25
                    $this->registerDeclaration($tokenIndex);
132 25
                    break;
133
            }
134
        }
135
136
        //Dropping empty namespace
137 25
        if (isset($this->namespaces[''])) {
138 14
            $this->namespaces['\\'] = $this->namespaces[''];
139 14
            unset($this->namespaces['']);
140
        }
141
    }
142
143
    /**
144
     * Handle namespace declaration.
145
     */
146 24
    private function registerNamespace(int $tokenIndex): void
147
    {
148 24
        $namespace = '';
149 24
        $localIndex = $tokenIndex + 1;
150
151
        do {
152 24
            $token = $this->tokens[$localIndex++];
153 24
            $namespace .= $token->text;
154
        } while (
155 24
            isset($this->tokens[$localIndex])
156 24
            && $this->tokens[$localIndex]->text !== '{'
157 24
            && $this->tokens[$localIndex]->text !== ';'
158
        );
159
160
        //Whitespaces
161 24
        $namespace = \trim($namespace);
162
163 24
        if ($this->tokens[$localIndex]->text === ';') {
164 23
            $endingIndex = \count($this->tokens) - 1;
165
        } else {
166 14
            $endingIndex = $this->endingToken($tokenIndex);
167
        }
168
169 24
        $this->namespaces[$namespace] = [
170 24
            self::O_TOKEN => $tokenIndex,
171 24
            self::C_TOKEN => $endingIndex,
172 24
        ];
173
    }
174
175
    /**
176
     * Handle declaration of class, trait of interface. Declaration will be stored under it's token
177
     * type in declarations array.
178
     */
179 25
    private function registerDeclaration(int $tokenIndex): void
180
    {
181 25
        $localIndex = $tokenIndex + 1;
182 25
        while ($this->tokens[$localIndex]->id !== T_STRING) {
183 25
            ++$localIndex;
184
        }
185
186 25
        $name = $this->tokens[$localIndex]->text;
187 25
        if (!empty($namespace = $this->activeNamespace($tokenIndex))) {
188 24
            $name = $namespace . self::NS_SEPARATOR . $name;
189
        }
190
191
        /** @var class-string|trait-string $name */
192 25
        $this->declarations[$name] = [
193 25
            self::O_TOKEN => $tokenIndex,
194 25
            self::C_TOKEN => $this->endingToken($tokenIndex),
195 25
        ];
196
    }
197
198
    /**
199
     * Check if token ID represents `ClassName::class` constant statement.
200
     */
201 25
    private function isClassNameConst(int $tokenIndex): bool
202
    {
203 25
        return $this->tokens[$tokenIndex]->id === T_CLASS
204 25
            && isset($this->tokens[$tokenIndex - 1])
205 25
            && $this->tokens[$tokenIndex - 1]->id === T_PAAMAYIM_NEKUDOTAYIM;
206
    }
207
208
    /**
209
     * Check if token ID represents anonymous class creation, e.g. `new class ('foo', 'bar')`.
210
     */
211 25
    private function isAnonymousClass(int $tokenIndex): bool
212
    {
213 25
        return $this->tokens[$tokenIndex]->id === T_CLASS
214 25
            && isset($this->tokens[$tokenIndex - 2])
215 25
            && $this->tokens[$tokenIndex - 2]->id === T_NEW;
216
    }
217
218
    /**
219
     * Check if token ID represents named parameter with name `class`, e.g. `foo(class: SomeClass::name)`.
220
     */
221 25
    private function isCorrectDeclaration(int $tokenIndex): bool
222
    {
223 25
        return \in_array($this->tokens[$tokenIndex]->id, [T_CLASS, T_TRAIT, T_INTERFACE, T_ENUM], true)
0 ignored issues
show
Bug introduced by
The constant Yiisoft\Classifier\T_ENUM was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
224 25
            && isset($this->tokens[$tokenIndex + 2])
225 25
            && $this->tokens[$tokenIndex + 1]->id === T_WHITESPACE
226 25
            && $this->tokens[$tokenIndex + 2]->id === T_STRING;
227
    }
228
229
    /**
230
     * Get namespace name active at specified token position.
231
     *
232
     * @return array-key
0 ignored issues
show
Documentation Bug introduced by
The doc comment array-key at position 0 could not be parsed: Unknown type name 'array-key' at position 0 in array-key.
Loading history...
233
     */
234 25
    private function activeNamespace(int $tokenIndex): string
235
    {
236 25
        foreach ($this->namespaces as $namespace => $position) {
237 24
            if ($tokenIndex >= $position[self::O_TOKEN] && $tokenIndex <= $position[self::C_TOKEN]) {
238 24
                return $namespace;
239
            }
240
        }
241
242
        //Seems like no namespace declaration
243 14
        $this->namespaces[''] = [
244 14
            self::O_TOKEN => 0,
245 14
            self::C_TOKEN => \count($this->tokens),
246 14
        ];
247
248 14
        return '';
249
    }
250
251
    /**
252
     * Find token index of ending brace.
253
     */
254 25
    private function endingToken(int $tokenIndex): int
255
    {
256 25
        $level = 0;
257 25
        $hasOpen = false;
258 25
        for ($localIndex = $tokenIndex; $localIndex < $this->countTokens; ++$localIndex) {
259 25
            $token = $this->tokens[$localIndex];
260 25
            if ($token->text === '{') {
261 25
                ++$level;
262 25
                $hasOpen = true;
263 25
                continue;
264
            }
265
266 25
            if ($token->text === '}') {
267 25
                --$level;
268
            }
269
270 25
            if ($hasOpen && $level === 0) {
271 25
                break;
272
            }
273
        }
274
275 25
        return $localIndex;
276
    }
277
}
278