Passed
Push — master ( b1e5bb...098a38 )
by Asmir
05:44 queued 02:35
created

getPropertyDocblockTypeHint()   B

Complexity

Conditions 8
Paths 8

Size

Total Lines 59
Code Lines 28

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 8
eloc 28
nc 8
nop 1
dl 0
loc 59
rs 8.4444
c 0
b 0
f 0

How to fix   Long Method   

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\VarTagValueNode;
8
use PHPStan\PhpDocParser\Ast\Type\ArrayTypeNode;
9
use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode;
10
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
11
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
12
use PHPStan\PhpDocParser\Ast\Type\UnionTypeNode;
13
use PHPStan\PhpDocParser\Lexer\Lexer;
14
use PHPStan\PhpDocParser\Parser\ConstExprParser;
15
use PHPStan\PhpDocParser\Parser\PhpDocParser;
16
use PHPStan\PhpDocParser\Parser\TokenIterator;
17
use PHPStan\PhpDocParser\Parser\TypeParser;
18
19
/**
20
 * @internal
21
 */
22
final class DocBlockTypeResolver
23
{
24
    /** resolve single use statements */
25
    private const SINGLE_USE_STATEMENTS_REGEX = '/^[^\S\r\n]*use[\s]*([^;\n]*)[\s]*;$/m';
26
    /** resolve group use statements */
27
    private const GROUP_USE_STATEMENTS_REGEX = '/^[^\S\r\n]*use[[\s]*([^;\n]*)[\s]*{([a-zA-Z0-9\s\n\r,]*)};$/m';
28
    private const GLOBAL_NAMESPACE_PREFIX = '\\';
29
30
    /**
31
     * @var PhpDocParser
32
     */
33
    protected $phpDocParser;
34
35
    /**
36
     * @var Lexer
37
     */
38
    protected $lexer;
39
40
    public function __construct()
41
    {
42
        $constExprParser = new ConstExprParser();
43
        $typeParser = new TypeParser($constExprParser);
44
45
        $this->phpDocParser = new PhpDocParser($typeParser, $constExprParser);
46
        $this->lexer = new Lexer();
47
    }
48
49
    /**
50
     * Attempts to retrieve additional type information from a PhpDoc block. Throws in case of ambiguous type
51
     * information and will return null if no helpful type information could be retrieved.
52
     *
53
     * @param \ReflectionProperty $reflectionProperty
54
     *
55
     * @return string|null
56
     */
57
    public function getPropertyDocblockTypeHint(\ReflectionProperty $reflectionProperty): ?string
58
    {
59
        if (!$reflectionProperty->getDocComment()) {
60
            return null;
61
        }
62
63
        // First we tokenize the PhpDoc comment and parse the tokens into a PhpDocNode.
64
        $tokens = $this->lexer->tokenize($reflectionProperty->getDocComment());
65
        $phpDocNode = $this->phpDocParser->parse(new TokenIterator($tokens));
66
67
        // Then we retrieve a flattened list of annotated types excluding null.
68
        $varTagValues = $phpDocNode->getVarTagValues();
69
        $types = $this->flattenVarTagValueTypes($varTagValues);
70
        $typesWithoutNull = $this->filterNullFromTypes($types);
71
72
        // The PhpDoc does not contain additional type information.
73
        if (0 === count($typesWithoutNull)) {
74
            return null;
75
        }
76
77
        // The PhpDoc contains multiple non-null types which produces ambiguity when deserializing.
78
        if (count($typesWithoutNull) > 1) {
79
            $typeHint = implode('|', array_map(static function (TypeNode $type) {
80
                return (string) $type;
81
            }, $types));
82
83
            throw new \InvalidArgumentException(sprintf("Can't use union type %s for collection in %s:%s", $typeHint, $reflectionProperty->getDeclaringClass()->getName(), $reflectionProperty->getName()));
84
        }
85
86
        // Only one type is left, so we only need to differentiate between arrays, generics and other types.
87
        $type = $typesWithoutNull[0];
88
89
        // Simple array without concrete type: array
90
        if ($this->isSimpleType($type, 'array')) {
91
            return null;
92
        }
93
94
        // Normal array syntax: Product[] | \Foo\Bar\Product[]
95
        if ($type instanceof ArrayTypeNode) {
96
            $resolvedType = $this->resolveTypeFromTypeNode($type->type, $reflectionProperty);
97
98
            return 'array<' . $resolvedType . '>';
99
        }
100
101
        // Generic array syntax: array<Product> | array<\Foo\Bar\Product> | array<int,Product>
102
        if ($type instanceof GenericTypeNode) {
103
            if (!$this->isSimpleType($type->type, 'array')) {
104
                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()));
105
            }
106
107
            $resolvedTypes = array_map(function (TypeNode $node) use ($reflectionProperty) {
108
                return $this->resolveTypeFromTypeNode($node, $reflectionProperty);
109
            }, $type->genericTypes);
110
111
            return 'array<' . implode(',', $resolvedTypes) . '>';
112
        }
