Completed
Push — master ( d0a24b...58b8f7 )
by Marcin
46s queued 39s
created

DocBlockTypeResolver   F

Complexity

Total Complexity 71

Size/Duplication

Total Lines 418
Duplicated Lines 0 %

Importance

Changes 5
Bugs 1 Features 0
Metric Value
eloc 145
dl 0
loc 418
rs 2.7199
c 5
b 1
f 0
wmc 71

21 Methods

Rating   Name   Duplication   Size   Complexity  
A resolveType() 0 7 3
B getDocBlocTypeHint() 0 48 9
A hasGlobalNamespacePrefix() 0 3 1
B resolveTypeFromDocblock() 0 40 9
A resolveTypeFromTypeNode() 0 7 2
A isPrimitiveType() 0 3 1
A flattenParamTagValueTypes() 0 17 4
A filterNullFromTypes() 0 3 2
A getMethodDocblockTypeHint() 0 3 1
A getPropertyDocblockTypeHint() 0 3 1
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 36 10
A gatherSingleUseStatements() 0 9 2
A __construct() 0 7 1
A flattenTagValueTypes() 0 13 3
B getPhpstanArrayType() 0 39 9

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\PhpDocTagNode;
9
use PHPStan\PhpDocParser\Ast\PhpDoc\ReturnTagValueNode;
10
use PHPStan\PhpDocParser\Ast\PhpDoc\TypeAliasImportTagValueNode;
11
use PHPStan\PhpDocParser\Ast\PhpDoc\TypeAliasTagValueNode;
12
use PHPStan\PhpDocParser\Ast\PhpDoc\VarTagValueNode;
13
use PHPStan\PhpDocParser\Ast\Type\ArrayTypeNode;
14
use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode;
15
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
16
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
17
use PHPStan\PhpDocParser\Ast\Type\UnionTypeNode;
18
use PHPStan\PhpDocParser\Lexer\Lexer;
19
use PHPStan\PhpDocParser\Parser\ConstExprParser;
20
use PHPStan\PhpDocParser\Parser\PhpDocParser;
21
use PHPStan\PhpDocParser\Parser\TokenIterator;
22
use PHPStan\PhpDocParser\Parser\TypeParser;
23
24
/**
25
 * @internal
26
 */
