Completed
Push — master ( dbdf9d...5ad886 )
by Jaap
02:58 queued 10s
created

TypeResolver::resolveCollection()   D

Complexity

Conditions 20
Paths 27

Size

Total Lines 78

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 31
CRAP Score 23.451

Importance

Changes 0
Metric Value
dl 0
loc 78
ccs 31
cts 39
cp 0.7949
rs 4.1666
c 0
b 0
f 0
cc 20
nc 27
nop 3
crap 23.451

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;
15
16
use ArrayIterator;
17
use InvalidArgumentException;
18
use phpDocumentor\Reflection\Types\Array_;
19
use phpDocumentor\Reflection\Types\Collection;
20
use phpDocumentor\Reflection\Types\Compound;
21
use phpDocumentor\Reflection\Types\Context;
22
use phpDocumentor\Reflection\Types\Integer;
23
use phpDocumentor\Reflection\Types\Iterable_;
24
use phpDocumentor\Reflection\Types\Nullable;
25
use phpDocumentor\Reflection\Types\Object_;
26
use phpDocumentor\Reflection\Types\String_;
27
use RuntimeException;
28
use const PREG_SPLIT_DELIM_CAPTURE;
29
use const PREG_SPLIT_NO_EMPTY;
30
use function array_keys;
31
use function array_pop;
32
use function class_exists;
33
use function class_implements;
34
use function count;
35
use function in_array;
36
use function preg_split;
37
use function strlen;
38
use function strpos;
39
use function strtolower;
40
use function substr;
41
use function trim;
42
43
final class TypeResolver
44
{
45
    /** @var string Definition of the ARRAY operator for types */
46
    private const OPERATOR_ARRAY = '[]';
47
48
    /** @var string Definition of the NAMESPACE operator in PHP */
49
    private const OPERATOR_NAMESPACE = '\\';
50
51
    /** @var int the iterator parser is inside a compound context */
52
    private const PARSER_IN_COMPOUND = 0;
53
54
    /** @var int the iterator parser is inside a nullable expression context */
55
    private const PARSER_IN_NULLABLE = 1;
56
57
    /** @var int the iterator parser is inside an array expression context */
58
    private const PARSER_IN_ARRAY_EXPRESSION = 2;
59
60
    /** @var int the iterator parser is inside a collection expression context */
61
    private const PARSER_IN_COLLECTION_EXPRESSION = 3;
62
63
    /**
64
     * @var array<string, string> List of recognized keywords and unto which Value Object they map
65
     * @psalm-var array<string, class-string<Type>>
66
     */
67
    private $keywords = [
68
        'string' => Types\String_::class,
69
        'int' => Types\Integer::class,
70
        'integer' => Types\Integer::class,
71
        'bool' => Types\Boolean::class,
72
        'boolean' => Types\Boolean::class,
73
        'real' => Types\Float_::class,
74
        'float' => Types\Float_::class,
75
        'double' => Types\Float_::class,
76
        'object' => Object_::class,
77
        'mixed' => Types\Mixed_::class,
78
        'array' => Array_::class,
79
        'resource' => Types\Resource_::class,
80
        'void' => Types\Void_::class,
81
        'null' => Types\Null_::class,
82
        'scalar' => Types\Scalar::class,
83
        'callback' => Types\Callable_::class,
84
        'callable' => Types\Callable_::class,
85
        'false' => Types\Boolean::class,
86
        'true' => Types\Boolean::class,
87
        'self' => Types\Self_::class,
88
        '$this' => Types\This::class,
89
        'static' => Types\Static_::class,
90
        'parent' => Types\Parent_::class,
91
        'iterable' => Iterable_::class,
92
    ];
93
94
    /** @var FqsenResolver */
95
    private $fqsenResolver;
96
97
    /**
98
     * Initializes this TypeResolver with the means to create and resolve Fqsen objects.
99
     */
100 54
    public function __construct(?FqsenResolver $fqsenResolver = null)
101
    {
102 54
        $this->fqsenResolver = $fqsenResolver ?: new FqsenResolver();
103 54
    }
104
105
    /**
106
     * Analyzes the given type and returns the FQCN variant.
107
     *
108
     * When a type is provided this method checks whether it is not a keyword or
109
     * Fully Qualified Class Name. If so it will use the given namespace and
110
     * aliases to expand the type to a FQCN representation.
111
     *
112
     * This method only works as expected if the namespace and aliases are set;
113
     * no dynamic reflection is being performed here.
114
     *
115
     * @uses Context::getNamespaceAliases() to check whether the first part of the relative type name should not be
116
     * replaced with another namespace.
117
     * @uses Context::getNamespace()        to determine with what to prefix the type name.
118
     *
119
     * @param string $type The relative or absolute type.
120
     */
121 51
    public function resolve(string $type, ?Context $context = null) : Type
122
    {
123 51
        $type = trim($type);
124 51
        if (!$type) {
125 1
            throw new InvalidArgumentException('Attempted to resolve "' . $type . '" but it appears to be empty');
126
        }
127
128 50
        if ($context === null) {
129 1
            $context = new Context('');
130
        }
131
132
        // split the type string into tokens `|`, `?`, `<`, `>`, `,`, `(`, `)[]`, '<', '>' and type names
133 50
        $tokens = preg_split(
134 50
            '/(\\||\\?|<|>|, ?|\\(|\\)(?:\\[\\])+)/',
135 50
            $type,
136 50
            -1,
137 50
            PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE
138
        );
139
140 50
        if ($tokens === false) {
141
            throw new InvalidArgumentException('Unable to split the type string "' . $type . '" into tokens');
142
        }
143
144 50
        $tokenIterator = new ArrayIterator($tokens);
145
146 50
        return $this->parseTypes($tokenIterator, $context, self::PARSER_IN_COMPOUND);
147
    }
148
149
    /**
150
     * Analyse each tokens and creates types
151
     *
152
     * @param ArrayIterator $tokens        the iterator on tokens
153
     * @param int           $parserContext on of self::PARSER_* constants, indicating
154
     * the context where we are in the parsing
155
     */
156 50
    private function parseTypes(ArrayIterator $tokens, Context $context, int $parserContext) : Type
157
    {
158 50
        $types = [];
159 50
        $token = '';
160 50
        while ($tokens->valid()) {
161 50
            $token = $tokens->current();
162
163 50
            if ($token === '|') {
164 14
                if (count($types) === 0) {
165
                    throw new RuntimeException(
166
                        'A type is missing before a type separator'
167
                    );
168
                }
169
170 14
                if ($parserContext !== self::PARSER_IN_COMPOUND
171 14
                    && $parserContext !== self::PARSER_IN_ARRAY_EXPRESSION
172 14
                    && $parserContext !== self::PARSER_IN_COLLECTION_EXPRESSION
173
                ) {
174
                    throw new RuntimeException(
175
                        'Unexpected type separator'
176
                    );
177
                }
178
179 14
                $tokens->next();
180 50
            } elseif ($token === '?') {
181 2
                if ($parserContext !== self::PARSER_IN_COMPOUND
182 2
                    && $parserContext !== self::PARSER_IN_ARRAY_EXPRESSION
183 2
                    && $parserContext !== self::PARSER_IN_COLLECTION_EXPRESSION
184
                ) {
185
                    throw new RuntimeException(
186
                        'Unexpected nullable character'
187
                    );
188
                }
189
190 2
                $tokens->next();
191 2
                $type    = $this->parseTypes($tokens, $context, self::PARSER_IN_NULLABLE);
192 2
                $types[] = new Nullable($type);
193 50
            } elseif ($token === '(') {
194 5
                $tokens->next();
195 5
                $type = $this->parseTypes($tokens, $context, self::PARSER_IN_ARRAY_EXPRESSION);
196
197 5
                $resolvedType = new Array_($type);
198
199 5
                $token = $tokens->current();
200
                // Someone did not properly close their array expression ..
201 5
                if ($token === null) {
202 1
                    break;
203
                }
204
205
                // we generate arrays corresponding to the number of '[]' after the ')'
206 4
                $numberOfArrays = (strlen($token) - 1) / 2;
207 4
                for ($i = 0; $i < $numberOfArrays - 1; ++$i) {
208 1
                    $resolvedType = new Array_($resolvedType);
209
                }
210
211 4
                $types[] = $resolvedType;
212 4
                $tokens->next();
213 50
            } elseif ($parserContext === self::PARSER_IN_ARRAY_EXPRESSION && $token[0] === ')') {
214 4
                break;
215 50
            } elseif ($token === '<') {
216 13
                if (count($types) === 0) {
217 1
                    throw new RuntimeException(
218 1
                        'Unexpected collection operator "<", class name is missing'
219
                    );
220
                }
221
222 12
                $classType = array_pop($types);
223 12
                if ($classType !== null) {
224 12
                    $types[] = $this->resolveCollection($tokens, $classType, $context);
225
                }
226
227 8
                $tokens->next();
228 49
            } elseif ($parserContext === self::PARSER_IN_COLLECTION_EXPRESSION
229 49
                && ($token === '>' || trim($token) === ',')
230
            ) {
231 10
                break;
232
            } else {
233 49
                $type = $this->resolveSingleType($token, $context);
234 49
                $tokens->next();
235 49
                if ($parserContext === self::PARSER_IN_NULLABLE) {
236 2
                    return $type;
237
                }
238
239 48
                $types[] = $type;
240
            }
241
        }
242
243 48
        if ($token === '|') {
244
            throw new RuntimeException(
245
                'A type is missing after a type separator'
246
            );
247
        }
248
249 48
        if (count($types) === 0) {
250 1
            if ($parserContext === self::PARSER_IN_NULLABLE) {
251
                throw new RuntimeException(
252
                    'A type is missing after a nullable character'
253
                );
254
            }
255
256 1
            if ($parserContext === self::PARSER_IN_ARRAY_EXPRESSION) {
257
                throw new RuntimeException(
258
                    'A type is missing in an array expression'
259
                );
260
            }
261
262 1
            if ($parserContext === self::PARSER_IN_COLLECTION_EXPRESSION) {
263
                throw new RuntimeException(
264 1
                    'A type is missing in a collection expression'
265
                );
266
            }
267 48
        } elseif (count($types) === 1) {
268 41
            return $types[0];
269
        }
270
271 14
        return new Compound($types);
272
    }
273
274
    /**
275
     * resolve the given type into a type object
276
     *
277
     * @param string $type the type string, representing a single type
278
     *
279
     * @return Type|Array_|Object_
280
     */
281 49
    private function resolveSingleType(string $type, Context $context)
282
    {
283
        switch (true) {
284 49
            case $this->isKeyword($type):
285 43
                return $this->resolveKeyword($type);
286 20
            case $this->isTypedArray($type):
287 5
                return $this->resolveTypedArray($type, $context);
288 17
            case $this->isFqsen($type):
289 9
                return $this->resolveTypedObject($type);
290 10
            case $this->isPartialStructuralElementName($type):
291 10
                return $this->resolveTypedObject($type, $context);
292
            // @codeCoverageIgnoreStart
293
            default:
294
                // I haven't got the foggiest how the logic would come here but added this as a defense.
295
                throw new RuntimeException(
296
                    'Unable to resolve type "' . $type . '", there is no known method to resolve it'
297
                );
298
        }
299
300
        // @codeCoverageIgnoreEnd
301
    }
302
303
    /**
304
     * Adds a keyword to the list of Keywords and associates it with a specific Value Object.
305
     */
306 3
    public function addKeyword(string $keyword, string $typeClassName) : void
307
    {
308 3
        if (!class_exists($typeClassName)) {
309 1
            throw new InvalidArgumentException(
310 1
                'The Value Object that needs to be created with a keyword "' . $keyword . '" must be an existing class'
311 1
                . ' but we could not find the class ' . $typeClassName
312
            );
313
        }
314
315 2
        if (!in_array(Type::class, class_implements($typeClassName), true)) {
316 1
            throw new InvalidArgumentException(
317 1
                'The class "' . $typeClassName . '" must implement the interface "phpDocumentor\Reflection\Type"'
318
            );
319
        }
320
321 1
        $this->keywords[$keyword] = $typeClassName;
322 1
    }
323
324
    /**
325
     * Detects whether the given type represents an array.
326
     *
327
     * @param string $type A relative or absolute type as defined in the phpDocumentor documentation.
328
     */
329 20
    private function isTypedArray(string $type) : bool
330
    {
331 20
        return substr($type, -2) === self::OPERATOR_ARRAY;
332
    }
333
334
    /**
335
     * Detects whether the given type represents a PHPDoc keyword.
336
     *
337
     * @param string $type A relative or absolute type as defined in the phpDocumentor documentation.
338
     */
339 49
    private function isKeyword(string $type) : bool
340
    {
341 49
        return in_array(strtolower($type), array_keys($this->keywords), true);
342
    }
343
344
    /**
345
     * Detects whether the given type represents a relative structural element name.
346
     *
347
     * @param string $type A relative or absolute type as defined in the phpDocumentor documentation.
348
     */
349 10
    private function isPartialStructuralElementName(string $type) : bool
350
    {
351 10
        return ($type[0] !== self::OPERATOR_NAMESPACE) && !$this->isKeyword($type);
352
    }
353
354
    /**
355
     * Tests whether the given type is a Fully Qualified Structural Element Name.
356
     */
357 17
    private function isFqsen(string $type) : bool
358
    {
359 17
        return strpos($type, self::OPERATOR_NAMESPACE) === 0;
360
    }
361
362
    /**
363
     * Resolves the given typed array string (i.e. `string[]`) into an Array object with the right types set.
364
     */
365 5
    private function resolveTypedArray(string $type, Context $context) : Array_
366
    {
367 5
        return new Array_($this->resolveSingleType(substr($type, 0, -2), $context));
368
    }
369
370
    /**
371
     * Resolves the given keyword (such as `string`) into a Type object representing that keyword.
372
     */
373 43
    private function resolveKeyword(string $type) : Type
374
    {
375 43
        $className = $this->keywords[strtolower($type)];
376 43
        return new $className();
377
    }
378
379
    /**
380
     * Resolves the given FQSEN string into an FQSEN object.
381
     */
382 17
    private function resolveTypedObject(string $type, ?Context $context = null) : Object_
383
    {
384 17
        return new Object_($this->fqsenResolver->resolve($type, $context));
385
    }
386
387
    /**
388
     * Resolves the collection values and keys
389
     *
390
     * @return Array_|Iterable_|Collection
391
     */
392 12
    private function resolveCollection(ArrayIterator $tokens, Type $classType, Context $context) : Type
393
    {
394 12
        $isArray = ((string) $classType === 'array');
395 12
        $isIterable = ((string) $classType === 'iterable');
396
397
        // allow only "array", "iterable" or class name before "<"
398 12
        if (!$isArray && !$isIterable
399 12
            && (!$classType instanceof Object_ || $classType->getFqsen() === null)) {
400 1
            throw new RuntimeException(
401 1
                $classType . ' is not a collection'
402
            );
403
        }
404
405 11
        $tokens->next();
406
407 11
        $valueType = $this->parseTypes($tokens, $context, self::PARSER_IN_COLLECTION_EXPRESSION);
408 11
        $keyType   = null;
409
410 11
        if ($tokens->current() !== null && trim($tokens->current()) === ',') {
411
            // if we have a comma, then we just parsed the key type, not the value type
412 7
            $keyType = $valueType;
413 7
            if ($isArray) {
414
                // check the key type for an "array" collection. We allow only
415
                // strings or integers.
416 5
                if (!$keyType instanceof String_ &&
417 5
                    !$keyType instanceof Integer &&
418 5
                    !$keyType instanceof Compound
419
                ) {
420 1
                    throw new RuntimeException(
421 1
                        'An array can have only integers or strings as keys'
422
                    );
423
                }
424
425 4
                if ($keyType instanceof Compound) {
426
                    foreach ($keyType->getIterator() as $item) {
427
                        if (!$item instanceof String_ &&
428
                            !$item instanceof Integer
429
                        ) {
430
                            throw new RuntimeException(
431
                                'An array can have only integers or strings as keys'
432
                            );
433
                        }
434
                    }
435
                }
436
            }
437
438 6
            $tokens->next();
439
            // now let's parse the value type
440 6
            $valueType = $this->parseTypes($tokens, $context, self::PARSER_IN_COLLECTION_EXPRESSION);
441
        }
442
443 9
        if ($tokens->current() !== '>') {
444 1
            if (empty($tokens->current())) {
445 1
                throw new RuntimeException(
446 1
                    'Collection: ">" is missing'
447
                );
448
            }
449
450
            throw new RuntimeException(
451
                'Unexpected character "' . $tokens->current() . '", ">" is missing'
452
            );
453
        }
454
455 8
        if ($isArray) {
456 4
            return new Array_($valueType, $keyType);
457
        }
458
459 4
        if ($isIterable) {
460 1
            return new Iterable_($valueType, $keyType);
461
        }
462
463
        /** @psalm-suppress RedundantCondition */
464 3
        if ($classType instanceof Object_) {
465 3
            return $this->makeCollectionFromObject($classType, $valueType, $keyType);
466
        }
467
468
        throw new RuntimeException('Invalid $classType provided');
469
    }
470
471 3
    private function makeCollectionFromObject(Object_ $object, Type $valueType, ?Type $keyType = null) : Collection
472
    {
473 3
        return new Collection($object->getFqsen(), $valueType, $keyType);
474
    }
475
}
476