113
114
        // Primitives and class names: Collection | \Foo\Bar\Product | string
115
        return $this->resolveTypeFromTypeNode($type, $reflectionProperty);
116
    }
117
118
    /**
119
     * Returns a flat list of types of the given var tags. Union types are flattened as well.
120
     *
121
     * @param VarTagValueNode[] $varTagValues
122
     *
123
     * @return TypeNode[]
124
     */
125
    private function flattenVarTagValueTypes(array $varTagValues): array
126
    {
127
        if ([] === $varTagValues) {
128
            return [];
129
        }
130
131
        return array_merge(...array_map(static function (VarTagValueNode $node) {
132
            if ($node->type instanceof UnionTypeNode) {
133
                return $node->type->types;
134
            }
135
136
            return [$node->type];
137
        }, $varTagValues));
138
    }
139
140
    /**
141
     * Filters the null type from the given types array. If no null type is found, the array is returned unchanged.
142
     *
143
     * @param TypeNode[] $types
144
     *
145
     * @return TypeNode[]
146
     */
147
    private function filterNullFromTypes(array $types): array
148
    {
149
        return array_values(array_filter(array_map(function (TypeNode $node) {
150
            return $this->isNullType($node) ? null : $node;
151
        }, $types)));
152
    }
153
154
    /**
155
     * Determines if the given type is a null type.
156
     *
157
     * @param TypeNode $typeNode
158
     *
159
     * @return bool
160
     */
161
    private function isNullType(TypeNode $typeNode): bool
162
    {
163
        return $this->isSimpleType($typeNode, 'null');
164
    }
165
166
    /**
167
     * Determines if the given node represents a simple type.
168
     *
169
     * @param TypeNode $typeNode
170
     * @param string $simpleType
171
     *
172
     * @return bool
173
     */
174
    private function isSimpleType(TypeNode $typeNode, string $simpleType): bool
175
    {
176
        return $typeNode instanceof IdentifierTypeNode && $typeNode->name === $simpleType;
177
    }
178
179
    /**
180
     * Attempts to resolve the fully qualified type from the given node. If the node is not suitable for type
181
     * retrieval, an exception is thrown.
182
     *
183
     * @param TypeNode $typeNode
184
     * @param \ReflectionProperty $reflectionProperty
185
     *
186
     * @return string
187
     *
188
     * @throws \InvalidArgumentException
189
     */
190
    private function resolveTypeFromTypeNode(TypeNode $typeNode, \ReflectionProperty $reflectionProperty): string
191
    {
192
        if (!($typeNode instanceof IdentifierTypeNode)) {
193
            throw new \InvalidArgumentException(sprintf("Can't use unsupported type %s for collection in %s:%s", (string) $typeNode, $reflectionProperty->getDeclaringClass()->getName(), $reflectionProperty->getName()));
194
        }
195
196
        return $this->resolveType($typeNode->name, $reflectionProperty);
197
    }
198
199
    private function expandClassNameUsingUseStatements(string $typeHint, \ReflectionClass $declaringClass, \ReflectionProperty $reflectionProperty): string