27
final class DocBlockTypeResolver
28
{
29
    /** resolve single use statements */
30
    private const SINGLE_USE_STATEMENTS_REGEX = '/^[^\S\r\n]*use[\s]*([^;\n]*)[\s]*;$/m';
31
32
    /** resolve group use statements */
33
    private const GROUP_USE_STATEMENTS_REGEX = '/^[^\S\r\n]*use[[\s]*([^;\n]*)[\s]*{([a-zA-Z0-9\s\n\r,]*)};$/m';
34
    private const GLOBAL_NAMESPACE_PREFIX = '\\';
35
    private const PHPSTAN_ARRAY_SHAPE = '/^([^\s]*) array{.*/m';
36
    private const PHPSTAN_ARRAY_TYPE = '/^([^\s]*) array<(.*)>/m';
37
38
    /**
39
     * @var PhpDocParser
40
     */
41
    protected $phpDocParser;
42
43
    /**
44
     * @var Lexer
45
     */
46
    protected $lexer;
47
48
    public function __construct()
49
    {
50
        $constExprParser = new ConstExprParser();
51
        $typeParser = new TypeParser($constExprParser);
52
53
        $this->phpDocParser = new PhpDocParser($typeParser, $constExprParser);
54
        $this->lexer = new Lexer();
55
    }
56
57
    /**
58
     * Attempts to retrieve additional type information from a PhpDoc block. Throws in case of ambiguous type
59
     * information and will return null if no helpful type information could be retrieved.
60
     *
61
     * @param \ReflectionMethod $reflectionMethod
62
     *
63
     * @return string|null
64
     */
65
    public function getMethodDocblockTypeHint(\ReflectionMethod $reflectionMethod): ?string
66
    {
67
        return $this->getDocBlocTypeHint($reflectionMethod);
68
    }
69
70
    /**
71
     * Attempts to retrieve additional type information from a PhpDoc block. Throws in case of ambiguous type
72
     * information and will return null if no helpful type information could be retrieved.
73
     *
74
     * @param \ReflectionProperty $reflectionProperty
75
     *
76
     * @return string|null
77
     */
78
    public function getPropertyDocblockTypeHint(\ReflectionProperty $reflectionProperty): ?string
79
    {
80
        return $this->getDocBlocTypeHint($reflectionProperty);
81
    }
82
83
    /**
84
     * @param \ReflectionMethod|\ReflectionProperty $reflector
85
     *
86
     * @return string|null
87
     */
88
    private function getDocBlocTypeHint($reflector): ?string
89
    {
90
        $types = $this->resolveTypeFromDocblock($reflector);
91
92
        // The PhpDoc does not contain additional type information.
93
        if (0 === count($types)) {
94
            return null;
95
        }
96
97
        // The PhpDoc contains multiple non-null types which produces ambiguity when deserializing.
98
        if (count($types) > 1) {
99
            return null;
100
        }
101
102
        // Only one type is left, so we only need to differentiate between arrays, generics and other types.
103
        $type = $types[0];
104
105
        // Simple array without concrete type: array
106
        if ($this->isSimpleType($type, 'array') || $this->isSimpleType($type, 'list')) {
107
            return null;
108
        }
109
110
        // Normal array syntax: Product[] | \Foo\Bar\Product[]
111
        if ($type instanceof ArrayTypeNode) {
112
            $resolvedType = $this->resolveTypeFromTypeNode($type->type, $reflector);
113
114
            return 'array<' . $resolvedType . '>';
115
        }
116
117
        // Generic array syntax: array<Product> | array<\Foo\Bar\Product> | array<int,Product>
118
        if ($type instanceof GenericTypeNode) {
119
            if ($this->isSimpleType($type->type, 'array')) {
120
                $resolvedTypes = array_map(fn (TypeNode $node) => $this->resolveTypeFromTypeNode($node, $reflector), $type->genericTypes);
121
122
                return 'array<' . implode(',', $resolvedTypes) . '>';
123
            }
124
125
            if ($this->isSimpleType($type->type, 'list')) {
126
                $resolvedTypes = array_map(fn (TypeNode $node) => $this->resolveTypeFromTypeNode($node, $reflector), $type->genericTypes);
127
128
                return 'array<int, ' . implode(',', $resolvedTypes) . '>';
129
            }
130
131
            throw new \InvalidArgumentException(sprintf("Can't use non-array generic type %s for collection in %s:%s", (string) $type->type, $reflector->getDeclaringClass()->getName(), $reflector->getName()));
132
        }
133
134
        // Primitives and class names: Collection | \Foo\Bar\Product | string
135
        return $this->resolveTypeFromTypeNode($type, $reflector);
136
    }
137
138
    /**
139
     * Returns a flat list of types of the given var tags. Union types are flattened as well.
140
     *
141
     * @param ReturnTagValueNode[]|VarTagValueNode[] $tagValues
142
     *
143
     * @return TypeNode[]
144
     */
145
    private function flattenTagValueTypes(array $tagValues): array
146
    {
147
        if ([] === $tagValues) {
148
            return [];
149
        }
150
151
        return array_merge(...array_map(static function ($node) {
152
            if ($node->type instanceof UnionTypeNode) {
153
                return $node->type->types;
154
            }
155
156
            return [$node->type];
157
        }, $tagValues));
158
    }
159
160
    /**
161
     * Returns a flat list of types of the given param tags. Union types are flattened as well.
162
     *
163
     * @param ParamTagValueNode[] $varTagValues
164
     *
165
     * @return TypeNode[]
166
     */
167
    private function flattenParamTagValueTypes(string $parameterName, array $varTagValues): array
168
    {
169
        if ([] === $varTagValues) {
170
            return [];
171
        }
172
173
        $parameterName = sprintf('$%s', $parameterName);
174
        $types = [];
175
        foreach ($varTagValues as $node) {
176
            if ($parameterName !== $node->parameterName) {
177
                continue;
178
            }
179
180
            $types[] = $node->type;
181
        }
182
183
        return $types;
184
    }
185
186
    /**
187
     * Filters the null type from the given types array. If no null type is found, the array is returned unchanged.
188
     *
189
     * @param TypeNode[] $types
190
     *
191
     * @return TypeNode[]
192
     */
193
    private function filterNullFromTypes(array $types): array
194
    {
195
        return array_values(array_filter(array_map(fn (TypeNode $node) => $this->isNullType($node) ? null : $node, $types)));
196
    }
197
198
    /**
199
     * Determines if the given type is a null type.
200
     *
201
     * @param TypeNode $typeNode
202
     *
203
     * @return bool
204
     */
205
    private function isNullType(TypeNode $typeNode): bool
206
    {
207
        return $this->isSimpleType($typeNode, 'null');
208
    }
209
210
    /**
211
     * Determines if the given node represents a simple type.
212
     *
213
     * @param TypeNode $typeNode
214
     * @param string $simpleType
215
     *
216
     * @return bool
217
     */
218
    private function isSimpleType(TypeNode $typeNode, string $simpleType): bool
219
    {
220
        return $typeNode instanceof IdentifierTypeNode && $typeNode->name === $simpleType;
221
    }
222
223
    /**
224
     * Attempts to resolve the fully qualified type from the given node. If the node is not suitable for type
225
     * retrieval, an exception is thrown.
226
     *
227
     * @param TypeNode $typeNode
228
     * @param \ReflectionMethod|\ReflectionProperty $reflector
229
     *
230
     * @return string
231
     *
232
     * @throws \InvalidArgumentException
233
     */
234
    private function resolveTypeFromTypeNode(TypeNode $typeNode, $reflector): string
235
    {
236
        if (!($typeNode instanceof IdentifierTypeNode)) {
237
            throw new \InvalidArgumentException(sprintf("Can't use unsupported type %s for collection in %s:%s", (string) $typeNode, $reflector->getDeclaringClass()->getName(), $reflector->getName()));
238
        }
239
240
        return $this->resolveType($typeNode->name, $reflector);
241
    }
242
243
    /**
244
     * @param \ReflectionMethod|\ReflectionProperty $reflector
245
     */
246
    private function expandClassNameUsingUseStatements(string $typeHint, \ReflectionClass $declaringClass, $reflector): string
247
    {
248
        $expandedClassName = $declaringClass->getNamespaceName() . '\\' . $typeHint;
249
        if ($this->isClassOrInterface($expandedClassName)) {
250
            return $expandedClassName;
251
        }
252
253
        $classContents = file_get_contents($declaringClass->getFileName());
254
        $foundUseStatements = $this->gatherGroupUseStatements($classContents);
255
        $foundUseStatements = array_merge($this->gatherSingleUseStatements($classContents), $foundUseStatements);
256
257
        foreach ($foundUseStatements as $statementClassName) {
258
            if ($alias = explode('as', $statementClassName)) {
259
                if (array_key_exists(1, $alias) && trim($alias[1]) === $typeHint) {
260
                    return trim($alias[0]);
261
                }
262
            }
263
264
            if ($this->endsWith($statementClassName, $typeHint)) {
265
                return $statementClassName;
266
            }
267
        }
268
269
        if ($declaringClass->getDocComment()) {
270
            $phpstanArrayType = $this->getPhpstanArrayType($declaringClass, $typeHint, $reflector);
271
272
            if ($phpstanArrayType) {
273
                return $phpstanArrayType;
274
            }
275
        }
276
277
        if ($this->isClassOrInterface($typeHint)) {
278
            return $typeHint;
279
        }
280
281
        throw new \InvalidArgumentException(sprintf("Can't use incorrect type %s for collection in %s:%s", $typeHint, $declaringClass->getName(), $reflector->getName()));
282
    }
283
284
    private function endsWith(string $statementClassToCheck, string $typeHintToSearchFor): bool
285
    {
286
        $typeHintToSearchFor = '\\' . $typeHintToSearchFor;
287
288
        return substr($statementClassToCheck, -strlen($typeHintToSearchFor)) === $typeHintToSearchFor;
289
    }
290
291
    private function isPrimitiveType(string $type): bool
292
    {
293
        return in_array($type, ['int', 'integer', 'float', 'bool', 'boolean', 'double', 'string']);
294
    }
295
296
    private function hasGlobalNamespacePrefix(string $typeHint): bool
297
    {
298
        return self::GLOBAL_NAMESPACE_PREFIX === $typeHint[0];
299
    }
300
301
    private function gatherGroupUseStatements(string $classContents): array
302
    {
303
        $foundUseStatements = [];
304
        preg_match_all(self::GROUP_USE_STATEMENTS_REGEX, $classContents, $foundGroupUseStatements);
305
        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...
306
            foreach (explode(',', $foundGroupUseStatements[2][$useStatementIndex]) as $singleUseStatement) {
307
                $foundUseStatements[] = trim($foundGroupUseStatements[1][$useStatementIndex]) . trim($singleUseStatement);
308
            }
309
        }
310
311
        return $foundUseStatements;
312
    }
