ContextFactory::extractUseStatements()   F
last analyzed

Complexity

Conditions 28
Paths 370

Size

Total Lines 107

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 39
CRAP Score 30.7641

Importance

Changes 0
Metric Value
dl 0
loc 107
ccs 39
cts 46
cp 0.8478
rs 0.9666
c 0
b 0
f 0
cc 28
nc 370
nop 1
crap 30.7641

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
/**
6
 * This file is part of phpDocumentor.
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 *
11
 * @link      http://phpdoc.org
12
 */
13
14
namespace phpDocumentor\Reflection\Types;
15
16
use ArrayIterator;
17
use InvalidArgumentException;
18
use ReflectionClass;
19
use ReflectionClassConstant;
20
use ReflectionMethod;
21
use ReflectionParameter;
22
use ReflectionProperty;
23
use Reflector;
24
use RuntimeException;
25
use UnexpectedValueException;
26
use function file_exists;
27
use function file_get_contents;
28
use function get_class;
29
use function in_array;
30
use function is_string;
31
use function token_get_all;
32
use function trim;
33
use const T_AS;
34
use const T_CLASS;
35
use const T_CURLY_OPEN;
36
use const T_DOLLAR_OPEN_CURLY_BRACES;
37
use const T_NAMESPACE;
38
use const T_NS_SEPARATOR;
39
use const T_STRING;
40
use const T_USE;
41
42
/**
43
 * Convenience class to create a Context for DocBlocks when not using the Reflection Component of phpDocumentor.
44
 *
45
 * For a DocBlock to be able to resolve types that use partial namespace names or rely on namespace imports we need to
46
 * provide a bit of context so that the DocBlock can read that and based on it decide how to resolve the types to
47
 * Fully Qualified names.
48
 *
49
 * @see Context for more information.
50
 */
