Passed
Pull Request — master (#1552)
by
unknown
04:07 queued 01:26
created

DocBlockTypeResolver::getDocBlocTypeHint()   C

Complexity

Conditions 13
Paths 18

Size

Total Lines 69
Code Lines 37

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 13
eloc 37
nc 18
nop 1
dl 0
loc 69
rs 6.6166
c 1
b 0
f 0

How to fix   Long Method    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
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\VarTagValueNode;
11
use PHPStan\PhpDocParser\Ast\Type\ArrayTypeNode;
12
use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode;
13
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
14
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
15
use PHPStan\PhpDocParser\Ast\Type\UnionTypeNode;
16
use PHPStan\PhpDocParser\Lexer\Lexer;
17
use PHPStan\PhpDocParser\Parser\ConstExprParser;
18
use PHPStan\PhpDocParser\Parser\PhpDocParser;
19
use PHPStan\PhpDocParser\Parser\TokenIterator;
20
use PHPStan\PhpDocParser\Parser\TypeParser;
21
22
use function sprintf;
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
            $isSimpleTypeArray = $this->isSimpleType($type->type, 'array');
120
            $isSimpleTypeList = $this->isSimpleType($type->type, 'list');
121
            if (!$isSimpleTypeArray && !$isSimpleTypeList) {
122
                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()));
123
            }
124
125
            if ($isSimpleTypeList) {
126
                $keyType = 'int';
127
                $valuesIndex = 0;
128
            } else {
129
                if (1 === count($type->genericTypes)) {
130
                    $keyType = null;
131
                    $valuesIndex = 0;
132
                } else {
133
                    $keyType = $this->resolveTypeFromTypeNode($type->genericTypes[0], $reflector);
134
                    $valuesIndex = 1;
135
                }
136
            }
137
138
            if ($type->genericTypes[$valuesIndex] instanceof UnionTypeNode) {
139
                $valueTypes = array_map(
140
                    fn (TypeNode $node) => $this->resolveTypeFromTypeNode($node, $reflector),
141
                    $type->genericTypes[$valuesIndex]->types,
0 ignored issues
show
Bug introduced by
Accessing types on the interface PHPStan\PhpDocParser\Ast\Type\TypeNode suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
142
                );
143
            } else {
144
                $valueType = $this->resolveTypeFromTypeNode($type->genericTypes[$valuesIndex], $reflector);
145
                $valueTypes = [$valueType];
146
            }
147
148
            if (null === $keyType) {
149
                return sprintf('array<%s>', implode(', ', $valueTypes));
150
            }
151
152
            return sprintf('array<%s, %s>', $keyType, implode('|', $valueTypes));
153
        }
154
155
        // Primitives and class names: Collection | \Foo\Bar\Product | string
156
        return $this->resolveTypeFromTypeNode($type, $reflector);
157
    }
158
159
    /**
160
     * Returns a flat list of types of the given var tags. Union types are flattened as well.
161
     *
162
     * @param ReturnTagValueNode[]|VarTagValueNode[] $tagValues
163
     *
164
     * @return TypeNode[]
165
     */
166
    private function flattenTagValueTypes(array $tagValues): array
167
    {
168
        if ([] === $tagValues) {
169
            return [];
170
        }
171
172
        return array_merge(...array_map(static function ($node) {
173
            if ($node->type instanceof UnionTypeNode) {
174
                return $node->type->types;
175
            }
176
177
            return [$node->type];
178
        }, $tagValues));
179
    }
180
181
    /**
182
     * Returns a flat list of types of the given param tags. Union types are flattened as well.
183
     *
184
     * @param ParamTagValueNode[] $varTagValues
185
     *
186
     * @return TypeNode[]
187
     */
188
    private function flattenParamTagValueTypes(string $parameterName, array $varTagValues): array
189
    {
190
        if ([] === $varTagValues) {
191
            return [];
192
        }
193
194
        $parameterName = sprintf('$%s', $parameterName);
195
        $types = [];
196
        foreach ($varTagValues as $node) {
197
            if ($parameterName !== $node->parameterName) {
198
                continue;
199
            }
200
201
            $types[] = $node->type;
202
        }
203
204
        return $types;
205
    }
206
207
    /**
208
     * Filters the null type from the given types array. If no null type is found, the array is returned unchanged.
209
     *
210
     * @param TypeNode[] $types
211
     *
212
     * @return TypeNode[]
213
     */
214
    private function filterNullFromTypes(array $types): array
215
    {
216
        return array_values(array_filter(array_map(fn (TypeNode $node) => $this->isNullType($node) ? null : $node, $types)));
217
    }
218
219
    /**
220
     * Determines if the given type is a null type.
221
     *
222
     * @param TypeNode $typeNode
223
     *
224
     * @return bool
225
     */