313
314
    private function gatherSingleUseStatements(string $classContents): array
315
    {
316
        $foundUseStatements = [];
317
        preg_match_all(self::SINGLE_USE_STATEMENTS_REGEX, $classContents, $foundSingleUseStatements);
318
        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...
319
            $foundUseStatements[] = trim($foundSingleUseStatements[1][$useStatementIndex]);
320
        }
321
322
        return $foundUseStatements;
323
    }
324
325
    /**
326
     * @param \ReflectionMethod|\ReflectionProperty $reflector
327
     */
328
    private function getDeclaringClassOrTrait($reflector): \ReflectionClass
329
    {
330
        foreach ($reflector->getDeclaringClass()->getTraits() as $trait) {
331
            foreach ($trait->getProperties() as $traitProperty) {
332
                if ($traitProperty->getName() === $reflector->getName()) {
333
                    return $this->getDeclaringClassOrTrait($traitProperty);
334
                }
335
            }
336
        }
337
338
        return $reflector->getDeclaringClass();
339
    }
340
341
    /**
342
     * @param \ReflectionMethod|\ReflectionProperty $reflector
343
     */
344
    private function resolveType(string $typeHint, $reflector): string
345
    {
346
        if (!$this->hasGlobalNamespacePrefix($typeHint) && !$this->isPrimitiveType($typeHint)) {
347
            $typeHint = $this->expandClassNameUsingUseStatements($typeHint, $this->getDeclaringClassOrTrait($reflector), $reflector);
348
        }
349
350
        return ltrim($typeHint, '\\');
351
    }
