TypeTokenFactory   B
last analyzed

Complexity

Total Complexity 51

Size/Duplication

Total Lines 304
Duplicated Lines 0 %

Test Coverage

Coverage 99.12%

Importance

Changes 2
Bugs 0 Features 0
Metric Value
wmc 51
eloc 108
c 2
b 0
f 0
dl 0
loc 304
ccs 112
cts 113
cp 0.9912
rs 7.92

8 Methods

Rating   Name   Duplication   Size   Complexity  
A getTypeFromTag() 0 22 3
A checkGenericArray() 0 9 2
A unwrapArray() 0 20 4
A stripSlashes() 0 7 2
A __construct() 0 4 1
C checkDocBlocks() 0 44 14
C create() 0 57 15
B getTypeFromDoc() 0 42 10

How to fix   Complexity   

Complex Class

Complex classes like TypeTokenFactory 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 TypeTokenFactory, and based on these observations, apply Extract Interface, too.

1
<?php
2
/*
3
 * Copyright (c) Nate Brunette.
4
 * Distributed under the MIT License (http://opensource.org/licenses/MIT)
5
 */
6
7
declare(strict_types=1);
8
9
namespace Tebru\Gson\Internal;
10
11
use InvalidArgumentException;
12
use phpDocumentor\Reflection\DocBlock\Tag;
13
use phpDocumentor\Reflection\DocBlock\Tags\InvalidTag;
0 ignored issues
show
Bug introduced by
The type phpDocumentor\Reflection\DocBlock\Tags\InvalidTag was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
14
use phpDocumentor\Reflection\DocBlock\Tags\Param;
15
use phpDocumentor\Reflection\DocBlock\Tags\Return_;
16
use phpDocumentor\Reflection\DocBlock\Tags\Var_;
17
use phpDocumentor\Reflection\DocBlockFactory;
18
use phpDocumentor\Reflection\Types\Compound;
19
use phpDocumentor\Reflection\Types\ContextFactory;
20
use phpDocumentor\Reflection\Types\Null_;
21
use ReflectionException;
22
use ReflectionMethod;
23
use ReflectionProperty;
24
use Reflector;
25
use Tebru\AnnotationReader\AnnotationCollection;
26
use Tebru\Gson\Annotation\Type;
27
use Tebru\PhpType\TypeToken;
28
29
/**
30
 * Class TypeToken
31
 *
32
 * Creates a [@see TypeToken] for a property
33
 *
34
 * @author Nate Brunette <[email protected]>
35
 */
