Completed
Push — master ( 9c9777...0675d2 )
by Jaap
02:23
created

TypeResolver::parseTypes()   D

Complexity

Conditions 19
Paths 28

Size

Total Lines 87
Code Lines 55

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 37
CRAP Score 31.6574

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 87
ccs 37
cts 55
cp 0.6727
rs 4.764
cc 19
eloc 55
nc 28
nop 3
crap 31.6574

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
 * This file is part of phpDocumentor.
4
 *
5
 * For the full copyright and license information, please view the LICENSE
6
 * file that was distributed with this source code.
7
 *
8
 * @copyright 2010-2015 Mike van Riel<[email protected]>
9
 * @license   http://www.opensource.org/licenses/mit-license.php MIT
10
 * @link      http://phpdoc.org
11
 */
12
13
namespace phpDocumentor\Reflection;
14
15
use phpDocumentor\Reflection\Types\Array_;
16
use phpDocumentor\Reflection\Types\Compound;
17
use phpDocumentor\Reflection\Types\Context;
18
use phpDocumentor\Reflection\Types\Iterable_;
19
use phpDocumentor\Reflection\Types\Nullable;
20
use phpDocumentor\Reflection\Types\Object_;
21
22
final class TypeResolver
23
{
24
    /** @var string Definition of the ARRAY operator for types */
25
    const OPERATOR_ARRAY = '[]';
26
27
    /** @var string Definition of the NAMESPACE operator in PHP */
28
    const OPERATOR_NAMESPACE = '\\';
29
30
    /** @var integer the iterator parser is inside a compound context */
31
    const PARSER_IN_COMPOUND = 0;
32
33
    /** @var integer the iterator parser is inside a nullable expression context */
34
    const PARSER_IN_NULLABLE = 1;
35
36
    /** @var integer the iterator parser is inside an array expression context */
37
    const PARSER_IN_ARRAY_EXPRESSION = 2;
38
39
    /** @var string[] List of recognized keywords and unto which Value Object they map */
40
    private $keywords = array(
41
        'string' => Types\String_::class,
42
        'int' => Types\Integer::class,
43
        'integer' => Types\Integer::class,
44
        'bool' => Types\Boolean::class,
45
        'boolean' => Types\Boolean::class,
46
        'float' => Types\Float_::class,
47
        'double' => Types\Float_::class,
48
        'object' => Object_::class,
49
        'mixed' => Types\Mixed_::class,
50
        'array' => Array_::class,
51
        'resource' => Types\Resource_::class,
52
        'void' => Types\Void_::class,
53
        'null' => Types\Null_::class,
54
        'scalar' => Types\Scalar::class,
55
        'callback' => Types\Callable_::class,
56
        'callable' => Types\Callable_::class,
57
        'false' => Types\Boolean::class,
58
        'true' => Types\Boolean::class,
59
        'self' => Types\Self_::class,
60
        '$this' => Types\This::class,
61
        'static' => Types\Static_::class,
62
        'parent' => Types\Parent_::class,
63
        'iterable' => Iterable_::class,
64
    );
65
66
    /** @var FqsenResolver */
67
    private $fqsenResolver;
68
69
    /**
70
     * Initializes this TypeResolver with the means to create and resolve Fqsen objects.
71
     *
72
     * @param FqsenResolver $fqsenResolver
73
     */
74 34
    public function __construct(FqsenResolver $fqsenResolver = null)
75
    {
76 34
        $this->fqsenResolver = $fqsenResolver ?: new FqsenResolver();
77 34
    }
78
79
    /**
80
     * Analyzes the given type and returns the FQCN variant.
81
     *
82
     * When a type is provided this method checks whether it is not a keyword or
83
     * Fully Qualified Class Name. If so it will use the given namespace and
84
     * aliases to expand the type to a FQCN representation.
85
     *
86
     * This method only works as expected if the namespace and aliases are set;
87
     * no dynamic reflection is being performed here.
88
     *
89
     * @param string $type     The relative or absolute type.
90
     * @param Context $context
91
     *
92
     * @uses Context::getNamespace()        to determine with what to prefix the type name.
93
     * @uses Context::getNamespaceAliases() to check whether the first part of the relative type name should not be
94
     *     replaced with another namespace.
95
     *
96
     * @return Type
97
     */
98 31
    public function resolve($type, Context $context = null)
99
    {
100 31
        if (!is_string($type)) {
101 1
            throw new \InvalidArgumentException(
102 1
                'Attempted to resolve type but it appeared not to be a string, received: ' . var_export($type, true)
103 1
            );
104
        }
105
106 30
        $type = trim($type);
107 30
        if (!$type) {
108 1
            throw new \InvalidArgumentException('Attempted to resolve "' . $type . '" but it appears to be empty');
109
        }
110
111 29
        if ($context === null) {
112
            $context = new Context('');
113
        }
114
115
        // split the type string into tokens `|`, `?`, `(`, `)[]` and type names
116 29
        $tokens = preg_split('/(\||\?|\(|\)(?:\[\])+)/', $type, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE);
117 29
        $tokenIterator = new \ArrayIterator($tokens);
118
119 29
        return $this->parseTypes($tokenIterator, $context, self::PARSER_IN_COMPOUND);
120
    }
121
122
    /**
123
     * Analyse each tokens and creates types
124
     *
125
     * @param \ArrayIterator $tokens  the iterator on tokens
126
     * @param Context        $context
127
     * @param integer        $parserContext on of self::PARSER_* constants, indicating
128
     *                        the context where we are in the parsing
129
     *
130
     * @return Type
131
     */
132 29
    private function parseTypes(\ArrayIterator $tokens, Context $context, $parserContext)
133
    {
134 29
        $types = array();
135 29
        $token = '';
136 29
        while ($tokens->valid()) {
137 29
            $token = $tokens->current();
138
139 29
            if ($token == '|') {
140 4
                if (count($types) == 0) {
141
                    throw new \RuntimeException(
142
                        'A type is missing before a type separator'
143
                    );
144
                }
145
                if ($parserContext !== self::PARSER_IN_COMPOUND
146 4
                    && $parserContext !== self::PARSER_IN_ARRAY_EXPRESSION) {
147
                    throw new \RuntimeException(
148
                        'Unexpected type separator'
149
                    );
150
                }
151 4
                $tokens->next();
152
0 ignored issues
show
Coding Style introduced by
Blank line found at end of control structure
Loading history...
153 29
            } else if ($token == '?') {
154
                if ($parserContext !== self::PARSER_IN_COMPOUND
155 1
                    && $parserContext !== self::PARSER_IN_ARRAY_EXPRESSION) {
156
                    throw new \RuntimeException(
157
                        'Unexpected nullable character'
158
                    );
159
                }
160
161 1
                $tokens->next();
162 1
                $type = $this->parseTypes($tokens, $context, self::PARSER_IN_NULLABLE);
163 1
                $types[] = new Nullable($type);
164
0 ignored issues
show
Coding Style introduced by
Blank line found at end of control structure
Loading history...
165 29
            } else if ($token === '(') {
166 1
                $tokens->next();
167 1
                $type = $this->parseTypes($tokens, $context, self::PARSER_IN_ARRAY_EXPRESSION);
168
169 1
                $resolvedType = new Array_($type);
170
171
                // we generates arrays corresponding to the number of '[]'
172
                // after the ')'
173 1
                $numberOfArrays = (strlen($tokens->current()) -1) / 2;
174 1
                for ($i = 0; $i < $numberOfArrays - 1; $i++) {
175
                    $resolvedType = new Array_($resolvedType);
176
                }
177 1
                $types[] = $resolvedType;
178 1
                $tokens->next();
179
0 ignored issues
show
Coding Style introduced by
Blank line found at end of control structure
Loading history...
180 1
            } else if ($parserContext === self::PARSER_IN_ARRAY_EXPRESSION
181 29
                       && $token[0] === ')'
182 29
                ) {
183 1
                break;
184
0 ignored issues
show
Coding Style introduced by
Blank line found at end of control structure
Loading history...
185
            } else {
186 29
                $type = $this->resolveSingleType($token, $context);
187 29
                $tokens->next();
188 29
                if ($parserContext === self::PARSER_IN_NULLABLE) {
189 1
                    return $type;
190
                }
191 28
                $types[] = $type;
192
            }
193 29
        }
194
195 29
        if ($token == '|') {
196
            throw new \RuntimeException(
197
                'A type is missing after a type separator'
198
            );
199
        }
200 29
        if (count($types) == 0) {
201
            if ($parserContext == self::PARSER_IN_NULLABLE) {
202
                throw new \RuntimeException(
203
                    'A type is missing after a nullable character'
204
                );
205
            }
206
            if ($parserContext == self::PARSER_IN_ARRAY_EXPRESSION) {
207
                throw new \RuntimeException(
208
                    'A type is missing in an array expression'
209
                );
210
            }
211
            throw new \RuntimeException(
212
                'No types in a compound list'
213
            );
214 29
        } else if (count($types) == 1) {
215 26
            return $types[0];
216
        }
217 4
        return new Compound($types);
218
    }
219
220
    /**
221
     * resolve the given type into a type object
222
     *
223
     * @param string    $type      the type string, representing a single type
224
     * @param Context   $context
225
     * @return Type|Array_|Object_
226
     */
227 29
    private function resolveSingleType($type, Context $context)
228
    {
229 29
        switch (true) {
230 29
            case $this->isKeyword($type):
231 23
                return $this->resolveKeyword($type);
232 8
            case $this->isTypedArray($type):
233 2
                return $this->resolveTypedArray($type, $context);
234 7
            case $this->isFqsen($type):
235 4
                return $this->resolveTypedObject($type);
236 5
            case $this->isPartialStructuralElementName($type):
237 5
                return $this->resolveTypedObject($type, $context);
238
            // @codeCoverageIgnoreStart
239
            default:
240
                // I haven't got the foggiest how the logic would come here but added this as a defense.
241
                throw new \RuntimeException(
242
                    'Unable to resolve type "' . $type . '", there is no known method to resolve it'
243
                );
244
        }
245
        // @codeCoverageIgnoreEnd
246
    }
247
248
    /**
249
     * Adds a keyword to the list of Keywords and associates it with a specific Value Object.
250
     *
251
     * @param string $keyword
252
     * @param string $typeClassName
253
     *
254
     * @return void
255
     */
256 3
    public function addKeyword($keyword, $typeClassName)
257
    {
258 3
        if (!class_exists($typeClassName)) {
259 1
            throw new \InvalidArgumentException(
260 1
                'The Value Object that needs to be created with a keyword "' . $keyword . '" must be an existing class'
261 1
                . ' but we could not find the class ' . $typeClassName
262 1
            );
263
        }
264
265 2
        if (!in_array(Type::class, class_implements($typeClassName))) {
266 1
            throw new \InvalidArgumentException(
267 1
                'The class "' . $typeClassName . '" must implement the interface "phpDocumentor\Reflection\Type"'
268 1
            );
269
        }
270
271 1
        $this->keywords[$keyword] = $typeClassName;
272 1
    }
273
274
    /**
275
     * Detects whether the given type represents an array.
276
     *
277
     * @param string $type A relative or absolute type as defined in the phpDocumentor documentation.
278
     *
279
     * @return bool
280
     */
281 8
    private function isTypedArray($type)
282
    {
283 8
        return substr($type, -2) === self::OPERATOR_ARRAY;
284
    }
285
286
    /**
287
     * Detects whether the given type represents a PHPDoc keyword.
288
     *
289
     * @param string $type A relative or absolute type as defined in the phpDocumentor documentation.
290
     *
291
     * @return bool
292
     */
293 29
    private function isKeyword($type)
294
    {
295 29
        return in_array(strtolower($type), array_keys($this->keywords), true);
296
    }
297
298
    /**
299
     * Detects whether the given type represents a relative structural element name.
300
     *
301
     * @param string $type A relative or absolute type as defined in the phpDocumentor documentation.
302
     *
303
     * @return bool
304
     */
305 5
    private function isPartialStructuralElementName($type)
306
    {
307 5
        return ($type[0] !== self::OPERATOR_NAMESPACE) && !$this->isKeyword($type);
308
    }
309
310
    /**
311
     * Tests whether the given type is a Fully Qualified Structural Element Name.
312
     *
313
     * @param string $type
314
     *
315
     * @return bool
316
     */
317 7
    private function isFqsen($type)
318
    {
319 7
        return strpos($type, self::OPERATOR_NAMESPACE) === 0;
320
    }
321
322
    /**
323
     * Resolves the given typed array string (i.e. `string[]`) into an Array object with the right types set.
324
     *
325
     * @param string $type
326
     * @param Context $context
327
     *
328
     * @return Array_
329
     */
330 2
    private function resolveTypedArray($type, Context $context)
331
    {
332 2
        return new Array_($this->resolveSingleType(substr($type, 0, -2), $context));
333
    }
334
335
    /**
336
     * Resolves the given keyword (such as `string`) into a Type object representing that keyword.
337
     *
338
     * @param string $type
339
     *
340
     * @return Type
341
     */
342 23
    private function resolveKeyword($type)
343
    {
344 23
        $className = $this->keywords[strtolower($type)];
345
346 23
        return new $className();
347
    }
348
349
    /**
350
     * Resolves the given FQSEN string into an FQSEN object.
351
     *
352
     * @param string $type
353
     * @param Context|null $context
354
     *
355
     * @return Object_
356
     */
357 7
    private function resolveTypedObject($type, Context $context = null)
358
    {
359 7
        return new Object_($this->fqsenResolver->resolve($type, $context));
360
    }
361
}
362