226
    private function isNullType(TypeNode $typeNode): bool
227
    {
228
        return $this->isSimpleType($typeNode, 'null');
229
    }
230
231
    /**
232
     * Determines if the given node represents a simple type.
233
     *
234
     * @param TypeNode $typeNode
235
     * @param string $simpleType
236
     *
237
     * @return bool
238
     */
239
    private function isSimpleType(TypeNode $typeNode, string $simpleType): bool
240
    {
241
        return $typeNode instanceof IdentifierTypeNode && $typeNode->name === $simpleType;
242
    }
243
244
    /**
245
     * Attempts to resolve the fully qualified type from the given node. If the node is not suitable for type
246
     * retrieval, an exception is thrown.
247
     *
248
     * @param TypeNode $typeNode
249
     * @param \ReflectionMethod|\ReflectionProperty $reflector
250
     *
251
     * @return string
252
     *
253
     * @throws \InvalidArgumentException
254
     */
255
    private function resolveTypeFromTypeNode(TypeNode $typeNode, $reflector): string
256
    {
257
        if (!($typeNode instanceof IdentifierTypeNode)) {
258
            throw new \InvalidArgumentException(sprintf("Can't use unsupported type %s for collection in %s:%s", (string) $typeNode, $reflector->getDeclaringClass()->getName(), $reflector->getName()));
259
        }
260
261
        return $this->resolveType($typeNode->name, $reflector);
262
    }
263
264
    /**
265
     * @param \ReflectionMethod|\ReflectionProperty $reflector
266
     */
267
    private function expandClassNameUsingUseStatements(string $typeHint, \ReflectionClass $declaringClass, $reflector): string
268
    {
269
        $expandedClassName = $declaringClass->getNamespaceName() . '\\' . $typeHint;
270
        if ($this->isClassOrInterface($expandedClassName)) {
271
            return $expandedClassName;
272
        }
273
274
        $classContents = file_get_contents($declaringClass->getFileName());
275
        $foundUseStatements = $this->gatherGroupUseStatements($classContents);
276
        $foundUseStatements = array_merge($this->gatherSingleUseStatements($classContents), $foundUseStatements);
277
278
        foreach ($foundUseStatements as $statementClassName) {
279
            if ($alias = explode('as', $statementClassName)) {
280
                if (array_key_exists(1, $alias) && trim($alias[1]) === $typeHint) {
281
                    return trim($alias[0]);
282
                }
283
            }
284
285
            if ($this->endsWith($statementClassName, $typeHint)) {
286
                return $statementClassName;
287
            }
288
        }
289
290
        if ($declaringClass->getDocComment()) {
291
            $phpstanArrayType = $this->getPhpstanType($declaringClass, $typeHint, $reflector);
292
293
            if ($phpstanArrayType) {
294
                return $phpstanArrayType;
295
            }
296
        }
297
298
        if ($this->isClassOrInterface($typeHint)) {
299
            return $typeHint;
300
        }
301
302
        throw new \InvalidArgumentException(sprintf("Can't use incorrect type %s for collection in %s:%s", $typeHint, $declaringClass->getName(), $reflector->getName()));
303
    }
304
305
    private function endsWith(string $statementClassToCheck, string $typeHintToSearchFor): bool
306
    {
307
        $typeHintToSearchFor = '\\' . $typeHintToSearchFor;
308
309
        return substr($statementClassToCheck, -strlen($typeHintToSearchFor)) === $typeHintToSearchFor;
310
    }
311
312
    private function isPrimitiveType(string $type): bool
313
    {
314
        return in_array($type, ['int', 'integer', 'float', 'bool', 'boolean', 'double', 'string']);
315
    }
316
317
    private function hasGlobalNamespacePrefix(string $typeHint): bool
318
    {
319
        return self::GLOBAL_NAMESPACE_PREFIX === $typeHint[0];
320
    }
321
322
    private function gatherGroupUseStatements(string $classContents): array
323
    {
324
        $foundUseStatements = [];
325
        preg_match_all(self::GROUP_USE_STATEMENTS_REGEX, $classContents, $foundGroupUseStatements);
326
        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...
327
            foreach (explode(',', $foundGroupUseStatements[2][$useStatementIndex]) as $singleUseStatement) {
328
                $foundUseStatements[] = trim($foundGroupUseStatements[1][$useStatementIndex]) . trim($singleUseStatement);
329
            }
330
        }
331
332
        return $foundUseStatements;
333
    }
334
335
    private function gatherSingleUseStatements(string $classContents): array
336
    {
337
        $foundUseStatements = [];
338
        preg_match_all(self::SINGLE_USE_STATEMENTS_REGEX, $classContents, $foundSingleUseStatements);
339
        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...
340
            $foundUseStatements[] = trim($foundSingleUseStatements[1][$useStatementIndex]);
341
        }