352
353
    private function isClassOrInterface(string $typeHint): bool
354
    {
355
        return class_exists($typeHint) || interface_exists($typeHint);
356
    }
357
358
    /**
359
     * @param \ReflectionMethod|\ReflectionProperty $reflector
360
     */
361
    private function resolveTypeFromDocblock($reflector): array
362
    {
363
        $docComment = $reflector->getDocComment();
364
        if (!$docComment && PHP_VERSION_ID >= 80000 && $reflector instanceof \ReflectionProperty && $reflector->isPromoted()) {
365
            $constructor = $reflector->getDeclaringClass()->getConstructor();
366
            if (!$constructor) {
367
                return [];
368
            }
369
370
            $docComment = $constructor->getDocComment();
371
372
            if (!$docComment) {
373
                return [];
374
            }
375
376
            $tokens = $this->lexer->tokenize($docComment);
377
            $phpDocNode = $this->phpDocParser->parse(new TokenIterator($tokens));
378
379
            return $this->flattenParamTagValueTypes($reflector->getName(), $phpDocNode->getParamTagValues());
380
        }
381
382
        if (!$docComment) {
383
            return [];
384
        }
385
386
        // First we tokenize the PhpDoc comment and parse the tokens into a PhpDocNode.
387
        $tokens = $this->lexer->tokenize($docComment);
388
        $phpDocNode = $this->phpDocParser->parse(new TokenIterator($tokens));
389
390
        if ($reflector instanceof \ReflectionProperty) {
391
            // Then we retrieve a flattened list of annotated types excluding null.
392
            $tagValues = $phpDocNode->getVarTagValues();
393
        } else {
394
            // Then we retrieve a flattened list of annotated types including null.
395
            $tagValues = $phpDocNode->getReturnTagValues();
396
        }
397
398
        $types = $this->flattenTagValueTypes($tagValues);
399
400
        return $this->filterNullFromTypes($types);
401
    }
402
403
    /**
404
     * @param \ReflectionMethod|\ReflectionProperty $reflector
405
     */
406
    private function getPhpstanArrayType(\ReflectionClass $declaringClass, string $typeHint, $reflector): ?string
407
    {
408
        $tokens = $this->lexer->tokenize($declaringClass->getDocComment());
409
        $phpDocNode = $this->phpDocParser->parse(new TokenIterator($tokens));
410
        $self = $this;
411
412
        foreach ($phpDocNode->children as $node) {
413
            if (
414
                $node instanceof PhpDocTagNode
415
                && $node->value instanceof TypeAliasTagValueNode
416
                && $node->value->alias === $typeHint
417
            ) {
418
                $phpstanType = $node->value->__toString();
419
                preg_match(self::PHPSTAN_ARRAY_SHAPE, $phpstanType, $foundPhpstanArray);
420
                if (isset($foundPhpstanArray[0])) {
421
                    return 'array';
422
                }
423
424
                preg_match(self::PHPSTAN_ARRAY_TYPE, $phpstanType, $foundPhpstanArray);
425
                if (isset($foundPhpstanArray[0])) {
426
                    $types = explode(',', $foundPhpstanArray[2]);
427
428
                    return sprintf('array<%s>', implode(
429
                        ',',
430
                        array_map(static fn (string $type) => $self->resolveType(trim($type), $reflector), $types),
431
                    ));
432
                }
433
            } elseif ($node instanceof PhpDocTagNode && $node->value instanceof TypeAliasImportTagValueNode) {
434
                $importedFromFqn = $this->resolveType($node->value->importedFrom->name, $reflector);
435
436
                return $this->getPhpstanArrayType(
437
                    new \ReflectionClass($importedFromFqn),
438
                    $node->value->importedAlias,
439
                    $reflector,
440
                );
441
            }
442
        }
443
444
        return null;
445
    }
446
}
447