36
final class TypeTokenFactory
37
{
38
    /**
39
     * @var DocBlockFactory
40
     */
41
    private $docBlockFactory;
42
43
    /**
44
     * @var ContextFactory
45
     */
46
    private $contextFactory;
47
48
    /**
49
     * Constructor
50
     *
51
     * @param null|DocBlockFactory $docBlockFactory
52
     * @param null|ContextFactory $contextFactory
53
     */
54 41
    public function __construct(?DocBlockFactory $docBlockFactory = null, ?ContextFactory $contextFactory = null)
55
    {
56 41
        $this->docBlockFactory = $docBlockFactory ?? DocBlockFactory::createInstance();
57 41
        $this->contextFactory = $contextFactory ?? new ContextFactory();
58 41
    }
59
60
    /**
61
     * Attempts to guess a property type based method type hints, defaults to wildcard type
62
     * - @Type annotation if it exists
63
     * - Getter return type if it exists
64
     * - Setter typehint if it exists
65
     * - Getter docblock
66
     * - Setter docblock
67
     * - Property docblock
68
     * - Property default value if it exists
69
     * - Setter default value if it exists
70
     * - Defaults to wildcard type
71
     * @param AnnotationCollection $annotations
72
     * @param ReflectionProperty|null $property
73
     * @param ReflectionMethod|null $getterMethod
74
     * @param ReflectionMethod|null $setterMethod
75
     * @return TypeToken
76
     * @throws ReflectionException
77
     */
78 35
    public function create(
79
        AnnotationCollection $annotations,
80
        ?ReflectionMethod $getterMethod = null,
81
        ?ReflectionMethod $setterMethod = null,
82
        ?ReflectionProperty $property = null
83
    ): TypeToken {
84
        /** @var Type $typeAnnotation */
85 35
        $typeAnnotation = $annotations->get(Type::class);
86
87 35
        if (null !== $typeAnnotation) {
88 1
            return $typeAnnotation->getType();
89
        }
90
91 34
        if (null !== $getterMethod && null !== $getterMethod->getReturnType()) {
92 4
            $getterType = TypeToken::create($getterMethod->getReturnType()->getName());
0 ignored issues
show
Bug introduced by
The method getName() does not exist on ReflectionType. It seems like you code against a sub-type of ReflectionType such as ReflectionNamedType. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

92
            $getterType = TypeToken::create($getterMethod->getReturnType()->/** @scrutinizer ignore-call */ getName());
Loading history...
93 4
            return $this->checkGenericArray($getterType, $property, $getterMethod, $setterMethod);
94
        }
95
96 30
        if (null !== $setterMethod && [] !== $setterMethod->getParameters()) {
97 6
            $parameter = $setterMethod->getParameters()[0];
98 6
            if (null !== $parameter->getType()) {
99 2
                $setterType = TypeToken::create($parameter->getType()->getName());
100 2
                return $this->checkGenericArray($setterType, $property, $getterMethod, $setterMethod);
101
            }
102
        }
103
104 28
        $type = $this->checkDocBlocks($property, $getterMethod, $setterMethod);
105 28
        if ($type !== null) {
106 20
            return $this->checkGenericArray(
107 20
                $type,
108 20
                $property,
109 20
                $getterMethod,
110 20
                $setterMethod
111
            );
112
        }
113
114 8
        if ($property !== null && $property->isDefault()) {
115 5
            $defaultProperty = $property->getDeclaringClass()->getDefaultProperties()[$property->getName()];
116 5
            if ($defaultProperty !== null) {
117 1
                return $this->checkGenericArray(
118 1
                    TypeToken::createFromVariable($defaultProperty),
119 1
                    $property,
120 1
                    $getterMethod,
121 1
                    $setterMethod
122
                );
123
            }
124
        }
125
126 7
        if (null !== $setterMethod && [] !== $setterMethod->getParameters()) {
127 3
            $parameter = $setterMethod->getParameters()[0];
128 3
            if ($parameter->isDefaultValueAvailable() && null !== $parameter->getDefaultValue()) {
129 1
                $setterType = TypeToken::create(gettype($parameter->getDefaultValue()));
130 1
                return $this->checkGenericArray($setterType, $property, $getterMethod, $setterMethod);
131
            }
132
        }
133
134 6
        return TypeToken::create(TypeToken::WILDCARD);
135
    }
136
137
    /**
138
     * Attempt to get type from docblocks
139
     *
140
     * Checking in the order of property, getter, setter:
141
     *   Attempt to pull the type from the relevant portion of the docblock, then
142
     *   convert that type to a [@see TypeToken] converting to the full class name or
143
     *   generic array syntax if relevant.
144
     *
145
     * @param null|ReflectionProperty $property
146
     * @param null|ReflectionMethod $getter
147
     * @param null|ReflectionMethod $setter
148
     * @return null|TypeToken
149
     */
150 30
    private function checkDocBlocks(
151
        ?ReflectionProperty $property,
152
        ?ReflectionMethod $getter,
153
        ?ReflectionMethod $setter
154
    ): ?TypeToken {
155 30
        $returnTag = null;
156
157 30
        if ($getter !== null) {
158 4
            $docComment = $getter->getDocComment() ?: null;
159 4
            $tag = $this->getTypeFromDoc($getter, $docComment, 'return');
160 4
            if ($tag !== null) {
161 2
                $returnTag = $tag;
162 2
                if ((string)$tag !== 'array') {
163 2
                    return $tag;
164
                }
165
            }
166
        }
167
168 28
        if ($setter !== null) {
169 5
            $docComment = $setter->getDocComment() ?: null;
170 5
            $parameters = $setter->getParameters();
171 5
            if (count($parameters) === 1) {
172 5
                $tag = $this->getTypeFromDoc($setter, $docComment, 'param', $parameters[0]->getName());
173 5
                if ($tag !== null) {
174 2
                    $returnTag = $tag;
175 2
                    if ((string)$tag !== 'array') {
176 2
                        return $tag;
177
                    }
178
                }
179
            }
180
        }
181
182 26
        if ($property !== null) {
183 23
            $docComment = $property->getDocComment() ?: null;
184 23
            $tag = $this->getTypeFromDoc($property, $docComment, 'var');
185 23
            if ($tag !== null) {
186 18
                $returnTag = $tag;
187 18
                if ((string)$tag !== 'array') {
188 17
                    return $tag;
189
                }
190
            }
191
        }
192
193 9
        return $returnTag;
194
    }
195
196
    /**
197
     * Get [@see TypeToken] from docblock
198
     *
199
     * @param Reflector $reflector
200
     * @param null|string $docComment
201
     * @param string $tagName
202
     * @param null|string $variableName
203
     * @return null|TypeToken
204
     */
205 30
    private function getTypeFromDoc(
206
        Reflector $reflector,
207
        ?string $docComment,
208
        string $tagName,
209
        ?string $variableName = null
210
    ): ?TypeToken {
211 30
        if ($docComment === null || $docComment === '') {
212 2
            return null;
213
        }
214
215
        try {
216 28
            $docblock = $this->docBlockFactory->create(
217 28
                $docComment,
218 28
                $this->contextFactory->createFromReflector($reflector)
219
            );
220 1
        } /** @noinspection BadExceptionsProcessingInspection */ catch (InvalidArgumentException $exception) {
221
            // exception likely caused by an empty type
222 1
            return null;
223
        }
224
225 27
        $tags = $docblock->getTagsByName($tagName);
226
227 27
        if (empty($tags)) {
228 2
            return null;
229
        }
230
231 25
        if (count($tags) === 1 && $tags[0] instanceof InvalidTag) {
232
            return null;
233
        }
234
235 25
        if ($tagName !== 'param') {
236 22
            return $this->getTypeFromTag($tags[0]);
237
        }
238
239
        /** @var Param $tag */
240 3
        foreach ($tags as $tag) {
241 3
            if ($tag->getVariableName() === $variableName) {
242 3
                return $this->getTypeFromTag($tag);
243
            }
244
        }
245
246 1
        return null;
247
    }
248
249
    /**
250
     * Get the type token from tag
251
     *
252
     * @param Var_|Param|Return_|Tag $tag
253
     * @return null|TypeToken
254
     */
255 24
    private function getTypeFromTag(Tag $tag): ?TypeToken
256
    {
257 24
        $type = $tag->getType();
0 ignored issues
show
Bug introduced by
The method getType() does not exist on phpDocumentor\Reflection\DocBlock\Tag. It seems like you code against a sub-type of phpDocumentor\Reflection\DocBlock\Tag such as phpDocumentor\Reflection\DocBlock\Tags\TagWithType. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

257
        /** @scrutinizer ignore-call */ 
258
        $type = $tag->getType();
Loading history...
258 24
        if (!$type instanceof Compound) {
259 19
            $type = $this->stripSlashes((string)$type);
260
261 19
            return TypeToken::create($this->unwrapArray($type));
262
        }
263
264 5
        $types = iterator_to_array($type->getIterator());
265
        $types = array_values(array_filter($types, static function ($innerType) {
266 5
            return !$innerType instanceof Null_;
267 5
        }));
268 5
        $count = count($types);
269
270 5
        if ($count !== 1) {
271 2
            return null;
272
        }
273
274 3
        $type = $this->stripSlashes((string)$types[0]);
275
276 3
        return TypeToken::create($this->unwrapArray($type));
277
    }
278
279
    /**
280
     * Remove the initial '\' if it exists
281
     *
282
     * @param string $type
283
     * @return string
284
     */
285 22
    private function stripSlashes(string $type): string
286
    {
287 22
        if ($type[0] === '\\') {
288 12
            $type = substr($type, 1);
289
        }
290
291 22
        return $type;
292
    }
293
294
    /**
295
     * Converts types as int[] to array<int>
296
     *
297
     * @param string $type
298
     * @return string
299
     */
300 22
    private function unwrapArray(string $type): string
301
    {
302
        // if not in array syntax
303 22
        if (strpos($type, '[]') === false) {
304
            // convert mixed to wildcard
305 17
            return $type === 'mixed' ? TypeToken::WILDCARD : $type;
306
        }
307
308 5
        $parts = explode('[]', $type);
309 5
        $primaryType = array_shift($parts);
310
311 5
        $numParts = count($parts);
312
313
        // same as mixed
314 5
        if ($primaryType === 'array') {
315 1
            $primaryType = TypeToken::WILDCARD;
316 1
            $numParts++;
317
        }
318
319 5
        return str_repeat('array<', $numParts) . $primaryType . str_repeat('>', $numParts);
320
    }
321
322
    /**
323
     * If the type is just 'array', check the docblock to see if there's a more specific type
324
     *
325
     * @param TypeToken $type
326
     * @param null|ReflectionProperty $property
327
     * @param null|ReflectionMethod $getter
328
     * @param null|ReflectionMethod $setter
329
     * @return TypeToken
330
     */
331 28
    private function checkGenericArray(
332
        TypeToken $type,
333
        ?ReflectionProperty $property,
334
        ?ReflectionMethod $getter,
335
        ?ReflectionMethod $setter
336
    ): TypeToken {
337 28
        return $type->phpType === TypeToken::HASH
338 6
            ? $this->checkDocBlocks($property, $getter, $setter) ?? $type
339 28
            : $type;
340
    }
341
}
342