Passed
Pull Request — master (#38)
by Sergei
05:23 queued 02:36
created

ReflectionFile   A

Complexity

Total Complexity 42

Size/Duplication

Total Lines 260
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 105
c 1
b 0
f 0
dl 0
loc 260
ccs 92
cts 92
cp 1
rs 9.0399
wmc 42

10 Methods

Rating   Name   Duplication   Size   Complexity  
A isClassNameConst() 0 5 3
A registerNamespace() 0 26 5
A isAnonymousClass() 0 5 3
A __construct() 0 8 1
A isCorrectDeclaration() 0 6 4
A activeNamespace() 0 15 4
C locateDeclarations() 0 40 12
A registerDeclaration() 0 16 3
A endingToken() 0 22 6
A getDeclarations() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like ReflectionFile often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ReflectionFile, and based on these observations, apply Extract Interface, too.

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