Passed
Pull Request — master (#1450)
by
unknown
02:23
created

DocBlockTypeResolver   C

Complexity

Total Complexity 54

Size/Duplication

Total Lines 319
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 110
dl 0
loc 319
rs 6.4799
c 0
b 0
f 0
wmc 54

18 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 7 1
A resolveType() 0 7 3
A flattenVarTagValueTypes() 0 13 3
A hasGlobalNamespacePrefix() 0 3 1
A resolveTypeFromDocblock() 0 25 5
A resolveTypeFromTypeNode() 0 7 2
A isPrimitiveType() 0 3 1
A flattenParamTagValueTypes() 0 17 4
A filterNullFromTypes() 0 5 2
B getPropertyDocblockTypeHint() 0 52 9
A isClassOrInterface() 0 3 2
A isSimpleType() 0 3 2
A endsWith() 0 5 1
A getDeclaringClassOrTrait() 0 11 4
A isNullType() 0 3 1
A gatherGroupUseStatements() 0 11 3
B expandClassNameUsingUseStatements() 0 28 8
A gatherSingleUseStatements() 0 9 2

How to fix   Complexity   

Complex Class

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

1
<?php
2
3
declare(strict_types=1);
4
5
namespace JMS\Serializer\Metadata\Driver\DocBlockDriver;
6
7
use PHPStan\PhpDocParser\Ast\PhpDoc\ParamTagValueNode;
8
use PHPStan\PhpDocParser\Ast\PhpDoc\VarTagValueNode;
9
use PHPStan\PhpDocParser\Ast\Type\ArrayTypeNode;
10
use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode;
11
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
12
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
13
use PHPStan\PhpDocParser\Ast\Type\UnionTypeNode;
14
use PHPStan\PhpDocParser\Lexer\Lexer;
15
use PHPStan\PhpDocParser\Parser\ConstExprParser;
16
use PHPStan\PhpDocParser\Parser\PhpDocParser;
17
use PHPStan\PhpDocParser\Parser\TokenIterator;
18
use PHPStan\PhpDocParser\Parser\TypeParser;
19
20
/**
21
 * @internal
22
 */
23
final class DocBlockTypeResolver
24
{
25
    /** resolve single use statements */
26
    private const SINGLE_USE_STATEMENTS_REGEX = '/^[^\S\r\n]*use[\s]*([^;\n]*)[\s]*;$/m';
27
    /** resolve group use statements */
28
    private const GROUP_USE_STATEMENTS_REGEX = '/^[^\S\r\n]*use[[\s]*([^;\n]*)[\s]*{([a-zA-Z0-9\s\n\r,]*)};$/m';
29
    private const GLOBAL_NAMESPACE_PREFIX = '\\';
30
31
    /**
32
     * @var PhpDocParser
33
     */
34
    protected $phpDocParser;
35
36
    /**
37
     * @var Lexer
38
     */
39
    protected $lexer;
40
41
    public function __construct()
42
    {
43
        $constExprParser = new ConstExprParser();
44
        $typeParser = new TypeParser($constExprParser);
45
46
        $this->phpDocParser = new PhpDocParser($typeParser, $constExprParser);
47
        $this->lexer = new Lexer();
48
    }
49
50
    /**
51
     * Attempts to retrieve additional type information from a PhpDoc block. Throws in case of ambiguous type
52
     * information and will return null if no helpful type information could be retrieved.
53
     *
54
     * @param \ReflectionProperty $reflectionProperty
55
     *
56
     * @return string|null
57
     */
58
    public function getPropertyDocblockTypeHint(\ReflectionProperty $reflectionProperty): ?string
59
    {
60
        $types = $this->resolveTypeFromDocblock($reflectionProperty);
61
62
        // The PhpDoc does not contain additional type information.
63
        if (0 === count($types)) {
0 ignored issues
show
Bug introduced by
It seems like $types can also be of type null; however, parameter $value of count() does only seem to accept Countable|array, 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

63
        if (0 === count(/** @scrutinizer ignore-type */ $types)) {
Loading history...
64
            return null;
65
        }
66
67
        // The PhpDoc contains multiple non-null types which produces ambiguity when deserializing.
68
        if (count($types) > 1) {
69
            return null;
70
        }
71
72
        // Only one type is left, so we only need to differentiate between arrays, generics and other types.
73
        $type = $types[0];
74
75
        // Simple array without concrete type: array
76
        if ($this->isSimpleType($type, 'array') || $this->isSimpleType($type, 'list')) {
77
            return null;
78
        }
79
80
        // Normal array syntax: Product[] | \Foo\Bar\Product[]
81
        if ($type instanceof ArrayTypeNode) {
82
            $resolvedType = $this->resolveTypeFromTypeNode($type->type, $reflectionProperty);
83
84
            return 'array<' . $resolvedType . '>';
85
        }
86
87
        // Generic array syntax: array<Product> | array<\Foo\Bar\Product> | array<int,Product>
88
        if ($type instanceof GenericTypeNode) {
89
            if ($this->isSimpleType($type->type, 'array')) {
90
                $resolvedTypes = array_map(function (TypeNode $node) use ($reflectionProperty) {
91
                    return $this->resolveTypeFromTypeNode($node, $reflectionProperty);
92
                }, $type->genericTypes);
93
94
                return 'array<' . implode(',', $resolvedTypes) . '>';
95
            }
96
97
            if ($this->isSimpleType($type->type, 'list')) {
98
                $resolvedTypes = array_map(function (TypeNode $node) use ($reflectionProperty) {
99
                    return $this->resolveTypeFromTypeNode($node, $reflectionProperty);
100
                }, $type->genericTypes);
101
102
                return 'array<int, ' . implode(',', $resolvedTypes) . '>';
103
            }
104
105
            throw new \InvalidArgumentException(sprintf("Can't use non-array generic type %s for collection in %s:%s", (string) $type->type, $reflectionProperty->getDeclaringClass()->getName(), $reflectionProperty->getName()));
106
        }
107
108
        // Primitives and class names: Collection | \Foo\Bar\Product | string
109
        return $this->resolveTypeFromTypeNode($type, $reflectionProperty);
110
    }
111
112
    /**
113
     * Returns a flat list of types of the given var tags. Union types are flattened as well.
114
     *
115
     * @param VarTagValueNode[] $varTagValues
116
     *
117
     * @return TypeNode[]
118
     */
119
    private function flattenVarTagValueTypes(array $varTagValues): array
120
    {
121
        if ([] === $varTagValues) {
122
            return [];
123
        }
124
125
        return array_merge(...array_map(static function (VarTagValueNode $node) {
126
            if ($node->type instanceof UnionTypeNode) {
127
                return $node->type->types;
128
            }
129
130
            return [$node->type];
131
        }, $varTagValues));
132
    }
133
134
    /**
135
     * Returns a flat list of types of the given param tags. Union types are flattened as well.
136
     *
137
     * @param ParamTagValueNode[] $varTagValues
138
     *
139
     * @return TypeNode[]
140
     */
141
    private function flattenParamTagValueTypes(string $parameterName, array $varTagValues): array
142
    {
143
        if ([] === $varTagValues) {
144
            return [];
145
        }
146
147
        $parameterName = sprintf('$%s', $parameterName);
148
        $types = [];
149
        foreach ($varTagValues as $node) {
150
            if ($parameterName !== $node->parameterName) {
151
                continue;
152
            }
153
154
            $types[] = $node->type;
155
        }
156
157
        return $types;
158
    }
159
160
    /**
161
     * Filters the null type from the given types array. If no null type is found, the array is returned unchanged.
162
     *
163
     * @param TypeNode[] $types
164
     *
165
     * @return TypeNode[]
166
     */
167
    private function filterNullFromTypes(array $types): array
168
    {
169
        return array_values(array_filter(array_map(function (TypeNode $node) {
170
            return $this->isNullType($node) ? null : $node;
171
        }, $types)));
172
    }
173
174
    /**
175
     * Determines if the given type is a null type.
176
     *
177
     * @param TypeNode $typeNode
178
     *
179
     * @return bool
180
     */
181
    private function isNullType(TypeNode $typeNode): bool
182
    {
183
        return $this->isSimpleType($typeNode, 'null');
184
    }
185
186
    /**
187
     * Determines if the given node represents a simple type.
188
     *
189
     * @param TypeNode $typeNode
190
     * @param string $simpleType
191
     *
192
     * @return bool
193
     */
194
    private function isSimpleType(TypeNode $typeNode, string $simpleType): bool
195
    {
196
        return $typeNode instanceof IdentifierTypeNode && $typeNode->name === $simpleType;
197
    }
198
199
    /**
200
     * Attempts to resolve the fully qualified type from the given node. If the node is not suitable for type
201
     * retrieval, an exception is thrown.
202
     *
203
     * @param TypeNode $typeNode
204
     * @param \ReflectionProperty $reflectionProperty
205
     *
206
     * @return string
207
     *
208
     * @throws \InvalidArgumentException
209
     */
210
    private function resolveTypeFromTypeNode(TypeNode $typeNode, \ReflectionProperty $reflectionProperty): string
211
    {
212
        if (!($typeNode instanceof IdentifierTypeNode)) {
213
            throw new \InvalidArgumentException(sprintf("Can't use unsupported type %s for collection in %s:%s", (string) $typeNode, $reflectionProperty->getDeclaringClass()->getName(), $reflectionProperty->getName()));
214
        }
215
216
        return $this->resolveType($typeNode->name, $reflectionProperty);
217
    }
218
219
    private function expandClassNameUsingUseStatements(string $typeHint, \ReflectionClass $declaringClass, \ReflectionProperty $reflectionProperty): string
220
    {
221
        if ($this->isClassOrInterface($typeHint)) {
222
            return $typeHint;
223
        }
224
225
        $expandedClassName = $declaringClass->getNamespaceName() . '\\' . $typeHint;
226
        if ($this->isClassOrInterface($expandedClassName)) {
227
            return $expandedClassName;
228
        }
229
230
        $classContents = file_get_contents($declaringClass->getFileName());
231
        $foundUseStatements = $this->gatherGroupUseStatements($classContents);
232
        $foundUseStatements = array_merge($this->gatherSingleUseStatements($classContents), $foundUseStatements);
233
234
        foreach ($foundUseStatements as $statementClassName) {
235
            if ($alias = explode('as', $statementClassName)) {
236
                if (array_key_exists(1, $alias) && trim($alias[1]) === $typeHint) {
237
                    return trim($alias[0]);
238
                }
239
            }
240
241
            if ($this->endsWith($statementClassName, $typeHint)) {
242
                return $statementClassName;
243
            }
244
        }
245
246
        throw new \InvalidArgumentException(sprintf("Can't use incorrect type %s for collection in %s:%s", $typeHint, $declaringClass->getName(), $reflectionProperty->getName()));
247
    }
248
249
    private function endsWith(string $statementClassToCheck, string $typeHintToSearchFor): bool
250
    {
251
        $typeHintToSearchFor = '\\' . $typeHintToSearchFor;
252
253
        return substr($statementClassToCheck, -strlen($typeHintToSearchFor)) === $typeHintToSearchFor;
254
    }
255
256
    private function isPrimitiveType(string $type): bool
257
    {
258
        return in_array($type, ['int', 'float', 'bool', 'string']);
259
    }
260
261
    private function hasGlobalNamespacePrefix(string $typeHint): bool
262
    {
263
        return self::GLOBAL_NAMESPACE_PREFIX === $typeHint[0];
264
    }
265
266
    private function gatherGroupUseStatements(string $classContents): array
267
    {
268
        $foundUseStatements = [];
269
        preg_match_all(self::GROUP_USE_STATEMENTS_REGEX, $classContents, $foundGroupUseStatements);
270
        for ($useStatementIndex = 0; $useStatementIndex < count($foundGroupUseStatements[0]); $useStatementIndex++) {
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration:

for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
271
            foreach (explode(',', $foundGroupUseStatements[2][$useStatementIndex]) as $singleUseStatement) {
272
                $foundUseStatements[] = trim($foundGroupUseStatements[1][$useStatementIndex]) . trim($singleUseStatement);
273
            }
274
        }
275
276
        return $foundUseStatements;
277
    }
278
279
    private function gatherSingleUseStatements(string $classContents): array
280
    {
281
        $foundUseStatements = [];
282
        preg_match_all(self::SINGLE_USE_STATEMENTS_REGEX, $classContents, $foundSingleUseStatements);
283
        for ($useStatementIndex = 0; $useStatementIndex < count($foundSingleUseStatements[0]); $useStatementIndex++) {
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration:

for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
284
            $foundUseStatements[] = trim($foundSingleUseStatements[1][$useStatementIndex]);
285
        }
286
287
        return $foundUseStatements;
288
    }
289
290
    private function getDeclaringClassOrTrait(\ReflectionProperty $reflectionProperty): \ReflectionClass
291
    {
292
        foreach ($reflectionProperty->getDeclaringClass()->getTraits() as $trait) {
293
            foreach ($trait->getProperties() as $traitProperty) {
294
                if ($traitProperty->getName() === $reflectionProperty->getName()) {
295
                    return $this->getDeclaringClassOrTrait($traitProperty);
296
                }
297
            }
298
        }
299
300
        return $reflectionProperty->getDeclaringClass();
301
    }
302
303
    private function resolveType(string $typeHint, \ReflectionProperty $reflectionProperty): string
304
    {
305
        if (!$this->hasGlobalNamespacePrefix($typeHint) && !$this->isPrimitiveType($typeHint)) {
306
            $typeHint = $this->expandClassNameUsingUseStatements($typeHint, $this->getDeclaringClassOrTrait($reflectionProperty), $reflectionProperty);
307
        }
308
309
        return ltrim($typeHint, '\\');
310
    }
311
312
    private function isClassOrInterface(string $typeHint): bool
313
    {
314
        return class_exists($typeHint) || interface_exists($typeHint);
315
    }
316
317
    private function resolveTypeFromDocblock(\ReflectionProperty $reflectionProperty): ?array
318
    {
319
        $docComment = $reflectionProperty->getDocComment();
320
        if (!$docComment && PHP_VERSION_ID >= 80000 && $reflectionProperty->isPromoted()) {
321
            $docComment = $reflectionProperty->getDeclaringClass()->getConstructor()->getDocComment();
322
323
            $tokens = $this->lexer->tokenize($docComment);
324
            $phpDocNode = $this->phpDocParser->parse(new TokenIterator($tokens));
325
326
            return $this->flattenParamTagValueTypes($reflectionProperty->getName(), $phpDocNode->getParamTagValues());
327
        }
328
329
        if (!$docComment) {
330
            return null;
331
        }
332
333
        // First we tokenize the PhpDoc comment and parse the tokens into a PhpDocNode.
334
        $tokens = $this->lexer->tokenize($docComment);
335
        $phpDocNode = $this->phpDocParser->parse(new TokenIterator($tokens));
336
337
        // Then we retrieve a flattened list of annotated types excluding null.
338
        $varTagValues = $phpDocNode->getVarTagValues();
339
        $types = $this->flattenVarTagValueTypes($varTagValues);
340
341
        return $this->filterNullFromTypes($types);
342
    }
343
}
344