Passed
Push — master ( daea2e...36f11c )
by Nate
50s queued 11s
created

TypeTokenFactory::checkDocBlocks()   C

Complexity

Conditions 14
Paths 52

Size

Total Lines 44
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 26
CRAP Score 14

Importance

Changes 0
Metric Value
cc 14
eloc 25
nc 52
nop 3
dl 0
loc 44
ccs 26
cts 26
cp 1
crap 14
rs 6.2666
c 0
b 0
f 0

How to fix   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
 * 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\Param;
14
use phpDocumentor\Reflection\DocBlock\Tags\Return_;
15
use phpDocumentor\Reflection\DocBlock\Tags\Var_;
16
use phpDocumentor\Reflection\DocBlockFactory;
17
use phpDocumentor\Reflection\Types\Compound;
18
use phpDocumentor\Reflection\Types\ContextFactory;
19
use phpDocumentor\Reflection\Types\Null_;
20
use ReflectionMethod;
21
use ReflectionProperty;
22
use Reflector;
23
use Tebru\AnnotationReader\AnnotationCollection;
24
use Tebru\Gson\Annotation\Type;
25
use Tebru\PhpType\TypeToken;
26
27
/**
28
 * Class TypeToken
29
 *
30
 * Creates a [@see TypeToken] for a property
31
 *
32
 * @author Nate Brunette <[email protected]>
33
 */