342
343
        return $foundUseStatements;
344
    }
345
346
    /**
347
     * @param \ReflectionMethod|\ReflectionProperty $reflector
348
     */
349
    private function getDeclaringClassOrTrait($reflector): \ReflectionClass
350
    {
351
        foreach ($reflector->getDeclaringClass()->getTraits() as $trait) {
352
            foreach ($trait->getProperties() as $traitProperty) {
353
                if ($traitProperty->getName() === $reflector->getName()) {
354
                    return $this->getDeclaringClassOrTrait($traitProperty);
355
                }
356
            }
357
        }
358
359
        return $reflector->getDeclaringClass();
360
    }
361
362
    /**
363
     * @param \ReflectionMethod|\ReflectionProperty $reflector
364
     */
365
    private function resolveType(string $typeHint, $reflector): string
366
    {
367
        if (!$this->hasGlobalNamespacePrefix($typeHint) && !$this->isPrimitiveType($typeHint)) {
368
            $typeHint = $this->expandClassNameUsingUseStatements($typeHint, $this->getDeclaringClassOrTrait($reflector), $reflector);
369
        }
370
371
        return ltrim($typeHint, '\\');
372
    }
373
374
    private function isClassOrInterface(string $typeHint): bool
375
    {
376
        return class_exists($typeHint) || interface_exists($typeHint);
377
    }
378
379
    /**
380
     * @param \ReflectionMethod|\ReflectionProperty $reflector
381
     */
382
    private function resolveTypeFromDocblock($reflector): array
383
    {
384
        $docComment = $reflector->getDocComment();
385
        if (!$docComment && PHP_VERSION_ID >= 80000 && $reflector instanceof \ReflectionProperty && $reflector->isPromoted()) {
386
            $constructor = $reflector->getDeclaringClass()->getConstructor();
387
            if (!$constructor) {
388
                return [];
389
            }
390
391
            $docComment = $constructor->getDocComment();
392
393
            if (!$docComment) {
394
                return [];
395
            }
396
397
            $tokens = $this->lexer->tokenize($docComment);
398
            $phpDocNode = $this->phpDocParser->parse(new TokenIterator($tokens));
399
400
            return $this->flattenParamTagValueTypes($reflector->getName(), $phpDocNode->getParamTagValues());
401
        }
402
403
        if (!$docComment) {
404
            return [];
405
        }
406
407
        // First we tokenize the PhpDoc comment and parse the tokens into a PhpDocNode.
408
        $tokens = $this->lexer->tokenize($docComment);
409
        $phpDocNode = $this->phpDocParser->parse(new TokenIterator($tokens));
410
411
        if ($reflector instanceof \ReflectionProperty) {
412
            // Then we retrieve a flattened list of annotated types excluding null.
413
            $tagValues = $phpDocNode->getVarTagValues();
414
        } else {
415
            // Then we retrieve a flattened list of annotated types including null.
416
            $tagValues = $phpDocNode->getReturnTagValues();
417
        }
418
419
        $types = $this->flattenTagValueTypes($tagValues);
420
421
        return $this->filterNullFromTypes($types);
422
    }
423
424
    /**
425
     * @param \ReflectionMethod|\ReflectionProperty $reflector
426
     */
427
    private function getPhpstanType(\ReflectionClass $declaringClass, string $typeHint, $reflector): ?string
428
    {
429
        $tokens = $this->lexer->tokenize($declaringClass->getDocComment());
430
        $phpDocNode = $this->phpDocParser->parse(new TokenIterator($tokens));
431
        $self = $this;
432
433
        foreach ($phpDocNode->children as $node) {
434
            if ($node instanceof PhpDocTagNode && '@phpstan-type' === $node->name) {
435
                $phpstanType = (string) $node->value;
436
                preg_match_all(self::PHPSTAN_ARRAY_SHAPE, $phpstanType, $foundPhpstanArray);
437
                if (isset($foundPhpstanArray[1][0]) && $foundPhpstanArray[1][0] === $typeHint) {
438
                    return 'array';
439
                }
440
441
                preg_match_all(self::PHPSTAN_ARRAY_TYPE, $phpstanType, $foundPhpstanArray);
442
                if (isset($foundPhpstanArray[2][0]) && $foundPhpstanArray[1][0] === $typeHint) {
443
                    $types = explode(',', $foundPhpstanArray[2][0]);
444
445
                    return sprintf('array<%s>', implode(
446
                        ',',
447
                        array_map(static fn (string $type) => $self->resolveType(trim($type), $reflector), $types),
448
                    ));
449
                }
450
            }
451
        }
452
453
        return null;
454
    }
455
}
456