51
final class ContextFactory
52
{
53
    /** The literal used at the end of a use statement. */
54
    private const T_LITERAL_END_OF_USE = ';';
55
56
    /** The literal used between sets of use statements */
57
    private const T_LITERAL_USE_SEPARATOR = ',';
58
59
    /**
60
     * Build a Context given a Class Reflection.
61
     *
62
     * @see Context for more information on Contexts.
63
     */
64 4
    public function createFromReflector(Reflector $reflector) : Context
65
    {
66 4
        if ($reflector instanceof ReflectionClass) {
67 4
            //phpcs:ignore SlevomatCodingStandard.Commenting.InlineDocCommentDeclaration.MissingVariable
68
            /** @var ReflectionClass<object> $reflector */
69
0 ignored issues
show
Documentation introduced by
The doc-type ReflectionClass<object> could not be parsed: Expected "|" or "end of type", but got "<" at position 15. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
70
            return $this->createFromReflectionClass($reflector);
71
        }
72
73
        if ($reflector instanceof ReflectionParameter) {
74
            return $this->createFromReflectionParameter($reflector);
75
        }
76
77
        if ($reflector instanceof ReflectionMethod) {
78
            return $this->createFromReflectionMethod($reflector);
79
        }
80
81
        if ($reflector instanceof ReflectionProperty) {
82
            return $this->createFromReflectionProperty($reflector);
83
        }
84
85
        if ($reflector instanceof ReflectionClassConstant) {
0 ignored issues
show
Bug introduced by
The class ReflectionClassConstant does not exist. Is this class maybe located in a folder that is not analyzed, or in a newer version of your dependencies than listed in your composer.lock/composer.json?
Loading history...
86
            return $this->createFromReflectionClassConstant($reflector);
87
        }
88
89
        throw new UnexpectedValueException('Unhandled \Reflector instance given:  ' . get_class($reflector));
90
    }
91
92
    private function createFromReflectionParameter(ReflectionParameter $parameter) : Context
93
    {
94
        $class = $parameter->getDeclaringClass();
95
        if (!$class) {
96
            throw new InvalidArgumentException('Unable to get class of ' . $parameter->getName());
97
        }
98
99
        //phpcs:ignore SlevomatCodingStandard.Commenting.InlineDocCommentDeclaration.MissingVariable
100
        /** @var ReflectionClass<object> $class */
101
0 ignored issues
show
Documentation introduced by
The doc-type ReflectionClass<object> could not be parsed: Expected "|" or "end of type", but got "<" at position 15. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
102
        return $this->createFromReflectionClass($class);
103
    }
104
105
    private function createFromReflectionMethod(ReflectionMethod $method) : Context
106
    {
107
        //phpcs:ignore SlevomatCodingStandard.Commenting.InlineDocCommentDeclaration.MissingVariable
108
        /** @var ReflectionClass<object> $class */
0 ignored issues
show
Documentation introduced by
The doc-type ReflectionClass<object> could not be parsed: Expected "|" or "end of type", but got "<" at position 15. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
109
        $class = $method->getDeclaringClass();
110
111
        return $this->createFromReflectionClass($class);
112
    }
113
114 4
    private function createFromReflectionProperty(ReflectionProperty $property) : Context
115
    {
116 4
        //phpcs:ignore SlevomatCodingStandard.Commenting.InlineDocCommentDeclaration.MissingVariable
117 4
        /** @var ReflectionClass<object> $class */
0 ignored issues
show
Documentation introduced by
The doc-type ReflectionClass<object> could not be parsed: Expected "|" or "end of type", but got "<" at position 15. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
118
        $class = $property->getDeclaringClass();
119 4
120 2
        return $this->createFromReflectionClass($class);
121 2
    }
122
123
    private function createFromReflectionClassConstant(ReflectionClassConstant $constant) : Context
124
    {
125 2
        //phpcs:ignore SlevomatCodingStandard.Commenting.InlineDocCommentDeclaration.MissingVariable
126
        /** @var ReflectionClass<object> $class */
0 ignored issues
show
Documentation introduced by
The doc-type ReflectionClass<object> could not be parsed: Expected "|" or "end of type", but got "<" at position 15. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
127
        $class = $constant->getDeclaringClass();
128 2
129
        return $this->createFromReflectionClass($class);
130
    }
131
132
    /**
133
     * @param ReflectionClass<object> $class
0 ignored issues
show
Documentation introduced by
The doc-type ReflectionClass<object> could not be parsed: Expected "|" or "end of type", but got "<" at position 15. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
134
     */
135
    private function createFromReflectionClass(ReflectionClass $class) : Context
136
    {
137
        $fileName  = $class->getFileName();
138
        $namespace = $class->getNamespaceName();
139
140 6
        if (is_string($fileName) && file_exists($fileName)) {
141
            $contents = file_get_contents($fileName);
142 6
            if ($contents === false) {
143 6
                throw new RuntimeException('Unable to read file "' . $fileName . '"');
144 6
            }
145 6
146
            return $this->createForNamespace($namespace, $contents);
147 6
        }
148 6
149 6
        return new Context($namespace, []);
150 6
    }
151 6
152 6
    /**
153
     * Build a Context for a namespace in the provided file contents.
154
     *
155
     * @see Context for more information on Contexts.
156 6
     *
157 6
     * @param string $namespace    It does not matter if a `\` precedes the namespace name,
158 6
     * this method first normalizes.
159 6
     * @param string $fileContents The file's contents to retrieve the aliases from with the given namespace.
160 6
     */
161 6
    public function createForNamespace(string $namespace, string $fileContents) : Context
162 6
    {
163 6
        $namespace        = trim($namespace, '\\');
164
        $useStatements    = [];
165
        $currentNamespace = '';
166 6
        $tokens           = new ArrayIterator(token_get_all($fileContents));
167
168
        while ($tokens->valid()) {
169 6
            $currentToken = $tokens->current();
170 6
            switch ($currentToken[0]) {
171
                case T_NAMESPACE:
172
                    $currentNamespace = $this->parseNamespace($tokens);
173 6
                    break;
174
                case T_CLASS:
175 6
                    // Fast-forward the iterator through the class so that any
176 6
                    // T_USE tokens found within are skipped - these are not
177 4
                    // valid namespace use statements so should be ignored.
178 4
                    $braceLevel      = 0;
179
                    $firstBraceFound = false;
180 4
                    while ($tokens->valid() && ($braceLevel > 0 || !$firstBraceFound)) {
181
                        $currentToken = $tokens->current();
182
                        if ($currentToken === '{'
183 6
                            || in_array($currentToken[0], [T_CURLY_OPEN, T_DOLLAR_OPEN_CURLY_BRACES], true)) {
184
                            if (!$firstBraceFound) {
185
                                $firstBraceFound = true;
186 6
                            }
187
188
                            ++$braceLevel;
189
                        }
190
191
                        if ($currentToken === '}') {
192 6
                            --$braceLevel;
193
                        }
194
195 6
                        $tokens->next();
196
                    }
197 6
198 6
                    break;
199
                case T_USE:
200 6
                    if ($currentNamespace === $namespace) {
201 6
                        $useStatements += $this->parseUseStatement($tokens);
202
                    }
203
204 6
                    break;
205
            }
206
207
            $tokens->next();
208
        }
209
210
        return new Context($namespace, $useStatements);
211
    }
212 4
213
    /**
214 4
     * Deduce the name from tokens when we are at the T_NAMESPACE token.
215
     *
216 4
     * @param ArrayIterator<int, string|array{0:int,1:string,2:int}> $tokens
217 4
     */
218
    private function parseNamespace(ArrayIterator $tokens) : string
219 4
    {
220 4
        // skip to the first string or namespace separator
221 4
        $this->skipToNextStringOrNamespaceSeparator($tokens);
222
223
        $name = '';
224
        while ($tokens->valid() && in_array($tokens->current()[0], [T_STRING, T_NS_SEPARATOR], true)) {
225
            $name .= $tokens->current()[1];
226
            $tokens->next();
227
        }
228
229
        return $name;
230
    }
231 6
232
    /**
233 6
     * Deduce the names of all imports when we are at the T_USE token.
234 6
     *
235
     * @param ArrayIterator<int, string|array{0:int,1:string,2:int}> $tokens
236 6
     *
237
     * @return string[]
238
     *
239
     * @psalm-return array<string, string>
240
     */
241
    private function parseUseStatement(ArrayIterator $tokens) : array
242
    {
243
        $uses = [];
244
245
        while ($tokens->valid()) {
246 4
            $this->skipToNextStringOrNamespaceSeparator($tokens);
247
248 4
            $uses += $this->extractUseStatements($tokens);
249 4
            $currentToken = $tokens->current();
250 4
            if ($currentToken[0] === self::T_LITERAL_END_OF_USE) {
251 4
                return $uses;
252 4
            }
253
        }
254 4
255 4
        return $uses;
256 4
    }
257 4
258
    /**
259 4
     * Fast-forwards the iterator as longs as we don't encounter a T_STRING or T_NS_SEPARATOR token.
260
     *
261 4
     * @param ArrayIterator<int, string|array{0:int,1:string,2:int}> $tokens
262 4
     */
263 4
    private function skipToNextStringOrNamespaceSeparator(ArrayIterator $tokens) : void
264 4
    {
265 4
        while ($tokens->valid()) {
266 4
            $currentToken = $tokens->current();
267 4
            if (in_array($currentToken[0], [T_STRING, T_NS_SEPARATOR], true)) {
268 4
                break;
269 4
            }
270 4
271 4
            $tokens->next();
272 4
        }
273 4
    }
274 4
275 4
    /**
276 4
     * Deduce the namespace name and alias of an import when we are at the T_USE token or have not reached the end of
277 4
     * a USE statement yet. This will return a key/value array of the alias => namespace.
278
     *
279 4
     * @param ArrayIterator<int, string|array{0:int,1:string,2:int}> $tokens
280
     *
281 4
     * @return string[]
282 4
     *
283
     * @psalm-suppress TypeDoesNotContainType
284 4
     *
285 4
     * @psalm-return array<string, string>
286 4
     */
287 4
    private function extractUseStatements(ArrayIterator $tokens) : array
288 4
    {
289 4
        $extractedUseStatements = [];
290 4
        $groupedNs              = '';
291
        $currentNs              = '';
292 4
        $currentAlias           = '';
293
        $state                  = 'start';
294 4
295 4
        while ($tokens->valid()) {
296
            $currentToken = $tokens->current();
297 4
            $tokenId      = is_string($currentToken) ? $currentToken : $currentToken[0];
298 4
            $tokenValue   = is_string($currentToken) ? null : $currentToken[1];
299 4
            switch ($state) {
300 4
                case 'start':
301 4
                    switch ($tokenId) {
302 4
                        case T_STRING:
303 4
                        case T_NS_SEPARATOR:
304 4
                            $currentNs   .= (string) $tokenValue;
305 4
                            $currentAlias =  $tokenValue;
306 4
                            break;
307 4
                        case T_CURLY_OPEN:
308 4
                        case '{':
309 4
                            $state     = 'grouped';
310 4
                            $groupedNs = $currentNs;
311 4
                            break;
312
                        case T_AS:
313
                            $state = 'start-alias';
314
                            break;
315 4
                        case self::T_LITERAL_USE_SEPARATOR:
316
                        case self::T_LITERAL_END_OF_USE:
317 4
                            $state = 'end';
318 4
                            break;
319
                        default:
320 4
                            break;
321 4
                    }
322 4
323 4
                    break;
324
                case 'start-alias':
325
                    switch ($tokenId) {
326
                        case T_STRING:
327
                            $currentAlias = $tokenValue;
328
                            break;
329 4
                        case self::T_LITERAL_USE_SEPARATOR:
330 4
                        case self::T_LITERAL_END_OF_USE:
331 4
                            $state = 'end';
332
                            break;
333 4
                        default:
334
                            break;
335
                    }
336
337 4
                    break;
338 4
                case 'grouped':
339
                    switch ($tokenId) {
340
                        case T_STRING:
341 4
                        case T_NS_SEPARATOR:
342
                            $currentNs   .= (string) $tokenValue;
343
                            $currentAlias = $tokenValue;
344 4
                            break;
345 4
                        case T_AS:
346
                            $state = 'grouped-alias';
347
                            break;
348 4
                        case self::T_LITERAL_USE_SEPARATOR:
349
                            $state                                          = 'grouped';
350
                            $extractedUseStatements[(string) $currentAlias] = $currentNs;
351
                            $currentNs                                      = $groupedNs;
352
                            $currentAlias                                   = '';
353
                            break;
354
                        case self::T_LITERAL_END_OF_USE:
355
                            $state = 'end';
356
                            break;
357
                        default:
358
                            break;
359
                    }
360
361
                    break;
362
                case 'grouped-alias':
363
                    switch ($tokenId) {
364
                        case T_STRING:
365
                            $currentAlias = $tokenValue;
366
                            break;
367
                        case self::T_LITERAL_USE_SEPARATOR:
368
                            $state                                          = 'grouped';
369
                            $extractedUseStatements[(string) $currentAlias] = $currentNs;
370
                            $currentNs                                      = $groupedNs;
371
                            $currentAlias                                   = '';
372
                            break;
373
                        case self::T_LITERAL_END_OF_USE:
374
                            $state = 'end';
375
                            break;
376
                        default:
377
                            break;
378
                    }
379
            }
380
381
            if ($state === 'end') {
382
                break;
383
            }
384
385
            $tokens->next();
386
        }
387
388
        if ($groupedNs !== $currentNs) {
389
            $extractedUseStatements[(string) $currentAlias] = $currentNs;
390
        }
391
392
        return $extractedUseStatements;
393
    }
394
}
395