Passed
Push — master ( 946dd7...52995d )
by Nate
39s
created

TypeTokenFactory::checkDocBlocks()   B

Complexity

Conditions 11
Paths 21

Size

Total Lines 33
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 19
CRAP Score 11

Importance

Changes 0
Metric Value
cc 11
eloc 18
nc 21
nop 3
dl 0
loc 33
ccs 19
cts 19
cp 1
crap 11
rs 7.3166
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 ReflectionMethod;
12
use ReflectionProperty;
13
use Tebru\AnnotationReader\AnnotationCollection;
14
use Tebru\Gson\Annotation\Type;
15
use Tebru\PhpType\TypeToken;
16
17
/**
18
 * Class TypeToken
19
 *
20
 * Creates a [@see TypeToken] for a property
21
 *
22
 * @author Nate Brunette <[email protected]>
23
 */
24
final class TypeTokenFactory
25
{
26
    /**
27
     * Regex to get full class names from imported use statements
28
     */
29
    private const USE_PATTERN = '/use\s+(?:(?<namespace>[^;]+\\\\)[^;]*[\s,{](?<classname>\w+)\s+as\s+:REPLACE:[^;]*};|(?<group>[^;]+\\\\){[^;]*:REPLACE:[^;]*};|(?<alias>[^;]+)\s+as\s+:REPLACE:;|(?<default>[^;]+:REPLACE:);)/';
30
31
    /**
32
     * Attempts to guess a property type based method type hints, defaults to wildcard type
33
     *
34
     * - @Type annotation if it exists
35
     * - Getter return type if it exists
36
     * - Setter typehint if it exists
37
     * - Getter docblock
38
     * - Setter docblock
39
     * - Property docblock
40
     * - Property default value if it exists
41
     * - Setter default value if it exists
42
     * - Defaults to wildcard type
43
     *
44
     * @param AnnotationCollection $annotations
45
     * @param ReflectionProperty|null $property
46
     * @param ReflectionMethod|null $getterMethod
47
     * @param ReflectionMethod|null $setterMethod
48
     * @return TypeToken
49
     */
50 34
    public function create(
51
        AnnotationCollection $annotations,
52
        ?ReflectionMethod $getterMethod = null,
53
        ?ReflectionMethod $setterMethod = null,
54
        ?ReflectionProperty $property = null
55
    ): TypeToken {
56
        /** @var Type $typeAnnotation */
57 34
        $typeAnnotation = $annotations->get(Type::class);
58
59 34
        if (null !== $typeAnnotation) {
60 1
            return $typeAnnotation->getType();
61
        }
62
63 33
        if (null !== $getterMethod && null !== $getterMethod->getReturnType()) {
64 4
            $getterType = TypeToken::create((string)$getterMethod->getReturnType());
65 4
            return $this->checkGenericArray($getterType, $property, $getterMethod, $setterMethod);
66
        }
67
68 29
        if (null !== $setterMethod && [] !== $setterMethod->getParameters()) {
69 5
            $parameter = $setterMethod->getParameters()[0];
70 5
            if (null !== $parameter->getType()) {
71 2
                $setterType = TypeToken::create((string)$parameter->getType());
72 2
                return $this->checkGenericArray($setterType, $property, $getterMethod, $setterMethod);
73
            }
74
        }
75
76 27
        $type = $this->checkDocBlocks($property, $getterMethod, $setterMethod);
77 27
        if ($type !== null) {
78 21
            return $this->checkGenericArray(
79 21
                $type,
80 21
                $property,
81 21
                $getterMethod,
82 21
                $setterMethod
83
            );
84
        }
85
86 6
        if ($property !== null && $property->isDefault()) {
87 4
            $defaultProperty = $property->getDeclaringClass()->getDefaultProperties()[$property->getName()];
88 4
            if ($defaultProperty !== null) {
89 1
                return $this->checkGenericArray(
90 1
                    TypeToken::createFromVariable($defaultProperty),
91 1
                    $property,
92 1
                    $getterMethod,
93 1
                    $setterMethod
94
                );
95
            }
96
        }
97
98 5
        if (null !== $setterMethod && [] !== $setterMethod->getParameters()) {
99 2
            $parameter = $setterMethod->getParameters()[0];
100 2
            if ($parameter->isDefaultValueAvailable() && null !== $parameter->getDefaultValue()) {
101 1
                $setterType = TypeToken::create(\gettype($parameter->getDefaultValue()));
102 1
                return $this->checkGenericArray($setterType, $property, $getterMethod, $setterMethod);
103
            }
104
        }
105
106 4
        return TypeToken::create(TypeToken::WILDCARD);
107
    }
108
109
    /**
110
     * Attempt to get type from docblocks
111
     *
112
     * Checking in the order of property, getter, setter:
113
     *   Attempt to pull the type from the relevant portion of the docblock, then
114
     *   convert that type to a [@see TypeToken] converting to the full class name or
115
     *   generic array syntax if relevant.
116
     *
117
     * @param null|ReflectionProperty $property
118
     * @param null|ReflectionMethod $getter
119
     * @param null|ReflectionMethod $setter
120
     * @return null|TypeToken
121
     */
122 29
    private function checkDocBlocks(
123
        ?ReflectionProperty $property,
124
        ?ReflectionMethod $getter,
125
        ?ReflectionMethod $setter
126
    ): ?TypeToken {
127 29
        if ($getter !== null) {
128 4
            $type = $this->getType($getter->getDocComment() ?: null, 'return');
129 4
            if ($type !== null) {
130 2
                $class = $getter->getDeclaringClass();
131 2
                return $this->getTypeToken($type, $class->getNamespaceName(), $class->getFileName());
132
            }
133
        }
134
135 27
        if ($setter !== null) {
136 4
            $parameters = $setter->getParameters();
137 4
            if (\count($parameters) === 1) {
138 4
                $type = $this->getType($setter->getDocComment() ?: null, 'param', $parameters[0]->getName());
139 4
                if ($type !== null) {
140 2
                    $class = $setter->getDeclaringClass();
141 2
                    return $this->getTypeToken($type, $class->getNamespaceName(), $class->getFileName());
142
                }
143
            }
144
        }
145
146 25
        if ($property !== null) {
147 23
            $type = $this->getType($property->getDocComment() ?: null, 'var');
148 23
            if ($type !== null) {
149 19
                $class = $property->getDeclaringClass();
150 19
                return $this->getTypeToken($type, $class->getNamespaceName(), $class->getFileName());
151
            }
152
        }
153
154 6
        return null;
155
    }
156
157
    /**
158
     * Parse docblock and return type for parameter
159
     *
160
     * @param string $comment
161
     * @param string $annotation
162
     * @param null|string $parameter
163
     * @return null|string
164
     */
165 29
    private function getType(?string $comment, string $annotation, ?string $parameter = null): ?string
166
    {
167 29
        if ($comment === null) {
168 3
            return null;
169
        }
170
171
        // for setters, we look for the param name as well
172 26
        $pattern = '/@'.$annotation.'\s+([a-zA-Z0-9|\[\]\\\\]+)';
173 26
        if ($parameter !== null) {
174 2
            $pattern .= '\s+\$'.$parameter;
175
        }
176 26
        $pattern .= '/';
177
178 26
        \preg_match($pattern, $comment, $matches);
179
180
        /** @var string|null $type */
181 26
        $type = $matches[1] ?? null;
182 26
        if ($type === null) {
183 1
            return null;
184
        }
185
186
        // if not nullable
187 25
        if (\strpos($type, '|') === false) {
188 20
            return $type;
189
        }
190
191
        // if > 2 types
192 5
        if (\substr_count($type, '|') !== 1) {
193 1
            return null;
194
        }
195
196
        // if one of the types is not null
197 4
        if (\stripos(\strtolower($type), 'null') === false) {
198 1
            return null;
199
        }
200
201
        // return the non-null type
202 3
        foreach (\explode('|', $type) as $potentialType) {
203 3
            $potentialType = \trim($potentialType);
204 3
            if (\strtolower($potentialType) !== 'null') {
205 3
                return $potentialType;
206
            }
207
        }
208
209
        // The should never be hit
210
        // @codeCoverageIgnoreStart
211
        return null;
212
        // @codeCoverageIgnoreEnd
213
    }
214
215
    /**
216
     * Converts types as int[] to array<int>
217
     *
218
     * @param string $type
219
     * @param string $namespace
220
     * @param string $filename
221
     * @return string
222
     */
223 23
    private function unwrapArray(string $type, string $namespace, string $filename): string
224
    {
225
        // if not in array syntax
226 23
        if (\strpos($type, '[]') === false) {
227
            // convert mixed to wildcard
228 23
            return $type === 'mixed' ? TypeToken::WILDCARD : $type;
229
        }
230
231 5
        $parts = \explode('[]', $type);
232 5
        $primaryType = \array_shift($parts);
233 5
        $numParts = \count($parts);
234
235 5
        $primaryTypeToken = $this->getTypeToken($primaryType, $namespace, $filename);
236
237 5
        return \str_repeat('array<', $numParts) . $primaryTypeToken->getRawType() . \str_repeat('>', $numParts);
238
    }
239
240
    /**
241
     * Using the type found in docblock, attempt to resolve imported classes
242
     *
243
     * @param string $type
244
     * @param string $namespace
245
     * @param string $filename
246
     * @return TypeToken
247
     */
248 23
    private function getTypeToken(string $type, string $namespace, string $filename): TypeToken
249
    {
250
        // convert syntax if generic array
251 23
        $type = $this->unwrapArray($type, $namespace, $filename);
252 23
        $typeToken = TypeToken::create($type);
253
254 23
        if (!$typeToken->isObject()) {
255 13
            return $typeToken;
256
        }
257
258 13
        $firstSlash = \strpos($type, '\\');
259 13
        if ($firstSlash === 0) {
260 1
            return TypeToken::create(substr($type, 1));
261
        }
262
263 12
        if ($firstSlash === false && (\class_exists($type) || \interface_exists($type))) {
264 1
            return $typeToken;
265
        }
266
267 11
        $pattern = \str_replace(':REPLACE:', $type, self::USE_PATTERN);
268 11
        \preg_match($pattern, \file_get_contents($filename), $matches);
269
270
        // normal use statement syntax
271 11
        if (!empty($matches['default'])) {
272 1
            return TypeToken::create($matches['default']);
273
        }
274
275
        // aliased use statement
276 10
        if (!empty($matches['alias'])) {
277 2
            return TypeToken::create($matches['alias']);
278
        }
279
280
        // group use statement
281 8
        if (!empty($matches['group'])) {
282 3
            return TypeToken::create($matches['group'].$type);
283
        }
284
285
        // grouped aliased use statement
286 5
        if (!empty($matches['namespace']) && !empty($matches['classname'])) {
287 1
            return TypeToken::create($matches['namespace'].$matches['classname']);
288
        }
289
290 4
        return TypeToken::create($namespace.'\\'.$type);
291
    }
292
293
    /**
294
     * If the type is just 'array', check the docblock to see if there's a more specific type
295
     *
296
     * @param TypeToken $type
297
     * @param null|ReflectionProperty $property
298
     * @param null|ReflectionMethod $getter
299
     * @param null|ReflectionMethod $setter
300
     * @return TypeToken
301
     */
302 29
    private function checkGenericArray(
303
        TypeToken $type,
304
        ?ReflectionProperty $property,
305
        ?ReflectionMethod $getter,
306
        ?ReflectionMethod $setter
307
    ): TypeToken {
308 29
        return $type->isArray()
309 6
            ? $this->checkDocBlocks($property, $getter, $setter) ?? $type
310 29
            : $type;
311
    }
312
}
313