200
    {
201
        if ($this->isClassOrInterface($typeHint)) {
202
            return $typeHint;
203
        }
204
205
        $expandedClassName = $declaringClass->getNamespaceName() . '\\' . $typeHint;
206
        if ($this->isClassOrInterface($expandedClassName)) {
207
            return $expandedClassName;
208
        }
209
210
        $classContents = file_get_contents($declaringClass->getFileName());
211
        $foundUseStatements = $this->gatherGroupUseStatements($classContents);
212
        $foundUseStatements = array_merge($this->gatherSingleUseStatements($classContents), $foundUseStatements);
213
214
        foreach ($foundUseStatements as $statementClassName) {
215
            if ($alias = explode('as', $statementClassName)) {
216
                if (array_key_exists(1, $alias) && trim($alias[1]) === $typeHint) {
217
                    return trim($alias[0]);
218
                }
219
            }
220
221
            if ($this->endsWith($statementClassName, $typeHint)) {
222
                return $statementClassName;
223
            }
224
        }
225
226
        throw new \InvalidArgumentException(sprintf("Can't use incorrect type %s for collection in %s:%s", $typeHint, $declaringClass->getName(), $reflectionProperty->getName()));
227
    }
228
229
    private function endsWith(string $statementClassToCheck, string $typeHintToSearchFor): bool
230
    {
231
        $typeHintToSearchFor = '\\' . $typeHintToSearchFor;
232
233
        return substr($statementClassToCheck, -strlen($typeHintToSearchFor)) === $typeHintToSearchFor;
234
    }
235
236
    private function isPrimitiveType(string $type): bool
237
    {
238
        return in_array($type, ['int', 'float', 'bool', 'string']);
239
    }
240
241
    private function hasGlobalNamespacePrefix(string $typeHint): bool
242
    {
243
        return self::GLOBAL_NAMESPACE_PREFIX === $typeHint[0];
244
    }
245
246
    private function gatherGroupUseStatements(string $classContents): array
247
    {
248
        $foundUseStatements = [];
249
        preg_match_all(self::GROUP_USE_STATEMENTS_REGEX, $classContents, $foundGroupUseStatements);
250
        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...
251
            foreach (explode(',', $foundGroupUseStatements[2][$useStatementIndex]) as $singleUseStatement) {
252
                $foundUseStatements[] = trim($foundGroupUseStatements[1][$useStatementIndex]) . trim($singleUseStatement);
253
            }
254
        }
255
256
        return $foundUseStatements;
257
    }
258
259
    private function gatherSingleUseStatements(string $classContents): array
260
    {
261
        $foundUseStatements = [];
262
        preg_match_all(self::SINGLE_USE_STATEMENTS_REGEX, $classContents, $foundSingleUseStatements);
263
        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...
264
            $foundUseStatements[] = trim($foundSingleUseStatements[1][$useStatementIndex]);
265
        }
266
267
        return $foundUseStatements;
268
    }
269
270
    private function getDeclaringClassOrTrait(\ReflectionProperty $reflectionProperty): \ReflectionClass
271
    {
272
        foreach ($reflectionProperty->getDeclaringClass()->getTraits() as $trait) {
273
            foreach ($trait->getProperties() as $traitProperty) {
274
                if ($traitProperty->getName() === $reflectionProperty->getName()) {
275
                    return $this->getDeclaringClassOrTrait($traitProperty);
276
                }
277
            }
278
        }
279
280
        return $reflectionProperty->getDeclaringClass();
281
    }
282
283
    private function resolveType(string $typeHint, \ReflectionProperty $reflectionProperty): string
284
    {
285
        if (!$this->hasGlobalNamespacePrefix($typeHint) && !$this->isPrimitiveType($typeHint)) {
286
            $typeHint = $this->expandClassNameUsingUseStatements($typeHint, $this->getDeclaringClassOrTrait($reflectionProperty), $reflectionProperty);
287
        }
288
289
        return ltrim($typeHint, '\\');
290
    }
291
292
    private function isClassOrInterface(string $typeHint): bool
293
    {
294
        return class_exists($typeHint) || interface_exists($typeHint);
295
    }
296
}
297