Passed
Pull Request — master (#1552)
by
unknown
02:58 queued 13s
created

DocBlockTypeResolver::getDocBlocTypeHint()   C

Complexity

Conditions 13
Paths 18

Size

Total Lines 71
Code Lines 38

Duplication

Lines 0
Ratio 0 %

Importance

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

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