34
final class TypeTokenFactory
35
{
36
    /**
37
     * @var DocBlockFactory
38
     */
39
    private $docBlockFactory;
40
41
    /**
42
     * @var ContextFactory
43
     */
44
    private $contextFactory;
45
46
    /**
47
     * Constructor
48
     *
49
     * @param null|DocBlockFactory $docBlockFactory
50
     * @param null|ContextFactory $contextFactory
51
     */
52 37
    public function __construct(?DocBlockFactory $docBlockFactory = null, ?ContextFactory $contextFactory = null)
53
    {
54 37
        $this->docBlockFactory = $docBlockFactory ?? DocBlockFactory::createInstance();
55 37
        $this->contextFactory = $contextFactory ?? new ContextFactory();
56 37
    }
57
58
    /**
59
     * Attempts to guess a property type based method type hints, defaults to wildcard type
60
     *
61
     * - @Type annotation if it exists
62
     * - Getter return type if it exists
63
     * - Setter typehint if it exists
64
     * - Getter docblock
65
     * - Setter docblock
66
     * - Property docblock
67
     * - Property default value if it exists
68
     * - Setter default value if it exists
69
     * - Defaults to wildcard type
70
     *
71
     * @param AnnotationCollection $annotations
72
     * @param ReflectionProperty|null $property
73
     * @param ReflectionMethod|null $getterMethod
74
     * @param ReflectionMethod|null $setterMethod
75
     * @return TypeToken
76
     */
77 35
    public function create(
78
        AnnotationCollection $annotations,
79
        ?ReflectionMethod $getterMethod = null,
80
        ?ReflectionMethod $setterMethod = null,
81
        ?ReflectionProperty $property = null
82
    ): TypeToken {
83
        /** @var Type $typeAnnotation */
84 35
        $typeAnnotation = $annotations->get(Type::class);
85
86 35
        if (null !== $typeAnnotation) {
87 1
            return $typeAnnotation->getType();
88
        }
89
90 34
        if (null !== $getterMethod && null !== $getterMethod->getReturnType()) {
91 4
            $getterType = TypeToken::create((string)$getterMethod->getReturnType());
92 4
            return $this->checkGenericArray($getterType, $property, $getterMethod, $setterMethod);
93
        }
94
95 30
        if (null !== $setterMethod && [] !== $setterMethod->getParameters()) {
96 6
            $parameter = $setterMethod->getParameters()[0];
97 6
            if (null !== $parameter->getType()) {
98 2
                $setterType = TypeToken::create((string)$parameter->getType());
99 2
                return $this->checkGenericArray($setterType, $property, $getterMethod, $setterMethod);
100
            }
101
        }
102
103 28
        $type = $this->checkDocBlocks($property, $getterMethod, $setterMethod);
104 28
        if ($type !== null) {
105 20
            return $this->checkGenericArray(
106 20
                $type,
107 20
                $property,
108 20
                $getterMethod,
109 20
                $setterMethod
110
            );
111
        }
112
113 8
        if ($property !== null && $property->isDefault()) {
114 5
            $defaultProperty = $property->getDeclaringClass()->getDefaultProperties()[$property->getName()];
115 5
            if ($defaultProperty !== null) {
116 1
                return $this->checkGenericArray(
117 1
                    TypeToken::createFromVariable($defaultProperty),
118 1
                    $property,
119 1
                    $getterMethod,
120 1
                    $setterMethod
121
                );
122
            }
123
        }
124
125 7
        if (null !== $setterMethod && [] !== $setterMethod->getParameters()) {
126 3
            $parameter = $setterMethod->getParameters()[0];
127 3
            if ($parameter->isDefaultValueAvailable() && null !== $parameter->getDefaultValue()) {
128 1
                $setterType = TypeToken::create(\gettype($parameter->getDefaultValue()));
129 1
                return $this->checkGenericArray($setterType, $property, $getterMethod, $setterMethod);
130
            }
131
        }
132
133 6
        return TypeToken::create(TypeToken::WILDCARD);
134
    }
135
136
    /**
137
     * Attempt to get type from docblocks
138
     *
139
     * Checking in the order of property, getter, setter:
140
     *   Attempt to pull the type from the relevant portion of the docblock, then
141
     *   convert that type to a [@see TypeToken] converting to the full class name or
142
     *   generic array syntax if relevant.
143
     *
144
     * @param null|ReflectionProperty $property
145
     * @param null|ReflectionMethod $getter
146
     * @param null|ReflectionMethod $setter
147
     * @return null|TypeToken
148
     */
149 30
    private function checkDocBlocks(
150
        ?ReflectionProperty $property,
151
        ?ReflectionMethod $getter,
152
        ?ReflectionMethod $setter
153
    ): ?TypeToken {
154 30
        $returnTag = null;
155
156 30
        if ($getter !== null) {
157 4
            $docComment = $getter->getDocComment() ?: null;
158 4
            $tag = $this->getTypeFromDoc($getter, $docComment, 'return');
0 ignored issues
show
Bug introduced by
It seems like $docComment can also be of type true; however, parameter $docComment of Tebru\Gson\Internal\Type...ctory::getTypeFromDoc() does only seem to accept null|string, maybe add an additional type check? ( Ignorable by Annotation )

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

158
            $tag = $this->getTypeFromDoc($getter, /** @scrutinizer ignore-type */ $docComment, 'return');
Loading history...
159 4
            if ($tag !== null) {
160 2
                $returnTag = $tag;
161 2
                if ((string)$tag !== 'array') {
162 2
                    return $tag;
163
                }
164
            }
165
        }
166
167 28
        if ($setter !== null) {
168 5
            $docComment = $setter->getDocComment() ?: null;
169 5
            $parameters = $setter->getParameters();
170 5
            if (\count($parameters) === 1) {
171 5
                $tag = $this->getTypeFromDoc($setter, $docComment, 'param', $parameters[0]->getName());
172 5
                if ($tag !== null) {
173 2
                    $returnTag = $tag;
174 2
                    if ((string)$tag !== 'array') {
175 2
                        return $tag;
176
                    }
177
                }
178
            }
179
        }
180
181 26
        if ($property !== null) {
182 23
            $docComment = $property->getDocComment() ?: null;
183 23
            $tag = $this->getTypeFromDoc($property, $docComment, 'var');
184 23
            if ($tag !== null) {
185 18
                $returnTag = $tag;
186 18
                if ((string)$tag !== 'array') {
187 17
                    return $tag;
188
                }
189
            }
190
        }
191
192 9
        return $returnTag;
193
    }
194
195
    /**
196
     * Get [@see TypeToken] from docblock
197
     *
198
     * @param Reflector $reflector
199
     * @param null|string $docComment
200
     * @param string $tagName
201
     * @param null|string $variableName
202
     * @return null|TypeToken
203
     */
204 30
    private function getTypeFromDoc(
205
        Reflector $reflector,
206
        ?string $docComment,
207
        string $tagName,
208
        ?string $variableName = null
209
    ): ?TypeToken {
210 30
        if ($docComment === null || $docComment === '') {
211 3
            return null;
212
        }
213
214
        try {
215 27
            $docblock = $this->docBlockFactory->create(
216 27
                $docComment,
217 27
                $this->contextFactory->createFromReflector($reflector)
218
            );
219 1
        } /** @noinspection BadExceptionsProcessingInspection */ catch (InvalidArgumentException $exception) {
220
            // exception likely caused by an empty type
221 1
            return null;
222
        }
223
224 26
        $tags = $docblock->getTagsByName($tagName);
225
226 26
        if (empty($tags)) {
227 1
            return null;
228
        }
229
230 25
        if ($tagName !== 'param') {
231 22
            return $this->getTypeFromTag($tags[0]);
232
        }
233
234
        /** @var Param $tag */
235 3
        foreach ($tags as $tag) {
236 3
            if ($tag->getVariableName() === $variableName) {
237 3
                return $this->getTypeFromTag($tag);
238
            }
239
        }
240
241 1
        return null;
242
    }
243
244
    /**
245
     * Get the type token from tag
246
     *
247
     * @param Var_|Param|Return_|Tag $tag
248
     * @return null|TypeToken
249
     */
250 24
    private function getTypeFromTag(Tag $tag): ?TypeToken
251
    {
252 24
        $type = $tag->getType();
253 24
        if (!$type instanceof Compound) {
254 19
            $type = $this->stripSlashes((string)$type);
255
256 19
            return TypeToken::create($this->unwrapArray($type));
257
        }
258
259 5
        $types = \iterator_to_array($type->getIterator());
260
        $types = \array_values(\array_filter($types, function ($innerType) {
261 5
            return !$innerType instanceof Null_;
262 5
        }));
263 5
        $count = \count($types);
264
265 5
        if ($count !== 1) {
266 2
            return null;
267
        }
268
269 3
        $type = $this->stripSlashes((string)$types[0]);
270
271 3
        return TypeToken::create($this->unwrapArray($type));
272
    }
273
274
    /**
275
     * Remove the initial '\' if it exists
276
     *
277
     * @param string $type
278
     * @return string
279
     */
280 22
    private function stripSlashes(string $type): string
281
    {
282 22
        if ($type[0] === '\\') {
283 12
            $type = substr($type, 1);
284
        }
285
286 22
        return $type;
287
    }
288
289
    /**
290
     * Converts types as int[] to array<int>
291
     *
292
     * @param string $type
293
     * @return string
294
     */
295 22
    private function unwrapArray(string $type): string
296
    {
297
        // if not in array syntax
298 22
        if (\strpos($type, '[]') === false) {
299
            // convert mixed to wildcard
300 17
            return $type === 'mixed' ? TypeToken::WILDCARD : $type;
301
        }
302
303 5
        $parts = \explode('[]', $type);
304 5
        $primaryType = \array_shift($parts);
305
306 5
        $numParts = \count($parts);
307
308
        // same as mixed
309 5
        if ($primaryType === 'array') {
310 1
            $primaryType = TypeToken::WILDCARD;
311 1
            $numParts++;
312
        }
313
314 5
        return \str_repeat('array<', $numParts) . $primaryType . \str_repeat('>', $numParts);
315
    }
316
317
    /**
318
     * If the type is just 'array', check the docblock to see if there's a more specific type
319
     *
320
     * @param TypeToken $type
321
     * @param null|ReflectionProperty $property
322
     * @param null|ReflectionMethod $getter
323
     * @param null|ReflectionMethod $setter
324
     * @return TypeToken
325
     */
326 28
    private function checkGenericArray(
327
        TypeToken $type,
328
        ?ReflectionProperty $property,
329
        ?ReflectionMethod $getter,
330
        ?ReflectionMethod $setter
331
    ): TypeToken {
332 28
        return $type->isArray()
333 6
            ? $this->checkDocBlocks($property, $getter, $setter) ?? $type
334 28
            : $type;
335
    }
336
}
337